From f0e7f32330d9df0f1354b14211e33da88afc4295 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 10 Dec 2017 17:50:02 +0900
Subject: [PATCH 0001/1250] Refactor

---
 src/api/server.ts          |  7 -------
 src/api/service/twitter.ts | 34 +++++++++++++++++++++++-----------
 2 files changed, 23 insertions(+), 18 deletions(-)

diff --git a/src/api/server.ts b/src/api/server.ts
index 026357b46..463b3f017 100644
--- a/src/api/server.ts
+++ b/src/api/server.ts
@@ -49,13 +49,6 @@ endpoints.forEach(endpoint =>
 app.post('/signup', require('./private/signup').default);
 app.post('/signin', require('./private/signin').default);
 
-app.use((req, res, next) => {
-	// req.headers['cookie'] は常に string ですが、型定義の都合上
-	// string | string[] になっているので string を明示しています
-	res.locals.user = ((req.headers['cookie'] as string || '').match(/i=(!\w+)/) || [null, null])[1];
-	next();
-});
-
 require('./service/github')(app);
 require('./service/twitter')(app);
 
diff --git a/src/api/service/twitter.ts b/src/api/service/twitter.ts
index f164cdc45..e03cd5acc 100644
--- a/src/api/service/twitter.ts
+++ b/src/api/service/twitter.ts
@@ -12,15 +12,24 @@ import config from '../../conf';
 import signin from '../common/signin';
 
 module.exports = (app: express.Application) => {
+	function getUserToken(req) {
+		// req.headers['cookie'] は常に string ですが、型定義の都合上
+		// string | string[] になっているので string を明示しています
+		return ((req.headers['cookie'] as string || '').match(/i=(!\w+)/) || [null, null])[1];
+	}
+
 	app.get('/disconnect/twitter', async (req, res): Promise<any> => {
-		if (res.locals.user == null) return res.send('plz signin');
+		const userToken = getUserToken(req);
+
+		if (userToken == null) return res.send('plz signin');
+
 		const user = await User.findOneAndUpdate({
-			token: res.locals.user
+			token: userToken
 		}, {
-				$set: {
-					twitter: null
-				}
-			});
+			$set: {
+				twitter: null
+			}
+		});
 
 		res.send(`Twitterの連携を解除しました :v:`);
 
@@ -50,9 +59,10 @@ module.exports = (app: express.Application) => {
 	});
 
 	app.get('/connect/twitter', async (req, res): Promise<any> => {
-		if (res.locals.user == null) return res.send('plz signin');
+		const userToken = getUserToken(req);
+		if (userToken == null) return res.send('plz signin');
 		const ctx = await twAuth.begin();
-		redis.set(res.locals.user, JSON.stringify(ctx));
+		redis.set(userToken, JSON.stringify(ctx));
 		res.redirect(ctx.url);
 	});
 
@@ -77,7 +87,9 @@ module.exports = (app: express.Application) => {
 	});
 
 	app.get('/tw/cb', (req, res): any => {
-		if (res.locals.user == null) {
+		const userToken = getUserToken(req);
+
+		if (userToken == null) {
 			// req.headers['cookie'] は常に string ですが、型定義の都合上
 			// string | string[] になっているので string を明示しています
 			const cookies = cookie.parse((req.headers['cookie'] as string || ''));
@@ -102,11 +114,11 @@ module.exports = (app: express.Application) => {
 				signin(res, user, true);
 			});
 		} else {
-			redis.get(res.locals.user, async (_, ctx) => {
+			redis.get(userToken, async (_, ctx) => {
 				const result = await twAuth.done(JSON.parse(ctx), req.query.oauth_verifier);
 
 				const user = await User.findOneAndUpdate({
-					token: res.locals.user
+					token: userToken
 				}, {
 					$set: {
 						twitter: {

From 959de37ecb89237a80ecfbb9c1800a5fb47940ea Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 10 Dec 2017 18:08:28 +0900
Subject: [PATCH 0002/1250] =?UTF-8?q?=E4=BB=96=E3=81=AE=E3=82=A6=E3=82=A7?=
 =?UTF-8?q?=E3=83=96=E3=82=B5=E3=82=A4=E3=83=88=E3=81=8B=E3=82=89=E7=9B=B4?=
 =?UTF-8?q?=E6=8E=A5MisskeyAPI=E3=82=92=E5=88=A9=E7=94=A8=E3=81=A7?=
 =?UTF-8?q?=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/server.ts          |  4 +---
 src/api/service/twitter.ts | 39 ++++++++++++++++++++++++++++++++++----
 2 files changed, 36 insertions(+), 7 deletions(-)

diff --git a/src/api/server.ts b/src/api/server.ts
index 463b3f017..e89d19609 100644
--- a/src/api/server.ts
+++ b/src/api/server.ts
@@ -26,9 +26,7 @@ app.use(bodyParser.json({
 		}
 	}
 }));
-app.use(cors({
-	origin: true
-}));
+app.use(cors());
 
 app.get('/', (req, res) => {
 	res.send('YEE HAW');
diff --git a/src/api/service/twitter.ts b/src/api/service/twitter.ts
index e03cd5acc..573895e8f 100644
--- a/src/api/service/twitter.ts
+++ b/src/api/service/twitter.ts
@@ -12,15 +12,31 @@ import config from '../../conf';
 import signin from '../common/signin';
 
 module.exports = (app: express.Application) => {
-	function getUserToken(req) {
+	function getUserToken(req: express.Request) {
 		// req.headers['cookie'] は常に string ですが、型定義の都合上
 		// string | string[] になっているので string を明示しています
 		return ((req.headers['cookie'] as string || '').match(/i=(!\w+)/) || [null, null])[1];
 	}
 
-	app.get('/disconnect/twitter', async (req, res): Promise<any> => {
-		const userToken = getUserToken(req);
+	function compareOrigin(req: express.Request) {
+		function normalizeUrl(url: string) {
+			return url[url.length - 1] === '/' ? url.substr(0, url.length - 1) : url;
+		}
 
+		// req.headers['cookie'] は常に string ですが、型定義の都合上
+		// string | string[] になっているので string を明示しています
+		const referer = req.headers['referer'] as string;
+
+		return (normalizeUrl(referer) == normalizeUrl(config.url));
+	}
+
+	app.get('/disconnect/twitter', async (req, res): Promise<any> => {
+		if (!compareOrigin(req)) {
+			res.status(400).send('invalid origin');
+			return;
+		}
+
+		const userToken = getUserToken(req);
 		if (userToken == null) return res.send('plz signin');
 
 		const user = await User.findOneAndUpdate({
@@ -59,8 +75,14 @@ module.exports = (app: express.Application) => {
 	});
 
 	app.get('/connect/twitter', async (req, res): Promise<any> => {
+		if (!compareOrigin(req)) {
+			res.status(400).send('invalid origin');
+			return;
+		}
+
 		const userToken = getUserToken(req);
 		if (userToken == null) return res.send('plz signin');
+
 		const ctx = await twAuth.begin();
 		redis.set(userToken, JSON.stringify(ctx));
 		res.redirect(ctx.url);
@@ -98,6 +120,7 @@ module.exports = (app: express.Application) => {
 
 			if (sessid == undefined) {
 				res.status(400).send('invalid session');
+				return;
 			}
 
 			redis.get(sessid, async (_, ctx) => {
@@ -109,13 +132,21 @@ module.exports = (app: express.Application) => {
 
 				if (user == null) {
 					res.status(404).send(`@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`);
+					return;
 				}
 
 				signin(res, user, true);
 			});
 		} else {
+			const verifier = req.query.oauth_verifier;
+
+			if (verifier == null) {
+				res.status(400).send('invalid session');
+				return;
+			}
+
 			redis.get(userToken, async (_, ctx) => {
-				const result = await twAuth.done(JSON.parse(ctx), req.query.oauth_verifier);
+				const result = await twAuth.done(JSON.parse(ctx), verifier);
 
 				const user = await User.findOneAndUpdate({
 					token: userToken

From 757a844affa3e7667d9fa6257144450cf8c488e6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 10 Dec 2017 18:12:39 +0900
Subject: [PATCH 0003/1250] oops

---
 src/api/service/twitter.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/api/service/twitter.ts b/src/api/service/twitter.ts
index 573895e8f..0e75ee0bd 100644
--- a/src/api/service/twitter.ts
+++ b/src/api/service/twitter.ts
@@ -23,7 +23,7 @@ module.exports = (app: express.Application) => {
 			return url[url.length - 1] === '/' ? url.substr(0, url.length - 1) : url;
 		}
 
-		// req.headers['cookie'] は常に string ですが、型定義の都合上
+		// req.headers['referer'] は常に string ですが、型定義の都合上
 		// string | string[] になっているので string を明示しています
 		const referer = req.headers['referer'] as string;
 

From dc14be59d6fa6830412c8092b63950f13089ae16 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Sun, 10 Dec 2017 20:39:26 +0900
Subject: [PATCH 0004/1250] =?UTF-8?q?channel=E4=BB=A5=E5=A4=96mk-images-vi?=
 =?UTF-8?q?ewer=E5=8C=96=E3=83=BBmk-images-viewer=E5=BC=B7=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/web/app/desktop/tags/images-viewer.tag   | 49 ++++++++++++++------
 src/web/app/desktop/tags/post-detail-sub.tag |  6 +--
 src/web/app/desktop/tags/post-detail.tag     |  7 +--
 3 files changed, 38 insertions(+), 24 deletions(-)

diff --git a/src/web/app/desktop/tags/images-viewer.tag b/src/web/app/desktop/tags/images-viewer.tag
index 44a61cb74..3edd1300b 100644
--- a/src/web/app/desktop/tags/images-viewer.tag
+++ b/src/web/app/desktop/tags/images-viewer.tag
@@ -1,32 +1,46 @@
 <mk-images-viewer>
-	<div class="image" ref="view" onmousemove={ mousemove } style={ 'background-image: url(' + image.url + '?thumbnail' } onclick={ click }><img src={ image.url + '?thumbnail&size=512' } alt={ image.name } title={ image.name }/></div>
+	<virtual each={ image in images }>
+		<mk-images-viewer-image ref="wrap" image={ image } images={ images }/>
+	</virtual>
 	<style>
 		:scope
-			display block
+			display grid
 			overflow hidden
 			border-radius 4px
+			grid-gap .25em
 
-			> .image
+			> div
 				cursor zoom-in
+				overflow hidden
+				background-position center
 
 				> img
-					display block
-					max-height 256px
-					max-width 100%
-					margin 0 auto
-
-				&:hover
-					> img
-						visibility hidden
+					visibility hidden
+					max-width: 100%
+					max-height: 256px
 
 				&:not(:hover)
-					background-image none !important
+					background-size cover
 
+				&:nth-child(1):nth-last-child(3)
+					grid-row 1 / 3
 	</style>
 	<script>
 		this.images = this.opts.images;
-		this.image = this.images[0];
 
+		this.on('mount', () => {
+			if(this.images.length >= 3) this.refs.wrap.style.gridAutoRows = "9em";
+			if(this.images.length == 2) this.refs.wrap.style.gridAutoRows = "12em";
+			if(this.images.length == 1) this.refs.wrap.style.gridAutoRows = "256px";
+			if(this.images.length == 4 || this.images.length == 2) this.refs.wrap.style.gridTemplateColumns = "repeat(2, 1fr)";
+			if(this.images.length == 3) this.refs.wrap.style.gridTemplateColumns = "65% 1fr";
+		})
+	</script>
+</mk-images-viewer>
+
+<mk-images-viewer-image>
+	<div ref="view" onmousemove={ mousemove } onmouseleave={ mouseleave } style={ 'background-image: url(' + image.url + '?thumbnail?size=512' } onclick={ click }><img ref="image" src={ image.url + '?thumbnail&size=512' } alt={ image.name } title={ image.name }/></div>
+	<script>
 		this.mousemove = e => {
 			const rect = this.refs.view.getBoundingClientRect();
 			const mouseX = e.clientX - rect.left;
@@ -34,12 +48,19 @@
 			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 = () => {
 			riot.mount(document.body.appendChild(document.createElement('mk-image-dialog')), {
 				image: this.image
 			});
 		};
+
+		this.image = this.opts.image;
 	</script>
-</mk-images-viewer>
+</mk-images-viewer-image>
\ No newline at end of file
diff --git a/src/web/app/desktop/tags/post-detail-sub.tag b/src/web/app/desktop/tags/post-detail-sub.tag
index e22386df9..99899929d 100644
--- a/src/web/app/desktop/tags/post-detail-sub.tag
+++ b/src/web/app/desktop/tags/post-detail-sub.tag
@@ -9,7 +9,7 @@
 				<span class="username">@{ post.user.username }</span>
 			</div>
 			<div class="right">
-				<a class="time" href={ '/' + this.post.user.username + '/' + this.post.id }>
+				<a class="time" href={ '/' + post.user.username + '/' + post.id }>
 					<mk-time time={ post.created_at }/>
 				</a>
 			</div>
@@ -17,9 +17,7 @@
 		<div class="body">
 			<div class="text" ref="text"></div>
 			<div class="media" if={ post.media }>
-				<virtual each={ file in post.media }>
-					<img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/>
-				</virtual>
+				<mk-images-viewer images={ post.media }/>
 			</div>
 		</div>
 	</div>
diff --git a/src/web/app/desktop/tags/post-detail.tag b/src/web/app/desktop/tags/post-detail.tag
index 37f90a6ff..23f7a4198 100644
--- a/src/web/app/desktop/tags/post-detail.tag
+++ b/src/web/app/desktop/tags/post-detail.tag
@@ -37,7 +37,7 @@
 			<div class="body">
 				<div class="text" ref="text"></div>
 				<div class="media" if={ p.media }>
-					<virtual each={ file in p.media }><img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/></virtual>
+					<mk-images-viewer images={ p.media }/>
 				</div>
 				<mk-poll if={ p.poll } post={ p }/>
 			</div>
@@ -208,11 +208,6 @@
 							> mk-url-preview
 								margin-top 8px
 
-						> .media
-							> img
-								display block
-								max-width 100%
-
 					> footer
 						font-size 1.2em
 

From adf7bb22a3acda285b4a1fa0dc4714d9a46bd509 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Sun, 10 Dec 2017 20:49:12 +0900
Subject: [PATCH 0005/1250] =?UTF-8?q?URL=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/web/app/desktop/tags/images-viewer.tag | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/app/desktop/tags/images-viewer.tag b/src/web/app/desktop/tags/images-viewer.tag
index 3edd1300b..9fd6b8de9 100644
--- a/src/web/app/desktop/tags/images-viewer.tag
+++ b/src/web/app/desktop/tags/images-viewer.tag
@@ -39,7 +39,7 @@
 </mk-images-viewer>
 
 <mk-images-viewer-image>
-	<div ref="view" onmousemove={ mousemove } onmouseleave={ mouseleave } style={ 'background-image: url(' + image.url + '?thumbnail?size=512' } onclick={ click }><img ref="image" src={ image.url + '?thumbnail&size=512' } alt={ image.name } title={ image.name }/></div>
+	<div ref="view" onmousemove={ mousemove } onmouseleave={ mouseleave } style={ 'background-image: url(' + image.url + '?thumbnail&size=512' } onclick={ click }><img ref="image" src={ image.url + '?thumbnail&size=512' } alt={ image.name } title={ image.name }/></div>
 	<script>
 		this.mousemove = e => {
 			const rect = this.refs.view.getBoundingClientRect();

From 01167fbf5c31ad6108b581d5b8b22022b0bac2c5 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Sun, 10 Dec 2017 21:01:25 +0900
Subject: [PATCH 0006/1250] =?UTF-8?q?=E6=8E=A8=E6=B8=AC=E3=81=A7CSS?=
 =?UTF-8?q?=E3=82=92=E5=8B=95=E3=81=8B=E3=81=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/web/app/desktop/tags/images-viewer.tag   | 1 +
 src/web/app/desktop/tags/post-detail-sub.tag | 5 -----
 src/web/app/desktop/tags/timeline.tag        | 5 -----
 3 files changed, 1 insertion(+), 10 deletions(-)

diff --git a/src/web/app/desktop/tags/images-viewer.tag b/src/web/app/desktop/tags/images-viewer.tag
index 9fd6b8de9..0369ea9f6 100644
--- a/src/web/app/desktop/tags/images-viewer.tag
+++ b/src/web/app/desktop/tags/images-viewer.tag
@@ -15,6 +15,7 @@
 				background-position center
 
 				> img
+					display block
 					visibility hidden
 					max-width: 100%
 					max-height: 256px
diff --git a/src/web/app/desktop/tags/post-detail-sub.tag b/src/web/app/desktop/tags/post-detail-sub.tag
index 99899929d..ab45b5523 100644
--- a/src/web/app/desktop/tags/post-detail-sub.tag
+++ b/src/web/app/desktop/tags/post-detail-sub.tag
@@ -105,11 +105,6 @@
 						> mk-url-preview
 							margin-top 8px
 
-					> .media
-						> img
-							display block
-							max-width 100%
-
 	</style>
 	<script>
 		import compile from '../../common/scripts/text-compiler';
diff --git a/src/web/app/desktop/tags/timeline.tag b/src/web/app/desktop/tags/timeline.tag
index 08e658a3c..77e4a573b 100644
--- a/src/web/app/desktop/tags/timeline.tag
+++ b/src/web/app/desktop/tags/timeline.tag
@@ -357,11 +357,6 @@
 								background $theme-color
 								border-radius 4px
 
-						> .media
-							> img
-								display block
-								max-width 100%
-
 						> mk-poll
 							font-size 80%
 

From 2ab7a5acc18d99bd55075596db1fb5092f90ad35 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 10 Dec 2017 21:57:17 +0900
Subject: [PATCH 0007/1250] :v:

---
 src/web/app/desktop/tags/images-viewer.tag | 79 ++++++++++++++++------
 1 file changed, 58 insertions(+), 21 deletions(-)

diff --git a/src/web/app/desktop/tags/images-viewer.tag b/src/web/app/desktop/tags/images-viewer.tag
index 0369ea9f6..1ad382dda 100644
--- a/src/web/app/desktop/tags/images-viewer.tag
+++ b/src/web/app/desktop/tags/images-viewer.tag
@@ -1,17 +1,71 @@
 <mk-images-viewer>
 	<virtual each={ image in images }>
-		<mk-images-viewer-image ref="wrap" image={ image } images={ images }/>
+		<mk-images-viewer-image image={ image } images={ images }/>
 	</virtual>
 	<style>
 		:scope
 			display grid
+			grid-gap .25em
+	</style>
+	<script>
+		this.images = this.opts.images;
+
+		this.on('mount', () => {
+			if (this.images.length == 1) {
+				this.root.style.gridTemplateRows = '256px';
+
+				this.tags['mk-images-viewer-image'].root.style.gridColumn = '1 / 2';
+				this.tags['mk-images-viewer-image'].root.style.gridRow = '1 / 2';
+			} else if (this.images.length == 2) {
+				this.root.style.gridTemplateColumns = '50% 50%';
+				this.root.style.gridTemplateRows = '256px';
+
+				this.tags['mk-images-viewer-image'][0].root.style.gridColumn = '1 / 2';
+				this.tags['mk-images-viewer-image'][0].root.style.gridRow = '1 / 2';
+				this.tags['mk-images-viewer-image'][1].root.style.gridColumn = '2 / 3';
+				this.tags['mk-images-viewer-image'][1].root.style.gridRow = '1 / 2';
+			} else if (this.images.length == 3) {
+				this.root.style.gridTemplateColumns = '70% 30%';
+				this.root.style.gridTemplateRows = '128px 128px';
+
+				this.tags['mk-images-viewer-image'][0].root.style.gridColumn = '1 / 2';
+				this.tags['mk-images-viewer-image'][0].root.style.gridRow = '1 / 3';
+				this.tags['mk-images-viewer-image'][1].root.style.gridColumn = '2 / 3';
+				this.tags['mk-images-viewer-image'][1].root.style.gridRow = '1 / 2';
+				this.tags['mk-images-viewer-image'][2].root.style.gridColumn = '2 / 3';
+				this.tags['mk-images-viewer-image'][2].root.style.gridRow = '2 / 3';
+			} else if (this.images.length == 4) {
+				this.root.style.gridTemplateColumns = '50% 50%';
+				this.root.style.gridTemplateRows = '128px 128px';
+
+				this.tags['mk-images-viewer-image'][0].root.style.gridColumn = '1 / 2';
+				this.tags['mk-images-viewer-image'][0].root.style.gridRow = '1 / 2';
+				this.tags['mk-images-viewer-image'][1].root.style.gridColumn = '2 / 3';
+				this.tags['mk-images-viewer-image'][1].root.style.gridRow = '1 / 2';
+				this.tags['mk-images-viewer-image'][2].root.style.gridColumn = '1 / 2';
+				this.tags['mk-images-viewer-image'][2].root.style.gridRow = '2 / 3';
+				this.tags['mk-images-viewer-image'][3].root.style.gridColumn = '2 / 3';
+				this.tags['mk-images-viewer-image'][3].root.style.gridRow = '2 / 3';
+			}
+		});
+	</script>
+</mk-images-viewer>
+
+<mk-images-viewer-image>
+	<div ref="view" onmousemove={ mousemove } onmouseleave={ mouseleave } style={ 'background-image: url(' + image.url + '?thumbnail&size=512' } onclick={ click }>
+		<img ref="image" src={ image.url + '?thumbnail&size=512' } alt={ image.name } title={ image.name }/>
+	</div>
+	<style>
+		:scope
+			display block
 			overflow hidden
 			border-radius 4px
-			grid-gap .25em
 
 			> div
 				cursor zoom-in
 				overflow hidden
+				width 100%
+				height 100%
 				background-position center
 
 				> img
@@ -23,24 +77,7 @@
 				&:not(:hover)
 					background-size cover
 
-				&:nth-child(1):nth-last-child(3)
-					grid-row 1 / 3
 	</style>
-	<script>
-		this.images = this.opts.images;
-
-		this.on('mount', () => {
-			if(this.images.length >= 3) this.refs.wrap.style.gridAutoRows = "9em";
-			if(this.images.length == 2) this.refs.wrap.style.gridAutoRows = "12em";
-			if(this.images.length == 1) this.refs.wrap.style.gridAutoRows = "256px";
-			if(this.images.length == 4 || this.images.length == 2) this.refs.wrap.style.gridTemplateColumns = "repeat(2, 1fr)";
-			if(this.images.length == 3) this.refs.wrap.style.gridTemplateColumns = "65% 1fr";
-		})
-	</script>
-</mk-images-viewer>
-
-<mk-images-viewer-image>
-	<div ref="view" onmousemove={ mousemove } onmouseleave={ mouseleave } style={ 'background-image: url(' + image.url + '?thumbnail&size=512' } onclick={ click }><img ref="image" src={ image.url + '?thumbnail&size=512' } alt={ image.name } title={ image.name }/></div>
 	<script>
 		this.mousemove = e => {
 			const rect = this.refs.view.getBoundingClientRect();
@@ -54,7 +91,7 @@
 
 		this.mouseleave = () => {
 			this.refs.view.style.backgroundPosition = "";
-		}
+		};
 
 		this.click = () => {
 			riot.mount(document.body.appendChild(document.createElement('mk-image-dialog')), {
@@ -64,4 +101,4 @@
 
 		this.image = this.opts.image;
 	</script>
-</mk-images-viewer-image>
\ No newline at end of file
+</mk-images-viewer-image>

From 4e7a9c75e96b5c6e2ac397f606cae0e17b7acdf3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 10 Dec 2017 22:32:09 +0900
Subject: [PATCH 0008/1250] :art:

---
 src/web/app/desktop/tags/images-viewer.tag    | 104 ------------------
 src/web/app/desktop/tags/images.tag           | 100 +++++++++++++++++
 src/web/app/desktop/tags/index.ts             |   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/sub-post-content.tag |   2 +-
 src/web/app/desktop/tags/timeline.tag         |   2 +-
 src/web/app/mobile/tags/images-viewer.tag     |  26 -----
 src/web/app/mobile/tags/images.tag            |  78 +++++++++++++
 src/web/app/mobile/tags/index.ts              |   2 +-
 src/web/app/mobile/tags/post-detail.tag       |   2 +-
 src/web/app/mobile/tags/sub-post-content.tag  |   2 +-
 src/web/app/mobile/tags/timeline.tag          |   2 +-
 13 files changed, 187 insertions(+), 139 deletions(-)
 delete mode 100644 src/web/app/desktop/tags/images-viewer.tag
 create mode 100644 src/web/app/desktop/tags/images.tag
 delete mode 100644 src/web/app/mobile/tags/images-viewer.tag
 create mode 100644 src/web/app/mobile/tags/images.tag

diff --git a/src/web/app/desktop/tags/images-viewer.tag b/src/web/app/desktop/tags/images-viewer.tag
deleted file mode 100644
index 1ad382dda..000000000
--- a/src/web/app/desktop/tags/images-viewer.tag
+++ /dev/null
@@ -1,104 +0,0 @@
-<mk-images-viewer>
-	<virtual each={ image in images }>
-		<mk-images-viewer-image image={ image } images={ images }/>
-	</virtual>
-	<style>
-		:scope
-			display grid
-			grid-gap .25em
-	</style>
-	<script>
-		this.images = this.opts.images;
-
-		this.on('mount', () => {
-			if (this.images.length == 1) {
-				this.root.style.gridTemplateRows = '256px';
-
-				this.tags['mk-images-viewer-image'].root.style.gridColumn = '1 / 2';
-				this.tags['mk-images-viewer-image'].root.style.gridRow = '1 / 2';
-			} else if (this.images.length == 2) {
-				this.root.style.gridTemplateColumns = '50% 50%';
-				this.root.style.gridTemplateRows = '256px';
-
-				this.tags['mk-images-viewer-image'][0].root.style.gridColumn = '1 / 2';
-				this.tags['mk-images-viewer-image'][0].root.style.gridRow = '1 / 2';
-				this.tags['mk-images-viewer-image'][1].root.style.gridColumn = '2 / 3';
-				this.tags['mk-images-viewer-image'][1].root.style.gridRow = '1 / 2';
-			} else if (this.images.length == 3) {
-				this.root.style.gridTemplateColumns = '70% 30%';
-				this.root.style.gridTemplateRows = '128px 128px';
-
-				this.tags['mk-images-viewer-image'][0].root.style.gridColumn = '1 / 2';
-				this.tags['mk-images-viewer-image'][0].root.style.gridRow = '1 / 3';
-				this.tags['mk-images-viewer-image'][1].root.style.gridColumn = '2 / 3';
-				this.tags['mk-images-viewer-image'][1].root.style.gridRow = '1 / 2';
-				this.tags['mk-images-viewer-image'][2].root.style.gridColumn = '2 / 3';
-				this.tags['mk-images-viewer-image'][2].root.style.gridRow = '2 / 3';
-			} else if (this.images.length == 4) {
-				this.root.style.gridTemplateColumns = '50% 50%';
-				this.root.style.gridTemplateRows = '128px 128px';
-
-				this.tags['mk-images-viewer-image'][0].root.style.gridColumn = '1 / 2';
-				this.tags['mk-images-viewer-image'][0].root.style.gridRow = '1 / 2';
-				this.tags['mk-images-viewer-image'][1].root.style.gridColumn = '2 / 3';
-				this.tags['mk-images-viewer-image'][1].root.style.gridRow = '1 / 2';
-				this.tags['mk-images-viewer-image'][2].root.style.gridColumn = '1 / 2';
-				this.tags['mk-images-viewer-image'][2].root.style.gridRow = '2 / 3';
-				this.tags['mk-images-viewer-image'][3].root.style.gridColumn = '2 / 3';
-				this.tags['mk-images-viewer-image'][3].root.style.gridRow = '2 / 3';
-			}
-		});
-	</script>
-</mk-images-viewer>
-
-<mk-images-viewer-image>
-	<div ref="view" onmousemove={ mousemove } onmouseleave={ mouseleave } style={ 'background-image: url(' + image.url + '?thumbnail&size=512' } onclick={ click }>
-		<img ref="image" src={ image.url + '?thumbnail&size=512' } alt={ image.name } title={ image.name }/>
-	</div>
-	<style>
-		:scope
-			display block
-			overflow hidden
-			border-radius 4px
-
-			> div
-				cursor zoom-in
-				overflow hidden
-				width 100%
-				height 100%
-				background-position center
-
-				> img
-					display block
-					visibility hidden
-					max-width: 100%
-					max-height: 256px
-
-				&:not(:hover)
-					background-size cover
-
-	</style>
-	<script>
-		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 = () => {
-			riot.mount(document.body.appendChild(document.createElement('mk-image-dialog')), {
-				image: this.image
-			});
-		};
-
-		this.image = this.opts.image;
-	</script>
-</mk-images-viewer-image>
diff --git a/src/web/app/desktop/tags/images.tag b/src/web/app/desktop/tags/images.tag
new file mode 100644
index 000000000..ce67d26a9
--- /dev/null
+++ b/src/web/app/desktop/tags/images.tag
@@ -0,0 +1,100 @@
+<mk-images>
+	<virtual each={ image in images }>
+		<mk-images-image image={ image }/>
+	</virtual>
+	<style>
+		:scope
+			display grid
+			grid-gap 4px
+			height 256px
+	</style>
+	<script>
+		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={ 'background-image: url(' + image.url + '?thumbnail&size=512' } onclick={ click } title={ image.name }></a>
+	<style>
+		: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>
+		this.image = this.opts.image;
+
+		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>
diff --git a/src/web/app/desktop/tags/index.ts b/src/web/app/desktop/tags/index.ts
index 3ec1d108a..30a13b584 100644
--- a/src/web/app/desktop/tags/index.ts
+++ b/src/web/app/desktop/tags/index.ts
@@ -76,7 +76,7 @@ require('./set-avatar-suggestion.tag');
 require('./set-banner-suggestion.tag');
 require('./repost-form.tag');
 require('./sub-post-content.tag');
-require('./images-viewer.tag');
+require('./images.tag');
 require('./image-dialog.tag');
 require('./donation.tag');
 require('./users-list.tag');
diff --git a/src/web/app/desktop/tags/post-detail-sub.tag b/src/web/app/desktop/tags/post-detail-sub.tag
index ab45b5523..cccd85c47 100644
--- a/src/web/app/desktop/tags/post-detail-sub.tag
+++ b/src/web/app/desktop/tags/post-detail-sub.tag
@@ -17,7 +17,7 @@
 		<div class="body">
 			<div class="text" ref="text"></div>
 			<div class="media" if={ post.media }>
-				<mk-images-viewer images={ post.media }/>
+				<mk-images images={ post.media }/>
 			</div>
 		</div>
 	</div>
diff --git a/src/web/app/desktop/tags/post-detail.tag b/src/web/app/desktop/tags/post-detail.tag
index 23f7a4198..47c71a6c1 100644
--- a/src/web/app/desktop/tags/post-detail.tag
+++ b/src/web/app/desktop/tags/post-detail.tag
@@ -37,7 +37,7 @@
 			<div class="body">
 				<div class="text" ref="text"></div>
 				<div class="media" if={ p.media }>
-					<mk-images-viewer images={ p.media }/>
+					<mk-images images={ p.media }/>
 				</div>
 				<mk-poll if={ p.poll } post={ 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 8989ff1c5..1a81b545b 100644
--- a/src/web/app/desktop/tags/sub-post-content.tag
+++ b/src/web/app/desktop/tags/sub-post-content.tag
@@ -8,7 +8,7 @@
 	</div>
 	<details if={ post.media }>
 		<summary>({ post.media.length }つのメディア)</summary>
-		<mk-images-viewer images={ post.media }/>
+		<mk-images images={ post.media }/>
 	</details>
 	<details if={ post.poll }>
 		<summary>投票</summary>
diff --git a/src/web/app/desktop/tags/timeline.tag b/src/web/app/desktop/tags/timeline.tag
index 77e4a573b..ed77a9e60 100644
--- a/src/web/app/desktop/tags/timeline.tag
+++ b/src/web/app/desktop/tags/timeline.tag
@@ -120,7 +120,7 @@
 					<a class="quote" if={ p.repost != null }>RP:</a>
 				</div>
 				<div class="media" if={ p.media }>
-					<mk-images-viewer images={ 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%
diff --git a/src/web/app/mobile/tags/images-viewer.tag b/src/web/app/mobile/tags/images-viewer.tag
deleted file mode 100644
index 8ef4a50be..000000000
--- a/src/web/app/mobile/tags/images-viewer.tag
+++ /dev/null
@@ -1,26 +0,0 @@
-<mk-images-viewer>
-	<div class="image" ref="view" onclick={ click }><img ref="img" src={ image.url + '?thumbnail&size=512' } alt={ image.name } title={ image.name }/></div>
-	<style>
-		:scope
-			display block
-			overflow hidden
-			border-radius 4px
-
-			> .image
-
-				> img
-					display block
-					max-height 256px
-					max-width 100%
-					margin 0 auto
-
-	</style>
-	<script>
-		this.images = this.opts.images;
-		this.image = this.images[0];
-
-		this.click = () => {
-			window.open(this.image.url);
-		};
-	</script>
-</mk-images-viewer>
diff --git a/src/web/app/mobile/tags/images.tag b/src/web/app/mobile/tags/images.tag
new file mode 100644
index 000000000..aaa80e4fd
--- /dev/null
+++ b/src/web/app/mobile/tags/images.tag
@@ -0,0 +1,78 @@
+<mk-images>
+	<virtual each={ image in images }>
+		<mk-images-image image={ image }/>
+	</virtual>
+	<style>
+		:scope
+			display grid
+			grid-gap 4px
+			height 256px
+
+			@media (max-width 500px)
+				height 192px
+	</style>
+	<script>
+		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={ 'background-image: url(' + image.url + '?thumbnail&size=512' } title={ image.name }></a>
+	<style>
+		: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>
+		this.image = this.opts.image;
+	</script>
+</mk-images-image>
diff --git a/src/web/app/mobile/tags/index.ts b/src/web/app/mobile/tags/index.ts
index 19952c20c..fd5952ea1 100644
--- a/src/web/app/mobile/tags/index.ts
+++ b/src/web/app/mobile/tags/index.ts
@@ -25,7 +25,7 @@ require('./home-timeline.tag');
 require('./timeline.tag');
 require('./post-preview.tag');
 require('./sub-post-content.tag');
-require('./images-viewer.tag');
+require('./images.tag');
 require('./drive.tag');
 require('./drive-selector.tag');
 require('./drive-folder-selector.tag');
diff --git a/src/web/app/mobile/tags/post-detail.tag b/src/web/app/mobile/tags/post-detail.tag
index 9f212a249..1816d1bf9 100644
--- a/src/web/app/mobile/tags/post-detail.tag
+++ b/src/web/app/mobile/tags/post-detail.tag
@@ -34,7 +34,7 @@
 		<div class="body">
 			<div class="text" ref="text"></div>
 			<div class="media" if={ p.media }>
-				<virtual each={ file in p.media }><img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/></virtual>
+				<mk-images images={ p.media }/>
 			</div>
 			<mk-poll if={ p.poll } post={ p }/>
 		</div>
diff --git a/src/web/app/mobile/tags/sub-post-content.tag b/src/web/app/mobile/tags/sub-post-content.tag
index 9436b6c1d..adeb84dea 100644
--- a/src/web/app/mobile/tags/sub-post-content.tag
+++ b/src/web/app/mobile/tags/sub-post-content.tag
@@ -2,7 +2,7 @@
 	<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 }>
 		<summary>({ post.media.length }個のメディア)</summary>
-		<mk-images-viewer images={ post.media }/>
+		<mk-images images={ post.media }/>
 	</details>
 	<details if={ post.poll }>
 		<summary>%i18n:mobile.tags.mk-sub-post-content.poll%</summary>
diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag
index 19f90a1c1..9e85f97da 100644
--- a/src/web/app/mobile/tags/timeline.tag
+++ b/src/web/app/mobile/tags/timeline.tag
@@ -172,7 +172,7 @@
 					<a class="quote" if={ p.repost != null }>RP:</a>
 				</div>
 				<div class="media" if={ p.media }>
-					<mk-images-viewer images={ 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>

From 4732d1365be1ff532bd7491f884d7d27d32d6cba Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 10 Dec 2017 22:34:06 +0900
Subject: [PATCH 0009/1250] v3334

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1274b0fe9..39fc02528 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3334 (2017/12/10)
+-----------------
+* いい感じにした
+
 3322 (2017/12/10)
 -----------------
 * :art:
diff --git a/package.json b/package.json
index 369113ba6..2b55615cf 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3322",
+	"version": "0.0.3334",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 34c5096140bb64b63b44f5ef96b8ce2be43f835d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 02:54:34 +0900
Subject: [PATCH 0010/1250] :v:

---
 locales/en.yml                                |  7 ++--
 locales/ja.yml                                |  9 +++--
 src/web/app/desktop/tags/settings.tag         | 16 +++++----
 src/web/app/mobile/router.ts                  |  5 ---
 src/web/app/mobile/tags/index.ts              |  1 -
 src/web/app/mobile/tags/page/settings.tag     |  1 -
 src/web/app/mobile/tags/page/settings/api.tag | 36 -------------------
 7 files changed, 21 insertions(+), 54 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/page/settings/api.tag

diff --git a/locales/en.yml b/locales/en.yml
index 8392e170c..8e1dee826 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -199,7 +199,11 @@ ch:
 desktop:
   tags:
     mk-api-info:
-      regenerate-token: "Please enter the password"
+      intro: "APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。"
+      caution: "アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。"
+      regeneration-of-token: "万が一このトークンが漏れたりその可能性がある場合はトークンを再生成できます。"
+      regenerate-token: "Regenerate the token"
+      enter-password: "Please enter the password"
 
     mk-drive-browser-base-contextmenu:
       create-folder: "Create a folder"
@@ -524,7 +528,6 @@ mobile:
       applications: "Applications"
       twitter-integration: "Twitter integration"
       signin-history: "Sign in history"
-      api: "API"
       link: "MisskeyLink"
       settings: "Settings"
       signout: "Sign out"
diff --git a/locales/ja.yml b/locales/ja.yml
index f9d41d909..1497bdb6d 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -199,7 +199,11 @@ ch:
 desktop:
   tags:
     mk-api-info:
-      regenerate-token: "パスワードを入力してください"
+      intro: "APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。"
+      caution: "アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。"
+      regeneration-of-token: "万が一このトークンが漏れたりその可能性がある場合はトークンを再生成できます。"
+      regenerate-token: "トークンを再生成"
+      enter-password: "パスワードを入力してください"
 
     mk-drive-browser-base-contextmenu:
       create-folder: "フォルダーを作成"
@@ -523,8 +527,7 @@ mobile:
       profile: "プロフィール"
       applications: "アプリケーション"
       twitter-integration: "Twitter連携"
-      signin-history: "ログイン履歴"
-      api: "API"
+      signin-history: "サインイン履歴"
       link: "Misskeyリンク"
       settings: "設定"
       signout: "サインアウト"
diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag
index f7ecfe3e8..0a9a16250 100644
--- a/src/web/app/desktop/tags/settings.tag
+++ b/src/web/app/desktop/tags/settings.tag
@@ -196,18 +196,22 @@
 </mk-profile-setting>
 
 <mk-api-info>
-	<p>Token:<code>{ I.token }</code></p>
-	<p>APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。</p>
-	<p>アカウントを乗っ取られてしまう可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。</p>
-	<p>万が一このトークンが漏れたりその可能性がある場合は<a class="regenerate" onclick={ regenerateToken }>トークンを再生成</a>できます。(副作用として、ログインしているすべてのデバイスでログアウトが発生します)</p>
+	<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" onclick={ regenerateToken }>%i18n:desktop.tags.mk-api-info.regenerate-token%</button>
 	<style>
 		:scope
 			display block
 			color #4a535a
 
 			code
-				padding 4px
+				display inline-block
+				padding 4px 6px
+				color #555
 				background #eee
+				border-radius 2px
 	</style>
 	<script>
 		import passwordDialog from '../scripts/password-dialog';
@@ -216,7 +220,7 @@
 		this.mixin('api');
 
 		this.regenerateToken = () => {
-			passwordDialog('%i18n:desktop.tags.mk-api-info.regenerate-token%', password => {
+			passwordDialog('%i18n:desktop.tags.mk-api-info.enter-password%', password => {
 				this.api('i/regenerate_token', {
 					password: password
 				});
diff --git a/src/web/app/mobile/router.ts b/src/web/app/mobile/router.ts
index 0358d10e9..d0c6add0b 100644
--- a/src/web/app/mobile/router.ts
+++ b/src/web/app/mobile/router.ts
@@ -19,7 +19,6 @@ export default (mios: MiOS) => {
 	route('/i/settings',                 settings);
 	route('/i/settings/profile',         settingsProfile);
 	route('/i/settings/signin-history',  settingsSignin);
-	route('/i/settings/api',             settingsApi);
 	route('/i/settings/twitter',         settingsTwitter);
 	route('/i/settings/authorized-apps', settingsAuthorizedApps);
 	route('/post/new',                   newPost);
@@ -74,10 +73,6 @@ export default (mios: MiOS) => {
 		mount(document.createElement('mk-signin-history-page'));
 	}
 
-	function settingsApi() {
-		mount(document.createElement('mk-api-info-page'));
-	}
-
 	function settingsTwitter() {
 		mount(document.createElement('mk-twitter-setting-page'));
 	}
diff --git a/src/web/app/mobile/tags/index.ts b/src/web/app/mobile/tags/index.ts
index fd5952ea1..20934cdd8 100644
--- a/src/web/app/mobile/tags/index.ts
+++ b/src/web/app/mobile/tags/index.ts
@@ -14,7 +14,6 @@ require('./page/search.tag');
 require('./page/settings.tag');
 require('./page/settings/profile.tag');
 require('./page/settings/signin.tag');
-require('./page/settings/api.tag');
 require('./page/settings/authorized-apps.tag');
 require('./page/settings/twitter.tag');
 require('./page/messaging.tag');
diff --git a/src/web/app/mobile/tags/page/settings.tag b/src/web/app/mobile/tags/page/settings.tag
index 978978214..9a73b0af3 100644
--- a/src/web/app/mobile/tags/page/settings.tag
+++ b/src/web/app/mobile/tags/page/settings.tag
@@ -24,7 +24,6 @@
 		<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>
-		<li><a href="./settings/api">%fa:key%%i18n:mobile.tags.mk-settings-page.api%%fa:angle-right%</a></li>
 	</ul>
 	<ul>
 		<li><a onclick={ signout }>%fa:power-off%%i18n:mobile.tags.mk-settings-page.signout%</a></li>
diff --git a/src/web/app/mobile/tags/page/settings/api.tag b/src/web/app/mobile/tags/page/settings/api.tag
deleted file mode 100644
index 8de0e9696..000000000
--- a/src/web/app/mobile/tags/page/settings/api.tag
+++ /dev/null
@@ -1,36 +0,0 @@
-<mk-api-info-page>
-	<mk-ui ref="ui">
-		<mk-api-info/>
-	</mk-ui>
-	<style>
-		:scope
-			display block
-	</style>
-	<script>
-		import ui from '../../../scripts/ui-event';
-
-		this.on('mount', () => {
-			document.title = 'Misskey | API';
-			ui.trigger('title', '%fa:key%API');
-		});
-	</script>
-</mk-api-info-page>
-
-<mk-api-info>
-	<p>Token:<code>{ I.token }</code></p>
-	<p>APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。</p>
-	<p>アカウントを乗っ取られてしまう可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。</p>
-	<p>万が一このトークンが漏れたりその可能性がある場合はデスクトップ版Misskeyから再生成できます。</p>
-	<style>
-		:scope
-			display block
-			color #4a535a
-
-			code
-				padding 4px
-				background #eee
-	</style>
-	<script>
-		this.mixin('i');
-	</script>
-</mk-api-info>

From 4b3477d3a2dbd115cb5eaad649a3ed8d85cba166 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 02:59:05 +0900
Subject: [PATCH 0011/1250] #984

---
 src/file/server.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/file/server.ts b/src/file/server.ts
index 990b6ccbe..3bda5b14f 100644
--- a/src/file/server.ts
+++ b/src/file/server.ts
@@ -82,6 +82,7 @@ function thumbnail(data: stream.Readable, type: string, resize: number): ISend {
 	const stream = g
 		.compress('jpeg')
 		.quality(80)
+		.interlace('line')
 		.noProfile() // Remove EXIF
 		.stream();
 

From a32da4ca726908c50652d9e8a4119d1e2f66768d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 03:25:58 +0900
Subject: [PATCH 0012/1250] #983

---
 locales/en.yml                         |  1 -
 locales/ja.yml                         |  1 -
 src/web/app/desktop/tags/post-form.tag | 69 ++++++++++-------------
 src/web/app/mobile/tags/post-form.tag  | 76 ++++++++++----------------
 4 files changed, 60 insertions(+), 87 deletions(-)

diff --git a/locales/en.yml b/locales/en.yml
index 8e1dee826..9ac9a36cd 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -591,7 +591,6 @@ mobile:
       submit: "Post"
       reply-placeholder: "Reply to this post..."
       post-placeholder: "What's happening?"
-      attach-media-from-local: "Attach media from your device"
 
     mk-search-posts:
       empty: "There is no post related to the 「{}」"
diff --git a/locales/ja.yml b/locales/ja.yml
index 1497bdb6d..2f95998a8 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -591,7 +591,6 @@ mobile:
       submit: "投稿"
       reply-placeholder: "この投稿への返信..."
       post-placeholder: "いまどうしてる?"
-      attach-media-from-local: "デバイスからメディアを添付"
 
     mk-search-posts:
       empty: "「{}」に関する投稿は見つかりませんでした。"
diff --git a/src/web/app/desktop/tags/post-form.tag b/src/web/app/desktop/tags/post-form.tag
index 8e5171c83..0b4c07906 100644
--- a/src/web/app/desktop/tags/post-form.tag
+++ b/src/web/app/desktop/tags/post-form.tag
@@ -1,13 +1,12 @@
 <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 }" if={ files.length != 0 }>
-			<ul>
-				<li each={ files }>
+		<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" onclick={ removeFile } src="/assets/desktop/remove.png" title="%i18n:desktop.tags.mk-post-form.attach-cancel%" alt=""/>
 				</li>
-				<li class="add" if={ files.length < 4 } title="%i18n:desktop.tags.mk-post-form.attach-media-from-local%" onclick={ selectFile }>%fa:plus%</li>
 			</ul>
 			<p class="remain">{ 4 - files.length }/4</p>
 		</div>
@@ -118,8 +117,9 @@
 						> li
 							display block
 							float left
-							margin 4px
+							margin 0
 							padding 0
+							border solid 4px transparent
 							cursor move
 
 							&:hover > .remove
@@ -140,29 +140,6 @@
 								height 16px
 								cursor pointer
 
-						> .add
-							display block
-							float left
-							margin 4px
-							padding 0
-							border dashed 2px rgba($theme-color, 0.2)
-							cursor pointer
-
-							&:hover
-								border-color rgba($theme-color, 0.3)
-
-								> i
-									color rgba($theme-color, 0.4)
-
-							> i
-								display block
-								width 60px
-								height 60px
-								line-height 60px
-								text-align center
-								font-size 1.2em
-								color rgba($theme-color, 0.2)
-
 				> mk-poll-editor
 					background lighten($theme-color, 98%)
 					border solid 1px rgba($theme-color, 0.1)
@@ -306,6 +283,7 @@
 
 	</style>
 	<script>
+		import Sortable from 'sortablejs';
 		import getKao from '../../common/scripts/get-kao';
 		import notify from '../scripts/notify';
 		import Autocomplete from '../scripts/autocomplete';
@@ -365,6 +343,10 @@
 				this.trigger('change-files', this.files);
 				this.update();
 			}
+
+			new Sortable(this.refs.media, {
+				animation: 150
+			});
 		});
 
 		this.on('unmount', () => {
@@ -413,14 +395,17 @@
 			const data = e.dataTransfer.getData('text');
 			if (data == null) return false;
 
-			// パース
-			// TODO: Validate JSON
-			const obj = JSON.parse(data);
+			try {
+				// パース
+				const obj = JSON.parse(data);
+
+				// (ドライブの)ファイルだったら
+				if (obj.type == 'file') {
+					this.files.push(obj.file);
+					this.update();
+				}
+			} catch (e) {
 
-			// (ドライブの)ファイルだったら
-			if (obj.type == 'file') {
-				this.files.push(obj.file);
-				this.update();
 			}
 		};
 
@@ -483,13 +468,19 @@
 		this.post = e => {
 			this.wait = true;
 
-			const files = this.files && this.files.length > 0
-				? this.files.map(f => f.id)
-				: undefined;
+			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: files,
+				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
diff --git a/src/web/app/mobile/tags/post-form.tag b/src/web/app/mobile/tags/post-form.tag
index 3ac7296f7..f09f40bb5 100644
--- a/src/web/app/mobile/tags/post-form.tag
+++ b/src/web/app/mobile/tags/post-form.tag
@@ -9,12 +9,11 @@
 	<div class="form">
 		<mk-post-preview 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" if={ files.length != 0 }>
+		<div class="attaches" show={ files.length != 0 }>
 			<ul class="files" ref="attaches">
-				<li class="file" each={ files }>
-					<div class="img" style="background-image: url({ url + '?thumbnail&size=64' })" title={ name }></div>
+				<li class="file" each={ files } data-id={ id }>
+					<div class="img" style="background-image: url({ url + '?thumbnail&size=64' })" title={ name } onclick={ removeFile }></div>
 				</li>
-				<li class="add" if={ files.length < 4 } title="%i18n:mobile.tags.mk-post-form.attach-media-from-local%" onclick={ selectFile }>%fa:plus%</li>
 			</ul>
 		</div>
 		<mk-poll-editor if={ poll } ref="poll" ondestroy={ onPollDestroyed }/>
@@ -93,12 +92,9 @@
 						> .file
 							display block
 							float left
-							margin 4px
+							margin 0
 							padding 0
-							cursor move
-
-							&:hover > .remove
-								display block
+							border solid 4px transparent
 
 							> .img
 								width 64px
@@ -106,38 +102,6 @@
 								background-size cover
 								background-position center center
 
-							> .remove
-								display none
-								position absolute
-								top -6px
-								right -6px
-								width 16px
-								height 16px
-								cursor pointer
-
-						> .add
-							display block
-							float left
-							margin 4px
-							padding 0
-							border dashed 2px rgba($theme-color, 0.2)
-							cursor pointer
-
-							&:hover
-								border-color rgba($theme-color, 0.3)
-
-								> [data-fa]
-									color rgba($theme-color, 0.4)
-
-							> [data-fa]
-								display block
-								width 60px
-								height 60px
-								line-height 60px
-								text-align center
-								font-size 1.2em
-								color rgba($theme-color, 0.2)
-
 				> mk-uploader
 					margin 8px 0 0 0
 					padding 8px
@@ -181,6 +145,7 @@
 
 	</style>
 	<script>
+		import Sortable from 'sortablejs';
 		import getKao from '../../common/scripts/get-kao';
 
 		this.mixin('api');
@@ -200,6 +165,10 @@
 			});
 
 			this.refs.text.focus();
+
+			new Sortable(this.refs.attaches, {
+				animation: 150
+			});
 		});
 
 		this.onkeydown = e => {
@@ -247,6 +216,13 @@
 			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.update();
+		};
+
 		this.addPoll = () => {
 			this.poll = true;
 		};
@@ -258,15 +234,23 @@
 		};
 
 		this.post = () => {
-			this.wait = true;
+			this.update({
+				wait: true
+			});
 
-			const files = this.files && this.files.length > 0
-				? this.files.map(f => f.id)
-				: undefined;
+			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: files,
+				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 => {

From 98c6dfde50876fc125a0e823107b8732bfc4c51f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 03:38:31 +0900
Subject: [PATCH 0013/1250] :v:

---
 locales/en.yml                        |  2 ++
 locales/ja.yml                        |  2 ++
 src/web/app/desktop/tags/settings.tag | 27 ++++++++-------------------
 webpack/module/rules/index.ts         |  2 ++
 webpack/module/rules/license.ts       | 22 ++++++++++++++++++++++
 5 files changed, 36 insertions(+), 19 deletions(-)
 create mode 100644 webpack/module/rules/license.ts

diff --git a/locales/en.yml b/locales/en.yml
index 9ac9a36cd..b49af68bd 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -365,6 +365,8 @@ desktop:
       security: "Security"
       password: "Password"
       2fa: "Two-factor authentication"
+      other: "Other"
+      license: "License"
 
     mk-timeline-post:
       reposted-by: "Reposted by {}"
diff --git a/locales/ja.yml b/locales/ja.yml
index 2f95998a8..afafa5a63 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -365,6 +365,8 @@ desktop:
       security: "セキュリティ"
       password: "パスワード"
       2fa: "二段階認証"
+      other: "その他"
+      license: "ライセンス"
 
     mk-timeline-post:
       reposted-by: "{}がRepost"
diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag
index 0a9a16250..2f36d9b3e 100644
--- a/src/web/app/desktop/tags/settings.tag
+++ b/src/web/app/desktop/tags/settings.tag
@@ -8,6 +8,7 @@
 		<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' }>
@@ -54,6 +55,11 @@
 			<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>
 		:scope
@@ -96,8 +102,9 @@
 
 				> section
 					margin 32px
+					color #4a535a
 
-					h1
+					> h1
 						display block
 						margin 0 0 1em 0
 						padding 0 0 8px 0
@@ -105,24 +112,6 @@
 						color #555
 						border-bottom solid 1px #eee
 
-					label.checkbox
-						> input
-							position absolute
-							top 0
-							left 0
-
-							&:checked + p
-								color $theme-color
-
-						> p
-							width calc(100% - 32px)
-							margin 0 0 0 32px
-							font-weight bold
-
-							&:last-child
-								font-weight normal
-								color #999
-
 	</style>
 	<script>
 		this.page = 'profile';
diff --git a/webpack/module/rules/index.ts b/webpack/module/rules/index.ts
index 79740ce48..b6a0a5e2e 100644
--- a/webpack/module/rules/index.ts
+++ b/webpack/module/rules/index.ts
@@ -1,4 +1,5 @@
 import i18n from './i18n';
+import license from './license';
 import fa from './fa';
 import base64 from './base64';
 import themeColor from './theme-color';
@@ -8,6 +9,7 @@ import typescript from './typescript';
 
 export default (lang, locale) => [
 	i18n(lang, locale),
+	license(),
 	fa(),
 	base64(),
 	themeColor(),
diff --git a/webpack/module/rules/license.ts b/webpack/module/rules/license.ts
new file mode 100644
index 000000000..1795af960
--- /dev/null
+++ b/webpack/module/rules/license.ts
@@ -0,0 +1,22 @@
+/**
+ * Inject license
+ */
+
+import * as fs from 'fs';
+const StringReplacePlugin = require('string-replace-webpack-plugin');
+
+const license = fs.readFileSync(__dirname + '/../../../LICENSE', 'utf-8')
+	.replace(/\r\n/g, '\n')
+	.replace(/(.)\n(.)/g, '$1 $2')
+	.replace(/(^|\n)(.*?)($|\n)/g, '<p>$2</p>');
+
+export default () => ({
+	enforce: 'pre',
+	test: /\.(tag|js)$/,
+	exclude: /node_modules/,
+	loader: StringReplacePlugin.replace({
+		replacements: [{
+			pattern: '%license%', replacement: () => license
+		}]
+	})
+});

From 230fd540c94ecdc33b742ae42ad8b316ce21a60f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 03:39:17 +0900
Subject: [PATCH 0014/1250] v3339

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 39fc02528..d25107389 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3339 (2017/12/11)
+-----------------
+* なんか
+
 3334 (2017/12/10)
 -----------------
 * いい感じにした
diff --git a/package.json b/package.json
index 2b55615cf..f03446531 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3334",
+	"version": "0.0.3339",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 3341e574b91021aec827d74a73d12e9c7e6b6058 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 12:20:36 +0900
Subject: [PATCH 0015/1250] Fix bug

---
 gulpfile.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/gulpfile.ts b/gulpfile.ts
index 641500bbe..ee11a02dc 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -32,6 +32,7 @@ fontawesome.library.add(solid);
 fontawesome.library.add(brands);
 
 import version from './src/version';
+import config from './src/conf';
 
 const uglify = uglifyComposer(uglifyes, console);
 
@@ -142,6 +143,7 @@ gulp.task('webpack', done => {
 gulp.task('build:client:script', () =>
 	gulp.src(['./src/web/app/boot.js', './src/web/app/safe.js'])
 		.pipe(replace('VERSION', JSON.stringify(version)))
+		.pipe(replace('API', JSON.stringify(config.api_url)))
 		.pipe(isProduction ? uglify({
 			toplevel: true
 		} as any) : gutil.noop())

From 260c57ac61fe3b3d126cfcd99a54fd3b47fc1507 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 13:33:33 +0900
Subject: [PATCH 0016/1250] #986

---
 src/api/common/add-file-to-drive.ts           | 39 ++++++++++-
 src/api/endpoints/drive/files/create.ts       | 14 ++--
 src/web/app/desktop/tags/drive/file.tag       | 16 ++++-
 src/web/app/desktop/tags/images.tag           | 13 +++-
 src/web/app/mobile/tags/drive/file-viewer.tag |  7 +-
 src/web/app/mobile/tags/drive/file.tag        |  6 +-
 src/web/app/mobile/tags/images.tag            |  6 +-
 tools/migration/node.2017-12-11.js            | 67 +++++++++++++++++++
 8 files changed, 157 insertions(+), 11 deletions(-)
 create mode 100644 tools/migration/node.2017-12-11.js

diff --git a/src/api/common/add-file-to-drive.ts b/src/api/common/add-file-to-drive.ts
index 109e88610..427b54d72 100644
--- a/src/api/common/add-file-to-drive.ts
+++ b/src/api/common/add-file-to-drive.ts
@@ -110,7 +110,7 @@ const addFile = async (
 		}
 	}
 
-	const [wh, folder] = await Promise.all([
+	const [wh, averageColor, folder] = await Promise.all([
 		// Width and height (when image)
 		(async () => {
 			// 画像かどうか
@@ -125,14 +125,45 @@ const addFile = async (
 				return null;
 			}
 
+			log('calculate image width and height...');
+
 			// Calculate width and height
 			const g = gm(fs.createReadStream(path), name);
 			const size = await prominence(g).size();
 
-			log('image width and height is calculated');
+			log(`image width and height is calculated: ${size.width}, ${size.height}`);
 
 			return [size.width, size.height];
 		})(),
+		// average color (when image)
+		(async () => {
+			// 画像かどうか
+			if (!/^image\/.*$/.test(mime)) {
+				return null;
+			}
+
+			const imageType = mime.split('/')[1];
+
+			// 画像でもPNGかJPEGでないならスキップ
+			if (imageType != 'png' && imageType != 'jpeg') {
+				return null;
+			}
+
+			log('calculate average color...');
+
+			const buffer = await prominence(gm(fs.createReadStream(path), name)
+				.setFormat('ppm')
+				.resize(1, 1)) // 1pxのサイズに縮小して平均色を取得するというハック
+				.toBuffer();
+
+			const r = buffer.readUInt8(buffer.length - 3);
+			const g = buffer.readUInt8(buffer.length - 2);
+			const b = buffer.readUInt8(buffer.length - 1);
+
+			log(`average color is calculated: ${r}, ${g}, ${b}`);
+
+			return [r, g, b];
+		})(),
 		// folder
 		(async () => {
 			if (!folderId) {
@@ -188,6 +219,10 @@ const addFile = async (
 		properties['height'] = wh[1];
 	}
 
+	if (averageColor) {
+		properties['average_color'] = averageColor;
+	}
+
 	return addToGridFS(detectedName, readable, mime, {
 		user_id: user._id,
 		folder_id: folder !== null ? folder._id : null,
diff --git a/src/api/endpoints/drive/files/create.ts b/src/api/endpoints/drive/files/create.ts
index 7546eca30..437348a1e 100644
--- a/src/api/endpoints/drive/files/create.ts
+++ b/src/api/endpoints/drive/files/create.ts
@@ -38,9 +38,15 @@ module.exports = async (file, params, user): Promise<any> => {
 	const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
 	if (folderIdErr) throw 'invalid folder_id param';
 
-	// Create file
-	const driveFile = await create(user, file.path, name, null, folderId);
+	try {
+		// Create file
+		const driveFile = await create(user, file.path, name, null, folderId);
 
-	// Serialize
-	return serialize(driveFile);
+		// Serialize
+		return serialize(driveFile);
+	} catch (e) {
+		console.error(e);
+
+		throw e;
+	}
 };
diff --git a/src/web/app/desktop/tags/drive/file.tag b/src/web/app/desktop/tags/drive/file.tag
index 0f019d95b..8b3d36b3f 100644
--- a/src/web/app/desktop/tags/drive/file.tag
+++ b/src/web/app/desktop/tags/drive/file.tag
@@ -5,7 +5,9 @@
 	<div class="label" 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"><img src={ file.url + '?thumbnail&size=128' } alt=""/></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>
 	<style>
 		:scope
@@ -139,6 +141,7 @@
 
 	</style>
 	<script>
+		import anime from 'animejs';
 		import bytesToSize from '../../../common/scripts/bytes-to-size';
 
 		this.mixin('i');
@@ -199,5 +202,16 @@
 			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/images.tag b/src/web/app/desktop/tags/images.tag
index ce67d26a9..5e4be481d 100644
--- a/src/web/app/desktop/tags/images.tag
+++ b/src/web/app/desktop/tags/images.tag
@@ -53,7 +53,13 @@
 </mk-images>
 
 <mk-images-image>
-	<a ref="view" href={ image.url } onmousemove={ mousemove } onmouseleave={ mouseleave } style={ 'background-image: url(' + image.url + '?thumbnail&size=512' } onclick={ click } title={ image.name }></a>
+	<a ref="view"
+		href={ image.url }
+		onmousemove={ mousemove }
+		onmouseleave={ mouseleave }
+		style={ styles }
+		onclick={ click }
+		title={ image.name }></a>
 	<style>
 		:scope
 			display block
@@ -74,6 +80,11 @@
 	</style>
 	<script>
 		this.image = this.opts.image;
+		this.styles = {
+			'background-color': `rgb(${this.image.properties.average_color.join(',')})`,
+			'background-image': `url(${this.image.url}?thumbnail&size=512)`
+		};
+		console.log(this.styles);
 
 		this.mousemove = e => {
 			const rect = this.refs.view.getBoundingClientRect();
diff --git a/src/web/app/mobile/tags/drive/file-viewer.tag b/src/web/app/mobile/tags/drive/file-viewer.tag
index 48fc83fa6..259873d95 100644
--- a/src/web/app/mobile/tags/drive/file-viewer.tag
+++ b/src/web/app/mobile/tags/drive/file-viewer.tag
@@ -1,6 +1,11 @@
 <mk-drive-file-viewer>
 	<div class="preview">
-		<img if={ kind == 'image' } src={ file.url } alt={ file.name } title={ file.name } onload={ onImageLoaded } ref="img">
+		<img 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 }>
 			<span class="size">
diff --git a/src/web/app/mobile/tags/drive/file.tag b/src/web/app/mobile/tags/drive/file.tag
index 196dd1141..684df7dd0 100644
--- a/src/web/app/mobile/tags/drive/file.tag
+++ b/src/web/app/mobile/tags/drive/file.tag
@@ -1,7 +1,7 @@
 <mk-drive-file data-is-selected={ isSelected }>
 	<a onclick={ onclick } href="/i/drive/file/{ file.id }">
 		<div class="container">
-			<div class="thumbnail" style={ 'background-image: url(' + file.url + '?thumbnail&size=128)' }></div>
+			<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>
 				<!--
@@ -132,6 +132,10 @@
 
 		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 => {
diff --git a/src/web/app/mobile/tags/images.tag b/src/web/app/mobile/tags/images.tag
index aaa80e4fd..b200eefe7 100644
--- a/src/web/app/mobile/tags/images.tag
+++ b/src/web/app/mobile/tags/images.tag
@@ -56,7 +56,7 @@
 </mk-images>
 
 <mk-images-image>
-	<a ref="view" href={ image.url } target="_blank" style={ 'background-image: url(' + image.url + '?thumbnail&size=512' } title={ image.name }></a>
+	<a ref="view" href={ image.url } target="_blank" style={ styles } title={ image.name }></a>
 	<style>
 		:scope
 			display block
@@ -74,5 +74,9 @@
 	</style>
 	<script>
 		this.image = this.opts.image;
+		this.styles = {
+			'background-color': `rgb(${this.image.properties.average_color.join(',')})`,
+			'background-image': `url(${this.image.url}?thumbnail&size=512)`
+		};
 	</script>
 </mk-images-image>
diff --git a/tools/migration/node.2017-12-11.js b/tools/migration/node.2017-12-11.js
new file mode 100644
index 000000000..3a3fef051
--- /dev/null
+++ b/tools/migration/node.2017-12-11.js
@@ -0,0 +1,67 @@
+// for Node.js interpret
+
+const { default: DriveFile, getGridFSBucket } = require('../../built/api/models/drive-file')
+const { default: zip } = require('@prezzemolo/zip')
+
+const _gm = require('gm');
+const gm = _gm.subClass({
+	imageMagick: true
+});
+
+const migrate = doc => new Promise(async (res, rej) => {
+	const bucket = await getGridFSBucket();
+
+	const readable = bucket.openDownloadStream(doc._id);
+
+	gm(readable)
+		.setFormat('ppm')
+		.resize(1, 1)
+		.toBuffer(async (err, buffer) => {
+			if (err) rej(err);
+			const r = buffer.readUInt8(buffer.length - 3);
+			const g = buffer.readUInt8(buffer.length - 2);
+			const b = buffer.readUInt8(buffer.length - 1);
+
+			const result = await DriveFile.update(doc._id, {
+				$set: {
+					'metadata.properties.average_color': [r, g, b]
+				}
+			})
+
+			res(result.ok === 1);
+		});
+});
+
+async function main() {
+	const query = {
+		contentType: {
+			$in: [
+				'image/png',
+				'image/jpeg'
+			]
+		}
+	}
+
+	const count = await DriveFile.count(query);
+
+	const dop = Number.parseInt(process.argv[2]) || 5
+	const idop = ((count - (count % dop)) / dop) + 1
+
+	return zip(
+		1,
+		async (time) => {
+			console.log(`${time} / ${idop}`)
+			const doc = await DriveFile.find(query, {
+				limit: dop, skip: time * dop
+			})
+			return Promise.all(doc.map(migrate))
+		},
+		idop
+	).then(a => {
+		const rv = []
+		a.forEach(e => rv.push(...e))
+		return rv
+	})
+}
+
+main().then(console.dir).catch(console.error)

From f598dbeb21f8a9763644acc3cb8fb489b2f3ead2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 13:34:29 +0900
Subject: [PATCH 0017/1250] v3342

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d25107389..d2525b16c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3342 (2017/12/11)
+-----------------
+* なんか
+
 3339 (2017/12/11)
 -----------------
 * なんか
diff --git a/package.json b/package.json
index f03446531..84d694d2a 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3339",
+	"version": "0.0.3342",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From b4ab5aacef47494bb2909439b6011d142e2988b2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 13:34:36 +0900
Subject: [PATCH 0018/1250] Fix bug

---
 .../node.1510016282.change-gridfs-metadata-name-to-filename.js  | 2 +-
 tools/migration/node.1510056272.issue_882.js                    | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/tools/migration/node.1510016282.change-gridfs-metadata-name-to-filename.js b/tools/migration/node.1510016282.change-gridfs-metadata-name-to-filename.js
index 9128d852c..d7b2a6eff 100644
--- a/tools/migration/node.1510016282.change-gridfs-metadata-name-to-filename.js
+++ b/tools/migration/node.1510016282.change-gridfs-metadata-name-to-filename.js
@@ -34,7 +34,7 @@ async function main () {
 		1,
 		async (time) => {
 			console.log(`${time} / ${idop}`)
-			const doc = await db.get('drive_files').find(query, {
+			const doc = await DriveFile.find(query, {
 				limit: dop, skip: time * dop
 			})
 			return Promise.all(doc.map(applyNewChange))
diff --git a/tools/migration/node.1510056272.issue_882.js b/tools/migration/node.1510056272.issue_882.js
index aa1141325..302ef3de6 100644
--- a/tools/migration/node.1510056272.issue_882.js
+++ b/tools/migration/node.1510056272.issue_882.js
@@ -31,7 +31,7 @@ async function main() {
 		1,
 		async (time) => {
 			console.log(`${time} / ${idop}`)
-			const doc = await db.get('drive_files').find(query, {
+			const doc = await DriveFile.find(query, {
 				limit: dop, skip: time * dop
 			})
 			return Promise.all(doc.map(migrate))

From 3062a9b1a9c5e8a061993f81f217bb4f6605fff0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 13:52:06 +0900
Subject: [PATCH 0019/1250] :v:

---
 tools/migration/node.2017-12-11.js | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/tools/migration/node.2017-12-11.js b/tools/migration/node.2017-12-11.js
index 3a3fef051..b9686b8b4 100644
--- a/tools/migration/node.2017-12-11.js
+++ b/tools/migration/node.2017-12-11.js
@@ -17,7 +17,11 @@ const migrate = doc => new Promise(async (res, rej) => {
 		.setFormat('ppm')
 		.resize(1, 1)
 		.toBuffer(async (err, buffer) => {
-			if (err) rej(err);
+			if (err) {
+				console.error(err);
+				res(false);
+				return;
+			}
 			const r = buffer.readUInt8(buffer.length - 3);
 			const g = buffer.readUInt8(buffer.length - 2);
 			const b = buffer.readUInt8(buffer.length - 1);

From 9630ea7490bcf4eaf5036a4414af06bb53d57c78 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 14:07:51 +0900
Subject: [PATCH 0020/1250] :v:

---
 src/api/serializers/drive-file.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/api/serializers/drive-file.ts b/src/api/serializers/drive-file.ts
index 92a9492d8..003e09ee7 100644
--- a/src/api/serializers/drive-file.ts
+++ b/src/api/serializers/drive-file.ts
@@ -56,6 +56,8 @@ export default (
 
 	_target.url = `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`;
 
+	if (_target.properties == null) _target.properties = {};
+
 	if (opts.detail) {
 		if (_target.folder_id) {
 			// Populate folder

From 498af038538f5a7936a49cbd8a1455063ff4fdeb Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 14:19:51 +0900
Subject: [PATCH 0021/1250] Fix bug

---
 src/web/app/desktop/tags/images.tag | 2 +-
 src/web/app/mobile/tags/images.tag  | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/web/app/desktop/tags/images.tag b/src/web/app/desktop/tags/images.tag
index 5e4be481d..eeaf4cd3d 100644
--- a/src/web/app/desktop/tags/images.tag
+++ b/src/web/app/desktop/tags/images.tag
@@ -81,7 +81,7 @@
 	<script>
 		this.image = this.opts.image;
 		this.styles = {
-			'background-color': `rgb(${this.image.properties.average_color.join(',')})`,
+			'background-color': this.image.properties.average_color ? `rgb(${this.image.properties.average_color.join(',')})` : 'transparent',
 			'background-image': `url(${this.image.url}?thumbnail&size=512)`
 		};
 		console.log(this.styles);
diff --git a/src/web/app/mobile/tags/images.tag b/src/web/app/mobile/tags/images.tag
index b200eefe7..5899364ae 100644
--- a/src/web/app/mobile/tags/images.tag
+++ b/src/web/app/mobile/tags/images.tag
@@ -75,7 +75,7 @@
 	<script>
 		this.image = this.opts.image;
 		this.styles = {
-			'background-color': `rgb(${this.image.properties.average_color.join(',')})`,
+			'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>

From 71f885a1699c85e6defcdc3e8e2004e2e37f8d58 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 14:20:19 +0900
Subject: [PATCH 0022/1250] v3347

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d2525b16c..9ab4698ab 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3347 (2017/12/11)
+-----------------
+* バグ修正
+
 3342 (2017/12/11)
 -----------------
 * なんか
diff --git a/package.json b/package.json
index 84d694d2a..db3b08d31 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3342",
+	"version": "0.0.3347",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 51f046ec5ac27f5bc783938d033f8033fea6b2a5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 16:43:35 +0900
Subject: [PATCH 0023/1250] Fix bug

---
 src/api/private/signin.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/api/private/signin.ts b/src/api/private/signin.ts
index 7376921e2..a26c8f6c5 100644
--- a/src/api/private/signin.ts
+++ b/src/api/private/signin.ts
@@ -6,8 +6,10 @@ import Signin from '../models/signin';
 import serialize from '../serializers/signin';
 import event from '../event';
 import signin from '../common/signin';
+import config from '../../conf';
 
 export default async (req: express.Request, res: express.Response) => {
+	res.header('Access-Control-Allow-Origin', config.url);
 	res.header('Access-Control-Allow-Credentials', 'true');
 
 	const username = req.body['username'];

From fff91828c9a4475809d7bb1ebd9d331e5fc56ff1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 11 Dec 2017 16:46:38 +0900
Subject: [PATCH 0024/1250] Clean up

---
 src/web/app/desktop/tags/images.tag | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/web/app/desktop/tags/images.tag b/src/web/app/desktop/tags/images.tag
index eeaf4cd3d..29540747f 100644
--- a/src/web/app/desktop/tags/images.tag
+++ b/src/web/app/desktop/tags/images.tag
@@ -84,7 +84,6 @@
 			'background-color': this.image.properties.average_color ? `rgb(${this.image.properties.average_color.join(',')})` : 'transparent',
 			'background-image': `url(${this.image.url}?thumbnail&size=512)`
 		};
-		console.log(this.styles);
 
 		this.mousemove = e => {
 			const rect = this.refs.view.getBoundingClientRect();

From bd355994bfd83294c3a431707183fa2d65f71897 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 11 Dec 2017 19:48:29 +0000
Subject: [PATCH 0025/1250] fix(package): update @types/mongodb to version
 2.2.17

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index db3b08d31..d8bce1b99 100644
--- a/package.json
+++ b/package.json
@@ -54,7 +54,7 @@
 		"@types/is-url": "1.2.28",
 		"@types/js-yaml": "3.10.1",
 		"@types/mocha": "2.2.44",
-		"@types/mongodb": "2.2.16",
+		"@types/mongodb": "2.2.17",
 		"@types/monk": "1.0.6",
 		"@types/morgan": "1.7.35",
 		"@types/ms": "0.7.30",

From 67faca09916ad8075c5639e0b0e1ec43b2e18553 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 11 Dec 2017 20:01:55 +0000
Subject: [PATCH 0026/1250] fix(package): update @types/node to version 8.0.58

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index db3b08d31..0ee1078a8 100644
--- a/package.json
+++ b/package.json
@@ -59,7 +59,7 @@
 		"@types/morgan": "1.7.35",
 		"@types/ms": "0.7.30",
 		"@types/multer": "1.3.6",
-		"@types/node": "8.0.57",
+		"@types/node": "8.0.58",
 		"@types/page": "1.5.32",
 		"@types/proxy-addr": "2.0.0",
 		"@types/qrcode": "0.8.0",

From 439f8fe20255cc72afbdda3c55f4ac06df5a900e Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 12 Dec 2017 00:24:25 +0000
Subject: [PATCH 0027/1250] fix(package): update @types/chai to version 4.0.10

Closes #988
---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index db3b08d31..ef795895c 100644
--- a/package.json
+++ b/package.json
@@ -30,7 +30,7 @@
 		"@prezzemolo/zip": "0.0.3",
 		"@types/bcryptjs": "2.4.1",
 		"@types/body-parser": "1.16.8",
-		"@types/chai": "4.0.8",
+		"@types/chai": "4.0.10",
 		"@types/chai-http": "3.0.3",
 		"@types/compression": "0.0.35",
 		"@types/cookie": "0.3.1",

From e0bf340dab5f24ac6252315efb15eeafb04e9627 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 12 Dec 2017 01:27:35 +0000
Subject: [PATCH 0028/1250] fix(package): update @types/request to version
 2.0.9

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index db3b08d31..b8d2a644e 100644
--- a/package.json
+++ b/package.json
@@ -65,7 +65,7 @@
 		"@types/qrcode": "0.8.0",
 		"@types/ratelimiter": "2.1.28",
 		"@types/redis": "2.8.2",
-		"@types/request": "2.0.8",
+		"@types/request": "2.0.9",
 		"@types/rimraf": "2.0.2",
 		"@types/riot": "3.6.1",
 		"@types/seedrandom": "2.4.27",

From b47aac5201f15842431c7f8a5696714b598c0f06 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 12 Dec 2017 21:31:03 +0900
Subject: [PATCH 0029/1250] Fix

---
 src/web/app/desktop/tags/ui.tag | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/app/desktop/tags/ui.tag b/src/web/app/desktop/tags/ui.tag
index bc0a937a5..059d88528 100644
--- a/src/web/app/desktop/tags/ui.tag
+++ b/src/web/app/desktop/tags/ui.tag
@@ -165,7 +165,7 @@
 					transition color 0.5s ease, border 0.5s ease
 					font-family FontAwesome, sans-serif
 
-					&:placeholder-shown
+					&::placeholder
 						color #9eaba8
 
 					&:hover

From b4402663f9ae47cf58a27b0019b97e73d6fe2377 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 13 Dec 2017 20:13:39 +0900
Subject: [PATCH 0030/1250] Update config.md

---
 docs/config.md | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/docs/config.md b/docs/config.md
index 653fff1a7..a9987c9ce 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -49,4 +49,12 @@ sw:
   # VAPIDの秘密鍵
   private_key:
 
+# Twitterインテグレーションの設定(利用しない場合は省略可能)
+twitter:
+  # インテグレーション用アプリのコンシューマーキー
+  consumer_key: 
+
+  # インテグレーション用アプリのコンシューマーシークレット
+  consumer_secret: 
+
 ```

From 6e748ef1713cae0c0f1d1d9102dde8e1640fa41d Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 13 Dec 2017 19:27:08 +0000
Subject: [PATCH 0031/1250] fix(package): update @types/node to version 8.5.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index db366a4a0..c801b1e90 100644
--- a/package.json
+++ b/package.json
@@ -59,7 +59,7 @@
 		"@types/morgan": "1.7.35",
 		"@types/ms": "0.7.30",
 		"@types/multer": "1.3.6",
-		"@types/node": "8.0.58",
+		"@types/node": "8.5.0",
 		"@types/page": "1.5.32",
 		"@types/proxy-addr": "2.0.0",
 		"@types/qrcode": "0.8.0",

From bd6fa94109c3a6d55c1f8213630e04a6975febc0 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 13 Dec 2017 20:31:12 +0000
Subject: [PATCH 0032/1250] fix(package): update @types/redis to version 2.8.3

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index c801b1e90..724ff2043 100644
--- a/package.json
+++ b/package.json
@@ -64,7 +64,7 @@
 		"@types/proxy-addr": "2.0.0",
 		"@types/qrcode": "0.8.0",
 		"@types/ratelimiter": "2.1.28",
-		"@types/redis": "2.8.2",
+		"@types/redis": "2.8.3",
 		"@types/request": "2.0.9",
 		"@types/rimraf": "2.0.2",
 		"@types/riot": "3.6.1",

From 2f4ba439f3b05b73c08051a760d538c778b3f82e Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 14 Dec 2017 07:11:20 +0900
Subject: [PATCH 0033/1250] Update post-form.tag

---
 src/web/app/mobile/tags/post-form.tag | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/app/mobile/tags/post-form.tag b/src/web/app/mobile/tags/post-form.tag
index f09f40bb5..05466a6ec 100644
--- a/src/web/app/mobile/tags/post-form.tag
+++ b/src/web/app/mobile/tags/post-form.tag
@@ -12,7 +12,7 @@
 		<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=64' })" title={ name } onclick={ removeFile }></div>
+					<div class="img" style="background-image: url({ url + '?thumbnail&size=128' })" onclick={ removeFile }></div>
 				</li>
 			</ul>
 		</div>

From c9a2a5fae463bf4ab680d7489691db1959db308f Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 13 Dec 2017 23:26:25 +0000
Subject: [PATCH 0034/1250] fix(package): update @types/inquirer to version
 0.0.36

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 724ff2043..7725dcbce 100644
--- a/package.json
+++ b/package.json
@@ -49,7 +49,7 @@
 		"@types/gulp-replace": "0.0.31",
 		"@types/gulp-uglify": "3.0.3",
 		"@types/gulp-util": "3.0.34",
-		"@types/inquirer": "0.0.35",
+		"@types/inquirer": "0.0.36",
 		"@types/is-root": "1.0.0",
 		"@types/is-url": "1.2.28",
 		"@types/js-yaml": "3.10.1",

From 1cadba414310242145f29e6ed80a9c59b92da102 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 13 Dec 2017 23:47:49 +0000
Subject: [PATCH 0035/1250] fix(package): update @types/node to version 8.5.1

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 724ff2043..bc43909a3 100644
--- a/package.json
+++ b/package.json
@@ -59,7 +59,7 @@
 		"@types/morgan": "1.7.35",
 		"@types/ms": "0.7.30",
 		"@types/multer": "1.3.6",
-		"@types/node": "8.5.0",
+		"@types/node": "8.5.1",
 		"@types/page": "1.5.32",
 		"@types/proxy-addr": "2.0.0",
 		"@types/qrcode": "0.8.0",

From 8bab409c655662c5899f8b80d19c8d0d72e72a13 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 14 Dec 2017 02:53:58 +0000
Subject: [PATCH 0036/1250] fix(package): update uglifyjs-webpack-plugin to
 version 1.1.3

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 724ff2043..84e301743 100644
--- a/package.json
+++ b/package.json
@@ -165,7 +165,7 @@
 		"tslint": "5.8.0",
 		"typescript": "2.6.2",
 		"uglify-es": "3.2.0",
-		"uglifyjs-webpack-plugin": "1.1.2",
+		"uglifyjs-webpack-plugin": "1.1.3",
 		"uuid": "3.1.0",
 		"vhost": "3.0.2",
 		"web-push": "3.2.5",

From 3d5e8900d06946e3d7b08b714ec0d5d3e8a2fb0a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 14 Dec 2017 12:32:23 +0900
Subject: [PATCH 0037/1250] Refactor

---
 src/web/app/desktop/tags/image-dialog.tag | 61 ----------------------
 src/web/app/desktop/tags/images.tag       | 62 +++++++++++++++++++++++
 src/web/app/desktop/tags/index.ts         |  1 -
 3 files changed, 62 insertions(+), 62 deletions(-)
 delete mode 100644 src/web/app/desktop/tags/image-dialog.tag

diff --git a/src/web/app/desktop/tags/image-dialog.tag b/src/web/app/desktop/tags/image-dialog.tag
deleted file mode 100644
index 39d16ca13..000000000
--- a/src/web/app/desktop/tags/image-dialog.tag
+++ /dev/null
@@ -1,61 +0,0 @@
-<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 }/>
-	<style>
-		: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>
-		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.unmount()
-			});
-		};
-	</script>
-</mk-image-dialog>
diff --git a/src/web/app/desktop/tags/images.tag b/src/web/app/desktop/tags/images.tag
index 29540747f..0cd408576 100644
--- a/src/web/app/desktop/tags/images.tag
+++ b/src/web/app/desktop/tags/images.tag
@@ -108,3 +108,65 @@
 		};
 	</script>
 </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 }/>
+	<style>
+		: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>
+		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.unmount()
+			});
+		};
+	</script>
+</mk-image-dialog>
diff --git a/src/web/app/desktop/tags/index.ts b/src/web/app/desktop/tags/index.ts
index 30a13b584..4edda8353 100644
--- a/src/web/app/desktop/tags/index.ts
+++ b/src/web/app/desktop/tags/index.ts
@@ -77,7 +77,6 @@ require('./set-banner-suggestion.tag');
 require('./repost-form.tag');
 require('./sub-post-content.tag');
 require('./images.tag');
-require('./image-dialog.tag');
 require('./donation.tag');
 require('./users-list.tag');
 require('./user-following.tag');

From 3a15b616041825dcf09e8644b78f5f2b02c713ba Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 14 Dec 2017 13:31:17 +0900
Subject: [PATCH 0038/1250] :v:

---
 docs/api/endpoints/posts/create.yaml | 53 ++++++++++++++++++++++++++++
 src/api/endpoints/posts/create.ts    |  4 ++-
 test/api.js                          | 34 ++++++++++--------
 3 files changed, 76 insertions(+), 15 deletions(-)
 create mode 100644 docs/api/endpoints/posts/create.yaml

diff --git a/docs/api/endpoints/posts/create.yaml b/docs/api/endpoints/posts/create.yaml
new file mode 100644
index 000000000..db91775cb
--- /dev/null
+++ b/docs/api/endpoints/posts/create.yaml
@@ -0,0 +1,53 @@
+endpoint: "posts/create"
+
+desc:
+  ja: "投稿します。"
+  en: "Compose new post."
+
+params:
+  - name: "text"
+    type: "string"
+    required: true
+    desc:
+      ja: "投稿の本文"
+      en: "Text of a post"
+  - name: "media_ids"
+    type: "id(DriveFile)[]"
+    required: false
+    desc:
+      ja: "添付するメディア"
+      en: "Media you want to attach"
+  - name: "reply_id"
+    type: "id(Post)"
+    required: false
+    desc:
+      ja: "返信する投稿"
+      en: "A post you want to reply"
+  - name: "repost_id"
+    type: "id(Post)"
+    required: false
+    desc:
+      ja: "引用する投稿"
+      en: "A post you want to quote"
+  - name: "poll"
+    type: "object(poll)"
+    required: false
+    desc:
+      ja: "投票"
+      en: "A poll"
+
+paramDefs:
+  poll:
+    - name: "choices"
+      type: "string[]"
+      required: true
+      desc:
+        ja: "投票の選択肢"
+        en: "Choices of a poll"
+
+res:
+  - name: "created_post"
+    type: "entity(Post)"
+    desc:
+      ja: "作成した投稿"
+      en: "A post that created"
diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index ae4959dae..7270efaf7 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -222,7 +222,9 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 	const postObj = await serialize(post);
 
 	// Reponse
-	res(postObj);
+	res({
+		created_post: postObj
+	});
 
 	//#region Post processes
 
diff --git a/test/api.js b/test/api.js
index 49f1faa53..500b9adb7 100644
--- a/test/api.js
+++ b/test/api.js
@@ -224,7 +224,8 @@ describe('API', () => {
 			const res = await request('/posts/create', post, me);
 			res.should.have.status(200);
 			res.body.should.be.a('object');
-			res.body.should.have.property('text').eql(post.text);
+			res.body.should.have.property('created_post');
+			res.body.created_post.should.have.property('text').eql(post.text);
 		}));
 
 		it('ファイルを添付できる', async(async () => {
@@ -237,7 +238,8 @@ describe('API', () => {
 			}, me);
 			res.should.have.status(200);
 			res.body.should.be.a('object');
-			res.body.should.have.property('media_ids').eql([file._id.toString()]);
+			res.body.should.have.property('created_post');
+			res.body.created_post.should.have.property('media_ids').eql([file._id.toString()]);
 		}));
 
 		it('他人のファイルは添付できない', async(async () => {
@@ -283,10 +285,11 @@ describe('API', () => {
 			const res = await request('/posts/create', post, me);
 			res.should.have.status(200);
 			res.body.should.be.a('object');
-			res.body.should.have.property('text').eql(post.text);
-			res.body.should.have.property('reply_id').eql(post.reply_id);
-			res.body.should.have.property('reply');
-			res.body.reply.should.have.property('text').eql(himaPost.text);
+			res.body.should.have.property('created_post');
+			res.body.created_post.should.have.property('text').eql(post.text);
+			res.body.created_post.should.have.property('reply_id').eql(post.reply_id);
+			res.body.created_post.should.have.property('reply');
+			res.body.created_post.reply.should.have.property('text').eql(himaPost.text);
 		}));
 
 		it('repostできる', async(async () => {
@@ -303,9 +306,10 @@ describe('API', () => {
 			const res = await request('/posts/create', post, me);
 			res.should.have.status(200);
 			res.body.should.be.a('object');
-			res.body.should.have.property('repost_id').eql(post.repost_id);
-			res.body.should.have.property('repost');
-			res.body.repost.should.have.property('text').eql(himaPost.text);
+			res.body.should.have.property('created_post');
+			res.body.created_post.should.have.property('repost_id').eql(post.repost_id);
+			res.body.created_post.should.have.property('repost');
+			res.body.created_post.repost.should.have.property('text').eql(himaPost.text);
 		}));
 
 		it('引用repostできる', async(async () => {
@@ -323,10 +327,11 @@ describe('API', () => {
 			const res = await request('/posts/create', post, me);
 			res.should.have.status(200);
 			res.body.should.be.a('object');
-			res.body.should.have.property('text').eql(post.text);
-			res.body.should.have.property('repost_id').eql(post.repost_id);
-			res.body.should.have.property('repost');
-			res.body.repost.should.have.property('text').eql(himaPost.text);
+			res.body.should.have.property('created_post');
+			res.body.created_post.should.have.property('text').eql(post.text);
+			res.body.created_post.should.have.property('repost_id').eql(post.repost_id);
+			res.body.created_post.should.have.property('repost');
+			res.body.created_post.repost.should.have.property('text').eql(himaPost.text);
 		}));
 
 		it('文字数ぎりぎりで怒られない', async(async () => {
@@ -395,7 +400,8 @@ describe('API', () => {
 			}, me);
 			res.should.have.status(200);
 			res.body.should.be.a('object');
-			res.body.should.have.property('poll');
+			res.body.should.have.property('created_post');
+			res.body.created_post.should.have.property('poll');
 		}));
 
 		it('投票の選択肢が無くて怒られる', async(async () => {

From 7069aea5bb860a70168153917152b79a1a58382b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 14 Dec 2017 16:24:41 +0900
Subject: [PATCH 0039/1250] :pizza:

---
 gulpfile.ts                                   | 20 ++++-
 package.json                                  |  4 +
 src/web/app/app.styl                          | 38 +---------
 src/web/docs/api/endpoints/gulpfile.ts        | 75 +++++++++++++++++++
 .../web/docs}/api/endpoints/posts/create.yaml | 13 ++--
 src/web/docs/api/endpoints/style.styl         | 16 ++++
 src/web/docs/api/endpoints/view.pug           | 60 +++++++++++++++
 src/{ => web}/docs/api/entities/post.pug      |  0
 src/{ => web}/docs/api/entities/user.pug      |  0
 src/{ => web}/docs/api/getting-started.md     |  0
 src/{ => web}/docs/api/library.md             |  0
 src/{ => web}/docs/index.md                   |  0
 src/{ => web}/docs/link-to-twitter.md         |  0
 src/web/docs/style.styl                       | 69 +++++++++++++++++
 src/{ => web}/docs/tou.md                     |  0
 src/web/server.ts                             |  6 ++
 src/web/style.styl                            | 38 ++++++++++
 17 files changed, 295 insertions(+), 44 deletions(-)
 create mode 100644 src/web/docs/api/endpoints/gulpfile.ts
 rename {docs => src/web/docs}/api/endpoints/posts/create.yaml (87%)
 create mode 100644 src/web/docs/api/endpoints/style.styl
 create mode 100644 src/web/docs/api/endpoints/view.pug
 rename src/{ => web}/docs/api/entities/post.pug (100%)
 rename src/{ => web}/docs/api/entities/user.pug (100%)
 rename src/{ => web}/docs/api/getting-started.md (100%)
 rename src/{ => web}/docs/api/library.md (100%)
 rename src/{ => web}/docs/index.md (100%)
 rename src/{ => web}/docs/link-to-twitter.md (100%)
 create mode 100644 src/web/docs/style.styl
 rename src/{ => web}/docs/tou.md (100%)
 create mode 100644 src/web/style.styl

diff --git a/gulpfile.ts b/gulpfile.ts
index ee11a02dc..0bc18dd7c 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -13,6 +13,7 @@ import * as es from 'event-stream';
 import cssnano = require('gulp-cssnano');
 import * as uglifyComposer from 'gulp-uglify/composer';
 import pug = require('gulp-pug');
+import stylus = require('gulp-stylus');
 import * as rimraf from 'rimraf';
 import chalk from 'chalk';
 import imagemin = require('gulp-imagemin');
@@ -47,15 +48,32 @@ if (isDebug) {
 
 const constants = require('./src/const.json');
 
+require('./src/web/docs/api/endpoints/gulpfile.ts');
+
 gulp.task('build', [
 	'build:js',
 	'build:ts',
 	'build:copy',
-	'build:client'
+	'build:client',
+	'build:doc'
 ]);
 
 gulp.task('rebuild', ['clean', 'build']);
 
+gulp.task('build:doc', [
+	'doc:endpoints',
+	'doc:styles'
+]);
+
+gulp.task('doc:styles', () =>
+	gulp.src('./src/web/docs/**/*.styl')
+		.pipe(stylus())
+		.pipe(isProduction
+			? (cssnano as any)()
+			: gutil.noop())
+		.pipe(gulp.dest('./built/web/assets/docs/'))
+);
+
 gulp.task('build:js', () =>
 	gulp.src(['./src/**/*.js', '!./src/web/**/*.js'])
 		.pipe(gulp.dest('./built/'))
diff --git a/package.json b/package.json
index c20fd0c52..69090349e 100644
--- a/package.json
+++ b/package.json
@@ -53,6 +53,7 @@
 		"@types/is-root": "1.0.0",
 		"@types/is-url": "1.2.28",
 		"@types/js-yaml": "3.10.1",
+		"@types/mkdirp": "^0.5.2",
 		"@types/mocha": "2.2.44",
 		"@types/mongodb": "2.2.17",
 		"@types/monk": "1.0.6",
@@ -62,6 +63,7 @@
 		"@types/node": "8.5.1",
 		"@types/page": "1.5.32",
 		"@types/proxy-addr": "2.0.0",
+		"@types/pug": "^2.0.4",
 		"@types/qrcode": "0.8.0",
 		"@types/ratelimiter": "2.1.28",
 		"@types/redis": "2.8.3",
@@ -112,6 +114,7 @@
 		"gulp-pug": "3.3.0",
 		"gulp-rename": "1.2.2",
 		"gulp-replace": "0.6.1",
+		"gulp-stylus": "^2.6.0",
 		"gulp-tslint": "8.1.2",
 		"gulp-typescript": "3.2.3",
 		"gulp-uglify": "3.0.0",
@@ -122,6 +125,7 @@
 		"is-url": "1.2.2",
 		"js-yaml": "3.10.0",
 		"mecab-async": "0.1.2",
+		"mkdirp": "^0.5.1",
 		"mocha": "4.0.1",
 		"moji": "0.5.1",
 		"mongodb": "2.2.33",
diff --git a/src/web/app/app.styl b/src/web/app/app.styl
index de66df74d..22043b883 100644
--- a/src/web/app/app.styl
+++ b/src/web/app/app.styl
@@ -1,29 +1,4 @@
-json('../../const.json')
-
-@charset 'utf-8'
-
-$theme-color = themeColor
-$theme-color-foreground = themeColorForeground
-
-/*
-	::selection
-		background $theme-color
-		color #fff
-*/
-
-*
-	position relative
-	box-sizing border-box
-	background-clip padding-box !important
-	tap-highlight-color rgba($theme-color, 0.7)
-	-webkit-tap-highlight-color rgba($theme-color, 0.7)
-
-html, body
-	margin 0
-	padding 0
-	scroll-behavior smooth
-	text-size-adjust 100%
-	font-family sans-serif
+@import "../style"
 
 html
 	&.progress
@@ -96,17 +71,6 @@ body
 		100%
 			transform rotate(360deg)
 
-a
-	text-decoration none
-	color $theme-color
-	cursor pointer
-
-	&:hover
-		text-decoration underline
-
-	*
-		cursor pointer
-
 code
 	font-family Consolas, 'Courier New', Courier, Monaco, monospace
 
diff --git a/src/web/docs/api/endpoints/gulpfile.ts b/src/web/docs/api/endpoints/gulpfile.ts
new file mode 100644
index 000000000..a2c394470
--- /dev/null
+++ b/src/web/docs/api/endpoints/gulpfile.ts
@@ -0,0 +1,75 @@
+/**
+ * Gulp tasks
+ */
+
+import * as fs from 'fs';
+import * as path from 'path';
+import * as glob from 'glob';
+import * as gulp from 'gulp';
+import * as pug from 'pug';
+import * as yaml from 'js-yaml';
+import * as mkdirp from 'mkdirp';
+
+import config from './../../../../conf';
+
+const parseParam = param => {
+	const id = param.type.match(/^id\((.+?)\)/);
+	const object = param.type.match(/^object\((.+?)\)/);
+	const isArray = /\[\]$/.test(param.type);
+	if (id) {
+		param.kind = 'id';
+		param.type = 'string';
+		param.entity = id[1];
+		if (isArray) {
+			param.type += '[]';
+		}
+	}
+	if (object) {
+		param.kind = 'object';
+		param.type = 'object';
+		param.def = object[1];
+		if (isArray) {
+			param.type += '[]';
+		}
+	}
+
+	return param;
+};
+
+gulp.task('doc:endpoints', () => {
+	glob('./src/web/docs/api/endpoints/**/*.yaml', (globErr, files) => {
+		if (globErr) {
+			console.error(globErr);
+			return;
+		}
+		//console.log(files);
+		files.forEach(file => {
+			const ep = yaml.safeLoad(fs.readFileSync(file, 'utf-8'));
+			const vars = {
+				endpoint: ep.endpoint,
+				url: `${config.api_url}/${ep.endpoint}`,
+				desc: ep.desc,
+				params: ep.params.map(p => parseParam(p)),
+				paramDefs: Object.keys(ep.paramDefs).map(key => ({
+					name: key,
+					params: ep.paramDefs[key].map(p => parseParam(p))
+				})),
+				res: ep.res.map(p => parseParam(p))
+			};
+			pug.renderFile('./src/web/docs/api/endpoints/view.pug', vars, (renderErr, html) => {
+				if (renderErr) {
+					console.error(renderErr);
+					return;
+				}
+				const htmlPath = `./built/web/docs/api/endpoints/${ep.endpoint}.html`;
+				mkdirp(path.dirname(htmlPath), (mkdirErr) => {
+					if (mkdirErr) {
+						console.error(mkdirErr);
+						return;
+					}
+					fs.writeFileSync(htmlPath, html, 'utf-8');
+				});
+			});
+		});
+	});
+});
diff --git a/docs/api/endpoints/posts/create.yaml b/src/web/docs/api/endpoints/posts/create.yaml
similarity index 87%
rename from docs/api/endpoints/posts/create.yaml
rename to src/web/docs/api/endpoints/posts/create.yaml
index db91775cb..b6613038a 100644
--- a/docs/api/endpoints/posts/create.yaml
+++ b/src/web/docs/api/endpoints/posts/create.yaml
@@ -7,31 +7,31 @@ desc:
 params:
   - name: "text"
     type: "string"
-    required: true
+    optional: false
     desc:
       ja: "投稿の本文"
       en: "Text of a post"
   - name: "media_ids"
     type: "id(DriveFile)[]"
-    required: false
+    optional: true
     desc:
       ja: "添付するメディア"
       en: "Media you want to attach"
   - name: "reply_id"
     type: "id(Post)"
-    required: false
+    optional: true
     desc:
       ja: "返信する投稿"
       en: "A post you want to reply"
   - name: "repost_id"
     type: "id(Post)"
-    required: false
+    optional: true
     desc:
       ja: "引用する投稿"
       en: "A post you want to quote"
   - name: "poll"
     type: "object(poll)"
-    required: false
+    optional: true
     desc:
       ja: "投票"
       en: "A poll"
@@ -40,7 +40,7 @@ paramDefs:
   poll:
     - name: "choices"
       type: "string[]"
-      required: true
+      optional: false
       desc:
         ja: "投票の選択肢"
         en: "Choices of a poll"
@@ -48,6 +48,7 @@ paramDefs:
 res:
   - name: "created_post"
     type: "entity(Post)"
+    optional: false
     desc:
       ja: "作成した投稿"
       en: "A post that created"
diff --git a/src/web/docs/api/endpoints/style.styl b/src/web/docs/api/endpoints/style.styl
new file mode 100644
index 000000000..12c06fe3a
--- /dev/null
+++ b/src/web/docs/api/endpoints/style.styl
@@ -0,0 +1,16 @@
+@import "../../style"
+
+#url
+	padding 8px 12px
+	font-family Consolas, 'Courier New', Courier, Monaco, monospace
+	color #fff
+	background #222e40
+	border-radius 4px
+
+table
+	.name
+		font-weight bold
+
+	.type
+		font-family Consolas, 'Courier New', Courier, Monaco, monospace
+
diff --git a/src/web/docs/api/endpoints/view.pug b/src/web/docs/api/endpoints/view.pug
new file mode 100644
index 000000000..d9de9cb74
--- /dev/null
+++ b/src/web/docs/api/endpoints/view.pug
@@ -0,0 +1,60 @@
+doctype html
+
+mixin i18n(xs)
+	each text, lang in xs
+		span(class=`i18n ${lang}`)= text
+
+mixin table(params)
+	table
+		thead: tr
+			th Name
+			th Type
+			th Optional
+			th Description
+		tbody
+			each param in params
+				tr
+					td.name= param.name
+					td.type
+						if param.kind == 'id'
+							| #{param.type} (ID of
+							= ' '
+							a(href=`/docs/api/entities/${param.entity}`)= param.entity
+							| )
+						else if param.kind == 'object'
+							| #{param.type} (
+							a(href=`#${param.def}`)= param.def
+							| )
+						else
+							= param.type
+					td.optional= param.optional.toString()
+					td.desc: +i18n(param.desc)
+
+html
+	head
+		meta(charset="UTF-8")
+		title #{endpoint} | Misskey API
+		link(rel="stylesheet" href="/assets/docs/api/endpoints/style.css")
+
+	body
+		main
+			h1= endpoint
+
+			p#url= url
+
+			p#desc: +i18n(desc)
+
+			section
+				h2 Params
+				+table(params)
+
+				if paramDefs
+					each paramDef in paramDefs
+						section(id= paramDef.name)
+							h3= paramDef.name
+							+table(paramDef.params)
+
+			section
+				h2 Response
+				+table(res)
+
diff --git a/src/docs/api/entities/post.pug b/src/web/docs/api/entities/post.pug
similarity index 100%
rename from src/docs/api/entities/post.pug
rename to src/web/docs/api/entities/post.pug
diff --git a/src/docs/api/entities/user.pug b/src/web/docs/api/entities/user.pug
similarity index 100%
rename from src/docs/api/entities/user.pug
rename to src/web/docs/api/entities/user.pug
diff --git a/src/docs/api/getting-started.md b/src/web/docs/api/getting-started.md
similarity index 100%
rename from src/docs/api/getting-started.md
rename to src/web/docs/api/getting-started.md
diff --git a/src/docs/api/library.md b/src/web/docs/api/library.md
similarity index 100%
rename from src/docs/api/library.md
rename to src/web/docs/api/library.md
diff --git a/src/docs/index.md b/src/web/docs/index.md
similarity index 100%
rename from src/docs/index.md
rename to src/web/docs/index.md
diff --git a/src/docs/link-to-twitter.md b/src/web/docs/link-to-twitter.md
similarity index 100%
rename from src/docs/link-to-twitter.md
rename to src/web/docs/link-to-twitter.md
diff --git a/src/web/docs/style.styl b/src/web/docs/style.styl
new file mode 100644
index 000000000..9014df87f
--- /dev/null
+++ b/src/web/docs/style.styl
@@ -0,0 +1,69 @@
+@import "../style"
+
+body
+	margin 0
+	color #34495e
+
+main
+	padding 32px
+	width 100%
+	max-width 700px
+
+footer
+	padding:32px 0 0 0
+	margin 32px 0 0 0
+	border-top solid 1px #eee
+
+	.copyright
+		margin 16px 0 0 0
+		color #aaa
+
+section
+	margin 32px 0
+
+h1
+	margin 0 0 24px 0
+	padding 16px 0
+	font-size 1.5em
+	border-bottom solid 2px #eee
+
+h2
+	margin 0 0 24px 0
+	padding 0 0 16px 0
+	font-size 1.4em
+	border-bottom solid 1px #eee
+
+h3
+	margin 0
+	padding 0
+	font-size 1.25em
+
+h4
+	margin 0
+
+p
+	margin 1em 0
+	line-height 1.6em
+
+table
+	width 100%
+	border-spacing 0
+	border-collapse collapse
+
+	thead
+		font-weight bold
+		border-bottom solid 2px #eee
+
+		tr
+			th
+				text-align left
+
+	tbody
+		tr
+			border-bottom dashed 1px #eee
+
+	th, td
+		padding 8px 16px
+
+.i18n:not(.ja)
+	display none
diff --git a/src/docs/tou.md b/src/web/docs/tou.md
similarity index 100%
rename from src/docs/tou.md
rename to src/web/docs/tou.md
diff --git a/src/web/server.ts b/src/web/server.ts
index 1d3687f89..38e87754f 100644
--- a/src/web/server.ts
+++ b/src/web/server.ts
@@ -63,6 +63,12 @@ app.get('/manifest.json', (req, res) =>
  */
 app.get(/\/api:url/, require('./service/url-preview'));
 
+/**
+ * Docs
+ */
+app.get(/^\/docs\/([a-z_\-\/]+?)$/, (req, res) =>
+	res.sendFile(`${__dirname}/docs/${req.params[0]}.html`));
+
 /**
  * Routing
  */
diff --git a/src/web/style.styl b/src/web/style.styl
new file mode 100644
index 000000000..573df10d7
--- /dev/null
+++ b/src/web/style.styl
@@ -0,0 +1,38 @@
+json('../const.json')
+
+@charset 'utf-8'
+
+$theme-color = themeColor
+$theme-color-foreground = themeColorForeground
+
+/*
+	::selection
+		background $theme-color
+		color #fff
+*/
+
+*
+	position relative
+	box-sizing border-box
+	background-clip padding-box !important
+	tap-highlight-color rgba($theme-color, 0.7)
+	-webkit-tap-highlight-color rgba($theme-color, 0.7)
+
+html, body
+	margin 0
+	padding 0
+	scroll-behavior smooth
+	text-size-adjust 100%
+	font-family sans-serif
+
+a
+	text-decoration none
+	color $theme-color
+	cursor pointer
+
+	&:hover
+		text-decoration underline
+
+	*
+		cursor pointer
+

From d1c812e6ae8f187d795cad3ccb9c6ceb88632950 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 14 Dec 2017 09:36:26 +0000
Subject: [PATCH 0040/1250] fix(package): update style-loader to version 0.19.1

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 69090349e..97f290476 100644
--- a/package.json
+++ b/package.json
@@ -156,7 +156,7 @@
 		"sortablejs": "1.7.0",
 		"speakeasy": "2.0.0",
 		"string-replace-webpack-plugin": "0.1.3",
-		"style-loader": "0.19.0",
+		"style-loader": "0.19.1",
 		"stylus": "0.54.5",
 		"stylus-loader": "3.0.1",
 		"summaly": "2.0.3",

From 6c1d8eaba9ae3c2aba1d1c8990abec540475cf06 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 14 Dec 2017 11:34:10 +0000
Subject: [PATCH 0041/1250] fix(package): update uglifyjs-webpack-plugin to
 version 1.1.4

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 69090349e..9305cac9f 100644
--- a/package.json
+++ b/package.json
@@ -169,7 +169,7 @@
 		"tslint": "5.8.0",
 		"typescript": "2.6.2",
 		"uglify-es": "3.2.0",
-		"uglifyjs-webpack-plugin": "1.1.3",
+		"uglifyjs-webpack-plugin": "1.1.4",
 		"uuid": "3.1.0",
 		"vhost": "3.0.2",
 		"web-push": "3.2.5",

From f2eaa74197a27a647935daeca89c468087ef77ce Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 14 Dec 2017 22:36:04 +0900
Subject: [PATCH 0042/1250] :v:

---
 src/web/docs/api/endpoints/gulpfile.ts       | 39 +++++++++++++++-----
 src/web/docs/api/endpoints/posts/create.yaml | 21 +++++------
 src/web/docs/api/endpoints/view.pug          |  6 ++-
 3 files changed, 45 insertions(+), 21 deletions(-)

diff --git a/src/web/docs/api/endpoints/gulpfile.ts b/src/web/docs/api/endpoints/gulpfile.ts
index a2c394470..e375447c5 100644
--- a/src/web/docs/api/endpoints/gulpfile.ts
+++ b/src/web/docs/api/endpoints/gulpfile.ts
@@ -14,7 +14,8 @@ import config from './../../../../conf';
 
 const parseParam = param => {
 	const id = param.type.match(/^id\((.+?)\)/);
-	const object = param.type.match(/^object\((.+?)\)/);
+	const entity = param.type.match(/^entity\((.+?)\)/);
+	const isObject = /^object/.test(param.type);
 	const isArray = /\[\]$/.test(param.type);
 	if (id) {
 		param.kind = 'id';
@@ -24,18 +25,40 @@ const parseParam = param => {
 			param.type += '[]';
 		}
 	}
-	if (object) {
-		param.kind = 'object';
+	if (entity) {
+		param.kind = 'entity';
 		param.type = 'object';
-		param.def = object[1];
+		param.entity = entity[1];
 		if (isArray) {
 			param.type += '[]';
 		}
 	}
+	if (isObject) {
+		param.kind = 'object';
+	}
 
 	return param;
 };
 
+const extractDefs = params => {
+	const defs = [];
+
+	params.forEach(param => {
+		if (param.def) {
+			defs.push({
+				name: param.defName,
+				params: param.def.map(p => parseParam(p))
+			});
+
+			const childDefs = extractDefs(param.def);
+
+			defs.concat(childDefs);
+		}
+	});
+
+	return defs;
+};
+
 gulp.task('doc:endpoints', () => {
 	glob('./src/web/docs/api/endpoints/**/*.yaml', (globErr, files) => {
 		if (globErr) {
@@ -50,11 +73,9 @@ gulp.task('doc:endpoints', () => {
 				url: `${config.api_url}/${ep.endpoint}`,
 				desc: ep.desc,
 				params: ep.params.map(p => parseParam(p)),
-				paramDefs: Object.keys(ep.paramDefs).map(key => ({
-					name: key,
-					params: ep.paramDefs[key].map(p => parseParam(p))
-				})),
-				res: ep.res.map(p => parseParam(p))
+				paramDefs: extractDefs(ep.params),
+				res: ep.res.map(p => parseParam(p)),
+				resDefs: extractDefs(ep.res)
 			};
 			pug.renderFile('./src/web/docs/api/endpoints/view.pug', vars, (renderErr, html) => {
 				if (renderErr) {
diff --git a/src/web/docs/api/endpoints/posts/create.yaml b/src/web/docs/api/endpoints/posts/create.yaml
index b6613038a..feedf4f0d 100644
--- a/src/web/docs/api/endpoints/posts/create.yaml
+++ b/src/web/docs/api/endpoints/posts/create.yaml
@@ -7,7 +7,7 @@ desc:
 params:
   - name: "text"
     type: "string"
-    optional: false
+    optional: true
     desc:
       ja: "投稿の本文"
       en: "Text of a post"
@@ -30,20 +30,19 @@ params:
       ja: "引用する投稿"
       en: "A post you want to quote"
   - name: "poll"
-    type: "object(poll)"
+    type: "object"
     optional: true
     desc:
       ja: "投票"
       en: "A poll"
-
-paramDefs:
-  poll:
-    - name: "choices"
-      type: "string[]"
-      optional: false
-      desc:
-        ja: "投票の選択肢"
-        en: "Choices of a poll"
+    defName: "poll"
+    def:
+      - name: "choices"
+        type: "string[]"
+        optional: false
+        desc:
+          ja: "投票の選択肢"
+          en: "Choices of a poll"
 
 res:
   - name: "created_post"
diff --git a/src/web/docs/api/endpoints/view.pug b/src/web/docs/api/endpoints/view.pug
index d9de9cb74..b7b2658a3 100644
--- a/src/web/docs/api/endpoints/view.pug
+++ b/src/web/docs/api/endpoints/view.pug
@@ -21,9 +21,13 @@ mixin table(params)
 							= ' '
 							a(href=`/docs/api/entities/${param.entity}`)= param.entity
 							| )
+						else if param.kind == 'entity'
+							| #{param.type} (
+							a(href=`/docs/api/entities/${param.entity}`)= param.entity
+							| )
 						else if param.kind == 'object'
 							| #{param.type} (
-							a(href=`#${param.def}`)= param.def
+							a(href=`#${param.defName}`)= param.defName
 							| )
 						else
 							= param.type

From 6153b2d4f866726178020fc9b4e97dd6ca1ae1ec Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 14 Dec 2017 22:50:41 +0900
Subject: [PATCH 0043/1250] :v:

---
 src/web/docs/api/endpoints/posts/create.yaml | 4 ++--
 src/web/docs/api/endpoints/style.styl        | 2 ++
 src/web/docs/api/endpoints/view.pug          | 5 ++---
 src/web/docs/style.styl                      | 3 +++
 4 files changed, 9 insertions(+), 5 deletions(-)

diff --git a/src/web/docs/api/endpoints/posts/create.yaml b/src/web/docs/api/endpoints/posts/create.yaml
index feedf4f0d..498a99159 100644
--- a/src/web/docs/api/endpoints/posts/create.yaml
+++ b/src/web/docs/api/endpoints/posts/create.yaml
@@ -15,8 +15,8 @@ params:
     type: "id(DriveFile)[]"
     optional: true
     desc:
-      ja: "添付するメディア"
-      en: "Media you want to attach"
+      ja: "添付するメディア(1~4つ)"
+      en: "Media you want to attach (1~4)"
   - name: "reply_id"
     type: "id(Post)"
     optional: true
diff --git a/src/web/docs/api/endpoints/style.styl b/src/web/docs/api/endpoints/style.styl
index 12c06fe3a..ab74e100b 100644
--- a/src/web/docs/api/endpoints/style.styl
+++ b/src/web/docs/api/endpoints/style.styl
@@ -11,6 +11,8 @@ table
 	.name
 		font-weight bold
 
+	.name
 	.type
+	.optional
 		font-family Consolas, 'Courier New', Courier, Monaco, monospace
 
diff --git a/src/web/docs/api/endpoints/view.pug b/src/web/docs/api/endpoints/view.pug
index b7b2658a3..841ca8b3f 100644
--- a/src/web/docs/api/endpoints/view.pug
+++ b/src/web/docs/api/endpoints/view.pug
@@ -17,10 +17,9 @@ mixin table(params)
 					td.name= param.name
 					td.type
 						if param.kind == 'id'
-							| #{param.type} (ID of
-							= ' '
+							| #{param.type} (
 							a(href=`/docs/api/entities/${param.entity}`)= param.entity
-							| )
+							|  ID)
 						else if param.kind == 'entity'
 							| #{param.type} (
 							a(href=`/docs/api/entities/${param.entity}`)= param.entity
diff --git a/src/web/docs/style.styl b/src/web/docs/style.styl
index 9014df87f..5c484adc1 100644
--- a/src/web/docs/style.styl
+++ b/src/web/docs/style.styl
@@ -62,6 +62,9 @@ table
 		tr
 			border-bottom dashed 1px #eee
 
+			&:nth-child(odd)
+				background #fbfbfb
+
 	th, td
 		padding 8px 16px
 

From becbd8f8b4a77bfb0986030296dbc36f1d564a98 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 15 Dec 2017 00:23:45 +0900
Subject: [PATCH 0044/1250] :v:

---
 gulpfile.ts                                  |   4 +-
 src/web/docs/api/endpoints/posts/create.yaml |   8 +-
 src/web/docs/api/endpoints/style.styl        |  12 +-
 src/web/docs/api/endpoints/view.pug          |  75 ++++-------
 src/web/docs/api/entities/post.yaml          | 124 +++++++++++++++++++
 src/web/docs/api/entities/style.styl         |   1 +
 src/web/docs/api/entities/view.pug           |  23 ++++
 src/web/docs/api/{endpoints => }/gulpfile.ts |  78 ++++++++++--
 src/web/docs/api/mixins.pug                  |  33 +++++
 src/web/docs/api/style.styl                  |  11 ++
 src/web/docs/layout.pug                      |  16 +++
 11 files changed, 305 insertions(+), 80 deletions(-)
 create mode 100644 src/web/docs/api/entities/post.yaml
 create mode 100644 src/web/docs/api/entities/style.styl
 create mode 100644 src/web/docs/api/entities/view.pug
 rename src/web/docs/api/{endpoints => }/gulpfile.ts (50%)
 create mode 100644 src/web/docs/api/mixins.pug
 create mode 100644 src/web/docs/api/style.styl
 create mode 100644 src/web/docs/layout.pug

diff --git a/gulpfile.ts b/gulpfile.ts
index 0bc18dd7c..6807b6d57 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -48,7 +48,7 @@ if (isDebug) {
 
 const constants = require('./src/const.json');
 
-require('./src/web/docs/api/endpoints/gulpfile.ts');
+require('./src/web/docs/api/gulpfile.ts');
 
 gulp.task('build', [
 	'build:js',
@@ -61,7 +61,7 @@ gulp.task('build', [
 gulp.task('rebuild', ['clean', 'build']);
 
 gulp.task('build:doc', [
-	'doc:endpoints',
+	'doc:api',
 	'doc:styles'
 ]);
 
diff --git a/src/web/docs/api/endpoints/posts/create.yaml b/src/web/docs/api/endpoints/posts/create.yaml
index 498a99159..5e2307dab 100644
--- a/src/web/docs/api/endpoints/posts/create.yaml
+++ b/src/web/docs/api/endpoints/posts/create.yaml
@@ -10,7 +10,7 @@ params:
     optional: true
     desc:
       ja: "投稿の本文"
-      en: "Text of a post"
+      en: "The text of your post"
   - name: "media_ids"
     type: "id(DriveFile)[]"
     optional: true
@@ -22,19 +22,19 @@ params:
     optional: true
     desc:
       ja: "返信する投稿"
-      en: "A post you want to reply"
+      en: "The post you want to reply"
   - name: "repost_id"
     type: "id(Post)"
     optional: true
     desc:
       ja: "引用する投稿"
-      en: "A post you want to quote"
+      en: "The post you want to quote"
   - name: "poll"
     type: "object"
     optional: true
     desc:
       ja: "投票"
-      en: "A poll"
+      en: "The poll"
     defName: "poll"
     def:
       - name: "choices"
diff --git a/src/web/docs/api/endpoints/style.styl b/src/web/docs/api/endpoints/style.styl
index ab74e100b..07fb7ec2a 100644
--- a/src/web/docs/api/endpoints/style.styl
+++ b/src/web/docs/api/endpoints/style.styl
@@ -1,4 +1,4 @@
-@import "../../style"
+@import "../style"
 
 #url
 	padding 8px 12px
@@ -6,13 +6,3 @@
 	color #fff
 	background #222e40
 	border-radius 4px
-
-table
-	.name
-		font-weight bold
-
-	.name
-	.type
-	.optional
-		font-family Consolas, 'Courier New', Courier, Monaco, monospace
-
diff --git a/src/web/docs/api/endpoints/view.pug b/src/web/docs/api/endpoints/view.pug
index 841ca8b3f..cebef9fa5 100644
--- a/src/web/docs/api/endpoints/view.pug
+++ b/src/web/docs/api/endpoints/view.pug
@@ -1,63 +1,30 @@
-doctype html
+extends ../../layout.pug
+include ../mixins
 
-mixin i18n(xs)
-	each text, lang in xs
-		span(class=`i18n ${lang}`)= text
+block title
+	| #{endpoint} | Misskey API
 
-mixin table(params)
-	table
-		thead: tr
-			th Name
-			th Type
-			th Optional
-			th Description
-		tbody
-			each param in params
-				tr
-					td.name= param.name
-					td.type
-						if param.kind == 'id'
-							| #{param.type} (
-							a(href=`/docs/api/entities/${param.entity}`)= param.entity
-							|  ID)
-						else if param.kind == 'entity'
-							| #{param.type} (
-							a(href=`/docs/api/entities/${param.entity}`)= param.entity
-							| )
-						else if param.kind == 'object'
-							| #{param.type} (
-							a(href=`#${param.defName}`)= param.defName
-							| )
-						else
-							= param.type
-					td.optional= param.optional.toString()
-					td.desc: +i18n(param.desc)
+block meta
+	link(rel="stylesheet" href="/assets/docs/api/endpoints/style.css")
 
-html
-	head
-		meta(charset="UTF-8")
-		title #{endpoint} | Misskey API
-		link(rel="stylesheet" href="/assets/docs/api/endpoints/style.css")
+block main
+	h1= endpoint
 
-	body
-		main
-			h1= endpoint
+	p#url= url
 
-			p#url= url
+	p#desc: +i18n(desc)
 
-			p#desc: +i18n(desc)
+	section
+		h2 Params
+		+propTable(params)
 
-			section
-				h2 Params
-				+table(params)
+		if paramDefs
+			each paramDef in paramDefs
+				section(id= paramDef.name)
+					h3= paramDef.name
+					+propTable(paramDef.params)
 
-				if paramDefs
-					each paramDef in paramDefs
-						section(id= paramDef.name)
-							h3= paramDef.name
-							+table(paramDef.params)
-
-			section
-				h2 Response
-				+table(res)
+	section
+		h2 Response
+		+propTable(res)
 
diff --git a/src/web/docs/api/entities/post.yaml b/src/web/docs/api/entities/post.yaml
new file mode 100644
index 000000000..551f3b7c3
--- /dev/null
+++ b/src/web/docs/api/entities/post.yaml
@@ -0,0 +1,124 @@
+name: "Post"
+
+desc:
+  ja: "投稿。"
+  en: "A post."
+
+props:
+  - name: "id"
+    type: "id"
+    optional: false
+    desc:
+      ja: "投稿ID"
+      en: "The ID of this post"
+  - name: "created_at"
+    type: "date"
+    optional: false
+    desc:
+      ja: "投稿日時"
+      en: "The posted date of this post"
+  - name: "text"
+    type: "string"
+    optional: true
+    desc:
+      ja: "投稿の本文"
+      en: "The text of this post"
+  - name: "media_ids"
+    type: "id(DriveFile)[]"
+    optional: true
+    desc:
+      ja: "添付されているメディアのID"
+      en: "The IDs of the attached media"
+  - name: "media"
+    type: "entity(DriveFile)[]"
+    optional: true
+    desc:
+      ja: "添付されているメディア"
+      en: "The attached media"
+  - name: "user_id"
+    type: "id(User)"
+    optional: false
+    desc:
+      ja: "投稿者ID"
+      en: "The ID of author of this post"
+  - name: "user"
+    type: "entity(User)"
+    optional: true
+    desc:
+      ja: "投稿者"
+      en: "The author of this post"
+  - name: "my_reaction"
+    type: "string"
+    optional: true
+    desc:
+      ja: "この投稿に対する自分の<a href='/docs/api/reactions'>リアクション</a>"
+      en: "The your <a href='/docs/api/reactions'>reaction</a> of this post"
+  - name: "reaction_counts"
+    type: "object"
+    optional: false
+    desc:
+      ja: "<a href='/docs/api/reactions'>リアクション</a>をキーとし、この投稿に対するそのリアクションの数を値としたオブジェクト"
+  - name: "reply_id"
+    type: "id(Post)"
+    optional: true
+    desc:
+      ja: "返信した投稿のID"
+      en: "The ID of the replyed post"
+  - name: "reply"
+    type: "entity(Post)"
+    optional: true
+    desc:
+      ja: "返信した投稿"
+      en: "The replyed post"
+  - name: "repost_id"
+    type: "id(Post)"
+    optional: true
+    desc:
+      ja: "引用した投稿のID"
+      en: "The ID of the quoted post"
+  - name: "repost"
+    type: "entity(Post)"
+    optional: true
+    desc:
+      ja: "引用した投稿"
+      en: "The quoted post"
+  - name: "poll"
+    type: "object"
+    optional: true
+    desc:
+      ja: "投票"
+      en: "The poll"
+    defName: "poll"
+    def:
+      - name: "choices"
+        type: "object[]"
+        optional: false
+        desc:
+          ja: "投票の選択肢"
+          en: "The choices of this poll"
+        defName: "choice"
+        def:
+          - name: "id"
+            type: "number"
+            optional: false
+            desc:
+              ja: "選択肢ID"
+              en: "The ID of this choice"
+          - name: "is_voted"
+            type: "boolean"
+            optional: true
+            desc:
+              ja: "自分がこの選択肢に投票したかどうか"
+              en: "Whether you voted to this choice"
+          - name: "text"
+            type: "string"
+            optional: false
+            desc:
+              ja: "選択肢本文"
+              en: "The text of this choice"
+          - name: "votes"
+            type: "number"
+            optional: false
+            desc:
+              ja: "この選択肢に投票された数"
+              en: "The number voted for this choice"
diff --git a/src/web/docs/api/entities/style.styl b/src/web/docs/api/entities/style.styl
new file mode 100644
index 000000000..bddf0f53a
--- /dev/null
+++ b/src/web/docs/api/entities/style.styl
@@ -0,0 +1 @@
+@import "../style"
diff --git a/src/web/docs/api/entities/view.pug b/src/web/docs/api/entities/view.pug
new file mode 100644
index 000000000..f210582f1
--- /dev/null
+++ b/src/web/docs/api/entities/view.pug
@@ -0,0 +1,23 @@
+extends ../../layout.pug
+include ../mixins
+
+block title
+	| #{name} | Misskey API
+
+block meta
+	link(rel="stylesheet" href="/assets/docs/api/entities/style.css")
+
+block main
+	h1= name
+
+	p#desc: +i18n(desc)
+
+	section
+		h2 Properties
+		+propTable(props)
+
+		if propDefs
+			each propDef in propDefs
+				section(id= propDef.name)
+					h3= propDef.name
+					+propTable(propDef.params)
diff --git a/src/web/docs/api/endpoints/gulpfile.ts b/src/web/docs/api/gulpfile.ts
similarity index 50%
rename from src/web/docs/api/endpoints/gulpfile.ts
rename to src/web/docs/api/gulpfile.ts
index e375447c5..05567b623 100644
--- a/src/web/docs/api/endpoints/gulpfile.ts
+++ b/src/web/docs/api/gulpfile.ts
@@ -10,12 +10,15 @@ import * as pug from 'pug';
 import * as yaml from 'js-yaml';
 import * as mkdirp from 'mkdirp';
 
-import config from './../../../../conf';
+import config from './../../../conf';
+
+const kebab = string => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase();
 
 const parseParam = param => {
-	const id = param.type.match(/^id\((.+?)\)/);
+	const id = param.type.match(/^id\((.+?)\)|^id/);
 	const entity = param.type.match(/^entity\((.+?)\)/);
 	const isObject = /^object/.test(param.type);
+	const isDate = /^date/.test(param.type);
 	const isArray = /\[\]$/.test(param.type);
 	if (id) {
 		param.kind = 'id';
@@ -36,30 +39,53 @@ const parseParam = param => {
 	if (isObject) {
 		param.kind = 'object';
 	}
+	if (isDate) {
+		param.kind = 'date';
+		param.type = 'string';
+		if (isArray) {
+			param.type += '[]';
+		}
+	}
 
 	return param;
 };
 
+const sortParams = params => {
+	params.sort((a, b) => {
+		if (a.name < b.name)
+			return -1;
+		if (a.name > b.name)
+			return 1;
+		return 0;
+	});
+	return params;
+};
+
 const extractDefs = params => {
-	const defs = [];
+	let defs = [];
 
 	params.forEach(param => {
 		if (param.def) {
 			defs.push({
 				name: param.defName,
-				params: param.def.map(p => parseParam(p))
+				params: sortParams(param.def.map(p => parseParam(p)))
 			});
 
 			const childDefs = extractDefs(param.def);
 
-			defs.concat(childDefs);
+			defs = defs.concat(childDefs);
 		}
 	});
 
 	return defs;
 };
 
-gulp.task('doc:endpoints', () => {
+gulp.task('doc:api', [
+	'doc:api:endpoints',
+	'doc:api:entities'
+]);
+
+gulp.task('doc:api:endpoints', () => {
 	glob('./src/web/docs/api/endpoints/**/*.yaml', (globErr, files) => {
 		if (globErr) {
 			console.error(globErr);
@@ -72,10 +98,11 @@ gulp.task('doc:endpoints', () => {
 				endpoint: ep.endpoint,
 				url: `${config.api_url}/${ep.endpoint}`,
 				desc: ep.desc,
-				params: ep.params.map(p => parseParam(p)),
+				params: sortParams(ep.params.map(p => parseParam(p))),
 				paramDefs: extractDefs(ep.params),
-				res: ep.res.map(p => parseParam(p)),
-				resDefs: extractDefs(ep.res)
+				res: sortParams(ep.res.map(p => parseParam(p))),
+				resDefs: extractDefs(ep.res),
+				kebab
 			};
 			pug.renderFile('./src/web/docs/api/endpoints/view.pug', vars, (renderErr, html) => {
 				if (renderErr) {
@@ -94,3 +121,36 @@ gulp.task('doc:endpoints', () => {
 		});
 	});
 });
+
+gulp.task('doc:api:entities', () => {
+	glob('./src/web/docs/api/entities/**/*.yaml', (globErr, files) => {
+		if (globErr) {
+			console.error(globErr);
+			return;
+		}
+		files.forEach(file => {
+			const entity = yaml.safeLoad(fs.readFileSync(file, 'utf-8'));
+			const vars = {
+				name: entity.name,
+				desc: entity.desc,
+				props: sortParams(entity.props.map(p => parseParam(p))),
+				propDefs: extractDefs(entity.props),
+				kebab
+			};
+			pug.renderFile('./src/web/docs/api/entities/view.pug', vars, (renderErr, html) => {
+				if (renderErr) {
+					console.error(renderErr);
+					return;
+				}
+				const htmlPath = `./built/web/docs/api/entities/${kebab(entity.name)}.html`;
+				mkdirp(path.dirname(htmlPath), (mkdirErr) => {
+					if (mkdirErr) {
+						console.error(mkdirErr);
+						return;
+					}
+					fs.writeFileSync(htmlPath, html, 'utf-8');
+				});
+			});
+		});
+	});
+});
diff --git a/src/web/docs/api/mixins.pug b/src/web/docs/api/mixins.pug
new file mode 100644
index 000000000..b302c7826
--- /dev/null
+++ b/src/web/docs/api/mixins.pug
@@ -0,0 +1,33 @@
+mixin propTable(props)
+	table.props
+		thead: tr
+			th Name
+			th Type
+			th Optional
+			th Description
+		tbody
+			each prop in props
+				tr
+					td.name= prop.name
+					td.type
+						i= prop.type
+						if prop.kind == 'id'
+							if prop.entity
+								|  (
+								a(href=`/docs/api/entities/${kebab(prop.entity)}`)= prop.entity
+								|  ID)
+							else
+								|  (ID)
+						else if prop.kind == 'entity'
+							|   (
+							a(href=`/docs/api/entities/${kebab(prop.entity)}`)= prop.entity
+							| )
+						else if prop.kind == 'object'
+							if prop.def
+								|  (
+								a(href=`#${prop.defName}`)= prop.defName
+								| )
+						else if prop.kind == 'date'
+							|  (Date)
+					td.optional= prop.optional.toString()
+					td.desc: +i18n(prop.desc)
diff --git a/src/web/docs/api/style.styl b/src/web/docs/api/style.styl
new file mode 100644
index 000000000..3675a4da6
--- /dev/null
+++ b/src/web/docs/api/style.styl
@@ -0,0 +1,11 @@
+@import "../style"
+
+table.props
+	.name
+		font-weight bold
+
+	.name
+	.type
+	.optional
+		font-family Consolas, 'Courier New', Courier, Monaco, monospace
+
diff --git a/src/web/docs/layout.pug b/src/web/docs/layout.pug
new file mode 100644
index 000000000..68ca9eb62
--- /dev/null
+++ b/src/web/docs/layout.pug
@@ -0,0 +1,16 @@
+doctype html
+
+mixin i18n(xs)
+	each text, lang in xs
+		span(class=`i18n ${lang}`)!= text
+
+html
+	head
+		meta(charset="UTF-8")
+		title
+			block title
+		block meta
+
+	body
+		main
+			block main

From e63b6589d89591538533c7c09bf8dd53ddc4db43 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 15 Dec 2017 00:54:28 +0900
Subject: [PATCH 0045/1250] :v:

---
 src/web/docs/api/entities/post.pug  | 149 ----------------------------
 src/web/docs/api/entities/user.yaml | 137 +++++++++++++++++++++++++
 src/web/docs/api/gulpfile.ts        |   2 +-
 3 files changed, 138 insertions(+), 150 deletions(-)
 delete mode 100644 src/web/docs/api/entities/post.pug
 create mode 100644 src/web/docs/api/entities/user.yaml

diff --git a/src/web/docs/api/entities/post.pug b/src/web/docs/api/entities/post.pug
deleted file mode 100644
index 954f17271..000000000
--- a/src/web/docs/api/entities/post.pug
+++ /dev/null
@@ -1,149 +0,0 @@
-extend ../../BASE
-
-block title
-	| Entity: Post
-
-block content
-	h1 Post
-	p 投稿を表します。
-
-	section
-		h2 Properties
-		table.entity
-			thead: tr
-				td Name
-				td Type
-				td Description
-			tbody
-				tr.nullable.optional
-					td app
-					td: a(href='./app', target='_blank') App
-					td 投稿したアプリ
-				tr.nullable
-					td app_id
-					td ID
-					td 投稿したアプリのID
-				tr
-					td created_at
-					td Date
-					td 投稿日時
-				tr
-					td id
-					td ID
-					td 投稿ID
-				tr.optional
-					td is_liked
-					td Boolean
-					td いいね したかどうか
-				tr
-					td likes_count
-					td Number
-					td いいね数
-				tr.nullable.optional
-					td media_ids
-					td ID[]
-					td 添付されたメディアのIDの配列
-				tr.nullable.optional
-					td media
-					td: a(href='./drive-file', target='_blank') DriveFile[]
-					td 添付されたメディアの配列
-				tr
-					td replies_count
-					td Number
-					td 返信数
-				tr.optional
-					td reply
-					td: a(href='./post', target='_blank') Post
-					td 返信先の投稿
-				tr.nullable
-					td reply_id
-					td ID
-					td 返信先の投稿のID
-				tr.optional
-					td repost
-					td: a(href='./post', target='_blank') Post
-					td Repostした投稿
-				tr
-					td repost_count
-					td Number
-					td Repostされた数
-				tr.nullable
-					td repost_id
-					td ID
-					td Repostした投稿のID
-				tr.nullable
-					td text
-					td String
-					td 本文
-				tr.optional
-					td user
-					td: a(href='./user', target='_blank') User
-					td 投稿者
-				tr
-					td user_id
-					td ID
-					td 投稿者のID
-
-	section
-		h2 Example
-		pre: code.
-			{
-				"created_at": "2016-12-10T00:28:50.114Z",
-				"media_ids": null,
-				"reply_id": "584a16b15860fc52320137e3",
-				"repost_id": null,
-				"text": "小日向美穂だぞ!",
-				"user_id": "5848bf7764e572683f4402f8",
-				"app_id": null,
-				"likes_count": 1,
-				"replies_count": 1,
-				"id": "584b4c42d8e5186f8f755d0c",
-				"user": {
-					"birthday": null,
-					"created_at": "2016-12-08T02:03:35.332Z",
-					"bio": "女が嫌いです、女性は好きです",
-					"followers_count": 11,
-					"following_count": 11,
-					"links": null,
-					"location": "",
-					"name": "女が嫌い",
-					"posts_count": 26,
-					"likes_count": 2,
-					"liked_count": 20,
-					"username": "onnnagakirai",
-					"id": "5848bf7764e572683f4402f8",
-					"avatar_url": "https://file.himasaku.net/5848c0ec64e572683f4402fc",
-					"banner_url": "https://file.himasaku.net/5848c12864e572683f4402fd",
-					"is_following": true,
-					"is_followed": true
-				},
-				"reply": {
-					"created_at": "2016-12-09T02:28:01.563Z",
-					"media_ids": null,
-					"reply_id": "5849d35e547e4249be329884",
-					"repost_id": null,
-					"text": "アイコン小日向美穂?",
-					"user_id": "57d01a501fdf2d07be417afe",
-					"app_id": null,
-					"replies_count": 1,
-					"id": "584a16b15860fc52320137e3",
-					"user": {
-						"birthday": null,
-						"created_at": "2016-09-07T13:46:56.605Z",
-						"bio": "どうすれば君だけのために生きていけるの",
-						"followers_count": 51,
-						"following_count": 97,
-						"links": null,
-						"location": "川崎",
-						"name": "きな子",
-						"posts_count": 4813,
-						"username": "syuilo",
-						"likes_count": 3141,
-						"liked_count": 750,
-						"id": "57d01a501fdf2d07be417afe",
-						"avatar_url": "https://file.himasaku.net/583ddc6e64df272771f74c1a",
-						"banner_url": "https://file.himasaku.net/584bfc82d8e5186f8f755ec5"
-					}
-				},
-				"is_liked": true
-			}
diff --git a/src/web/docs/api/entities/user.yaml b/src/web/docs/api/entities/user.yaml
new file mode 100644
index 000000000..9b1efd1fe
--- /dev/null
+++ b/src/web/docs/api/entities/user.yaml
@@ -0,0 +1,137 @@
+name: "User"
+
+desc:
+  ja: "ユーザー。"
+  en: "A user."
+
+props:
+  - name: "id"
+    type: "id"
+    optional: false
+    desc:
+      ja: "ユーザーID"
+      en: "The ID of this user"
+  - name: "created_at"
+    type: "date"
+    optional: false
+    desc:
+      ja: "アカウント作成日時"
+      en: "The registered date of this user"
+  - name: "username"
+    type: "string"
+    optional: false
+    desc:
+      ja: "ユーザー名"
+      en: "The username of this user"
+  - name: "description"
+    type: "string"
+    optional: false
+    desc:
+      ja: "アカウントの説明(自己紹介)"
+      en: "The description of this user"
+  - name: "avatar_id"
+    type: "id(DriveFile)"
+    optional: true
+    desc:
+      ja: "アバターのID"
+      en: "The ID of the avatar of this user"
+  - name: "avatar_url"
+    type: "string"
+    optional: false
+    desc:
+      ja: "アバターのURL"
+      en: "The URL of the avatar of this user"
+  - name: "banner_id"
+    type: "id(DriveFile)"
+    optional: true
+    desc:
+      ja: "バナーのID"
+      en: "The ID of the banner of this user"
+  - name: "banner_url"
+    type: "string"
+    optional: false
+    desc:
+      ja: "バナーのURL"
+      en: "The URL of the banner of this user"
+  - name: "followers_count"
+    type: "number"
+    optional: false
+    desc:
+      ja: "フォロワーの数"
+      en: "The number of the followers for this user"
+  - name: "following_count"
+    type: "number"
+    optional: false
+    desc:
+      ja: "フォローしているユーザーの数"
+      en: "The number of the following users for this user"
+  - name: "last_used_at"
+    type: "date"
+    optional: false
+    desc:
+      ja: "最終利用日時"
+      en: "The last used date of this user"
+  - name: "posts_count"
+    type: "number"
+    optional: false
+    desc:
+      ja: "投稿の数"
+      en: "The number of the posts of this user"
+  - name: "pinned_post"
+    type: "entity(Post)"
+    optional: true
+    desc:
+      ja: "ピン留めされた投稿"
+      en: "The pinned post of this user"
+  - name: "pinned_post_id"
+    type: "id(Post)"
+    optional: true
+    desc:
+      ja: "ピン留めされた投稿のID"
+      en: "The ID of the pinned post of this user"
+  - name: "drive_capacity"
+    type: "number"
+    optional: false
+    desc:
+      ja: "ドライブの容量(bytes)"
+      en: "The capacity of drive of this user (bytes)"
+  - name: "twitter"
+    type: "object"
+    optional: true
+    desc:
+      ja: "連携されているTwitterアカウント情報"
+      en: "The info of the connected twitter account of this user"
+    defName: "twitter"
+    def:
+      - name: "user_id"
+        type: "string"
+        optional: false
+        desc:
+          ja: "ユーザーID"
+          en: "The user ID"
+      - name: "screen_name"
+        type: "string"
+        optional: false
+        desc:
+          ja: "ユーザー名"
+          en: "The screen name of this user"
+  - name: "profile"
+    type: "object"
+    optional: false
+    desc:
+      ja: "プロフィール"
+      en: "The profile of this user"
+    defName: "profile"
+    def:
+      - name: "location"
+        type: "string"
+        optional: true
+        desc:
+          ja: "場所"
+          en: "The location of this user"
+      - name: "birthday"
+        type: "string"
+        optional: true
+        desc:
+          ja: "誕生日 (YYYY-MM-DD)"
+          en: "The birthday of this user (YYYY-MM-DD)"
diff --git a/src/web/docs/api/gulpfile.ts b/src/web/docs/api/gulpfile.ts
index 05567b623..6453996d3 100644
--- a/src/web/docs/api/gulpfile.ts
+++ b/src/web/docs/api/gulpfile.ts
@@ -77,7 +77,7 @@ const extractDefs = params => {
 		}
 	});
 
-	return defs;
+	return sortParams(defs);
 };
 
 gulp.task('doc:api', [

From 160b095f259c8fe0162e4ec7f40bee84216b578c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 15 Dec 2017 04:43:48 +0900
Subject: [PATCH 0046/1250] :art:

---
 src/web/docs/style.styl | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/web/docs/style.styl b/src/web/docs/style.styl
index 5c484adc1..a4abc5a9a 100644
--- a/src/web/docs/style.styl
+++ b/src/web/docs/style.styl
@@ -56,7 +56,12 @@ table
 
 		tr
 			th
+				position sticky
+				top 0
+				z-index 1
 				text-align left
+				box-shadow 0 1px 0 0 #eee
+				background #fff
 
 	tbody
 		tr

From 828246a6b477b62edd5d63e48ee245b59d8caf5f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 15 Dec 2017 05:07:21 +0900
Subject: [PATCH 0047/1250] :v:

---
 src/web/docs/api/entities/user.pug  | 122 ----------------------------
 src/web/docs/api/entities/user.yaml |  10 +++
 2 files changed, 10 insertions(+), 122 deletions(-)
 delete mode 100644 src/web/docs/api/entities/user.pug

diff --git a/src/web/docs/api/entities/user.pug b/src/web/docs/api/entities/user.pug
deleted file mode 100644
index a37886bb1..000000000
--- a/src/web/docs/api/entities/user.pug
+++ /dev/null
@@ -1,122 +0,0 @@
-extend ../../BASE
-
-block title
-	| Entity: User
-
-block content
-	h1 User
-	p ユーザーを表します。
-
-	section
-		h2 Properties
-		table.entity
-			thead: tr
-				td Name
-				td Type
-				td Description
-			tbody
-				tr.nullable.optional
-					td avatar_id
-					td ID
-					td アバターに設定しているドライブのファイルのID
-				tr.nullable
-					td avatar_url
-					td String
-					td アバターURL
-				tr.nullable.optional
-					td banner_id
-					td ID
-					td バナーに設定しているドライブのファイルのID
-				tr.nullable
-					td banner_url
-					td String
-					td バナーURL
-				tr.nullable
-					td bio
-					td String
-					td プロフィール
-				tr.nullable
-					td birthday
-					td String
-					td 誕生日(YYYY-MM-DD)
-				tr
-					td created_at
-					td Date
-					td アカウント作成日時
-				tr.optional
-					td drive_capacity
-					td Number
-					td ドライブの最大容量(byte単位)
-				tr
-					td followers_count
-					td Number
-					td フォロワー数
-				tr
-					td following_count
-					td Number
-					td フォロー数
-				tr
-					td id
-					td ID
-					td ユーザーID
-				tr.optional
-					td is_bot
-					td Boolean
-					td botかどうか
-				tr.optional
-					td is_followed
-					td Boolean
-					td フォローされているか
-				tr.optional
-					td is_following
-					td Boolean
-					td フォローしているか
-				tr
-					td liked_count
-					td Number
-					td 投稿にいいねされた数
-				tr
-					td likes_count
-					td Number
-					td 投稿にいいねした数
-				tr.nullable
-					td location
-					td String
-					td 住処
-				tr
-					td name
-					td String
-					td ニックネーム
-				tr
-					td posts_count
-					td Number
-					td 投稿数
-				tr
-					td username
-					td String
-					td ユーザー名
-
-	section
-		h2 Example
-		pre: code.
-			{
-				"avatar_id": "583ddc6e64df272771f74c1a",
-				"avatar_url": "https://file.himasaku.net/583ddc6e64df272771f74c1a",
-				"banner_id": "584bfc82d8e5186f8f755ec5",
-				"banner_url": "https://file.himasaku.net/584bfc82d8e5186f8f755ec5",
-				"bio": "どうすれば君だけのために生きていけるの",
-				"birthday": "1997-12-06",
-				"created_at": "2016-09-07T13:46:56.605Z",
-				"drive_capacity": 1073741824,
-				"email": null,
-				"followers_count": 51,
-				"following_count": 97,
-				"id": "57d01a501fdf2d07be417afe",
-				"liked_count": 750,
-				"likes_count": 3130,
-				"links": null,
-				"location": "川崎",
-				"name": "きな子",
-				"posts_count": 4811,
-				"username": "syuilo"
-			}
diff --git a/src/web/docs/api/entities/user.yaml b/src/web/docs/api/entities/user.yaml
index 9b1efd1fe..abc3f300d 100644
--- a/src/web/docs/api/entities/user.yaml
+++ b/src/web/docs/api/entities/user.yaml
@@ -65,6 +65,16 @@ props:
     desc:
       ja: "フォローしているユーザーの数"
       en: "The number of the following users for this user"
+  - name: "is_following"
+    type: "boolean"
+    optional: true
+    desc:
+      ja: "自分がこのユーザーをフォローしているか"
+  - name: "is_followed"
+    type: "boolean"
+    optional: true
+    desc:
+      ja: "自分がこのユーザーにフォローされているか"
   - name: "last_used_at"
     type: "date"
     optional: false

From 8254a1dccf39d19735718d18d3a33417e58ba75e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 15 Dec 2017 06:41:57 +0900
Subject: [PATCH 0048/1250] :v:

---
 gulpfile.ts                         | 19 +-------
 src/web/docs/api/endpoints/view.pug |  3 +-
 src/web/docs/api/entities/view.pug  |  2 +-
 src/web/docs/api/gulpfile.ts        | 60 ++++++++++++++++---------
 src/web/docs/api/mixins.pug         |  6 +--
 src/web/docs/gulpfile.ts            | 64 ++++++++++++++++++++++++++
 src/web/docs/index.en.pug           |  9 ++++
 src/web/docs/index.ja.pug           |  9 ++++
 src/web/docs/index.md               |  4 --
 src/web/docs/layout.pug             | 23 +++++++---
 src/web/docs/style.styl             | 69 ++++++++++++++++-------------
 src/web/docs/vars.ts                | 36 +++++++++++++++
 12 files changed, 220 insertions(+), 84 deletions(-)
 create mode 100644 src/web/docs/gulpfile.ts
 create mode 100644 src/web/docs/index.en.pug
 create mode 100644 src/web/docs/index.ja.pug
 delete mode 100644 src/web/docs/index.md
 create mode 100644 src/web/docs/vars.ts

diff --git a/gulpfile.ts b/gulpfile.ts
index 6807b6d57..e7d477061 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -13,7 +13,6 @@ import * as es from 'event-stream';
 import cssnano = require('gulp-cssnano');
 import * as uglifyComposer from 'gulp-uglify/composer';
 import pug = require('gulp-pug');
-import stylus = require('gulp-stylus');
 import * as rimraf from 'rimraf';
 import chalk from 'chalk';
 import imagemin = require('gulp-imagemin');
@@ -48,32 +47,18 @@ if (isDebug) {
 
 const constants = require('./src/const.json');
 
-require('./src/web/docs/api/gulpfile.ts');
+require('./src/web/docs/gulpfile.ts');
 
 gulp.task('build', [
 	'build:js',
 	'build:ts',
 	'build:copy',
 	'build:client',
-	'build:doc'
+	'doc'
 ]);
 
 gulp.task('rebuild', ['clean', 'build']);
 
-gulp.task('build:doc', [
-	'doc:api',
-	'doc:styles'
-]);
-
-gulp.task('doc:styles', () =>
-	gulp.src('./src/web/docs/**/*.styl')
-		.pipe(stylus())
-		.pipe(isProduction
-			? (cssnano as any)()
-			: gutil.noop())
-		.pipe(gulp.dest('./built/web/assets/docs/'))
-);
-
 gulp.task('build:js', () =>
 	gulp.src(['./src/**/*.js', '!./src/web/**/*.js'])
 		.pipe(gulp.dest('./built/'))
diff --git a/src/web/docs/api/endpoints/view.pug b/src/web/docs/api/endpoints/view.pug
index cebef9fa5..cab814cab 100644
--- a/src/web/docs/api/endpoints/view.pug
+++ b/src/web/docs/api/endpoints/view.pug
@@ -12,7 +12,7 @@ block main
 
 	p#url= url
 
-	p#desc: +i18n(desc)
+	p#desc= desc[lang] || desc['ja']
 
 	section
 		h2 Params
@@ -27,4 +27,3 @@ block main
 	section
 		h2 Response
 		+propTable(res)
-
diff --git a/src/web/docs/api/entities/view.pug b/src/web/docs/api/entities/view.pug
index f210582f1..756e966b5 100644
--- a/src/web/docs/api/entities/view.pug
+++ b/src/web/docs/api/entities/view.pug
@@ -10,7 +10,7 @@ block meta
 block main
 	h1= name
 
-	p#desc: +i18n(desc)
+	p#desc= desc[lang] || desc['ja']
 
 	section
 		h2 Properties
diff --git a/src/web/docs/api/gulpfile.ts b/src/web/docs/api/gulpfile.ts
index 6453996d3..6cbae5ea2 100644
--- a/src/web/docs/api/gulpfile.ts
+++ b/src/web/docs/api/gulpfile.ts
@@ -12,6 +12,12 @@ import * as mkdirp from 'mkdirp';
 
 import config from './../../../conf';
 
+import generateVars from '../vars';
+
+const commonVars = generateVars();
+
+const langs = ['ja', 'en'];
+
 const kebab = string => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase();
 
 const parseParam = param => {
@@ -102,20 +108,25 @@ gulp.task('doc:api:endpoints', () => {
 				paramDefs: extractDefs(ep.params),
 				res: sortParams(ep.res.map(p => parseParam(p))),
 				resDefs: extractDefs(ep.res),
-				kebab
+				kebab,
+				common: commonVars
 			};
-			pug.renderFile('./src/web/docs/api/endpoints/view.pug', vars, (renderErr, html) => {
-				if (renderErr) {
-					console.error(renderErr);
-					return;
-				}
-				const htmlPath = `./built/web/docs/api/endpoints/${ep.endpoint}.html`;
-				mkdirp(path.dirname(htmlPath), (mkdirErr) => {
-					if (mkdirErr) {
-						console.error(mkdirErr);
+			langs.forEach(lang => {
+				pug.renderFile('./src/web/docs/api/endpoints/view.pug', Object.assign({}, vars, {
+					lang
+				}), (renderErr, html) => {
+					if (renderErr) {
+						console.error(renderErr);
 						return;
 					}
-					fs.writeFileSync(htmlPath, html, 'utf-8');
+					const htmlPath = `./built/web/docs/${lang}/api/endpoints/${ep.endpoint}.html`;
+					mkdirp(path.dirname(htmlPath), (mkdirErr) => {
+						if (mkdirErr) {
+							console.error(mkdirErr);
+							return;
+						}
+						fs.writeFileSync(htmlPath, html, 'utf-8');
+					});
 				});
 			});
 		});
@@ -135,20 +146,25 @@ gulp.task('doc:api:entities', () => {
 				desc: entity.desc,
 				props: sortParams(entity.props.map(p => parseParam(p))),
 				propDefs: extractDefs(entity.props),
-				kebab
+				kebab,
+				common: commonVars
 			};
-			pug.renderFile('./src/web/docs/api/entities/view.pug', vars, (renderErr, html) => {
-				if (renderErr) {
-					console.error(renderErr);
-					return;
-				}
-				const htmlPath = `./built/web/docs/api/entities/${kebab(entity.name)}.html`;
-				mkdirp(path.dirname(htmlPath), (mkdirErr) => {
-					if (mkdirErr) {
-						console.error(mkdirErr);
+			langs.forEach(lang => {
+				pug.renderFile('./src/web/docs/api/entities/view.pug', Object.assign({}, vars, {
+					lang
+				}), (renderErr, html) => {
+					if (renderErr) {
+						console.error(renderErr);
 						return;
 					}
-					fs.writeFileSync(htmlPath, html, 'utf-8');
+					const htmlPath = `./built/web/docs/${lang}/api/entities/${kebab(entity.name)}.html`;
+					mkdirp(path.dirname(htmlPath), (mkdirErr) => {
+						if (mkdirErr) {
+							console.error(mkdirErr);
+							return;
+						}
+						fs.writeFileSync(htmlPath, html, 'utf-8');
+					});
 				});
 			});
 		});
diff --git a/src/web/docs/api/mixins.pug b/src/web/docs/api/mixins.pug
index b302c7826..3ddd7cb48 100644
--- a/src/web/docs/api/mixins.pug
+++ b/src/web/docs/api/mixins.pug
@@ -14,13 +14,13 @@ mixin propTable(props)
 						if prop.kind == 'id'
 							if prop.entity
 								|  (
-								a(href=`/docs/api/entities/${kebab(prop.entity)}`)= prop.entity
+								a(href=`/docs/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity
 								|  ID)
 							else
 								|  (ID)
 						else if prop.kind == 'entity'
 							|   (
-							a(href=`/docs/api/entities/${kebab(prop.entity)}`)= prop.entity
+							a(href=`/docs/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity
 							| )
 						else if prop.kind == 'object'
 							if prop.def
@@ -30,4 +30,4 @@ mixin propTable(props)
 						else if prop.kind == 'date'
 							|  (Date)
 					td.optional= prop.optional.toString()
-					td.desc: +i18n(prop.desc)
+					td.desc!= prop.desc[lang] || prop.desc['ja']
diff --git a/src/web/docs/gulpfile.ts b/src/web/docs/gulpfile.ts
new file mode 100644
index 000000000..6f2351dac
--- /dev/null
+++ b/src/web/docs/gulpfile.ts
@@ -0,0 +1,64 @@
+/**
+ * Gulp tasks
+ */
+
+import * as fs from 'fs';
+import * as path from 'path';
+import * as glob from 'glob';
+import * as gulp from 'gulp';
+import * as pug from 'pug';
+//import * as yaml from 'js-yaml';
+import * as mkdirp from 'mkdirp';
+import stylus = require('gulp-stylus');
+import cssnano = require('gulp-cssnano');
+
+//import config from './../../conf';
+
+import generateVars from './vars';
+
+require('./api/gulpfile.ts');
+
+gulp.task('doc', [
+	'doc:docs',
+	'doc:api',
+	'doc:styles'
+]);
+
+const commonVars = generateVars();
+
+gulp.task('doc:docs', () => {
+	glob('./src/web/docs/**/*.*.pug', (globErr, files) => {
+		if (globErr) {
+			console.error(globErr);
+			return;
+		}
+		files.forEach(file => {
+			const [, name, lang] = file.match(/docs\/(.+?)\.(.+?)\.pug$/);
+			const vars = {
+				common: commonVars,
+				lang: lang
+			};
+			pug.renderFile(file, vars, (renderErr, html) => {
+				if (renderErr) {
+					console.error(renderErr);
+					return;
+				}
+				const htmlPath = `./built/web/docs/${lang}/${name}.html`;
+				mkdirp(path.dirname(htmlPath), (mkdirErr) => {
+					if (mkdirErr) {
+						console.error(mkdirErr);
+						return;
+					}
+					fs.writeFileSync(htmlPath, html, 'utf-8');
+				});
+			});
+		});
+	});
+});
+
+gulp.task('doc:styles', () =>
+	gulp.src('./src/web/docs/**/*.styl')
+		.pipe(stylus())
+		.pipe((cssnano as any)())
+		.pipe(gulp.dest('./built/web/assets/docs/'))
+);
diff --git a/src/web/docs/index.en.pug b/src/web/docs/index.en.pug
new file mode 100644
index 000000000..af0bba8b2
--- /dev/null
+++ b/src/web/docs/index.en.pug
@@ -0,0 +1,9 @@
+extends ./layout.pug
+
+block title
+	| Misskey Docs
+
+block main
+	h1 Misskey Docs
+
+	p Welcome to docs of Misskey.
diff --git a/src/web/docs/index.ja.pug b/src/web/docs/index.ja.pug
new file mode 100644
index 000000000..cd43045f6
--- /dev/null
+++ b/src/web/docs/index.ja.pug
@@ -0,0 +1,9 @@
+extends ./layout.pug
+
+block title
+	| Misskey ドキュメント
+
+block main
+	h1 Misskey ドキュメント
+
+	p Misskeyのドキュメントへようこそ
diff --git a/src/web/docs/index.md b/src/web/docs/index.md
deleted file mode 100644
index 0846cf27e..000000000
--- a/src/web/docs/index.md
+++ /dev/null
@@ -1,4 +0,0 @@
-Misskeyについて
-================================================================
-
-誰か書いて
diff --git a/src/web/docs/layout.pug b/src/web/docs/layout.pug
index 68ca9eb62..d6ecb4b6a 100644
--- a/src/web/docs/layout.pug
+++ b/src/web/docs/layout.pug
@@ -1,16 +1,29 @@
 doctype html
 
-mixin i18n(xs)
-	each text, lang in xs
-		span(class=`i18n ${lang}`)!= text
-
-html
+html(lang= lang)
 	head
 		meta(charset="UTF-8")
+		meta(name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no")
 		title
 			block title
+		link(rel="stylesheet" href="/assets/docs/style.css")
 		block meta
 
 	body
+		nav
+			ul
+				each doc in common.docs
+					li: a(href=`/docs/${lang}/${doc.name}`)= doc.title[lang] || doc.title['ja']
+			section
+				h2 API
+				ul
+					li Entities
+						ul
+							each entity in common.entities
+								li: a(href=`/docs/${lang}/api/entities/${common.kebab(entity)}`)= entity
+					li Endpoints
+						ul
+							each endpoint in common.endpoints
+								li: a(href=`/docs/${lang}/api/endpoints/${common.kebab(endpoint)}`)= endpoint
 		main
 			block main
diff --git a/src/web/docs/style.styl b/src/web/docs/style.styl
index a4abc5a9a..2e2f9f574 100644
--- a/src/web/docs/style.styl
+++ b/src/web/docs/style.styl
@@ -5,10 +5,49 @@ body
 	color #34495e
 
 main
+	margin 0 0 0 256px
 	padding 32px
 	width 100%
 	max-width 700px
 
+	section
+		margin 32px 0
+
+	h1
+		margin 0 0 24px 0
+		padding 16px 0
+		font-size 1.5em
+		border-bottom solid 2px #eee
+
+	h2
+		margin 0 0 24px 0
+		padding 0 0 16px 0
+		font-size 1.4em
+		border-bottom solid 1px #eee
+
+	h3
+		margin 0
+		padding 0
+		font-size 1.25em
+
+	h4
+		margin 0
+
+	p
+		margin 1em 0
+		line-height 1.6em
+
+nav
+	display block
+	position fixed
+	top 0
+	left 0
+	width 256px
+	height 100%
+	overflow auto
+	padding 32px
+	border-right solid 2px #eee
+
 footer
 	padding:32px 0 0 0
 	margin 32px 0 0 0
@@ -18,33 +57,6 @@ footer
 		margin 16px 0 0 0
 		color #aaa
 
-section
-	margin 32px 0
-
-h1
-	margin 0 0 24px 0
-	padding 16px 0
-	font-size 1.5em
-	border-bottom solid 2px #eee
-
-h2
-	margin 0 0 24px 0
-	padding 0 0 16px 0
-	font-size 1.4em
-	border-bottom solid 1px #eee
-
-h3
-	margin 0
-	padding 0
-	font-size 1.25em
-
-h4
-	margin 0
-
-p
-	margin 1em 0
-	line-height 1.6em
-
 table
 	width 100%
 	border-spacing 0
@@ -72,6 +84,3 @@ table
 
 	th, td
 		padding 8px 16px
-
-.i18n:not(.ja)
-	display none
diff --git a/src/web/docs/vars.ts b/src/web/docs/vars.ts
new file mode 100644
index 000000000..ed2149df4
--- /dev/null
+++ b/src/web/docs/vars.ts
@@ -0,0 +1,36 @@
+import * as fs from 'fs';
+import * as glob from 'glob';
+import * as yaml from 'js-yaml';
+
+export default function() {
+	const vars = {};
+
+	const endpoints = glob.sync('./src/web/docs/api/endpoints/**/*.yaml');
+	vars['endpoints'] = endpoints.map(ep => {
+		const _ep = yaml.safeLoad(fs.readFileSync(ep, 'utf-8'));
+		return _ep.endpoint;
+	});
+
+	const entities = glob.sync('./src/web/docs/api/entities/**/*.yaml');
+	vars['entities'] = entities.map(x => {
+		const _x = yaml.safeLoad(fs.readFileSync(x, 'utf-8'));
+		return _x.name;
+	});
+
+	const docs = glob.sync('./src/web/docs/**/*.*.pug');
+	vars['docs'] = {};
+	docs.forEach(x => {
+		const [, name, lang] = x.match(/docs\/(.+?)\.(.+?)\.pug$/);
+		if (vars['docs'][name] == null) {
+			vars['docs'][name] = {
+				name,
+				title: {}
+			};
+		}
+		vars['docs'][name]['title'][lang] = fs.readFileSync(x, 'utf-8').match(/\r\n\th1 (.+?)\r\n/)[1];
+	});
+
+	vars['kebab'] = string => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase();
+
+	return vars;
+}

From 8cd0512509c2524bd3c984a9a189af44a9e9c8aa Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 15 Dec 2017 06:45:19 +0900
Subject: [PATCH 0049/1250] :v:

---
 src/web/docs/vars.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/docs/vars.ts b/src/web/docs/vars.ts
index ed2149df4..80fdc9a7d 100644
--- a/src/web/docs/vars.ts
+++ b/src/web/docs/vars.ts
@@ -27,7 +27,7 @@ export default function() {
 				title: {}
 			};
 		}
-		vars['docs'][name]['title'][lang] = fs.readFileSync(x, 'utf-8').match(/\r\n\th1 (.+?)\r\n/)[1];
+		vars['docs'][name]['title'][lang] = fs.readFileSync(x, 'utf-8').match(/\r?\n\th1 (.+?)\r?\n/)[1];
 	});
 
 	vars['kebab'] = string => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase();

From 419cc6b95af3d0e856c8189eba17ed9042d44273 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, 15 Dec 2017 09:03:22 +0900
Subject: [PATCH 0050/1250] :v:

---
 src/web/docs/api/entities/drive-file.yaml | 73 +++++++++++++++++++++++
 1 file changed, 73 insertions(+)
 create mode 100644 src/web/docs/api/entities/drive-file.yaml

diff --git a/src/web/docs/api/entities/drive-file.yaml b/src/web/docs/api/entities/drive-file.yaml
new file mode 100644
index 000000000..2ebbb089a
--- /dev/null
+++ b/src/web/docs/api/entities/drive-file.yaml
@@ -0,0 +1,73 @@
+name: "DriveFile"
+
+desc:
+  ja: "ドライブのファイル。"
+  en: "A file of Drive."
+
+props:
+  - name: "id"
+    type: "id"
+    optional: false
+    desc:
+      ja: "ファイルID"
+      en: "The ID of this file"
+  - name: "created_at"
+    type: "date"
+    optional: false
+    desc:
+      ja: "アップロード日時"
+      en: "The upload date of this file"
+  - name: "user_id"
+    type: "id(User)"
+    optional: false
+    desc:
+      ja: "所有者ID"
+      en: "The ID of the owner of this file"
+  - name: "user"
+    type: "entity(User)"
+    optional: true
+    desc:
+      ja: "所有者"
+      en: "The owner of this file"
+  - name: "name"
+    type: "string"
+    optional: false
+    desc:
+      ja: "ファイル名"
+      en: "The name of this file"
+  - name: "md5"
+    type: "string"
+    optional: false
+    desc:
+      ja: "ファイルのMD5ハッシュ値"
+      en: "The md5 hash value of this file"
+  - name: "type"
+    type: "string"
+    optional: false
+    desc:
+      ja: "ファイルの種類"
+      en: "The type of this file"
+  - name: "datasize"
+    type: "number"
+    optional: false
+    desc:
+      ja: "ファイルサイズ(bytes)"
+      en: "The size of this file (bytes)"
+  - name: "url"
+    type: "string"
+    optional: false
+    desc:
+      ja: "ファイルのURL"
+      en: "The URL of this file"
+  - name: "folder_id"
+    type: "id(DriveFolder)"
+    optional: true
+    desc:
+      ja: "フォルダID"
+      en: "The ID of the folder of this file"
+  - name: "folder"
+    type: "entity(DriveFolder)"
+    optional: true
+    desc:
+      ja: "フォルダ"
+      en: "The folder of this file"

From 059c8d57457b3f1eb5e9c66f9908f1ebd1f291c4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 16 Dec 2017 00:19:10 +0900
Subject: [PATCH 0051/1250] :v:

---
 src/web/docs/api/endpoints/view.pug |  3 ---
 src/web/docs/api/entities/view.pug  |  3 ---
 src/web/docs/api/gulpfile.ts        | 14 ++++++++------
 src/web/docs/gulpfile.ts            | 24 +++++++++++++++++-------
 src/web/docs/index.en.pug           | 10 ++--------
 src/web/docs/index.ja.pug           | 10 ++--------
 src/web/docs/layout.pug             |  4 +++-
 src/web/docs/vars.ts                |  2 +-
 8 files changed, 33 insertions(+), 37 deletions(-)

diff --git a/src/web/docs/api/endpoints/view.pug b/src/web/docs/api/endpoints/view.pug
index cab814cab..d456022f6 100644
--- a/src/web/docs/api/endpoints/view.pug
+++ b/src/web/docs/api/endpoints/view.pug
@@ -1,9 +1,6 @@
 extends ../../layout.pug
 include ../mixins
 
-block title
-	| #{endpoint} | Misskey API
-
 block meta
 	link(rel="stylesheet" href="/assets/docs/api/endpoints/style.css")
 
diff --git a/src/web/docs/api/entities/view.pug b/src/web/docs/api/entities/view.pug
index 756e966b5..57c6d4cad 100644
--- a/src/web/docs/api/entities/view.pug
+++ b/src/web/docs/api/entities/view.pug
@@ -1,9 +1,6 @@
 extends ../../layout.pug
 include ../mixins
 
-block title
-	| #{name} | Misskey API
-
 block meta
 	link(rel="stylesheet" href="/assets/docs/api/entities/style.css")
 
diff --git a/src/web/docs/api/gulpfile.ts b/src/web/docs/api/gulpfile.ts
index 6cbae5ea2..139ae9241 100644
--- a/src/web/docs/api/gulpfile.ts
+++ b/src/web/docs/api/gulpfile.ts
@@ -108,12 +108,13 @@ gulp.task('doc:api:endpoints', () => {
 				paramDefs: extractDefs(ep.params),
 				res: sortParams(ep.res.map(p => parseParam(p))),
 				resDefs: extractDefs(ep.res),
-				kebab,
-				common: commonVars
 			};
 			langs.forEach(lang => {
 				pug.renderFile('./src/web/docs/api/endpoints/view.pug', Object.assign({}, vars, {
-					lang
+					lang,
+					title: ep.endpoint,
+					kebab,
+					common: commonVars
 				}), (renderErr, html) => {
 					if (renderErr) {
 						console.error(renderErr);
@@ -146,12 +147,13 @@ gulp.task('doc:api:entities', () => {
 				desc: entity.desc,
 				props: sortParams(entity.props.map(p => parseParam(p))),
 				propDefs: extractDefs(entity.props),
-				kebab,
-				common: commonVars
 			};
 			langs.forEach(lang => {
 				pug.renderFile('./src/web/docs/api/entities/view.pug', Object.assign({}, vars, {
-					lang
+					lang,
+					title: entity.name,
+					kebab,
+					common: commonVars
 				}), (renderErr, html) => {
 					if (renderErr) {
 						console.error(renderErr);
diff --git a/src/web/docs/gulpfile.ts b/src/web/docs/gulpfile.ts
index 6f2351dac..237784465 100644
--- a/src/web/docs/gulpfile.ts
+++ b/src/web/docs/gulpfile.ts
@@ -36,20 +36,30 @@ gulp.task('doc:docs', () => {
 			const [, name, lang] = file.match(/docs\/(.+?)\.(.+?)\.pug$/);
 			const vars = {
 				common: commonVars,
-				lang: lang
+				lang: lang,
+				title: fs.readFileSync(file, 'utf-8').match(/^h1 (.+?)\r?\n/)[1]
 			};
-			pug.renderFile(file, vars, (renderErr, html) => {
+			pug.renderFile(file, vars, (renderErr, content) => {
 				if (renderErr) {
 					console.error(renderErr);
 					return;
 				}
-				const htmlPath = `./built/web/docs/${lang}/${name}.html`;
-				mkdirp(path.dirname(htmlPath), (mkdirErr) => {
-					if (mkdirErr) {
-						console.error(mkdirErr);
+
+				pug.renderFile('./src/web/docs/layout.pug', Object.assign({}, vars, {
+					content
+				}), (renderErr2, html) => {
+					if (renderErr2) {
+						console.error(renderErr2);
 						return;
 					}
-					fs.writeFileSync(htmlPath, html, 'utf-8');
+					const htmlPath = `./built/web/docs/${lang}/${name}.html`;
+					mkdirp(path.dirname(htmlPath), (mkdirErr) => {
+						if (mkdirErr) {
+							console.error(mkdirErr);
+							return;
+						}
+						fs.writeFileSync(htmlPath, html, 'utf-8');
+					});
 				});
 			});
 		});
diff --git a/src/web/docs/index.en.pug b/src/web/docs/index.en.pug
index af0bba8b2..1fcc870d3 100644
--- a/src/web/docs/index.en.pug
+++ b/src/web/docs/index.en.pug
@@ -1,9 +1,3 @@
-extends ./layout.pug
+h1 Misskey Docs
 
-block title
-	| Misskey Docs
-
-block main
-	h1 Misskey Docs
-
-	p Welcome to docs of Misskey.
+p Welcome to docs of Misskey.
diff --git a/src/web/docs/index.ja.pug b/src/web/docs/index.ja.pug
index cd43045f6..4a0bf7fa1 100644
--- a/src/web/docs/index.ja.pug
+++ b/src/web/docs/index.ja.pug
@@ -1,9 +1,3 @@
-extends ./layout.pug
+h1 Misskey ドキュメント
 
-block title
-	| Misskey ドキュメント
-
-block main
-	h1 Misskey ドキュメント
-
-	p Misskeyのドキュメントへようこそ
+p Misskeyのドキュメントへようこそ
diff --git a/src/web/docs/layout.pug b/src/web/docs/layout.pug
index d6ecb4b6a..f8570dd3a 100644
--- a/src/web/docs/layout.pug
+++ b/src/web/docs/layout.pug
@@ -5,7 +5,7 @@ html(lang= lang)
 		meta(charset="UTF-8")
 		meta(name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no")
 		title
-			block title
+			| #{title} | Misskey Docs
 		link(rel="stylesheet" href="/assets/docs/style.css")
 		block meta
 
@@ -27,3 +27,5 @@ html(lang= lang)
 								li: a(href=`/docs/${lang}/api/endpoints/${common.kebab(endpoint)}`)= endpoint
 		main
 			block main
+			if content
+				| !{content}
diff --git a/src/web/docs/vars.ts b/src/web/docs/vars.ts
index 80fdc9a7d..37bc9d7b0 100644
--- a/src/web/docs/vars.ts
+++ b/src/web/docs/vars.ts
@@ -27,7 +27,7 @@ export default function() {
 				title: {}
 			};
 		}
-		vars['docs'][name]['title'][lang] = fs.readFileSync(x, 'utf-8').match(/\r?\n\th1 (.+?)\r?\n/)[1];
+		vars['docs'][name]['title'][lang] = fs.readFileSync(x, 'utf-8').match(/^h1 (.+?)\r?\n/)[1];
 	});
 
 	vars['kebab'] = string => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase();

From bb1d4e62371ec24b02c0a899286376266b02c3cd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 16 Dec 2017 01:06:28 +0900
Subject: [PATCH 0052/1250] :v:

---
 src/web/docs/api.ja.pug | 66 +++++++++++++++++++++++++++++++++++++++++
 src/web/docs/style.styl |  2 --
 src/web/docs/vars.ts    |  3 ++
 3 files changed, 69 insertions(+), 2 deletions(-)
 create mode 100644 src/web/docs/api.ja.pug

diff --git a/src/web/docs/api.ja.pug b/src/web/docs/api.ja.pug
new file mode 100644
index 000000000..588b926bd
--- /dev/null
+++ b/src/web/docs/api.ja.pug
@@ -0,0 +1,66 @@
+h1 Misskey API
+
+p MisskeyはWeb APIを公開しており、アプリケーションから様々な操作を行うことができます。
+
+section
+	h2 自分の所有するアカウントからAPIにアクセスする場合
+	p 「設定」で、APIにアクセスするのに必要なAPIキーを取得してください。
+	p APIにアクセスする際には、リクエストにAPIキーを「i」というパラメータ名で含めます。
+	p APIの詳しい使用法は「Misskey APIの利用」セクションをご覧ください。
+
+section
+	h2 アプリケーションからAPIにアクセスする場合
+	p
+		| あなたのWebサービスやアプリケーションなどからMisskey APIを利用したい場合、
+		| ユーザーにアカウントへのアクセスを許可してもらい、ユーザーのアクセストークンを取得する必要があります。
+	p アクセストークンを取得するまでの流れを説明します。
+
+	section
+		h3 1.アプリケーションを登録する
+		p まず、あなたのWebサービスやアプリケーションをMisskeyに登録します。
+		p デベロッパーセンターから登録を行ってください。
+		p 登録が済むとアプリケーションのシークレットキーが入手できます。
+
+	section
+		h3 2.ユーザーに認証させる
+		p あなたのアプリケーションを使ってもらうには、ユーザーにアカウントへのアクセスの許可をもらう必要があります。
+		p
+			| 認証セッションを開始するには、#{common.config.api_url}/auth/session/generate へパラメータに app_secret としてシークレットキーを含めたリクエストを送信します。
+			| リクエスト形式はJSONで、メソッドはPOSTです。
+			| レスポンスとして認証セッションのトークンや認証フォームのURLが取得できるので、認証フォームのURLをブラウザで表示し、ユーザーにフォームを提示してください。
+
+		p
+			| あなたのアプリがコールバックURLを設定している場合、
+			| ユーザーがアプリの連携を許可すると設定しているコールバックURLに token という名前でセッションのトークンが含まれたクエリを付けてリダイレクトします。
+
+		p
+			| あなたのアプリがコールバックURLを設定していない場合、ユーザーがアプリの連携を許可したことを(何らかの方法で(たとえばボタンを押させるなど))確認出来るようにしてください。
+
+	section
+		h3 3.ユーザーのアクセストークンを取得する
+		p ユーザーが連携を許可したら、#{common.config.api_url}/auth/session/userkey へ次のパラメータを含むリクエストを送信します:
+		table
+			thead
+				tr
+					th 名前
+					th 型
+					th 説明
+			tbody
+				tr
+					td app_secret
+					td string
+					td アプリのシークレットキー
+				tr
+					td token
+					td string
+					td セッションのトークン
+		p 上手くいけば、認証したユーザーのアクセストークンがレスポンスとして取得できます。おめでとうございます!
+
+	p アクセストークンが取得できたら、「ユーザーのアクセストークン+アプリのシークレットキーをsha256したもの」を「i」というパラメータでリクエストに含めるだけで、APIにアクセスできます。
+
+	p APIの詳しい使用法は「Misskey APIの利用」セクションをご覧ください。
+
+section
+	h2 Misskey APIの利用
+	p APIはすべてリクエストのパラメータ・レスポンスともにJSON形式です。また、すべてのエンドポイントはPOSTメソッドのみ受け付けます。
+	p APIリファレンスもご確認ください。
diff --git a/src/web/docs/style.styl b/src/web/docs/style.styl
index 2e2f9f574..f222e65bf 100644
--- a/src/web/docs/style.styl
+++ b/src/web/docs/style.styl
@@ -77,8 +77,6 @@ table
 
 	tbody
 		tr
-			border-bottom dashed 1px #eee
-
 			&:nth-child(odd)
 				background #fbfbfb
 
diff --git a/src/web/docs/vars.ts b/src/web/docs/vars.ts
index 37bc9d7b0..ffa262a06 100644
--- a/src/web/docs/vars.ts
+++ b/src/web/docs/vars.ts
@@ -1,6 +1,7 @@
 import * as fs from 'fs';
 import * as glob from 'glob';
 import * as yaml from 'js-yaml';
+import config from '../../conf';
 
 export default function() {
 	const vars = {};
@@ -32,5 +33,7 @@ export default function() {
 
 	vars['kebab'] = string => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase();
 
+	vars['config'] = config;
+
 	return vars;
 }

From 4c69544c582b1bfb96641857582a123e4d85ba16 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 16 Dec 2017 04:10:05 +0900
Subject: [PATCH 0053/1250] :v:

---
 src/web/docs/api/getting-started.md | 73 -----------------------------
 src/web/docs/api/library.md         |  8 ----
 src/web/docs/layout.pug             |  9 ++--
 src/web/docs/link-to-twitter.md     |  9 ----
 4 files changed, 5 insertions(+), 94 deletions(-)
 delete mode 100644 src/web/docs/api/getting-started.md
 delete mode 100644 src/web/docs/api/library.md
 delete mode 100644 src/web/docs/link-to-twitter.md

diff --git a/src/web/docs/api/getting-started.md b/src/web/docs/api/getting-started.md
deleted file mode 100644
index e13659914..000000000
--- a/src/web/docs/api/getting-started.md
+++ /dev/null
@@ -1,73 +0,0 @@
-Getting Started
-================================================================
-MisskeyはREST APIやStreaming APIを提供しており、プログラムからMisskeyの全ての機能を利用することができます。
-それらのAPIを利用するには、まずAPIを利用したいアカウントのアクセストークンを取得する必要があります:
-
-自分のアクセストークンを取得したい場合
-----------------------------------------------------------------
-自分自身のアクセストークンは、設定 > API で確認できます。
-<p class="tip">
-	アカウントを乗っ取られてしまう可能性があるため、トークンは第三者に教えないでください(アプリなどにも入力しないでください)。<br>
-	万が一トークンが漏れたりその可能性がある場合は トークンを再生成できます。(副作用として、ログインしているすべてのデバイスでログアウトが発生します)
-</p>
-
-他人のアクセストークンを取得する
-----------------------------------------------------------------
-不特定多数のユーザーからAPIを利用したい場合、アプリケーションを作成します。
-アプリケーションを作成すると、ユーザーが連携を許可した時に、そのユーザーのアクセストークンを取得することができます。
-
-アプリケーションを作成してアクセストークンを取得するまでの流れを説明します。
-
-### アプリケーションを作成する
-まずはあなたのアプリケーションを作成しましょう。
-				| <a href=#{dev_url} target="_blank">デベロッパーセンター</a>にアクセスし、アプリ > アプリ作成 に進みます。
-				br
-				| 次に、フォームに必要事項を記入します:
-			dl
-				dt アプリケーション名
-				dd あなたのアプリケーションの名前。
-				dt Named ID
-				dd アプリを識別する/a-z-/で構成されたID。
-				dt アプリの概要
-				dd アプリの簡単な説明を入力してください。
-				dt コールバックURL
-				dd あなたのアプリケーションがWebアプリケーションである場合、ユーザーが後述するフォームで認証を終えた際にリダイレクトするURLを設定できます。
-				dt 権限
-				dd アプリケーションが要求する権限。ここで要求した機能だけがAPIからアクセスできます。
-			p.tip
-				| 権限はアプリ作成後も変更できますが、新たな権限を付与する場合、その時点で関連付けられているユーザーはすべて無効になります。
-			p
-				| アプリケーションを作成すると、作ったアプリの管理ページに進みます。
-				br
-				| アプリのシークレットキー(App Secret)が表示されていますので、メモしておいてください。
-			p.tip
-				| アプリに成りすまされる可能性があるため、極力このシークレットキーは公開しないようにしてください。
-
-		section
-			h3 ユーザーに認証させる
-			p あなたのアプリを使ってもらうには、ユーザーにアカウントへアクセスすることを許可してもらい、Misskeyにそのユーザーのアクセストークンを発行してもらう必要があります。
-			p 認証セッションを開始するには、<code>#{api_url}/auth/session/generate</code>へパラメータに<code>app_secret</code>としてApp Secretを含めたリクエストを送信します。
-			p
-				| そうすると、レスポンスとして認証セッションのトークンや認証フォームのURLが取得できます。
-				br
-				| この認証フォームのURLをブラウザで表示し、ユーザーにフォームを表示してください。
-			section
-				h4 あなたのアプリがコールバックURLを設定している場合
-				p ユーザーがアプリの連携を許可すると設定しているコールバックURLに<code>token</code>という名前でセッションのトークンが含まれたクエリを付けてリダイレクトします。
-			section
-				h4 あなたのアプリがコールバックURLを設定していない場合
-				p ユーザーがアプリの連携を許可したことを(何らかの方法で(たとえばボタンを押させるなど))確認出来るようにしてください。
-			p
-				| 次に、<code>#{api_url}/auth/session/userkey</code>へ<code>app_secret</code>としてApp Secretを、<code>token</code>としてセッションのトークンをパラメータとして付与したリクエストを送信してください。
-				br
-				| 上手くいけば、認証したユーザーのアクセストークンがレスポンスとして取得できます。おめでとうございます!
-			p
-				| 以降アクセストークンは、<strong>ユーザーのアクセストークン+アプリのシークレットキーをsha256したもの</strong>として扱います。
-
-	p アクセストークンを取得できたら、あとは簡単です。REST APIなら、リクエストにアクセストークンを<code>i</code>としてパラメータに含めるだけです。
-
-	section
-		h2 リクエスト形式
-		p <code>application/json</code>を受け付けます。
-		p.tip
-			| 現在<code>application/x-www-form-urlencoded</code>も受け付けていますが、将来的にこのサポートはされなくなる予定です。
diff --git a/src/web/docs/api/library.md b/src/web/docs/api/library.md
deleted file mode 100644
index 71ddbe345..000000000
--- a/src/web/docs/api/library.md
+++ /dev/null
@@ -1,8 +0,0 @@
-ライブラリ
-================================================================
-
-Misskey APIを便利に利用するためのライブラリ一覧です。
-
-.NET
-----------------------------------------------------------------
-* **[Misq (公式)](https://github.com/syuilo/Misq)**
diff --git a/src/web/docs/layout.pug b/src/web/docs/layout.pug
index f8570dd3a..ac3743d2f 100644
--- a/src/web/docs/layout.pug
+++ b/src/web/docs/layout.pug
@@ -3,28 +3,29 @@ doctype html
 html(lang= lang)
 	head
 		meta(charset="UTF-8")
-		meta(name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no")
+		meta(name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no")
 		title
 			| #{title} | Misskey Docs
 		link(rel="stylesheet" href="/assets/docs/style.css")
 		block meta
+		base(href=`/docs/${lang}/`)
 
 	body
 		nav
 			ul
 				each doc in common.docs
-					li: a(href=`/docs/${lang}/${doc.name}`)= doc.title[lang] || doc.title['ja']
+					li: a(href=`./${doc.name}`)= doc.title[lang] || doc.title['ja']
 			section
 				h2 API
 				ul
 					li Entities
 						ul
 							each entity in common.entities
-								li: a(href=`/docs/${lang}/api/entities/${common.kebab(entity)}`)= entity
+								li: a(href=`./api/entities/${common.kebab(entity)}`)= entity
 					li Endpoints
 						ul
 							each endpoint in common.endpoints
-								li: a(href=`/docs/${lang}/api/endpoints/${common.kebab(endpoint)}`)= endpoint
+								li: a(href=`./api/endpoints/${common.kebab(endpoint)}`)= endpoint
 		main
 			block main
 			if content
diff --git a/src/web/docs/link-to-twitter.md b/src/web/docs/link-to-twitter.md
deleted file mode 100644
index 77fb74457..000000000
--- a/src/web/docs/link-to-twitter.md
+++ /dev/null
@@ -1,9 +0,0 @@
-Twitterと連携する
-================================================================
-
-設定 -> Twitter から、お使いのMisskeyアカウントとお使いのTwitterアカウントを関連付けることができます。
-アカウントの関連付けを行うと、プロフィールにTwitterアカウントへのリンクが表示されたりなどします。
-
-MisskeyがあなたのTwitterアカウントでツイートしたり誰かをフォローしたりといったことは、
-一切行いませんのでご安心ください。(Misskeyはそのような権限を取得しないので、行おうと思っても行えません)
-Twitterのアプリケーション認証フォームでこの権限の詳細を確認することができます。また、いつでも連携を取り消すことができます。

From 1a123fc78c8028475cfbab460ec5787d1916f3cb Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 16 Dec 2017 05:04:02 +0900
Subject: [PATCH 0054/1250] :v:

---
 .../docs/api/endpoints/posts/timeline.yaml    | 32 +++++++++++++++++++
 src/web/docs/api/endpoints/view.pug           |  7 ++--
 src/web/docs/api/gulpfile.ts                  |  4 +--
 3 files changed, 38 insertions(+), 5 deletions(-)
 create mode 100644 src/web/docs/api/endpoints/posts/timeline.yaml

diff --git a/src/web/docs/api/endpoints/posts/timeline.yaml b/src/web/docs/api/endpoints/posts/timeline.yaml
new file mode 100644
index 000000000..e1d78c082
--- /dev/null
+++ b/src/web/docs/api/endpoints/posts/timeline.yaml
@@ -0,0 +1,32 @@
+endpoint: "posts/timeline"
+
+desc:
+  ja: "タイムラインを取得します。"
+  en: "Get your timeline."
+
+params:
+  - name: "limit"
+    type: "number"
+    optional: true
+    desc:
+      ja: "取得する最大の数"
+  - name: "since_id"
+    type: "id(Post)"
+    optional: true
+    desc:
+      ja: "指定すると、この投稿を基点としてより新しい投稿を取得します"
+  - name: "max_id"
+    type: "id(Post)"
+    optional: true
+    desc:
+      ja: "指定すると、この投稿を基点としてより古い投稿を取得します"
+  - name: "since_date"
+    type: "number"
+    optional: true
+    desc:
+      ja: "指定した時間を基点としてより新しい投稿を取得します。数値は、1970 年 1 月 1 日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。"
+  - name: "max_date"
+    type: "number"
+    optional: true
+    desc:
+      ja: "指定した時間を基点としてより古い投稿を取得します。数値は、1970 年 1 月 1 日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。"
diff --git a/src/web/docs/api/endpoints/view.pug b/src/web/docs/api/endpoints/view.pug
index d456022f6..62a6f59ed 100644
--- a/src/web/docs/api/endpoints/view.pug
+++ b/src/web/docs/api/endpoints/view.pug
@@ -21,6 +21,7 @@ block main
 					h3= paramDef.name
 					+propTable(paramDef.params)
 
-	section
-		h2 Response
-		+propTable(res)
+	if res
+		section
+			h2 Response
+			+propTable(res)
diff --git a/src/web/docs/api/gulpfile.ts b/src/web/docs/api/gulpfile.ts
index 139ae9241..908280453 100644
--- a/src/web/docs/api/gulpfile.ts
+++ b/src/web/docs/api/gulpfile.ts
@@ -106,8 +106,8 @@ gulp.task('doc:api:endpoints', () => {
 				desc: ep.desc,
 				params: sortParams(ep.params.map(p => parseParam(p))),
 				paramDefs: extractDefs(ep.params),
-				res: sortParams(ep.res.map(p => parseParam(p))),
-				resDefs: extractDefs(ep.res),
+				res: ep.res ? sortParams(ep.res.map(p => parseParam(p))) : null,
+				resDefs: ep.res ? extractDefs(ep.res) : null,
 			};
 			langs.forEach(lang => {
 				pug.renderFile('./src/web/docs/api/endpoints/view.pug', Object.assign({}, vars, {

From 9f47060e0a9392c0777528f6cf7bcc338bd72a8f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 16 Dec 2017 22:28:51 +0900
Subject: [PATCH 0055/1250] v3390

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9ab4698ab..a8e1fee78 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3390 (2017/12/16)
+-----------------
+* ドキュメントなど
+
 3347 (2017/12/11)
 -----------------
 * バグ修正
diff --git a/package.json b/package.json
index 69090349e..29ba72bbe 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3347",
+	"version": "0.0.3390",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 563df8a7a5b8e3ed6b27a4537d63b82a4447852f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 17 Dec 2017 01:41:22 +0900
Subject: [PATCH 0056/1250] :v:

---
 docs/setup.en.md                            |  2 +-
 docs/setup.ja.md                            |  2 +-
 gulpfile.ts                                 | 15 ++++-----------
 package.json                                |  2 --
 src/config.ts                               |  4 ++--
 src/web/app/common/tags/introduction.tag    |  2 +-
 src/web/app/common/tags/nav-links.tag       |  5 ++++-
 src/web/app/common/tags/signup.tag          |  4 +++-
 src/web/app/common/tags/twitter-setting.tag |  2 +-
 src/web/app/desktop/tags/pages/entrance.tag |  2 +-
 src/web/app/mobile/tags/ui.tag              |  4 +++-
 src/web/docs/about.en.pug                   |  3 +++
 src/web/docs/about.ja.pug                   |  3 +++
 src/web/docs/api/endpoints/view.pug         |  2 +-
 src/web/docs/api/entities/view.pug          |  2 +-
 src/web/docs/api/mixins.pug                 |  4 ++--
 src/web/docs/gulpfile.ts                    |  2 +-
 src/web/docs/layout.pug                     |  4 ++--
 src/web/docs/server.ts                      | 21 +++++++++++++++++++++
 src/web/docs/tou.ja.pug                     |  3 +++
 src/web/docs/tou.md                         |  4 ----
 src/web/server.ts                           | 11 +++++------
 tools/letsencrypt/get-cert.sh               |  2 +-
 webpack/plugins/consts.ts                   |  2 +-
 24 files changed, 65 insertions(+), 42 deletions(-)
 create mode 100644 src/web/docs/about.en.pug
 create mode 100644 src/web/docs/about.ja.pug
 create mode 100644 src/web/docs/server.ts
 create mode 100644 src/web/docs/tou.ja.pug
 delete mode 100644 src/web/docs/tou.md

diff --git a/docs/setup.en.md b/docs/setup.en.md
index b81245d89..13b0bdaeb 100644
--- a/docs/setup.en.md
+++ b/docs/setup.en.md
@@ -24,7 +24,7 @@ Note that Misskey uses following subdomains:
 
 * **api**.*{primary domain}*
 * **auth**.*{primary domain}*
-* **about**.*{primary domain}*
+* **docs**.*{primary domain}*
 * **ch**.*{primary domain}*
 * **stats**.*{primary domain}*
 * **status**.*{primary domain}*
diff --git a/docs/setup.ja.md b/docs/setup.ja.md
index 1662d1ee5..564c79097 100644
--- a/docs/setup.ja.md
+++ b/docs/setup.ja.md
@@ -25,7 +25,7 @@ Misskeyは以下のサブドメインを使います:
 
 * **api**.*{primary domain}*
 * **auth**.*{primary domain}*
-* **about**.*{primary domain}*
+* **docs**.*{primary domain}*
 * **ch**.*{primary domain}*
 * **stats**.*{primary domain}*
 * **status**.*{primary domain}*
diff --git a/gulpfile.ts b/gulpfile.ts
index e7d477061..3b7a12640 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -9,7 +9,6 @@ import * as gulp from 'gulp';
 import * as gutil from 'gulp-util';
 import * as ts from 'gulp-typescript';
 import tslint from 'gulp-tslint';
-import * as es from 'event-stream';
 import cssnano = require('gulp-cssnano');
 import * as uglifyComposer from 'gulp-uglify/composer';
 import pug = require('gulp-pug');
@@ -74,16 +73,10 @@ gulp.task('build:ts', () => {
 });
 
 gulp.task('build:copy', () =>
-	es.merge(
-		gulp.src([
-			'./src/**/assets/**/*',
-			'!./src/web/app/**/assets/**/*'
-		]).pipe(gulp.dest('./built/')) as any,
-		gulp.src([
-			'./src/web/about/**/*',
-			'!./src/web/about/**/*.pug'
-		]).pipe(gulp.dest('./built/web/about/')) as any
-	)
+	gulp.src([
+		'./src/**/assets/**/*',
+		'!./src/web/app/**/assets/**/*'
+	]).pipe(gulp.dest('./built/'))
 );
 
 gulp.task('test', ['lint', 'mocha']);
diff --git a/package.json b/package.json
index 29ba72bbe..8c0cf340d 100644
--- a/package.json
+++ b/package.json
@@ -38,7 +38,6 @@
 		"@types/debug": "0.0.30",
 		"@types/deep-equal": "1.0.1",
 		"@types/elasticsearch": "5.0.19",
-		"@types/event-stream": "3.3.33",
 		"@types/eventemitter3": "2.0.2",
 		"@types/express": "4.0.39",
 		"@types/gm": "1.17.33",
@@ -99,7 +98,6 @@
 		"diskusage": "0.2.4",
 		"elasticsearch": "14.0.0",
 		"escape-regexp": "0.0.1",
-		"event-stream": "3.3.4",
 		"eventemitter3": "3.0.0",
 		"exif-js": "2.3.0",
 		"express": "4.16.2",
diff --git a/src/config.ts b/src/config.ts
index 3ff800758..3ffefe278 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -101,7 +101,7 @@ type Mixin = {
 	secondary_scheme: string;
 	api_url: string;
 	auth_url: string;
-	about_url: string;
+	docs_url: string;
 	ch_url: string;
 	stats_url: string;
 	status_url: string;
@@ -131,7 +131,7 @@ export default function load() {
 	mixin.auth_url = `${mixin.scheme}://auth.${mixin.host}`;
 	mixin.ch_url = `${mixin.scheme}://ch.${mixin.host}`;
 	mixin.dev_url = `${mixin.scheme}://dev.${mixin.host}`;
-	mixin.about_url = `${mixin.scheme}://about.${mixin.host}`;
+	mixin.docs_url = `${mixin.scheme}://docs.${mixin.host}`;
 	mixin.stats_url = `${mixin.scheme}://stats.${mixin.host}`;
 	mixin.status_url = `${mixin.scheme}://status.${mixin.host}`;
 	mixin.drive_url = `${mixin.secondary_scheme}://file.${mixin.secondary_host}`;
diff --git a/src/web/app/common/tags/introduction.tag b/src/web/app/common/tags/introduction.tag
index 3256688d1..28afc6fa4 100644
--- a/src/web/app/common/tags/introduction.tag
+++ b/src/web/app/common/tags/introduction.tag
@@ -3,7 +3,7 @@
 		<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={ _ABOUT_URL_ } target="_blank">もっと知りたい方はこちら</a></p>
+		<p><a href={ _DOCS_URL_ } target="_blank">もっと知りたい方はこちら</a></p>
 	</article>
 	<style>
 		:scope
diff --git a/src/web/app/common/tags/nav-links.tag b/src/web/app/common/tags/nav-links.tag
index 71f0453db..ea122575a 100644
--- a/src/web/app/common/tags/nav-links.tag
+++ b/src/web/app/common/tags/nav-links.tag
@@ -1,7 +1,10 @@
 <mk-nav-links>
-	<a href={ _ABOUT_URL_ }>%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>
+	<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>
 		:scope
 			display inline
 	</style>
+	<script>
+		this.aboutUrl = `${_DOCS_URL_}/${_LANG_}/about`;
+	</script>
 </mk-nav-links>
diff --git a/src/web/app/common/tags/signup.tag b/src/web/app/common/tags/signup.tag
index 4816fe66d..b488efb92 100644
--- a/src/web/app/common/tags/signup.tag
+++ b/src/web/app/common/tags/signup.tag
@@ -34,7 +34,7 @@
 		</label>
 		<label class="agree-tou">
 			<input name="agree-tou" type="checkbox" autocomplete="off" required="required"/>
-			<p><a href="https://github.com/syuilo/misskey/blob/master/src/docs/tou.md" target="_blank">利用規約</a>に同意する</p>
+			<p><a href={ touUrl } target="_blank">利用規約</a>に同意する</p>
 		</label>
 		<button onclick={ onsubmit }>%i18n:common.tags.mk-signup.create%</button>
 	</form>
@@ -182,6 +182,8 @@
 		this.passwordRetypeState = null;
 		this.recaptchaed = false;
 
+		this.aboutUrl = `${_DOCS_URL_}/${_LANG_}/tou`;
+
 		window.onRecaptchaed = () => {
 			this.recaptchaed = true;
 			this.update();
diff --git a/src/web/app/common/tags/twitter-setting.tag b/src/web/app/common/tags/twitter-setting.tag
index 3b70505ba..4d57cfa55 100644
--- a/src/web/app/common/tags/twitter-setting.tag
+++ b/src/web/app/common/tags/twitter-setting.tag
@@ -1,5 +1,5 @@
 <mk-twitter-setting>
-	<p>%i18n:common.tags.mk-twitter-setting.description%<a href={ _ABOUT_URL_ + '/link-to-twitter' } target="_blank">%i18n:common.tags.mk-twitter-setting.detail%</a></p>
+	<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>
diff --git a/src/web/app/desktop/tags/pages/entrance.tag b/src/web/app/desktop/tags/pages/entrance.tag
index 44548e418..b07b22c80 100644
--- a/src/web/app/desktop/tags/pages/entrance.tag
+++ b/src/web/app/desktop/tags/pages/entrance.tag
@@ -150,7 +150,7 @@
 </mk-entrance>
 
 <mk-entrance-signin>
-	<a class="help" href={ _ABOUT_URL_ + '/help' } title="お困りですか?">%fa:question%</a>
+	<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' }/>
 			<p>{ user ? user.name : 'アカウント' }</p>
diff --git a/src/web/app/mobile/tags/ui.tag b/src/web/app/mobile/tags/ui.tag
index 62e128489..621f89f33 100644
--- a/src/web/app/mobile/tags/ui.tag
+++ b/src/web/app/mobile/tags/ui.tag
@@ -248,7 +248,7 @@
 				<li><a href="/i/settings">%fa:cog%%i18n:mobile.tags.mk-ui-nav.settings%%fa:angle-right%</a></li>
 			</ul>
 		</div>
-		<a href={ _ABOUT_URL_ }><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>
 	<style>
 		:scope
@@ -359,6 +359,8 @@
 		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);
diff --git a/src/web/docs/about.en.pug b/src/web/docs/about.en.pug
new file mode 100644
index 000000000..893d9dd6a
--- /dev/null
+++ b/src/web/docs/about.en.pug
@@ -0,0 +1,3 @@
+h1 About Misskey
+
+p Misskey is a mini blog SNS.
diff --git a/src/web/docs/about.ja.pug b/src/web/docs/about.ja.pug
new file mode 100644
index 000000000..fec933b0c
--- /dev/null
+++ b/src/web/docs/about.ja.pug
@@ -0,0 +1,3 @@
+h1 Misskeyについて
+
+p MisskeyはミニブログSNSです。
diff --git a/src/web/docs/api/endpoints/view.pug b/src/web/docs/api/endpoints/view.pug
index 62a6f59ed..9ba1c4e85 100644
--- a/src/web/docs/api/endpoints/view.pug
+++ b/src/web/docs/api/endpoints/view.pug
@@ -2,7 +2,7 @@ extends ../../layout.pug
 include ../mixins
 
 block meta
-	link(rel="stylesheet" href="/assets/docs/api/endpoints/style.css")
+	link(rel="stylesheet" href="/assets/api/endpoints/style.css")
 
 block main
 	h1= endpoint
diff --git a/src/web/docs/api/entities/view.pug b/src/web/docs/api/entities/view.pug
index 57c6d4cad..6fc05bd55 100644
--- a/src/web/docs/api/entities/view.pug
+++ b/src/web/docs/api/entities/view.pug
@@ -2,7 +2,7 @@ extends ../../layout.pug
 include ../mixins
 
 block meta
-	link(rel="stylesheet" href="/assets/docs/api/entities/style.css")
+	link(rel="stylesheet" href="/assets/api/entities/style.css")
 
 block main
 	h1= name
diff --git a/src/web/docs/api/mixins.pug b/src/web/docs/api/mixins.pug
index 3ddd7cb48..518069857 100644
--- a/src/web/docs/api/mixins.pug
+++ b/src/web/docs/api/mixins.pug
@@ -14,13 +14,13 @@ mixin propTable(props)
 						if prop.kind == 'id'
 							if prop.entity
 								|  (
-								a(href=`/docs/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity
+								a(href=`/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity
 								|  ID)
 							else
 								|  (ID)
 						else if prop.kind == 'entity'
 							|   (
-							a(href=`/docs/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity
+							a(href=`/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity
 							| )
 						else if prop.kind == 'object'
 							if prop.def
diff --git a/src/web/docs/gulpfile.ts b/src/web/docs/gulpfile.ts
index 237784465..61e44a1dc 100644
--- a/src/web/docs/gulpfile.ts
+++ b/src/web/docs/gulpfile.ts
@@ -70,5 +70,5 @@ gulp.task('doc:styles', () =>
 	gulp.src('./src/web/docs/**/*.styl')
 		.pipe(stylus())
 		.pipe((cssnano as any)())
-		.pipe(gulp.dest('./built/web/assets/docs/'))
+		.pipe(gulp.dest('./built/web/docs/assets/'))
 );
diff --git a/src/web/docs/layout.pug b/src/web/docs/layout.pug
index ac3743d2f..bc9710d7c 100644
--- a/src/web/docs/layout.pug
+++ b/src/web/docs/layout.pug
@@ -6,9 +6,9 @@ html(lang= lang)
 		meta(name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no")
 		title
 			| #{title} | Misskey Docs
-		link(rel="stylesheet" href="/assets/docs/style.css")
+		link(rel="stylesheet" href="/assets/style.css")
 		block meta
-		base(href=`/docs/${lang}/`)
+		base(href=`/${lang}/`)
 
 	body
 		nav
diff --git a/src/web/docs/server.ts b/src/web/docs/server.ts
new file mode 100644
index 000000000..b2e50457e
--- /dev/null
+++ b/src/web/docs/server.ts
@@ -0,0 +1,21 @@
+/**
+ * Docs Server
+ */
+
+import * as express from 'express';
+
+/**
+ * Init app
+ */
+const app = express();
+app.disable('x-powered-by');
+
+app.use('/assets', express.static(`${__dirname}/assets`));
+
+/**
+ * Routing
+ */
+app.get(/^\/([a-z_\-\/]+?)$/, (req, res) =>
+	res.sendFile(`${__dirname}/${req.params[0]}.html`));
+
+module.exports = app;
diff --git a/src/web/docs/tou.ja.pug b/src/web/docs/tou.ja.pug
new file mode 100644
index 000000000..7663258f8
--- /dev/null
+++ b/src/web/docs/tou.ja.pug
@@ -0,0 +1,3 @@
+h1 利用規約
+
+p 公序良俗に反する行為はおやめください。
diff --git a/src/web/docs/tou.md b/src/web/docs/tou.md
deleted file mode 100644
index fbf87867b..000000000
--- a/src/web/docs/tou.md
+++ /dev/null
@@ -1,4 +0,0 @@
-利用規約
-================================================================
-
-公序良俗に反する行為はおやめください。
diff --git a/src/web/server.ts b/src/web/server.ts
index 38e87754f..062d1f197 100644
--- a/src/web/server.ts
+++ b/src/web/server.ts
@@ -10,6 +10,9 @@ import * as express from 'express';
 import * as bodyParser from 'body-parser';
 import * as favicon from 'serve-favicon';
 import * as compression from 'compression';
+import vhost = require('vhost');
+
+import config from '../conf';
 
 /**
  * Init app
@@ -17,6 +20,8 @@ import * as compression from 'compression';
 const app = express();
 app.disable('x-powered-by');
 
+app.use(vhost(`docs.${config.host}`, require('./docs/server')));
+
 app.use(bodyParser.urlencoded({ extended: true }));
 app.use(bodyParser.json({
 	type: ['application/json', 'text/plain']
@@ -63,12 +68,6 @@ app.get('/manifest.json', (req, res) =>
  */
 app.get(/\/api:url/, require('./service/url-preview'));
 
-/**
- * Docs
- */
-app.get(/^\/docs\/([a-z_\-\/]+?)$/, (req, res) =>
-	res.sendFile(`${__dirname}/docs/${req.params[0]}.html`));
-
 /**
  * Routing
  */
diff --git a/tools/letsencrypt/get-cert.sh b/tools/letsencrypt/get-cert.sh
index 409f2fa5e..d44deb144 100644
--- a/tools/letsencrypt/get-cert.sh
+++ b/tools/letsencrypt/get-cert.sh
@@ -4,7 +4,7 @@ certbot certonly --standalone\
   -d $1\
   -d api.$1\
   -d auth.$1\
-  -d about.$1\
+  -d docs.$1\
   -d ch.$1\
   -d stats.$1\
   -d status.$1\
diff --git a/webpack/plugins/consts.ts b/webpack/plugins/consts.ts
index 7d1ff7c8d..6e18fa296 100644
--- a/webpack/plugins/consts.ts
+++ b/webpack/plugins/consts.ts
@@ -16,7 +16,7 @@ export default lang => {
 		_VERSION_: version,
 		_STATUS_URL_: config.status_url,
 		_STATS_URL_: config.stats_url,
-		_ABOUT_URL_: config.about_url,
+		_DOCS_URL_: config.docs_url,
 		_API_URL_: config.api_url,
 		_DEV_URL_: config.dev_url,
 		_CH_URL_: config.ch_url,

From ca46e5e52bb385d6a645ad2316f685fe71dc598a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 17 Dec 2017 02:56:53 +0900
Subject: [PATCH 0057/1250] v3392

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index a8e1fee78..f1f0c8138 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3392 (2017/12/17)
+-----------------
+* ドキュメントなど
+
 3390 (2017/12/16)
 -----------------
 * ドキュメントなど
diff --git a/package.json b/package.json
index 8c0cf340d..6e7bf7ef5 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3390",
+	"version": "0.0.3392",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 2bbfffefce04a0b898143e340beae34deec3304f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 17 Dec 2017 04:02:30 +0900
Subject: [PATCH 0058/1250] :v:

---
 locales/en.yml                        | 18 ++++++++++++++++++
 webpack/langs.ts => locales/index.ts  |  6 +++---
 locales/ja.yml                        | 18 ++++++++++++++++++
 src/web/docs/api/endpoints/style.styl | 15 ++++++++++++++-
 src/web/docs/api/endpoints/view.pug   | 11 ++++++++---
 src/web/docs/api/entities/view.pug    |  2 +-
 src/web/docs/api/gulpfile.ts          |  9 +++++++--
 src/web/docs/api/mixins.pug           | 14 +++++++++-----
 src/web/docs/gulpfile.ts              |  3 ++-
 src/web/docs/layout.pug               | 12 +++++++++---
 src/web/docs/style.styl               | 17 ++++++++---------
 src/web/docs/vars.ts                  |  7 +++++--
 webpack/webpack.config.ts             |  4 ++--
 13 files changed, 104 insertions(+), 32 deletions(-)
 rename webpack/langs.ts => locales/index.ts (85%)

diff --git a/locales/en.yml b/locales/en.yml
index b49af68bd..57e0c4116 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -183,6 +183,24 @@ common:
     mk-uploader:
       waiting: "Waiting"
 
+docs:
+  edit-this-page-on-github: "Caught a mistake or want to contribute to the documentation? "
+  edit-this-page-on-github-link: "Edit this page on Github!"
+
+  api:
+    entities:
+      properties: "Properties"
+    endpoints:
+      params: "Parameters"
+      res: "Response"
+    props:
+      name: "Name"
+      type: "Type"
+      optional: "Optional"
+      description: "Description"
+      yes: "Yes"
+      no: "No"
+
 ch:
   tags:
     mk-index:
diff --git a/webpack/langs.ts b/locales/index.ts
similarity index 85%
rename from webpack/langs.ts
rename to locales/index.ts
index 409b25504..0593af366 100644
--- a/webpack/langs.ts
+++ b/locales/index.ts
@@ -10,12 +10,12 @@ const loadLang = lang => yaml.safeLoad(
 
 const native = loadLang('ja');
 
-const langs = Object.entries({
+const langs = {
 	'en': loadLang('en'),
 	'ja': native
-});
+};
 
-langs.map(([, locale]) => {
+Object.entries(langs).map(([, locale]) => {
 	// Extend native language (Japanese)
 	locale = Object.assign({}, native, locale);
 });
diff --git a/locales/ja.yml b/locales/ja.yml
index afafa5a63..ee52f0716 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -183,6 +183,24 @@ common:
     mk-uploader:
       waiting: "待機中"
 
+docs:
+  edit-this-page-on-github: "間違いや改善点を見つけましたか?"
+  edit-this-page-on-github-link: "このページをGitHubで編集"
+
+  api:
+    entities:
+      properties: "プロパティ"
+    endpoints:
+      params: "パラメータ"
+      res: "レスポンス"
+    props:
+      name: "名前"
+      type: "型"
+      optional: "オプション"
+      description: "説明"
+      yes: "はい"
+      no: "いいえ"
+
 ch:
   tags:
     mk-index:
diff --git a/src/web/docs/api/endpoints/style.styl b/src/web/docs/api/endpoints/style.styl
index 07fb7ec2a..2af9fe9a7 100644
--- a/src/web/docs/api/endpoints/style.styl
+++ b/src/web/docs/api/endpoints/style.styl
@@ -1,8 +1,21 @@
 @import "../style"
 
 #url
-	padding 8px 12px
+	padding 8px 12px 8px 8px
 	font-family Consolas, 'Courier New', Courier, Monaco, monospace
 	color #fff
 	background #222e40
 	border-radius 4px
+
+	> .method
+		display inline-block
+		margin 0 8px 0 0
+		padding 0 6px
+		color #f4fcff
+		background #17afc7
+		border-radius 4px
+		user-select none
+		pointer-events none
+
+	> .host
+		opacity 0.7
diff --git a/src/web/docs/api/endpoints/view.pug b/src/web/docs/api/endpoints/view.pug
index 9ba1c4e85..90084ab27 100644
--- a/src/web/docs/api/endpoints/view.pug
+++ b/src/web/docs/api/endpoints/view.pug
@@ -7,12 +7,17 @@ block meta
 block main
 	h1= endpoint
 
-	p#url= url
+	p#url
+		span.method POST
+		span.host
+			= url.host
+			| /
+		span.path= url.path
 
 	p#desc= desc[lang] || desc['ja']
 
 	section
-		h2 Params
+		h2= common.i18n[lang]['docs']['api']['endpoints']['params']
 		+propTable(params)
 
 		if paramDefs
@@ -23,5 +28,5 @@ block main
 
 	if res
 		section
-			h2 Response
+			h2= common.i18n[lang]['docs']['api']['endpoints']['res']
 			+propTable(res)
diff --git a/src/web/docs/api/entities/view.pug b/src/web/docs/api/entities/view.pug
index 6fc05bd55..99e786c69 100644
--- a/src/web/docs/api/entities/view.pug
+++ b/src/web/docs/api/entities/view.pug
@@ -10,7 +10,7 @@ block main
 	p#desc= desc[lang] || desc['ja']
 
 	section
-		h2 Properties
+		h2= common.i18n[lang]['docs']['api']['entities']['properties']
 		+propTable(props)
 
 		if propDefs
diff --git a/src/web/docs/api/gulpfile.ts b/src/web/docs/api/gulpfile.ts
index 908280453..2e8409c59 100644
--- a/src/web/docs/api/gulpfile.ts
+++ b/src/web/docs/api/gulpfile.ts
@@ -16,7 +16,7 @@ import generateVars from '../vars';
 
 const commonVars = generateVars();
 
-const langs = ['ja', 'en'];
+const langs = Object.keys(commonVars.i18n);
 
 const kebab = string => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase();
 
@@ -102,7 +102,10 @@ gulp.task('doc:api:endpoints', () => {
 			const ep = yaml.safeLoad(fs.readFileSync(file, 'utf-8'));
 			const vars = {
 				endpoint: ep.endpoint,
-				url: `${config.api_url}/${ep.endpoint}`,
+				url: {
+					host: config.api_url,
+					path: ep.endpoint
+				},
 				desc: ep.desc,
 				params: sortParams(ep.params.map(p => parseParam(p))),
 				paramDefs: extractDefs(ep.params),
@@ -113,6 +116,7 @@ gulp.task('doc:api:endpoints', () => {
 				pug.renderFile('./src/web/docs/api/endpoints/view.pug', Object.assign({}, vars, {
 					lang,
 					title: ep.endpoint,
+					src: `https://github.com/syuilo/misskey/tree/master/src/web/docs/api/endpoints/${ep.endpoint}.yaml`,
 					kebab,
 					common: commonVars
 				}), (renderErr, html) => {
@@ -152,6 +156,7 @@ gulp.task('doc:api:entities', () => {
 				pug.renderFile('./src/web/docs/api/entities/view.pug', Object.assign({}, vars, {
 					lang,
 					title: entity.name,
+					src: `https://github.com/syuilo/misskey/tree/master/src/web/docs/api/entities/${kebab(entity.name)}.yaml`,
 					kebab,
 					common: commonVars
 				}), (renderErr, html) => {
diff --git a/src/web/docs/api/mixins.pug b/src/web/docs/api/mixins.pug
index 518069857..b563a121d 100644
--- a/src/web/docs/api/mixins.pug
+++ b/src/web/docs/api/mixins.pug
@@ -1,10 +1,10 @@
 mixin propTable(props)
 	table.props
 		thead: tr
-			th Name
-			th Type
-			th Optional
-			th Description
+			th= common.i18n[lang]['docs']['api']['props']['name']
+			th= common.i18n[lang]['docs']['api']['props']['type']
+			th= common.i18n[lang]['docs']['api']['props']['optional']
+			th= common.i18n[lang]['docs']['api']['props']['description']
 		tbody
 			each prop in props
 				tr
@@ -29,5 +29,9 @@ mixin propTable(props)
 								| )
 						else if prop.kind == 'date'
 							|  (Date)
-					td.optional= prop.optional.toString()
+					td.optional
+						if prop.optional
+							= common.i18n[lang]['docs']['api']['props']['yes']
+						else
+							= common.i18n[lang]['docs']['api']['props']['no']
 					td.desc!= prop.desc[lang] || prop.desc['ja']
diff --git a/src/web/docs/gulpfile.ts b/src/web/docs/gulpfile.ts
index 61e44a1dc..6668abdec 100644
--- a/src/web/docs/gulpfile.ts
+++ b/src/web/docs/gulpfile.ts
@@ -37,7 +37,8 @@ gulp.task('doc:docs', () => {
 			const vars = {
 				common: commonVars,
 				lang: lang,
-				title: fs.readFileSync(file, 'utf-8').match(/^h1 (.+?)\r?\n/)[1]
+				title: fs.readFileSync(file, 'utf-8').match(/^h1 (.+?)\r?\n/)[1],
+				src: `https://github.com/syuilo/misskey/tree/master/src/web/docs/${name}.${lang}.pug`,
 			};
 			pug.renderFile(file, vars, (renderErr, content) => {
 				if (renderErr) {
diff --git a/src/web/docs/layout.pug b/src/web/docs/layout.pug
index bc9710d7c..c37967ab8 100644
--- a/src/web/docs/layout.pug
+++ b/src/web/docs/layout.pug
@@ -27,6 +27,12 @@ html(lang= lang)
 							each endpoint in common.endpoints
 								li: a(href=`./api/endpoints/${common.kebab(endpoint)}`)= endpoint
 		main
-			block main
-			if content
-				| !{content}
+			article
+				block main
+				if content
+					| !{content}
+
+			footer
+				p
+					= common.i18n[lang]['docs']['edit-this-page-on-github']
+					a(href=src target="_blank")= common.i18n[lang]['docs']['edit-this-page-on-github-link']
diff --git a/src/web/docs/style.styl b/src/web/docs/style.styl
index f222e65bf..285b92bdb 100644
--- a/src/web/docs/style.styl
+++ b/src/web/docs/style.styl
@@ -37,6 +37,14 @@ main
 		margin 1em 0
 		line-height 1.6em
 
+	footer
+		margin 32px 0 0 0
+		border-top solid 2px #eee
+
+		.copyright
+			margin 16px 0 0 0
+			color #aaa
+
 nav
 	display block
 	position fixed
@@ -48,15 +56,6 @@ nav
 	padding 32px
 	border-right solid 2px #eee
 
-footer
-	padding:32px 0 0 0
-	margin 32px 0 0 0
-	border-top solid 1px #eee
-
-	.copyright
-		margin 16px 0 0 0
-		color #aaa
-
 table
 	width 100%
 	border-spacing 0
diff --git a/src/web/docs/vars.ts b/src/web/docs/vars.ts
index ffa262a06..2c744be61 100644
--- a/src/web/docs/vars.ts
+++ b/src/web/docs/vars.ts
@@ -1,10 +1,11 @@
 import * as fs from 'fs';
 import * as glob from 'glob';
 import * as yaml from 'js-yaml';
+import langs from '../../../locales';
 import config from '../../conf';
 
-export default function() {
-	const vars = {};
+export default function(): { [key: string]: any } {
+	const vars = {} as { [key: string]: any };
 
 	const endpoints = glob.sync('./src/web/docs/api/endpoints/**/*.yaml');
 	vars['endpoints'] = endpoints.map(ep => {
@@ -35,5 +36,7 @@ export default function() {
 
 	vars['config'] = config;
 
+	vars['i18n'] = langs;
+
 	return vars;
 }
diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts
index 753d89fed..124bd975b 100644
--- a/webpack/webpack.config.ts
+++ b/webpack/webpack.config.ts
@@ -5,10 +5,10 @@
 import module_ from './module';
 import plugins from './plugins';
 
-import langs from './langs';
+import langs from '../locales';
 import version from '../src/version';
 
-module.exports = langs.map(([lang, locale]) => {
+module.exports = Object.entries(langs).map(([lang, locale]) => {
 	// Chunk name
 	const name = lang;
 

From deb01c75aa66861e23124c0724fbb84254ec06dd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 17 Dec 2017 04:05:33 +0900
Subject: [PATCH 0059/1250] :v:

---
 src/web/docs/layout.pug | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/src/web/docs/layout.pug b/src/web/docs/layout.pug
index c37967ab8..22d89375a 100644
--- a/src/web/docs/layout.pug
+++ b/src/web/docs/layout.pug
@@ -8,24 +8,23 @@ html(lang= lang)
 			| #{title} | Misskey Docs
 		link(rel="stylesheet" href="/assets/style.css")
 		block meta
-		base(href=`/${lang}/`)
 
 	body
 		nav
 			ul
 				each doc in common.docs
-					li: a(href=`./${doc.name}`)= doc.title[lang] || doc.title['ja']
+					li: a(href=`/${lang}/${doc.name}`)= doc.title[lang] || doc.title['ja']
 			section
 				h2 API
 				ul
 					li Entities
 						ul
 							each entity in common.entities
-								li: a(href=`./api/entities/${common.kebab(entity)}`)= entity
+								li: a(href=`/${lang}/api/entities/${common.kebab(entity)}`)= entity
 					li Endpoints
 						ul
 							each endpoint in common.endpoints
-								li: a(href=`./api/endpoints/${common.kebab(endpoint)}`)= endpoint
+								li: a(href=`/${lang}/api/endpoints/${common.kebab(endpoint)}`)= endpoint
 		main
 			article
 				block main

From ead208db14faedf906d87c5227b06e4d21d0d7b9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 17 Dec 2017 04:31:24 +0900
Subject: [PATCH 0060/1250] :v:

---
 src/const.json                              | 1 +
 src/web/app/common/tags/copyright.tag       | 7 -------
 src/web/app/common/tags/index.ts            | 1 -
 src/web/app/desktop/tags/pages/entrance.tag | 4 ++--
 src/web/app/mobile/tags/page/entrance.tag   | 4 ++--
 src/web/docs/layout.pug                     | 1 +
 src/web/docs/style.styl                     | 2 +-
 src/web/docs/vars.ts                        | 3 +++
 webpack/plugins/consts.ts                   | 1 +
 9 files changed, 11 insertions(+), 13 deletions(-)
 delete mode 100644 src/web/app/common/tags/copyright.tag

diff --git a/src/const.json b/src/const.json
index 924b4dd8b..0ee6ac206 100644
--- a/src/const.json
+++ b/src/const.json
@@ -1,4 +1,5 @@
 {
+	"copyright": "Copyright (c) 2014-2017 syuilo",
 	"themeColor": "#ff4e45",
 	"themeColorForeground": "#fff"
 }
diff --git a/src/web/app/common/tags/copyright.tag b/src/web/app/common/tags/copyright.tag
deleted file mode 100644
index 9c3f1f648..000000000
--- a/src/web/app/common/tags/copyright.tag
+++ /dev/null
@@ -1,7 +0,0 @@
-<mk-copyright>
-	<span>(c) syuilo 2014-2017</span>
-	<style>
-		:scope
-			display block
-	</style>
-</mk-copyright>
diff --git a/src/web/app/common/tags/index.ts b/src/web/app/common/tags/index.ts
index 2f4e1181d..df99d93cc 100644
--- a/src/web/app/common/tags/index.ts
+++ b/src/web/app/common/tags/index.ts
@@ -12,7 +12,6 @@ require('./signin.tag');
 require('./signup.tag');
 require('./forkit.tag');
 require('./introduction.tag');
-require('./copyright.tag');
 require('./signin-history.tag');
 require('./twitter-setting.tag');
 require('./authorized-apps.tag');
diff --git a/src/web/app/desktop/tags/pages/entrance.tag b/src/web/app/desktop/tags/pages/entrance.tag
index b07b22c80..974f49a4f 100644
--- a/src/web/app/desktop/tags/pages/entrance.tag
+++ b/src/web/app/desktop/tags/pages/entrance.tag
@@ -18,7 +18,7 @@
 	<footer>
 		<div>
 			<mk-nav-links/>
-			<mk-copyright/>
+			<p class="c">{ _COPYRIGHT_ }</p>
 		</div>
 	</footer>
 	<!-- ↓ https://github.com/riot/riot/issues/2134 (将来的)-->
@@ -101,7 +101,7 @@
 					text-align center
 					border-top solid 1px #fff
 
-					> mk-copyright
+					> .c
 						margin 0
 						line-height 64px
 						font-size 10px
diff --git a/src/web/app/mobile/tags/page/entrance.tag b/src/web/app/mobile/tags/page/entrance.tag
index 380fb780b..191874caf 100644
--- a/src/web/app/mobile/tags/page/entrance.tag
+++ b/src/web/app/mobile/tags/page/entrance.tag
@@ -8,7 +8,7 @@
 		</div>
 	</main>
 	<footer>
-		<mk-copyright/>
+		<p class="c">{ _COPYRIGHT_ }</p>
 	</footer>
 	<style>
 		:scope
@@ -34,7 +34,7 @@
 						margin 16px auto 0 auto
 
 			> footer
-				> mk-copyright
+				> .c
 					margin 0
 					text-align center
 					line-height 64px
diff --git a/src/web/docs/layout.pug b/src/web/docs/layout.pug
index 22d89375a..ee8018ec6 100644
--- a/src/web/docs/layout.pug
+++ b/src/web/docs/layout.pug
@@ -35,3 +35,4 @@ html(lang= lang)
 				p
 					= common.i18n[lang]['docs']['edit-this-page-on-github']
 					a(href=src target="_blank")= common.i18n[lang]['docs']['edit-this-page-on-github-link']
+				small= common.copyright
diff --git a/src/web/docs/style.styl b/src/web/docs/style.styl
index 285b92bdb..32a2264f1 100644
--- a/src/web/docs/style.styl
+++ b/src/web/docs/style.styl
@@ -41,7 +41,7 @@ main
 		margin 32px 0 0 0
 		border-top solid 2px #eee
 
-		.copyright
+		> small
 			margin 16px 0 0 0
 			color #aaa
 
diff --git a/src/web/docs/vars.ts b/src/web/docs/vars.ts
index 2c744be61..da590d7bd 100644
--- a/src/web/docs/vars.ts
+++ b/src/web/docs/vars.ts
@@ -3,6 +3,7 @@ import * as glob from 'glob';
 import * as yaml from 'js-yaml';
 import langs from '../../../locales';
 import config from '../../conf';
+const constants = require('../../const.json');
 
 export default function(): { [key: string]: any } {
 	const vars = {} as { [key: string]: any };
@@ -38,5 +39,7 @@ export default function(): { [key: string]: any } {
 
 	vars['i18n'] = langs;
 
+	vars['copyright'] = constants.copyright;
+
 	return vars;
 }
diff --git a/webpack/plugins/consts.ts b/webpack/plugins/consts.ts
index 6e18fa296..16a569162 100644
--- a/webpack/plugins/consts.ts
+++ b/webpack/plugins/consts.ts
@@ -13,6 +13,7 @@ export default lang => {
 		_RECAPTCHA_SITEKEY_: config.recaptcha.site_key,
 		_SW_PUBLICKEY_: config.sw ? config.sw.public_key : null,
 		_THEME_COLOR_: constants.themeColor,
+		_COPYRIGHT_: constants.copyright,
 		_VERSION_: version,
 		_STATUS_URL_: config.status_url,
 		_STATS_URL_: config.stats_url,

From 56e18415579ef5692da63d38330c59fdb9ef788a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 17 Dec 2017 04:32:29 +0900
Subject: [PATCH 0061/1250] v3400

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f1f0c8138..d3be42879 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3400 (2017/12/17)
+-----------------
+* なんか
+
 3392 (2017/12/17)
 -----------------
 * ドキュメントなど
diff --git a/package.json b/package.json
index 01d68c206..181c20a03 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3392",
+	"version": "0.0.3400",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 3c3b1ec8183e3995a5e2917f30c4e6f769fd19b2 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 17 Dec 2017 07:55:57 +0900
Subject: [PATCH 0062/1250] Update api.ja.pug

---
 src/web/docs/api.ja.pug | 47 +++++++++++++++++++++++++++++++++++------
 1 file changed, 40 insertions(+), 7 deletions(-)

diff --git a/src/web/docs/api.ja.pug b/src/web/docs/api.ja.pug
index 588b926bd..dbe17fe66 100644
--- a/src/web/docs/api.ja.pug
+++ b/src/web/docs/api.ja.pug
@@ -1,25 +1,54 @@
 h1 Misskey API
 
-p MisskeyはWeb APIを公開しており、アプリケーションから様々な操作を行うことができます。
+p MisskeyはWeb APIを公開しており、様々な操作をプログラム上から行うことができます。
+p APIを自分のアカウントから利用する場合と、アプリケーションから利用する場合で利用手順が異なりますので、それぞれのケースについて説明します。
 
 section
 	h2 自分の所有するアカウントからAPIにアクセスする場合
-	p 「設定」で、APIにアクセスするのに必要なAPIキーを取得してください。
+	p 「設定 > API」で、APIにアクセスするのに必要なAPIキーを取得してください。
 	p APIにアクセスする際には、リクエストにAPIキーを「i」というパラメータ名で含めます。
 	p APIの詳しい使用法は「Misskey APIの利用」セクションをご覧ください。
 
 section
 	h2 アプリケーションからAPIにアクセスする場合
 	p
-		| あなたのWebサービスやアプリケーションなどからMisskey APIを利用したい場合、
-		| ユーザーにアカウントへのアクセスを許可してもらい、ユーザーのアクセストークンを取得する必要があります。
-	p アクセストークンを取得するまでの流れを説明します。
+		| 直接ユーザーのAPIキーをアプリケーションが扱うのは危険なので、
+		| アプリケーションからAPIを利用する際には、アプリケーションとアプリケーションを利用するユーザーが結び付けられた専用のトークン(アクセストークン)をMisskeyに発行してもらい、
+		| そのトークンをリクエストのパラメータに含める必要があります。
+		| (アクセストークンは、ユーザーが自分のアカウントにあなたのアプリケーションがアクセスすることを許可した場合のみ発行されます)
+
+	p それでは、アクセストークンを取得するまでの流れを説明します。
 
 	section
 		h3 1.アプリケーションを登録する
 		p まず、あなたのWebサービスやアプリケーションをMisskeyに登録します。
-		p デベロッパーセンターから登録を行ってください。
-		p 登録が済むとアプリケーションのシークレットキーが入手できます。
+		p
+			a(href=common.config.dev_url, target="_blank") デベロッパーセンター
+			| にアクセスし、「アプリ > アプリ作成」に進みます。
+			| フォームに必要事項を記入し、アプリを作成してください。フォームの記入欄の説明は以下の通りです:
+
+		table
+			thead
+				tr
+					th 名前
+					th 説明
+			tbody
+				tr
+					td アプリケーション名
+					td あなたのアプリケーションやWebサービスの名称。
+				tr
+					td アプリの概要
+					td あなたのアプリケーションやWebサービスの簡単な説明や紹介。
+				tr
+					td コールバックURL
+					td あなたのアプリケーションがWebサービスである場合、ユーザーが後述する認証フォームで認証を終えた際にリダイレクトするURLを設定できます。
+				tr
+					td 権限
+					td あなたのアプリケーションやWebサービスが要求する権限。ここで要求した機能だけがAPIからアクセスできます。
+
+		p
+			| 登録が済むとアプリケーションのシークレットキーが入手できます。このシークレットキーは後で使用します。
+			| アプリに成りすまされる可能性があるため、極力このシークレットキーは公開しないようにしてください。
 
 	section
 		h3 2.ユーザーに認証させる
@@ -64,3 +93,7 @@ section
 	h2 Misskey APIの利用
 	p APIはすべてリクエストのパラメータ・レスポンスともにJSON形式です。また、すべてのエンドポイントはPOSTメソッドのみ受け付けます。
 	p APIリファレンスもご確認ください。
+	
+	section
+		h3 レートリミット
+		p Misskey APIにはレートリミットがあり、短時間のうちに多数のリクエストを送信すると、一定時間APIを利用することができなくなることがあります。

From 5fe7fcd3a0edd374e3276cbf986a183c6a88efb4 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 17 Dec 2017 13:04:19 +0900
Subject: [PATCH 0063/1250] Update api.ja.pug

---
 src/web/docs/api.ja.pug | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/web/docs/api.ja.pug b/src/web/docs/api.ja.pug
index dbe17fe66..5514a4097 100644
--- a/src/web/docs/api.ja.pug
+++ b/src/web/docs/api.ja.pug
@@ -1,7 +1,7 @@
 h1 Misskey API
 
 p MisskeyはWeb APIを公開しており、様々な操作をプログラム上から行うことができます。
-p APIを自分のアカウントから利用する場合と、アプリケーションから利用する場合で利用手順が異なりますので、それぞれのケースについて説明します。
+p APIを自分のアカウントから利用する場合(自分のアカウントのみ操作したい場合)と、アプリケーションから利用する場合(不特定のアカウントを操作したい場合)とで利用手順が異なりますので、それぞれのケースについて説明します。
 
 section
 	h2 自分の所有するアカウントからAPIにアクセスする場合
@@ -85,7 +85,7 @@ section
 					td セッションのトークン
 		p 上手くいけば、認証したユーザーのアクセストークンがレスポンスとして取得できます。おめでとうございます!
 
-	p アクセストークンが取得できたら、「ユーザーのアクセストークン+アプリのシークレットキーをsha256したもの」を「i」というパラメータでリクエストに含めるだけで、APIにアクセスできます。
+	p アクセストークンが取得できたら、「ユーザーのアクセストークン+アプリのシークレットキーをsha256したもの」を「i」というパラメータでリクエストに含めると、APIにアクセスすることができます。
 
 	p APIの詳しい使用法は「Misskey APIの利用」セクションをご覧ください。
 

From 02370d280f1dd4e285952959b9c198b2d2fb84f8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 17 Dec 2017 14:35:30 +0900
Subject: [PATCH 0064/1250] =?UTF-8?q?=E3=81=AA=E3=82=93=E3=81=8B=E3=82=82?=
 =?UTF-8?q?=E3=81=86=E3=82=81=E3=81=A3=E3=81=A1=E3=82=83=E5=A4=89=E3=81=88?=
 =?UTF-8?q?=E3=81=9F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 gulpfile.ts                         | 12 +-----
 src/common/build/fa.ts              | 57 +++++++++++++++++++++++++++++
 src/common/build/i18n.ts            | 50 +++++++++++++++++++++++++
 src/web/app/desktop/ui.styl         |  2 +-
 src/web/const.styl                  |  4 ++
 src/web/docs/api.ja.pug             | 10 ++---
 src/web/docs/api/endpoints/view.pug |  4 +-
 src/web/docs/api/entities/view.pug  |  2 +-
 src/web/docs/api/gulpfile.ts        | 11 +++++-
 src/web/docs/api/mixins.pug         | 12 +++---
 src/web/docs/gulpfile.ts            |  8 ++--
 src/web/docs/layout.pug             |  7 +++-
 src/web/docs/style.styl             |  1 +
 src/web/docs/ui.styl                | 19 ++++++++++
 src/web/docs/vars.ts                |  7 ++--
 src/web/style.styl                  |  5 +--
 webpack/module/index.ts             |  4 +-
 webpack/module/rules/fa.ts          | 47 +-----------------------
 webpack/module/rules/i18n.ts        | 33 ++---------------
 webpack/module/rules/index.ts       |  4 +-
 webpack/webpack.config.ts           |  4 +-
 21 files changed, 185 insertions(+), 118 deletions(-)
 create mode 100644 src/common/build/fa.ts
 create mode 100644 src/common/build/i18n.ts
 create mode 100644 src/web/const.styl
 create mode 100644 src/web/docs/ui.styl

diff --git a/gulpfile.ts b/gulpfile.ts
index 3b7a12640..21870473e 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -20,16 +20,8 @@ import * as mocha from 'gulp-mocha';
 import * as replace from 'gulp-replace';
 import * as htmlmin from 'gulp-htmlmin';
 const uglifyes = require('uglify-es');
-import * as fontawesome from '@fortawesome/fontawesome';
-import * as regular from '@fortawesome/fontawesome-free-regular';
-import * as solid from '@fortawesome/fontawesome-free-solid';
-import * as brands from '@fortawesome/fontawesome-free-brands';
-
-// Add icons
-fontawesome.library.add(regular);
-fontawesome.library.add(solid);
-fontawesome.library.add(brands);
 
+import { fa } from './src/common/build/fa';
 import version from './src/version';
 import config from './src/conf';
 
@@ -179,7 +171,7 @@ gulp.task('build:client:pug', [
 			.pipe(pug({
 				locals: {
 					themeColor: constants.themeColor,
-					facss: fontawesome.dom.css(),
+					facss: fa.dom.css(),
 					//hljscss: fs.readFileSync('./node_modules/highlight.js/styles/default.css', 'utf8')
 					hljscss: fs.readFileSync('./src/web/assets/code-highlight.css', 'utf8')
 				}
diff --git a/src/common/build/fa.ts b/src/common/build/fa.ts
new file mode 100644
index 000000000..0c21be950
--- /dev/null
+++ b/src/common/build/fa.ts
@@ -0,0 +1,57 @@
+/**
+ * Replace fontawesome symbols
+ */
+
+import * as fontawesome from '@fortawesome/fontawesome';
+import * as regular from '@fortawesome/fontawesome-free-regular';
+import * as solid from '@fortawesome/fontawesome-free-solid';
+import * as brands from '@fortawesome/fontawesome-free-brands';
+
+// Add icons
+fontawesome.library.add(regular);
+fontawesome.library.add(solid);
+fontawesome.library.add(brands);
+
+export const pattern = /%fa:(.+?)%/g;
+
+export const replacement = (_, key) => {
+	const args = key.split(' ');
+	let prefix = 'fas';
+	const classes = [];
+	let transform = '';
+	let name;
+
+	args.forEach(arg => {
+		if (arg == 'R' || arg == 'S' || arg == 'B') {
+			prefix =
+				arg == 'R' ? 'far' :
+				arg == 'S' ? 'fas' :
+				arg == 'B' ? 'fab' :
+				'';
+		} else if (arg[0] == '.') {
+			classes.push('fa-' + arg.substr(1));
+		} else if (arg[0] == '-') {
+			transform = arg.substr(1).split('|').join(' ');
+		} else {
+			name = arg;
+		}
+	});
+
+	const icon = fontawesome.icon({ prefix, iconName: name }, {
+		classes: classes
+	});
+
+	if (icon) {
+		icon.transform = fontawesome.parse.transform(transform);
+		return `<i data-fa class="${name}">${icon.html[0]}</i>`;
+	} else {
+		console.warn(`'${name}' not found in fa`);
+		return '';
+	}
+};
+
+export default (src: string) => {
+	return src.replace(pattern, replacement);
+};
+
+export const fa = fontawesome;
diff --git a/src/common/build/i18n.ts b/src/common/build/i18n.ts
new file mode 100644
index 000000000..1ae22147c
--- /dev/null
+++ b/src/common/build/i18n.ts
@@ -0,0 +1,50 @@
+/**
+ * Replace i18n texts
+ */
+
+import locale from '../../../locales';
+
+export default class Replacer {
+	private lang: string;
+
+	public pattern = /"%i18n:(.+?)%"|'%i18n:(.+?)%'|%i18n:(.+?)%/g;
+
+	constructor(lang: string) {
+		this.lang = lang;
+
+		this.get = this.get.bind(this);
+		this.replacement = this.replacement.bind(this);
+	}
+
+	private get(key: string) {
+		let text = locale[this.lang];
+
+		// Check the key existance
+		const error = key.split('.').some(k => {
+			if (text.hasOwnProperty(k)) {
+				text = text[k];
+				return false;
+			} else {
+				return true;
+			}
+		});
+
+		if (error) {
+			console.warn(`key '${key}' not found in '${this.lang}'`);
+			return key; // Fallback
+		} else {
+			return text;
+		}
+	}
+
+	public replacement(match, a, b, c) {
+		const key = a || b || c;
+		if (match[0] == '"') {
+			return '"' + this.get(key).replace(/"/g, '\\"') + '"';
+		} else if (match[0] == "'") {
+			return '\'' + this.get(key).replace(/'/g, '\\\'') + '\'';
+		} else {
+			return this.get(key);
+		}
+	}
+}
diff --git a/src/web/app/desktop/ui.styl b/src/web/app/desktop/ui.styl
index cb98bf06a..058271876 100644
--- a/src/web/app/desktop/ui.styl
+++ b/src/web/app/desktop/ui.styl
@@ -1,4 +1,4 @@
-@import "../app"
+@import "../../const"
 
 button
 	font-family sans-serif
diff --git a/src/web/const.styl b/src/web/const.styl
new file mode 100644
index 000000000..b6560701d
--- /dev/null
+++ b/src/web/const.styl
@@ -0,0 +1,4 @@
+json('../const.json')
+
+$theme-color = themeColor
+$theme-color-foreground = themeColorForeground
diff --git a/src/web/docs/api.ja.pug b/src/web/docs/api.ja.pug
index 5514a4097..2584b0858 100644
--- a/src/web/docs/api.ja.pug
+++ b/src/web/docs/api.ja.pug
@@ -7,6 +7,7 @@ section
 	h2 自分の所有するアカウントからAPIにアクセスする場合
 	p 「設定 > API」で、APIにアクセスするのに必要なAPIキーを取得してください。
 	p APIにアクセスする際には、リクエストにAPIキーを「i」というパラメータ名で含めます。
+	div.ui.info.warn: p %fa:exclamation-triangle%アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。
 	p APIの詳しい使用法は「Misskey APIの利用」セクションをご覧ください。
 
 section
@@ -15,7 +16,7 @@ section
 		| 直接ユーザーのAPIキーをアプリケーションが扱うのは危険なので、
 		| アプリケーションからAPIを利用する際には、アプリケーションとアプリケーションを利用するユーザーが結び付けられた専用のトークン(アクセストークン)をMisskeyに発行してもらい、
 		| そのトークンをリクエストのパラメータに含める必要があります。
-		| (アクセストークンは、ユーザーが自分のアカウントにあなたのアプリケーションがアクセスすることを許可した場合のみ発行されます)
+	div.ui.info: p %fa:info-circle%アクセストークンは、ユーザーが自分のアカウントにあなたのアプリケーションがアクセスすることを許可した場合のみ発行されます
 
 	p それでは、アクセストークンを取得するまでの流れを説明します。
 
@@ -46,9 +47,8 @@ section
 					td 権限
 					td あなたのアプリケーションやWebサービスが要求する権限。ここで要求した機能だけがAPIからアクセスできます。
 
-		p
-			| 登録が済むとアプリケーションのシークレットキーが入手できます。このシークレットキーは後で使用します。
-			| アプリに成りすまされる可能性があるため、極力このシークレットキーは公開しないようにしてください。
+		p 登録が済むとアプリケーションのシークレットキーが入手できます。このシークレットキーは後で使用します。
+		div.ui.info.warn: p %fa:exclamation-triangle%アプリに成りすまされる可能性があるため、極力このシークレットキーは公開しないようにしてください。
 
 	section
 		h3 2.ユーザーに認証させる
@@ -93,7 +93,7 @@ section
 	h2 Misskey APIの利用
 	p APIはすべてリクエストのパラメータ・レスポンスともにJSON形式です。また、すべてのエンドポイントはPOSTメソッドのみ受け付けます。
 	p APIリファレンスもご確認ください。
-	
+
 	section
 		h3 レートリミット
 		p Misskey APIにはレートリミットがあり、短時間のうちに多数のリクエストを送信すると、一定時間APIを利用することができなくなることがあります。
diff --git a/src/web/docs/api/endpoints/view.pug b/src/web/docs/api/endpoints/view.pug
index 90084ab27..d271a5517 100644
--- a/src/web/docs/api/endpoints/view.pug
+++ b/src/web/docs/api/endpoints/view.pug
@@ -17,7 +17,7 @@ block main
 	p#desc= desc[lang] || desc['ja']
 
 	section
-		h2= common.i18n[lang]['docs']['api']['endpoints']['params']
+		h2 %i18n:docs.api.endpoints.params%
 		+propTable(params)
 
 		if paramDefs
@@ -28,5 +28,5 @@ block main
 
 	if res
 		section
-			h2= common.i18n[lang]['docs']['api']['endpoints']['res']
+			h2 %i18n:docs.api.endpoints.res%
 			+propTable(res)
diff --git a/src/web/docs/api/entities/view.pug b/src/web/docs/api/entities/view.pug
index 99e786c69..2156463dc 100644
--- a/src/web/docs/api/entities/view.pug
+++ b/src/web/docs/api/entities/view.pug
@@ -10,7 +10,7 @@ block main
 	p#desc= desc[lang] || desc['ja']
 
 	section
-		h2= common.i18n[lang]['docs']['api']['entities']['properties']
+		h2 %i18n:docs.api.entities.properties%
 		+propTable(props)
 
 		if propDefs
diff --git a/src/web/docs/api/gulpfile.ts b/src/web/docs/api/gulpfile.ts
index 2e8409c59..4c30871a0 100644
--- a/src/web/docs/api/gulpfile.ts
+++ b/src/web/docs/api/gulpfile.ts
@@ -10,13 +10,16 @@ import * as pug from 'pug';
 import * as yaml from 'js-yaml';
 import * as mkdirp from 'mkdirp';
 
+import locales from '../../../../locales';
+import I18nReplacer from '../../../common/build/i18n';
+import fa from '../../../common/build/fa';
 import config from './../../../conf';
 
 import generateVars from '../vars';
 
 const commonVars = generateVars();
 
-const langs = Object.keys(commonVars.i18n);
+const langs = Object.keys(locales);
 
 const kebab = string => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase();
 
@@ -124,6 +127,9 @@ gulp.task('doc:api:endpoints', () => {
 						console.error(renderErr);
 						return;
 					}
+					const i18n = new I18nReplacer(lang);
+					html = html.replace(i18n.pattern, i18n.replacement);
+					html = fa(html);
 					const htmlPath = `./built/web/docs/${lang}/api/endpoints/${ep.endpoint}.html`;
 					mkdirp(path.dirname(htmlPath), (mkdirErr) => {
 						if (mkdirErr) {
@@ -164,6 +170,9 @@ gulp.task('doc:api:entities', () => {
 						console.error(renderErr);
 						return;
 					}
+					const i18n = new I18nReplacer(lang);
+					html = html.replace(i18n.pattern, i18n.replacement);
+					html = fa(html);
 					const htmlPath = `./built/web/docs/${lang}/api/entities/${kebab(entity.name)}.html`;
 					mkdirp(path.dirname(htmlPath), (mkdirErr) => {
 						if (mkdirErr) {
diff --git a/src/web/docs/api/mixins.pug b/src/web/docs/api/mixins.pug
index b563a121d..686bf6a2b 100644
--- a/src/web/docs/api/mixins.pug
+++ b/src/web/docs/api/mixins.pug
@@ -1,10 +1,10 @@
 mixin propTable(props)
 	table.props
 		thead: tr
-			th= common.i18n[lang]['docs']['api']['props']['name']
-			th= common.i18n[lang]['docs']['api']['props']['type']
-			th= common.i18n[lang]['docs']['api']['props']['optional']
-			th= common.i18n[lang]['docs']['api']['props']['description']
+			th %i18n:docs.api.props.name%
+			th %i18n:docs.api.props.type%
+			th %i18n:docs.api.props.optional%
+			th %i18n:docs.api.props.description%
 		tbody
 			each prop in props
 				tr
@@ -31,7 +31,7 @@ mixin propTable(props)
 							|  (Date)
 					td.optional
 						if prop.optional
-							= common.i18n[lang]['docs']['api']['props']['yes']
+							| %i18n:docs.api.props.yes%
 						else
-							= common.i18n[lang]['docs']['api']['props']['no']
+							| %i18n:docs.api.props.no%
 					td.desc!= prop.desc[lang] || prop.desc['ja']
diff --git a/src/web/docs/gulpfile.ts b/src/web/docs/gulpfile.ts
index 6668abdec..71033e1bc 100644
--- a/src/web/docs/gulpfile.ts
+++ b/src/web/docs/gulpfile.ts
@@ -7,13 +7,12 @@ import * as path from 'path';
 import * as glob from 'glob';
 import * as gulp from 'gulp';
 import * as pug from 'pug';
-//import * as yaml from 'js-yaml';
 import * as mkdirp from 'mkdirp';
 import stylus = require('gulp-stylus');
 import cssnano = require('gulp-cssnano');
 
-//import config from './../../conf';
-
+import I18nReplacer from '../../common/build/i18n';
+import fa from '../../common/build/fa';
 import generateVars from './vars';
 
 require('./api/gulpfile.ts');
@@ -53,6 +52,9 @@ gulp.task('doc:docs', () => {
 						console.error(renderErr2);
 						return;
 					}
+					const i18n = new I18nReplacer(lang);
+					html = html.replace(i18n.pattern, i18n.replacement);
+					html = fa(html);
 					const htmlPath = `./built/web/docs/${lang}/${name}.html`;
 					mkdirp(path.dirname(htmlPath), (mkdirErr) => {
 						if (mkdirErr) {
diff --git a/src/web/docs/layout.pug b/src/web/docs/layout.pug
index ee8018ec6..9dfd0ab7a 100644
--- a/src/web/docs/layout.pug
+++ b/src/web/docs/layout.pug
@@ -9,6 +9,9 @@ html(lang= lang)
 		link(rel="stylesheet" href="/assets/style.css")
 		block meta
 
+		//- FontAwesome style
+		style #{common.facss}
+
 	body
 		nav
 			ul
@@ -33,6 +36,6 @@ html(lang= lang)
 
 			footer
 				p
-					= common.i18n[lang]['docs']['edit-this-page-on-github']
-					a(href=src target="_blank")= common.i18n[lang]['docs']['edit-this-page-on-github-link']
+					| %i18n:docs.edit-this-page-on-github%
+					a(href=src target="_blank") %i18n:docs.edit-this-page-on-github-link%
 				small= common.copyright
diff --git a/src/web/docs/style.styl b/src/web/docs/style.styl
index 32a2264f1..414be5c53 100644
--- a/src/web/docs/style.styl
+++ b/src/web/docs/style.styl
@@ -1,4 +1,5 @@
 @import "../style"
+@import "./ui"
 
 body
 	margin 0
diff --git a/src/web/docs/ui.styl b/src/web/docs/ui.styl
new file mode 100644
index 000000000..8d5515712
--- /dev/null
+++ b/src/web/docs/ui.styl
@@ -0,0 +1,19 @@
+.ui.info
+	display block
+	margin 1em 0
+	padding 0 1em
+	font-size 90%
+	color rgba(#000, 0.87)
+	background #f8f8f9
+	border-radius 4px
+	overflow hidden
+
+	> p
+		opacity 0.8
+
+		> [data-fa]:first-child
+			margin-right 0.25em
+
+	&.warn
+		color #573a08
+		background #FFFAF3
diff --git a/src/web/docs/vars.ts b/src/web/docs/vars.ts
index da590d7bd..65b224fbf 100644
--- a/src/web/docs/vars.ts
+++ b/src/web/docs/vars.ts
@@ -1,7 +1,8 @@
 import * as fs from 'fs';
 import * as glob from 'glob';
 import * as yaml from 'js-yaml';
-import langs from '../../../locales';
+
+import { fa } from '../../common/build/fa';
 import config from '../../conf';
 const constants = require('../../const.json');
 
@@ -37,9 +38,9 @@ export default function(): { [key: string]: any } {
 
 	vars['config'] = config;
 
-	vars['i18n'] = langs;
-
 	vars['copyright'] = constants.copyright;
 
+	vars['facss'] = fa.dom.css();
+
 	return vars;
 }
diff --git a/src/web/style.styl b/src/web/style.styl
index 573df10d7..c25fc8fb5 100644
--- a/src/web/style.styl
+++ b/src/web/style.styl
@@ -1,9 +1,6 @@
-json('../const.json')
-
 @charset 'utf-8'
 
-$theme-color = themeColor
-$theme-color-foreground = themeColorForeground
+@import "./const"
 
 /*
 	::selection
diff --git a/webpack/module/index.ts b/webpack/module/index.ts
index 15f36557c..088aca723 100644
--- a/webpack/module/index.ts
+++ b/webpack/module/index.ts
@@ -1,5 +1,5 @@
 import rules from './rules';
 
-export default (lang, locale) => ({
-	rules: rules(lang, locale)
+export default lang => ({
+	rules: rules(lang)
 });
diff --git a/webpack/module/rules/fa.ts b/webpack/module/rules/fa.ts
index 47c72a28a..891b78ece 100644
--- a/webpack/module/rules/fa.ts
+++ b/webpack/module/rules/fa.ts
@@ -3,16 +3,7 @@
  */
 
 const StringReplacePlugin = require('string-replace-webpack-plugin');
-
-import * as fontawesome from '@fortawesome/fontawesome';
-import * as regular from '@fortawesome/fontawesome-free-regular';
-import * as solid from '@fortawesome/fontawesome-free-solid';
-import * as brands from '@fortawesome/fontawesome-free-brands';
-
-// Add icons
-fontawesome.library.add(regular);
-fontawesome.library.add(solid);
-fontawesome.library.add(brands);
+import { pattern, replacement } from '../../../src/common/build/fa';
 
 export default () => ({
 	enforce: 'pre',
@@ -20,41 +11,7 @@ export default () => ({
 	exclude: /node_modules/,
 	loader: StringReplacePlugin.replace({
 		replacements: [{
-			pattern: /%fa:(.+?)%/g, replacement: (_, key) => {
-				const args = key.split(' ');
-				let prefix = 'fas';
-				const classes = [];
-				let transform = '';
-				let name;
-
-				args.forEach(arg => {
-					if (arg == 'R' || arg == 'S' || arg == 'B') {
-						prefix =
-							arg == 'R' ? 'far' :
-							arg == 'S' ? 'fas' :
-							arg == 'B' ? 'fab' :
-							'';
-					} else if (arg[0] == '.') {
-						classes.push('fa-' + arg.substr(1));
-					} else if (arg[0] == '-') {
-						transform = arg.substr(1).split('|').join(' ');
-					} else {
-						name = arg;
-					}
-				});
-
-				const icon = fontawesome.icon({ prefix, iconName: name }, {
-					classes: classes
-				});
-
-				if (icon) {
-					icon.transform = fontawesome.parse.transform(transform);
-					return `<i data-fa class="${name}">${icon.html[0]}</i>`;
-				} else {
-					console.warn(`'${name}' not found in fa`);
-					return '';
-				}
-			}
+			pattern, replacement
 		}]
 	})
 });
diff --git a/webpack/module/rules/i18n.ts b/webpack/module/rules/i18n.ts
index aa4e58448..7261548be 100644
--- a/webpack/module/rules/i18n.ts
+++ b/webpack/module/rules/i18n.ts
@@ -3,28 +3,10 @@
  */
 
 const StringReplacePlugin = require('string-replace-webpack-plugin');
+import Replacer from '../../../src/common/build/i18n';
 
-export default (lang, locale) => {
-	function get(key: string) {
-		let text = locale;
-
-		// Check the key existance
-		const error = key.split('.').some(k => {
-			if (text.hasOwnProperty(k)) {
-				text = text[k];
-				return false;
-			} else {
-				return true;
-			}
-		});
-
-		if (error) {
-			console.warn(`key '${key}' not found in '${lang}'`);
-			return key; // Fallback
-		} else {
-			return text;
-		}
-	}
+export default lang => {
+	const replacer = new Replacer(lang);
 
 	return {
 		enforce: 'pre',
@@ -32,14 +14,7 @@ export default (lang, locale) => {
 		exclude: /node_modules/,
 		loader: StringReplacePlugin.replace({
 			replacements: [{
-				pattern: /"%i18n:(.+?)%"/g, replacement: (_, key) =>
-					'"' + get(key).replace(/"/g, '\\"') + '"'
-			}, {
-				pattern: /'%i18n:(.+?)%'/g, replacement: (_, key) =>
-					'\'' + get(key).replace(/'/g, '\\\'') + '\''
-			}, {
-				pattern: /%i18n:(.+?)%/g, replacement: (_, key) =>
-					get(key)
+				pattern: replacer.pattern, replacement: replacer.replacement
 			}]
 		})
 	};
diff --git a/webpack/module/rules/index.ts b/webpack/module/rules/index.ts
index b6a0a5e2e..b02bdef72 100644
--- a/webpack/module/rules/index.ts
+++ b/webpack/module/rules/index.ts
@@ -7,8 +7,8 @@ import tag from './tag';
 import stylus from './stylus';
 import typescript from './typescript';
 
-export default (lang, locale) => [
-	i18n(lang, locale),
+export default lang => [
+	i18n(lang),
 	license(),
 	fa(),
 	base64(),
diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts
index 124bd975b..d67b8ef77 100644
--- a/webpack/webpack.config.ts
+++ b/webpack/webpack.config.ts
@@ -8,7 +8,7 @@ import plugins from './plugins';
 import langs from '../locales';
 import version from '../src/version';
 
-module.exports = Object.entries(langs).map(([lang, locale]) => {
+module.exports = Object.keys(langs).map(lang => {
 	// Chunk name
 	const name = lang;
 
@@ -32,7 +32,7 @@ module.exports = Object.entries(langs).map(([lang, locale]) => {
 	return {
 		name,
 		entry,
-		module: module_(lang, locale),
+		module: module_(lang),
 		plugins: plugins(version, lang),
 		output,
 		resolve: {

From 6e4125269a31dfb45e703b04e20eebad9e5e232a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 17 Dec 2017 14:35:54 +0900
Subject: [PATCH 0065/1250] v3404

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d3be42879..e9ae2cb17 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3404 (2017/12/17)
+-----------------
+* なんか
+
 3400 (2017/12/17)
 -----------------
 * なんか
diff --git a/package.json b/package.json
index 181c20a03..ef5a76059 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3400",
+	"version": "0.0.3404",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 7560b901ba4e003707c935d6f4a19472c1ea15c3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 17 Dec 2017 15:15:54 +0900
Subject: [PATCH 0066/1250] =?UTF-8?q?=E3=83=AC=E3=82=B9=E3=83=9D=E3=83=B3?=
 =?UTF-8?q?=E3=82=B7=E3=83=96=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/web/docs/style.styl | 28 +++++++++++++++++++++++-----
 1 file changed, 23 insertions(+), 5 deletions(-)

diff --git a/src/web/docs/style.styl b/src/web/docs/style.styl
index 414be5c53..d2f91fb7c 100644
--- a/src/web/docs/style.styl
+++ b/src/web/docs/style.styl
@@ -4,6 +4,7 @@
 body
 	margin 0
 	color #34495e
+	word-break break-word
 
 main
 	margin 0 0 0 256px
@@ -49,16 +50,37 @@ main
 nav
 	display block
 	position fixed
+	z-index 10000
 	top 0
 	left 0
 	width 256px
 	height 100%
 	overflow auto
 	padding 32px
+	background #fff
 	border-right solid 2px #eee
 
+@media (max-width 1025px)
+	main
+		margin 0
+		max-width 100%
+
+	nav
+		position relative
+		width 100%
+		max-height 128px
+		background #f9f9f9
+		border-right none
+
+@media (max-width 512px)
+	main
+		padding 16px
+
 table
+	display block
 	width 100%
+	max-width 100%
+	overflow auto
 	border-spacing 0
 	border-collapse collapse
 
@@ -68,12 +90,7 @@ table
 
 		tr
 			th
-				position sticky
-				top 0
-				z-index 1
 				text-align left
-				box-shadow 0 1px 0 0 #eee
-				background #fff
 
 	tbody
 		tr
@@ -82,3 +99,4 @@ table
 
 	th, td
 		padding 8px 16px
+		min-width 128px

From 7e67bc7cb276feed79f48d73c419e31ffc45a55a Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sun, 17 Dec 2017 10:41:49 +0000
Subject: [PATCH 0067/1250] fix(package): update cropperjs to version 1.2.1

Closes #1006
---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index ef5a76059..9f3e1eb38 100644
--- a/package.json
+++ b/package.json
@@ -90,7 +90,7 @@
 		"compression": "1.7.1",
 		"cookie": "0.3.1",
 		"cors": "2.8.4",
-		"cropperjs": "1.1.3",
+		"cropperjs": "1.2.1",
 		"css-loader": "0.28.7",
 		"debug": "3.1.0",
 		"deep-equal": "1.0.1",

From 2c717048564ff51ea88906e913c04eb671871e02 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 18 Dec 2017 00:37:19 +0900
Subject: [PATCH 0068/1250] Update api.ja.pug

---
 src/web/docs/api.ja.pug | 22 +++++++++++-----------
 1 file changed, 11 insertions(+), 11 deletions(-)

diff --git a/src/web/docs/api.ja.pug b/src/web/docs/api.ja.pug
index 2584b0858..78749e83e 100644
--- a/src/web/docs/api.ja.pug
+++ b/src/web/docs/api.ja.pug
@@ -22,7 +22,7 @@ section
 
 	section
 		h3 1.アプリケーションを登録する
-		p まず、あなたのWebサービスやアプリケーションをMisskeyに登録します。
+		p まず、あなたのアプリケーションやWebサービス(以後、あなたのアプリと呼びます)をMisskeyに登録します。
 		p
 			a(href=common.config.dev_url, target="_blank") デベロッパーセンター
 			| にアクセスし、「アプリ > アプリ作成」に進みます。
@@ -36,23 +36,23 @@ section
 			tbody
 				tr
 					td アプリケーション名
-					td あなたのアプリケーションやWebサービスの名称。
+					td あなたのアプリの名称。
 				tr
 					td アプリの概要
-					td あなたのアプリケーションやWebサービスの簡単な説明や紹介。
+					td あなたのアプリの簡単な説明や紹介。
 				tr
 					td コールバックURL
-					td あなたのアプリケーションがWebサービスである場合、ユーザーが後述する認証フォームで認証を終えた際にリダイレクトするURLを設定できます。
+					td あなたのアプリがWebサービスである場合、ユーザーが後述する認証フォームで認証を終えた際にリダイレクトするURLを設定できます。
 				tr
 					td 権限
-					td あなたのアプリケーションやWebサービスが要求する権限。ここで要求した機能だけがAPIからアクセスできます。
+					td あなたのアプリが要求する権限。ここで要求した機能だけがAPIからアクセスできます。
 
-		p 登録が済むとアプリケーションのシークレットキーが入手できます。このシークレットキーは後で使用します。
+		p 登録が済むとあなたのアプリのシークレットキーが入手できます。このシークレットキーは後で使用します。
 		div.ui.info.warn: p %fa:exclamation-triangle%アプリに成りすまされる可能性があるため、極力このシークレットキーは公開しないようにしてください。
 
 	section
 		h3 2.ユーザーに認証させる
-		p あなたのアプリケーションを使ってもらうには、ユーザーにアカウントへのアクセスの許可をもらう必要があります。
+		p あなたのアプリを使ってもらうには、ユーザーにアカウントへのアクセスの許可をもらう必要があります。
 		p
 			| 認証セッションを開始するには、#{common.config.api_url}/auth/session/generate へパラメータに app_secret としてシークレットキーを含めたリクエストを送信します。
 			| リクエスト形式はJSONで、メソッドはPOSTです。
@@ -60,10 +60,10 @@ section
 
 		p
 			| あなたのアプリがコールバックURLを設定している場合、
-			| ユーザーがアプリの連携を許可すると設定しているコールバックURLに token という名前でセッションのトークンが含まれたクエリを付けてリダイレクトします。
+			| ユーザーがあなたのアプリの連携を許可すると設定しているコールバックURLに token という名前でセッションのトークンが含まれたクエリを付けてリダイレクトします。
 
 		p
-			| あなたのアプリがコールバックURLを設定していない場合、ユーザーがアプリの連携を許可したことを(何らかの方法で(たとえばボタンを押させるなど))確認出来るようにしてください。
+			| あなたのアプリがコールバックURLを設定していない場合、ユーザーがあなたのアプリの連携を許可したことを(何らかの方法で(たとえばボタンを押させるなど))確認出来るようにしてください。
 
 	section
 		h3 3.ユーザーのアクセストークンを取得する
@@ -78,14 +78,14 @@ section
 				tr
 					td app_secret
 					td string
-					td アプリのシークレットキー
+					td あなたのアプリのシークレットキー
 				tr
 					td token
 					td string
 					td セッションのトークン
 		p 上手くいけば、認証したユーザーのアクセストークンがレスポンスとして取得できます。おめでとうございます!
 
-	p アクセストークンが取得できたら、「ユーザーのアクセストークン+アプリのシークレットキーをsha256したもの」を「i」というパラメータでリクエストに含めると、APIにアクセスすることができます。
+	p アクセストークンが取得できたら、「ユーザーのアクセストークン+あなたのアプリのシークレットキーをsha256したもの」を「i」というパラメータでリクエストに含めると、APIにアクセスすることができます。
 
 	p APIの詳しい使用法は「Misskey APIの利用」セクションをご覧ください。
 

From 6e75e577213cc3dcd0a0c93864fcafec95d1bbed Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 18 Dec 2017 04:17:18 +0900
Subject: [PATCH 0069/1250] :v:

---
 src/web/docs/api.ja.pug |  4 ++++
 src/web/docs/style.styl | 11 +++++++++++
 2 files changed, 15 insertions(+)

diff --git a/src/web/docs/api.ja.pug b/src/web/docs/api.ja.pug
index 78749e83e..167098111 100644
--- a/src/web/docs/api.ja.pug
+++ b/src/web/docs/api.ja.pug
@@ -87,6 +87,10 @@ section
 
 	p アクセストークンが取得できたら、「ユーザーのアクセストークン+あなたのアプリのシークレットキーをsha256したもの」を「i」というパラメータでリクエストに含めると、APIにアクセスすることができます。
 
+	p 「i」パラメータの生成方法を擬似コードで表すと次のようになります:
+	pre: code
+		| const i = sha256(accessToken + secretKey);
+
 	p APIの詳しい使用法は「Misskey APIの利用」セクションをご覧ください。
 
 section
diff --git a/src/web/docs/style.styl b/src/web/docs/style.styl
index d2f91fb7c..27c93a99e 100644
--- a/src/web/docs/style.styl
+++ b/src/web/docs/style.styl
@@ -100,3 +100,14 @@ table
 	th, td
 		padding 8px 16px
 		min-width 128px
+
+code
+	padding 8px 10px
+	font-family Consolas, 'Courier New', Courier, Monaco, monospace
+	color #295c92
+	background #f2f2f2
+	border-radius 4px
+
+pre
+	> code
+		display block

From 30e7a38f3af18f6fe739a173dc61faeb2000337f Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sun, 17 Dec 2017 22:28:15 +0000
Subject: [PATCH 0070/1250] fix(package): update riot-tag-loader to version
 1.1.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 9f3e1eb38..6a1157c92 100644
--- a/package.json
+++ b/package.json
@@ -146,7 +146,7 @@
 		"request": "2.83.0",
 		"rimraf": "2.6.2",
 		"riot": "3.7.4",
-		"riot-tag-loader": "1.0.0",
+		"riot-tag-loader": "1.1.0",
 		"rndstr": "1.0.0",
 		"s-age": "1.1.0",
 		"seedrandom": "2.4.3",

From 42438bf0855a95c7341b5e1f4812eba00a950b44 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 19 Dec 2017 13:18:17 +0900
Subject: [PATCH 0071/1250] :v:

---
 src/web/app/desktop/tags/drive/browser.tag | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/web/app/desktop/tags/drive/browser.tag b/src/web/app/desktop/tags/drive/browser.tag
index 901daabfd..a60a46b79 100644
--- a/src/web/app/desktop/tags/drive/browser.tag
+++ b/src/web/app/desktop/tags/drive/browser.tag
@@ -18,14 +18,16 @@
 				<virtual each={ folder in folders }>
 					<mk-drive-browser-folder class="folder" folder={ folder }/>
 				</virtual>
-				<div class="padding" each={ folders }></div>
+				<!-- 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>
 			</div>
 			<div class="files" ref="filesContainer" if={ files.length > 0 }>
 				<virtual each={ file in files }>
 					<mk-drive-browser-file class="file" file={ file }/>
 				</virtual>
-				<div class="padding" each={ files }></div>
+				<!-- 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>
 			</div>
 			<div class="empty" if={ files.length == 0 && folders.length == 0 && !fetching }>

From f4b598368ee2ada183344245177dbad4c9acf431 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 19 Dec 2017 13:19:06 +0900
Subject: [PATCH 0072/1250] :art:

---
 src/web/docs/style.styl | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/web/docs/style.styl b/src/web/docs/style.styl
index 27c93a99e..3dcb3e169 100644
--- a/src/web/docs/style.styl
+++ b/src/web/docs/style.styl
@@ -8,9 +8,9 @@ body
 
 main
 	margin 0 0 0 256px
-	padding 32px
+	padding 64px
 	width 100%
-	max-width 700px
+	max-width 768px
 
 	section
 		margin 32px 0
@@ -72,6 +72,10 @@ nav
 		background #f9f9f9
 		border-right none
 
+@media (max-width 768px)
+	main
+		padding 32px
+
 @media (max-width 512px)
 	main
 		padding 16px

From 6c71b78004ad404b0b608ec65053b8d37c7736d6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 19 Dec 2017 13:21:39 +0900
Subject: [PATCH 0073/1250] v3415

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e9ae2cb17..bd4f2c1f1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3415 (2017/12/19)
+-----------------
+* デザインの調整
+
 3404 (2017/12/17)
 -----------------
 * なんか
diff --git a/package.json b/package.json
index 6a1157c92..fa8310f87 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3404",
+	"version": "0.0.3415",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 03b5716db9737e09d6918487e8f96027623712e5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 19 Dec 2017 13:25:07 +0900
Subject: [PATCH 0074/1250] Fix bug

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index fa8310f87..39922127c 100644
--- a/package.json
+++ b/package.json
@@ -146,7 +146,7 @@
 		"request": "2.83.0",
 		"rimraf": "2.6.2",
 		"riot": "3.7.4",
-		"riot-tag-loader": "1.1.0",
+		"riot-tag-loader": "1.0.0",
 		"rndstr": "1.0.0",
 		"s-age": "1.1.0",
 		"seedrandom": "2.4.3",

From 2859a1f13bad0daa7c76e61babb4e1e86ff3bbff Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Tue, 19 Dec 2017 16:10:33 +0900
Subject: [PATCH 0075/1250] Update api.ja.pug

---
 src/web/docs/api.ja.pug | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/docs/api.ja.pug b/src/web/docs/api.ja.pug
index 167098111..2bb08f7f3 100644
--- a/src/web/docs/api.ja.pug
+++ b/src/web/docs/api.ja.pug
@@ -42,7 +42,7 @@ section
 					td あなたのアプリの簡単な説明や紹介。
 				tr
 					td コールバックURL
-					td あなたのアプリがWebサービスである場合、ユーザーが後述する認証フォームで認証を終えた際にリダイレクトするURLを設定できます。
+					td ユーザーが後述する認証フォームで認証を終えた際にリダイレクトするURLを設定できます。あなたのアプリがWebサービスである場合に有用です。
 				tr
 					td 権限
 					td あなたのアプリが要求する権限。ここで要求した機能だけがAPIからアクセスできます。

From f398b027de090b7de4f4a298124650d262358320 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 21 Dec 2017 02:20:02 +0900
Subject: [PATCH 0076/1250] #1021

---
 src/api/endpoints/channels.ts                 | 16 ++++++------
 src/api/endpoints/channels/posts.ts           | 16 ++++++------
 src/api/endpoints/drive/files.ts              | 16 ++++++------
 src/api/endpoints/drive/folders.ts            | 16 ++++++------
 src/api/endpoints/drive/stream.ts             | 16 ++++++------
 src/api/endpoints/i/notifications.ts          | 16 ++++++------
 src/api/endpoints/i/signin_history.ts         | 16 ++++++------
 src/api/endpoints/messaging/messages.ts       | 16 ++++++------
 src/api/endpoints/posts.ts                    | 16 ++++++------
 src/api/endpoints/posts/mentions.ts           | 16 ++++++------
 src/api/endpoints/posts/reposts.ts            | 16 ++++++------
 src/api/endpoints/posts/timeline.ts           | 26 +++++++++----------
 src/api/endpoints/users.ts                    | 16 ++++++------
 src/api/endpoints/users/posts.ts              | 26 +++++++++----------
 src/web/app/common/tags/messaging/room.tag    |  2 +-
 .../desktop/tags/home-widgets/mentions.tag    |  2 +-
 .../desktop/tags/home-widgets/timeline.tag    |  4 +--
 src/web/app/desktop/tags/notifications.tag    |  2 +-
 src/web/app/desktop/tags/user-timeline.tag    |  4 +--
 src/web/app/mobile/tags/drive.tag             |  2 +-
 src/web/app/mobile/tags/home-timeline.tag     |  2 +-
 src/web/app/mobile/tags/notifications.tag     |  2 +-
 src/web/app/mobile/tags/user-timeline.tag     |  2 +-
 .../docs/api/endpoints/posts/timeline.yaml    |  4 +--
 24 files changed, 135 insertions(+), 135 deletions(-)

diff --git a/src/api/endpoints/channels.ts b/src/api/endpoints/channels.ts
index e10c94389..14817d9bd 100644
--- a/src/api/endpoints/channels.ts
+++ b/src/api/endpoints/channels.ts
@@ -21,13 +21,13 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
 	if (sinceIdErr) return rej('invalid since_id param');
 
-	// Get 'max_id' parameter
-	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) return rej('invalid max_id param');
+	// Get 'until_id' parameter
+	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+	if (untilIdErr) return rej('invalid until_id param');
 
-	// Check if both of since_id and max_id is specified
-	if (sinceId && maxId) {
-		return rej('cannot set since_id and max_id');
+	// Check if both of since_id and until_id is specified
+	if (sinceId && untilId) {
+		return rej('cannot set since_id and until_id');
 	}
 
 	// Construct query
@@ -40,9 +40,9 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 		query._id = {
 			$gt: sinceId
 		};
-	} else if (maxId) {
+	} else if (untilId) {
 		query._id = {
-			$lt: maxId
+			$lt: untilId
 		};
 	}
 
diff --git a/src/api/endpoints/channels/posts.ts b/src/api/endpoints/channels/posts.ts
index 5c071a124..9c2d607ed 100644
--- a/src/api/endpoints/channels/posts.ts
+++ b/src/api/endpoints/channels/posts.ts
@@ -22,13 +22,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
 	if (sinceIdErr) return rej('invalid since_id param');
 
-	// Get 'max_id' parameter
-	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) return rej('invalid max_id param');
+	// Get 'until_id' parameter
+	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+	if (untilIdErr) return rej('invalid until_id param');
 
-	// Check if both of since_id and max_id is specified
-	if (sinceId && maxId) {
-		return rej('cannot set since_id and max_id');
+	// Check if both of since_id and until_id is specified
+	if (sinceId && untilId) {
+		return rej('cannot set since_id and until_id');
 	}
 
 	// Get 'channel_id' parameter
@@ -58,9 +58,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		query._id = {
 			$gt: sinceId
 		};
-	} else if (maxId) {
+	} else if (untilId) {
 		query._id = {
-			$lt: maxId
+			$lt: untilId
 		};
 	}
 	//#endregion Construct query
diff --git a/src/api/endpoints/drive/files.ts b/src/api/endpoints/drive/files.ts
index b2e094775..3d5f81339 100644
--- a/src/api/endpoints/drive/files.ts
+++ b/src/api/endpoints/drive/files.ts
@@ -22,13 +22,13 @@ module.exports = async (params, user, app) => {
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
 	if (sinceIdErr) throw 'invalid since_id param';
 
-	// Get 'max_id' parameter
-	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) throw 'invalid max_id param';
+	// Get 'until_id' parameter
+	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+	if (untilIdErr) throw 'invalid until_id param';
 
-	// Check if both of since_id and max_id is specified
-	if (sinceId && maxId) {
-		throw 'cannot set since_id and max_id';
+	// Check if both of since_id and until_id is specified
+	if (sinceId && untilId) {
+		throw 'cannot set since_id and until_id';
 	}
 
 	// Get 'folder_id' parameter
@@ -52,9 +52,9 @@ module.exports = async (params, user, app) => {
 		query._id = {
 			$gt: sinceId
 		};
-	} else if (maxId) {
+	} else if (untilId) {
 		query._id = {
-			$lt: maxId
+			$lt: untilId
 		};
 	}
 	if (type) {
diff --git a/src/api/endpoints/drive/folders.ts b/src/api/endpoints/drive/folders.ts
index d49ef0af0..7944e2c6a 100644
--- a/src/api/endpoints/drive/folders.ts
+++ b/src/api/endpoints/drive/folders.ts
@@ -22,13 +22,13 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
 	if (sinceIdErr) return rej('invalid since_id param');
 
-	// Get 'max_id' parameter
-	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) return rej('invalid max_id param');
+	// Get 'until_id' parameter
+	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+	if (untilIdErr) return rej('invalid until_id param');
 
-	// Check if both of since_id and max_id is specified
-	if (sinceId && maxId) {
-		return rej('cannot set since_id and max_id');
+	// Check if both of since_id and until_id is specified
+	if (sinceId && untilId) {
+		return rej('cannot set since_id and until_id');
 	}
 
 	// Get 'folder_id' parameter
@@ -48,9 +48,9 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
 		query._id = {
 			$gt: sinceId
 		};
-	} else if (maxId) {
+	} else if (untilId) {
 		query._id = {
-			$lt: maxId
+			$lt: untilId
 		};
 	}
 
diff --git a/src/api/endpoints/drive/stream.ts b/src/api/endpoints/drive/stream.ts
index 7ee255e5d..5b0eb0a0d 100644
--- a/src/api/endpoints/drive/stream.ts
+++ b/src/api/endpoints/drive/stream.ts
@@ -21,13 +21,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
 	if (sinceIdErr) return rej('invalid since_id param');
 
-	// Get 'max_id' parameter
-	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) return rej('invalid max_id param');
+	// Get 'until_id' parameter
+	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+	if (untilIdErr) return rej('invalid until_id param');
 
-	// Check if both of since_id and max_id is specified
-	if (sinceId && maxId) {
-		return rej('cannot set since_id and max_id');
+	// Check if both of since_id and until_id is specified
+	if (sinceId && untilId) {
+		return rej('cannot set since_id and until_id');
 	}
 
 	// Get 'type' parameter
@@ -46,9 +46,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		query._id = {
 			$gt: sinceId
 		};
-	} else if (maxId) {
+	} else if (untilId) {
 		query._id = {
-			$lt: maxId
+			$lt: untilId
 		};
 	}
 	if (type) {
diff --git a/src/api/endpoints/i/notifications.ts b/src/api/endpoints/i/notifications.ts
index 607e0768a..48254e5e6 100644
--- a/src/api/endpoints/i/notifications.ts
+++ b/src/api/endpoints/i/notifications.ts
@@ -36,13 +36,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
 	if (sinceIdErr) return rej('invalid since_id param');
 
-	// Get 'max_id' parameter
-	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) return rej('invalid max_id param');
+	// Get 'until_id' parameter
+	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+	if (untilIdErr) return rej('invalid until_id param');
 
-	// Check if both of since_id and max_id is specified
-	if (sinceId && maxId) {
-		return rej('cannot set since_id and max_id');
+	// Check if both of since_id and until_id is specified
+	if (sinceId && untilId) {
+		return rej('cannot set since_id and until_id');
 	}
 
 	const query = {
@@ -73,9 +73,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		query._id = {
 			$gt: sinceId
 		};
-	} else if (maxId) {
+	} else if (untilId) {
 		query._id = {
-			$lt: maxId
+			$lt: untilId
 		};
 	}
 
diff --git a/src/api/endpoints/i/signin_history.ts b/src/api/endpoints/i/signin_history.ts
index 1a6e50c7c..e38bfa4d9 100644
--- a/src/api/endpoints/i/signin_history.ts
+++ b/src/api/endpoints/i/signin_history.ts
@@ -21,13 +21,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
 	if (sinceIdErr) return rej('invalid since_id param');
 
-	// Get 'max_id' parameter
-	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) return rej('invalid max_id param');
+	// Get 'until_id' parameter
+	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+	if (untilIdErr) return rej('invalid until_id param');
 
-	// Check if both of since_id and max_id is specified
-	if (sinceId && maxId) {
-		return rej('cannot set since_id and max_id');
+	// Check if both of since_id and until_id is specified
+	if (sinceId && untilId) {
+		return rej('cannot set since_id and until_id');
 	}
 
 	const query = {
@@ -43,9 +43,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		query._id = {
 			$gt: sinceId
 		};
-	} else if (maxId) {
+	} else if (untilId) {
 		query._id = {
-			$lt: maxId
+			$lt: untilId
 		};
 	}
 
diff --git a/src/api/endpoints/messaging/messages.ts b/src/api/endpoints/messaging/messages.ts
index 7b270924e..3d3c6950a 100644
--- a/src/api/endpoints/messaging/messages.ts
+++ b/src/api/endpoints/messaging/messages.ts
@@ -44,13 +44,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
 	if (sinceIdErr) return rej('invalid since_id param');
 
-	// Get 'max_id' parameter
-	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) return rej('invalid max_id param');
+	// Get 'until_id' parameter
+	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+	if (untilIdErr) return rej('invalid until_id param');
 
-	// Check if both of since_id and max_id is specified
-	if (sinceId && maxId) {
-		return rej('cannot set since_id and max_id');
+	// Check if both of since_id and until_id is specified
+	if (sinceId && untilId) {
+		return rej('cannot set since_id and until_id');
 	}
 
 	const query = {
@@ -72,9 +72,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		query._id = {
 			$gt: sinceId
 		};
-	} else if (maxId) {
+	} else if (untilId) {
 		query._id = {
-			$lt: maxId
+			$lt: untilId
 		};
 	}
 
diff --git a/src/api/endpoints/posts.ts b/src/api/endpoints/posts.ts
index f6efcc108..db166cd67 100644
--- a/src/api/endpoints/posts.ts
+++ b/src/api/endpoints/posts.ts
@@ -36,13 +36,13 @@ module.exports = (params) => new Promise(async (res, rej) => {
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
 	if (sinceIdErr) return rej('invalid since_id param');
 
-	// Get 'max_id' parameter
-	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) return rej('invalid max_id param');
+	// Get 'until_id' parameter
+	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+	if (untilIdErr) return rej('invalid until_id param');
 
-	// Check if both of since_id and max_id is specified
-	if (sinceId && maxId) {
-		return rej('cannot set since_id and max_id');
+	// Check if both of since_id and until_id is specified
+	if (sinceId && untilId) {
+		return rej('cannot set since_id and until_id');
 	}
 
 	// Construct query
@@ -55,9 +55,9 @@ module.exports = (params) => new Promise(async (res, rej) => {
 		query._id = {
 			$gt: sinceId
 		};
-	} else if (maxId) {
+	} else if (untilId) {
 		query._id = {
-			$lt: maxId
+			$lt: untilId
 		};
 	}
 
diff --git a/src/api/endpoints/posts/mentions.ts b/src/api/endpoints/posts/mentions.ts
index 0ebe8be50..3bb4ec3fa 100644
--- a/src/api/endpoints/posts/mentions.ts
+++ b/src/api/endpoints/posts/mentions.ts
@@ -27,13 +27,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
 	if (sinceIdErr) return rej('invalid since_id param');
 
-	// Get 'max_id' parameter
-	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) return rej('invalid max_id param');
+	// Get 'until_id' parameter
+	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+	if (untilIdErr) return rej('invalid until_id param');
 
-	// Check if both of since_id and max_id is specified
-	if (sinceId && maxId) {
-		return rej('cannot set since_id and max_id');
+	// Check if both of since_id and until_id is specified
+	if (sinceId && untilId) {
+		return rej('cannot set since_id and until_id');
 	}
 
 	// Construct query
@@ -58,9 +58,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		query._id = {
 			$gt: sinceId
 		};
-	} else if (maxId) {
+	} else if (untilId) {
 		query._id = {
-			$lt: maxId
+			$lt: untilId
 		};
 	}
 
diff --git a/src/api/endpoints/posts/reposts.ts b/src/api/endpoints/posts/reposts.ts
index b701ff757..bcc6163a1 100644
--- a/src/api/endpoints/posts/reposts.ts
+++ b/src/api/endpoints/posts/reposts.ts
@@ -25,13 +25,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
 	if (sinceIdErr) return rej('invalid since_id param');
 
-	// Get 'max_id' parameter
-	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) return rej('invalid max_id param');
+	// Get 'until_id' parameter
+	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+	if (untilIdErr) return rej('invalid until_id param');
 
-	// Check if both of since_id and max_id is specified
-	if (sinceId && maxId) {
-		return rej('cannot set since_id and max_id');
+	// Check if both of since_id and until_id is specified
+	if (sinceId && untilId) {
+		return rej('cannot set since_id and until_id');
 	}
 
 	// Lookup post
@@ -55,9 +55,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		query._id = {
 			$gt: sinceId
 		};
-	} else if (maxId) {
+	} else if (untilId) {
 		query._id = {
-			$lt: maxId
+			$lt: untilId
 		};
 	}
 
diff --git a/src/api/endpoints/posts/timeline.ts b/src/api/endpoints/posts/timeline.ts
index 0d08b9546..91cba0a04 100644
--- a/src/api/endpoints/posts/timeline.ts
+++ b/src/api/endpoints/posts/timeline.ts
@@ -25,21 +25,21 @@ module.exports = async (params, user, app) => {
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
 	if (sinceIdErr) throw 'invalid since_id param';
 
-	// Get 'max_id' parameter
-	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) throw 'invalid max_id param';
+	// Get 'until_id' parameter
+	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+	if (untilIdErr) throw 'invalid until_id param';
 
 	// Get 'since_date' parameter
 	const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$;
 	if (sinceDateErr) throw 'invalid since_date param';
 
-	// Get 'max_date' parameter
-	const [maxDate, maxDateErr] = $(params.max_date).optional.number().$;
-	if (maxDateErr) throw 'invalid max_date param';
+	// Get 'until_date' parameter
+	const [untilDate, untilDateErr] = $(params.until_date).optional.number().$;
+	if (untilDateErr) throw 'invalid until_date param';
 
-	// Check if only one of since_id, max_id, since_date, max_date specified
-	if ([sinceId, maxId, sinceDate, maxDate].filter(x => x != null).length > 1) {
-		throw 'only one of since_id, max_id, since_date, max_date can be specified';
+	// Check if only one of since_id, until_id, since_date, until_date specified
+	if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) {
+		throw 'only one of since_id, until_id, since_date, until_date can be specified';
 	}
 
 	const { followingIds, watchingChannelIds } = await rap({
@@ -85,18 +85,18 @@ module.exports = async (params, user, app) => {
 		query._id = {
 			$gt: sinceId
 		};
-	} else if (maxId) {
+	} else if (untilId) {
 		query._id = {
-			$lt: maxId
+			$lt: untilId
 		};
 	} else if (sinceDate) {
 		sort._id = 1;
 		query.created_at = {
 			$gt: new Date(sinceDate)
 		};
-	} else if (maxDate) {
+	} else if (untilDate) {
 		query.created_at = {
-			$lt: new Date(maxDate)
+			$lt: new Date(untilDate)
 		};
 	}
 	//#endregion
diff --git a/src/api/endpoints/users.ts b/src/api/endpoints/users.ts
index 134f262fb..f3c9b66a5 100644
--- a/src/api/endpoints/users.ts
+++ b/src/api/endpoints/users.ts
@@ -21,13 +21,13 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
 	if (sinceIdErr) return rej('invalid since_id param');
 
-	// Get 'max_id' parameter
-	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) return rej('invalid max_id param');
+	// Get 'until_id' parameter
+	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+	if (untilIdErr) return rej('invalid until_id param');
 
-	// Check if both of since_id and max_id is specified
-	if (sinceId && maxId) {
-		return rej('cannot set since_id and max_id');
+	// Check if both of since_id and until_id is specified
+	if (sinceId && untilId) {
+		return rej('cannot set since_id and until_id');
 	}
 
 	// Construct query
@@ -40,9 +40,9 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 		query._id = {
 			$gt: sinceId
 		};
-	} else if (maxId) {
+	} else if (untilId) {
 		query._id = {
-			$lt: maxId
+			$lt: untilId
 		};
 	}
 
diff --git a/src/api/endpoints/users/posts.ts b/src/api/endpoints/users/posts.ts
index fe821cf17..0d8384a43 100644
--- a/src/api/endpoints/users/posts.ts
+++ b/src/api/endpoints/users/posts.ts
@@ -42,21 +42,21 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
 	if (sinceIdErr) return rej('invalid since_id param');
 
-	// Get 'max_id' parameter
-	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) return rej('invalid max_id param');
+	// Get 'until_id' parameter
+	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+	if (untilIdErr) return rej('invalid until_id param');
 
 	// Get 'since_date' parameter
 	const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$;
 	if (sinceDateErr) throw 'invalid since_date param';
 
-	// Get 'max_date' parameter
-	const [maxDate, maxDateErr] = $(params.max_date).optional.number().$;
-	if (maxDateErr) throw 'invalid max_date param';
+	// Get 'until_date' parameter
+	const [untilDate, untilDateErr] = $(params.until_date).optional.number().$;
+	if (untilDateErr) throw 'invalid until_date param';
 
-	// Check if only one of since_id, max_id, since_date, max_date specified
-	if ([sinceId, maxId, sinceDate, maxDate].filter(x => x != null).length > 1) {
-		throw 'only one of since_id, max_id, since_date, max_date can be specified';
+	// Check if only one of since_id, until_id, since_date, until_date specified
+	if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) {
+		throw 'only one of since_id, until_id, since_date, until_date can be specified';
 	}
 
 	const q = userId !== undefined
@@ -88,18 +88,18 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 		query._id = {
 			$gt: sinceId
 		};
-	} else if (maxId) {
+	} else if (untilId) {
 		query._id = {
-			$lt: maxId
+			$lt: untilId
 		};
 	} else if (sinceDate) {
 		sort._id = 1;
 		query.created_at = {
 			$gt: new Date(sinceDate)
 		};
-	} else if (maxDate) {
+	} else if (untilDate) {
 		query.created_at = {
-			$lt: new Date(maxDate)
+			$lt: new Date(untilDate)
 		};
 	}
 
diff --git a/src/web/app/common/tags/messaging/room.tag b/src/web/app/common/tags/messaging/room.tag
index a149e1de2..7b4d1be56 100644
--- a/src/web/app/common/tags/messaging/room.tag
+++ b/src/web/app/common/tags/messaging/room.tag
@@ -254,7 +254,7 @@
 			this.api('messaging/messages', {
 				user_id: this.user.id,
 				limit: max + 1,
-				max_id: this.moreMessagesIsInStock ? this.messages[0].id : undefined
+				until_id: this.moreMessagesIsInStock ? this.messages[0].id : undefined
 			}).then(messages => {
 				if (messages.length == max + 1) {
 					this.moreMessagesIsInStock = true;
diff --git a/src/web/app/desktop/tags/home-widgets/mentions.tag b/src/web/app/desktop/tags/home-widgets/mentions.tag
index a48c7239a..268728307 100644
--- a/src/web/app/desktop/tags/home-widgets/mentions.tag
+++ b/src/web/app/desktop/tags/home-widgets/mentions.tag
@@ -101,7 +101,7 @@
 			});
 			this.api('posts/mentions', {
 				following: this.mode == 'following',
-				max_id: this.refs.timeline.tail().id
+				until_id: this.refs.timeline.tail().id
 			}).then(posts => {
 				this.update({
 					moreLoading: false
diff --git a/src/web/app/desktop/tags/home-widgets/timeline.tag b/src/web/app/desktop/tags/home-widgets/timeline.tag
index 4c58aa4aa..9571b09f3 100644
--- a/src/web/app/desktop/tags/home-widgets/timeline.tag
+++ b/src/web/app/desktop/tags/home-widgets/timeline.tag
@@ -86,7 +86,7 @@
 			});
 
 			this.api('posts/timeline', {
-				max_date: this.date ? this.date.getTime() : undefined
+				until_date: this.date ? this.date.getTime() : undefined
 			}).then(posts => {
 				this.update({
 					isLoading: false,
@@ -103,7 +103,7 @@
 				moreLoading: true
 			});
 			this.api('posts/timeline', {
-				max_id: this.refs.timeline.tail().id
+				until_id: this.refs.timeline.tail().id
 			}).then(posts => {
 				this.update({
 					moreLoading: false
diff --git a/src/web/app/desktop/tags/notifications.tag b/src/web/app/desktop/tags/notifications.tag
index 3218c00f6..39862487e 100644
--- a/src/web/app/desktop/tags/notifications.tag
+++ b/src/web/app/desktop/tags/notifications.tag
@@ -283,7 +283,7 @@
 
 			this.api('i/notifications', {
 				limit: max + 1,
-				max_id: this.notifications[this.notifications.length - 1].id
+				until_id: this.notifications[this.notifications.length - 1].id
 			}).then(notifications => {
 				if (notifications.length == max + 1) {
 					this.moreNotifications = true;
diff --git a/src/web/app/desktop/tags/user-timeline.tag b/src/web/app/desktop/tags/user-timeline.tag
index 2b05f6b5c..134aeee28 100644
--- a/src/web/app/desktop/tags/user-timeline.tag
+++ b/src/web/app/desktop/tags/user-timeline.tag
@@ -96,7 +96,7 @@
 		this.fetch = cb => {
 			this.api('users/posts', {
 				user_id: this.user.id,
-				max_date: this.date ? this.date.getTime() : undefined,
+				until_date: this.date ? this.date.getTime() : undefined,
 				with_replies: this.mode == 'with-replies'
 			}).then(posts => {
 				this.update({
@@ -116,7 +116,7 @@
 			this.api('users/posts', {
 				user_id: this.user.id,
 				with_replies: this.mode == 'with-replies',
-				max_id: this.refs.timeline.tail().id
+				until_id: this.refs.timeline.tail().id
 			}).then(posts => {
 				this.update({
 					moreLoading: false
diff --git a/src/web/app/mobile/tags/drive.tag b/src/web/app/mobile/tags/drive.tag
index 41dbfddae..2a3ff23bf 100644
--- a/src/web/app/mobile/tags/drive.tag
+++ b/src/web/app/mobile/tags/drive.tag
@@ -430,7 +430,7 @@
 			this.api('drive/files', {
 				folder_id: this.folder ? this.folder.id : null,
 				limit: max + 1,
-				max_id: this.files[this.files.length - 1].id
+				until_id: this.files[this.files.length - 1].id
 			}).then(files => {
 				if (files.length == max + 1) {
 					this.moreFiles = true;
diff --git a/src/web/app/mobile/tags/home-timeline.tag b/src/web/app/mobile/tags/home-timeline.tag
index e96823fa1..397d2b398 100644
--- a/src/web/app/mobile/tags/home-timeline.tag
+++ b/src/web/app/mobile/tags/home-timeline.tag
@@ -47,7 +47,7 @@
 
 		this.more = () => {
 			return this.api('posts/timeline', {
-				max_id: this.refs.timeline.tail().id
+				until_id: this.refs.timeline.tail().id
 			});
 		};
 
diff --git a/src/web/app/mobile/tags/notifications.tag b/src/web/app/mobile/tags/notifications.tag
index c3500d1b8..742cc4514 100644
--- a/src/web/app/mobile/tags/notifications.tag
+++ b/src/web/app/mobile/tags/notifications.tag
@@ -146,7 +146,7 @@
 
 			this.api('i/notifications', {
 				limit: max + 1,
-				max_id: this.notifications[this.notifications.length - 1].id
+				until_id: this.notifications[this.notifications.length - 1].id
 			}).then(notifications => {
 				if (notifications.length == max + 1) {
 					this.moreNotifications = true;
diff --git a/src/web/app/mobile/tags/user-timeline.tag b/src/web/app/mobile/tags/user-timeline.tag
index 4dbe719f5..86ead5971 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,
-				max_id: this.refs.timeline.tail().id
+				until_id: this.refs.timeline.tail().id
 			});
 		};
 	</script>
diff --git a/src/web/docs/api/endpoints/posts/timeline.yaml b/src/web/docs/api/endpoints/posts/timeline.yaml
index e1d78c082..01976b061 100644
--- a/src/web/docs/api/endpoints/posts/timeline.yaml
+++ b/src/web/docs/api/endpoints/posts/timeline.yaml
@@ -15,7 +15,7 @@ params:
     optional: true
     desc:
       ja: "指定すると、この投稿を基点としてより新しい投稿を取得します"
-  - name: "max_id"
+  - name: "until_id"
     type: "id(Post)"
     optional: true
     desc:
@@ -25,7 +25,7 @@ params:
     optional: true
     desc:
       ja: "指定した時間を基点としてより新しい投稿を取得します。数値は、1970 年 1 月 1 日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。"
-  - name: "max_date"
+  - name: "until_date"
     type: "number"
     optional: true
     desc:

From b0fdb3d1730842914abae2937cdf9076172d9534 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 21 Dec 2017 04:01:44 +0900
Subject: [PATCH 0077/1250] #1017 #155

---
 src/api/endpoints/posts/search.ts             | 96 ++++++++++++++++---
 .../app/common/scripts/parse-search-query.ts  | 41 ++++++++
 src/web/app/desktop/router.ts                 |  4 +-
 src/web/app/desktop/tags/search-posts.tag     |  6 +-
 src/web/app/desktop/tags/ui.tag               |  2 +-
 src/web/app/mobile/router.ts                  |  4 +-
 src/web/app/mobile/tags/search-posts.tag      |  6 +-
 src/web/app/mobile/tags/ui.tag                |  2 +-
 src/web/docs/search.ja.pug                    | 38 ++++++++
 9 files changed, 172 insertions(+), 27 deletions(-)
 create mode 100644 src/web/app/common/scripts/parse-search-query.ts
 create mode 100644 src/web/docs/search.ja.pug

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index b434f6434..dba7a53b5 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -5,6 +5,7 @@ import * as mongo from 'mongodb';
 import $ from 'cafy';
 const escapeRegexp = require('escape-regexp');
 import Post from '../../models/post';
+import User from '../../models/user';
 import serialize from '../../serializers/post';
 import config from '../../../conf';
 
@@ -16,33 +17,98 @@ import config from '../../../conf';
  * @return {Promise<any>}
  */
 module.exports = (params, me) => new Promise(async (res, rej) => {
-	// Get 'query' parameter
-	const [query, queryError] = $(params.query).string().pipe(x => x != '').$;
-	if (queryError) return rej('invalid query param');
+	// Get 'text' parameter
+	const [text, textError] = $(params.text).optional.string().$;
+	if (textError) return rej('invalid text param');
+
+	// Get 'user_id' parameter
+	const [userId, userIdErr] = $(params.user_id).optional.id().$;
+	if (userIdErr) return rej('invalid user_id param');
+
+	// Get 'username' parameter
+	const [username, usernameErr] = $(params.username).optional.string().$;
+	if (usernameErr) return rej('invalid username param');
+
+	// Get 'include_replies' parameter
+	const [includeReplies = true, includeRepliesErr] = $(params.include_replies).optional.boolean().$;
+	if (includeRepliesErr) return rej('invalid include_replies param');
+
+	// Get 'with_media' parameter
+	const [withMedia = false, withMediaErr] = $(params.with_media).optional.boolean().$;
+	if (withMediaErr) return rej('invalid with_media param');
+
+	// Get 'since_date' parameter
+	const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$;
+	if (sinceDateErr) throw 'invalid since_date param';
+
+	// Get 'until_date' parameter
+	const [untilDate, untilDateErr] = $(params.until_date).optional.number().$;
+	if (untilDateErr) throw 'invalid until_date param';
 
 	// Get 'offset' parameter
 	const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$;
 	if (offsetErr) return rej('invalid offset param');
 
-	// Get 'max' parameter
-	const [max = 10, maxErr] = $(params.max).optional.number().range(1, 30).$;
-	if (maxErr) return rej('invalid max param');
+	// Get 'limit' parameter
+	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 30).$;
+	if (limitErr) return rej('invalid limit param');
 
-	// If Elasticsearch is available, search by $
+	let user = userId;
+
+	if (user == null && username != null) {
+		const _user = await User.findOne({
+			username_lower: username.toLowerCase()
+		});
+		if (_user) {
+			user = _user._id;
+		}
+	}
+
+	// If Elasticsearch is available, search by it
 	// If not, search by MongoDB
 	(config.elasticsearch.enable ? byElasticsearch : byNative)
-		(res, rej, me, query, offset, max);
+		(res, rej, me, text, user, includeReplies, withMedia, sinceDate, untilDate, offset, limit);
 });
 
 // Search by MongoDB
-async function byNative(res, rej, me, query, offset, max) {
-	const escapedQuery = escapeRegexp(query);
+async function byNative(res, rej, me, text, userId, includeReplies, withMedia, sinceDate, untilDate, offset, max) {
+	const q: any = {};
+
+	if (text) {
+		q.$and = text.split(' ').map(x => ({
+			text: new RegExp(escapeRegexp(x))
+		}));
+	}
+
+	if (userId) {
+		q.user_id = userId;
+	}
+
+	if (!includeReplies) {
+		q.reply_id = null;
+	}
+
+	if (withMedia) {
+		q.media_ids = {
+			$exists: true,
+			$ne: null
+		};
+	}
+
+	if (sinceDate) {
+		q.created_at = {
+			$gt: new Date(sinceDate)
+		};
+	}
+
+	if (untilDate) {
+		if (q.created_at == undefined) q.created_at = {};
+		q.created_at.$lt = new Date(untilDate);
+	}
 
 	// Search posts
 	const posts = await Post
-		.find({
-			text: new RegExp(escapedQuery)
-		}, {
+		.find(q, {
 			sort: {
 				_id: -1
 			},
@@ -56,7 +122,7 @@ async function byNative(res, rej, me, query, offset, max) {
 }
 
 // Search by Elasticsearch
-async function byElasticsearch(res, rej, me, query, offset, max) {
+async function byElasticsearch(res, rej, me, text, userId, includeReplies, withMedia, sinceDate, untilDate, offset, max) {
 	const es = require('../../db/elasticsearch');
 
 	es.search({
@@ -68,7 +134,7 @@ async function byElasticsearch(res, rej, me, query, offset, max) {
 			query: {
 				simple_query_string: {
 					fields: ['text'],
-					query: query,
+					query: text,
 					default_operator: 'and'
 				}
 			},
diff --git a/src/web/app/common/scripts/parse-search-query.ts b/src/web/app/common/scripts/parse-search-query.ts
new file mode 100644
index 000000000..adcbfbb8f
--- /dev/null
+++ b/src/web/app/common/scripts/parse-search-query.ts
@@ -0,0 +1,41 @@
+export default function(qs: string) {
+	const q = {
+		text: ''
+	};
+
+	qs.split(' ').forEach(x => {
+		if (/^([a-z_]+?):(.+?)$/.test(x)) {
+			const [key, value] = x.split(':');
+			switch (key) {
+				case 'user':
+					q['username'] = value;
+					break;
+				case 'reply':
+					q['include_replies'] = value == 'true';
+					break;
+				case 'media':
+					q['with_media'] = value == 'true';
+					break;
+				case 'until':
+				case 'since':
+					// YYYY-MM-DD
+					if (/^[0-9]+\-[0-9]+\-[0-9]+$/) {
+						const [yyyy, mm, dd] = value.split('-');
+						q[`${key}_date`] = (new Date(parseInt(yyyy, 10), parseInt(mm, 10) - 1, parseInt(dd, 10))).getTime();
+					}
+					break;
+				default:
+					q[key] = value;
+					break;
+			}
+		} else {
+			q.text += x + ' ';
+		}
+	});
+
+	if (q.text) {
+		q.text = q.text.trim();
+	}
+
+	return q;
+}
diff --git a/src/web/app/desktop/router.ts b/src/web/app/desktop/router.ts
index 27b63ab2e..ce68c4f2d 100644
--- a/src/web/app/desktop/router.ts
+++ b/src/web/app/desktop/router.ts
@@ -16,7 +16,7 @@ export default (mios: MiOS) => {
 	route('/i/messaging/:user',      messaging);
 	route('/i/mentions',             mentions);
 	route('/post::post',             post);
-	route('/search::query',          search);
+	route('/search',                 search);
 	route('/:user',                  user.bind(null, 'home'));
 	route('/:user/graphs',           user.bind(null, 'graphs'));
 	route('/:user/:post',            post);
@@ -47,7 +47,7 @@ export default (mios: MiOS) => {
 
 	function search(ctx) {
 		const el = document.createElement('mk-search-page');
-		el.setAttribute('query', ctx.params.query);
+		el.setAttribute('query', ctx.querystring.substr(2));
 		mount(el);
 	}
 
diff --git a/src/web/app/desktop/tags/search-posts.tag b/src/web/app/desktop/tags/search-posts.tag
index 52f765d1a..c6b24837d 100644
--- a/src/web/app/desktop/tags/search-posts.tag
+++ b/src/web/app/desktop/tags/search-posts.tag
@@ -33,6 +33,8 @@
 
 	</style>
 	<script>
+		import parse from '../../common/scripts/parse-search-query';
+
 		this.mixin('api');
 
 		this.query = this.opts.query;
@@ -45,9 +47,7 @@
 			document.addEventListener('keydown', this.onDocumentKeydown);
 			window.addEventListener('scroll', this.onScroll);
 
-			this.api('posts/search', {
-				query: this.query
-			}).then(posts => {
+			this.api('posts/search', parse(this.query)).then(posts => {
 				this.update({
 					isLoading: false,
 					isEmpty: posts.length == 0
diff --git a/src/web/app/desktop/tags/ui.tag b/src/web/app/desktop/tags/ui.tag
index 059d88528..3dfdeec01 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:' + this.refs.q.value);
+			this.page('/search?q=' + encodeURIComponent(this.refs.q.value));
 		};
 	</script>
 </mk-ui-header-search>
diff --git a/src/web/app/mobile/router.ts b/src/web/app/mobile/router.ts
index d0c6add0b..afb9aa620 100644
--- a/src/web/app/mobile/router.ts
+++ b/src/web/app/mobile/router.ts
@@ -23,7 +23,7 @@ export default (mios: MiOS) => {
 	route('/i/settings/authorized-apps', settingsAuthorizedApps);
 	route('/post/new',                   newPost);
 	route('/post::post',                 post);
-	route('/search::query',              search);
+	route('/search',                     search);
 	route('/:user',                      user.bind(null, 'overview'));
 	route('/:user/graphs',               user.bind(null, 'graphs'));
 	route('/:user/followers',            userFollowers);
@@ -83,7 +83,7 @@ export default (mios: MiOS) => {
 
 	function search(ctx) {
 		const el = document.createElement('mk-search-page');
-		el.setAttribute('query', ctx.params.query);
+		el.setAttribute('query', ctx.querystring.substr(2));
 		mount(el);
 	}
 
diff --git a/src/web/app/mobile/tags/search-posts.tag b/src/web/app/mobile/tags/search-posts.tag
index 967764bc2..023a35bf6 100644
--- a/src/web/app/mobile/tags/search-posts.tag
+++ b/src/web/app/mobile/tags/search-posts.tag
@@ -15,6 +15,8 @@
 				width calc(100% - 32px)
 	</style>
 	<script>
+		import parse from '../../common/scripts/parse-search-query';
+
 		this.mixin('api');
 
 		this.max = 30;
@@ -24,9 +26,7 @@
 		this.withMedia = this.opts.withMedia;
 
 		this.init = new Promise((res, rej) => {
-			this.api('posts/search', {
-				query: this.query
-			}).then(posts => {
+			this.api('posts/search', parse(this.query)).then(posts => {
 				res(posts);
 				this.trigger('loaded');
 			});
diff --git a/src/web/app/mobile/tags/ui.tag b/src/web/app/mobile/tags/ui.tag
index 621f89f33..77ad14530 100644
--- a/src/web/app/mobile/tags/ui.tag
+++ b/src/web/app/mobile/tags/ui.tag
@@ -413,7 +413,7 @@
 		this.search = () => {
 			const query = window.prompt('%i18n:mobile.tags.mk-ui-nav.search%');
 			if (query == null || query == '') return;
-			this.page('/search:' + query);
+			this.page('/search?q=' + encodeURIComponent(query));
 		};
 	</script>
 </mk-ui-nav>
diff --git a/src/web/docs/search.ja.pug b/src/web/docs/search.ja.pug
new file mode 100644
index 000000000..f7ec9519f
--- /dev/null
+++ b/src/web/docs/search.ja.pug
@@ -0,0 +1,38 @@
+h1 検索
+
+p 投稿を検索することができます。
+p
+	| キーワードを半角スペースで区切ると、and検索になります。
+	| 例えば、「git コミット」と検索すると、「gitで編集したファイルの特定の行だけコミットする方法がわからない」などがマッチします。
+
+section
+	h2 オプション
+	p
+		| オプションを使用して、より高度な検索をすることもできます。
+		| オプションを指定するには、「オプション名:値」という形式でクエリに含めます。
+	p 利用可能なオプション一覧です:
+
+	table
+		thead
+			tr
+				th 名前
+				th 説明
+		tbody
+			tr
+				td user
+				td ユーザー名。投稿者を限定します。
+			tr
+				td reply
+				td 返信を含めるか否か。(trueかfalse)
+			tr
+				td media
+				td メディアが添付されているか。(trueかfalse)
+			tr
+				td until
+				td 上限の日時。(YYYY-MM-DD)
+			tr
+				td since
+				td 下限の日時。(YYYY-MM-DD)
+
+	p 例えば、「@syuiloの2017年11月1日から2017年12月31日までの『Misskey』というテキストを含む返信ではない投稿」を検索したい場合、クエリは以下のようになります:
+	code user:syuilo since:2017-11-01 until:2017-12-31 reply:false Misskey

From 48110883a4fa67ba888e6e7822c8c14144f0960f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 21 Dec 2017 04:02:46 +0900
Subject: [PATCH 0078/1250] v3420

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index bd4f2c1f1..c8687534b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3420 (2017/12/21)
+-----------------
+* 検索機能を大幅に強化
+
 3415 (2017/12/19)
 -----------------
 * デザインの調整
diff --git a/package.json b/package.json
index 39922127c..6ab49852b 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3415",
+	"version": "0.0.3420",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 0512de864625deac74aae0edc7be581cd5488d23 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, 21 Dec 2017 06:31:56 +0900
Subject: [PATCH 0079/1250] #1023

---
 src/api/endpoints/posts/search.ts             | 21 ++++++++++++++++---
 .../app/common/scripts/parse-search-query.ts  |  3 +++
 src/web/docs/search.ja.pug                    |  3 +++
 3 files changed, 24 insertions(+), 3 deletions(-)

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index dba7a53b5..88cdd32da 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -6,6 +6,7 @@ import $ from 'cafy';
 const escapeRegexp = require('escape-regexp');
 import Post from '../../models/post';
 import User from '../../models/user';
+import getFriends from '../../common/get-friends';
 import serialize from '../../serializers/post';
 import config from '../../../conf';
 
@@ -29,6 +30,10 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [username, usernameErr] = $(params.username).optional.string().$;
 	if (usernameErr) return rej('invalid username param');
 
+	// Get 'following' parameter
+	const [following = null, followingErr] = $(params.following).optional.nullable.boolean().$;
+	if (followingErr) return rej('invalid following param');
+
 	// Get 'include_replies' parameter
 	const [includeReplies = true, includeRepliesErr] = $(params.include_replies).optional.boolean().$;
 	if (includeRepliesErr) return rej('invalid include_replies param');
@@ -67,11 +72,11 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	// If Elasticsearch is available, search by it
 	// If not, search by MongoDB
 	(config.elasticsearch.enable ? byElasticsearch : byNative)
-		(res, rej, me, text, user, includeReplies, withMedia, sinceDate, untilDate, offset, limit);
+		(res, rej, me, text, user, following, includeReplies, withMedia, sinceDate, untilDate, offset, limit);
 });
 
 // Search by MongoDB
-async function byNative(res, rej, me, text, userId, includeReplies, withMedia, sinceDate, untilDate, offset, max) {
+async function byNative(res, rej, me, text, userId, following, includeReplies, withMedia, sinceDate, untilDate, offset, max) {
 	const q: any = {};
 
 	if (text) {
@@ -84,6 +89,16 @@ async function byNative(res, rej, me, text, userId, includeReplies, withMedia, s
 		q.user_id = userId;
 	}
 
+	if (following != null) {
+		const ids = await getFriends(me._id, false);
+		q.user_id = {};
+		if (following) {
+			q.user_id.$in = ids;
+		} else {
+			q.user_id.$nin = ids;
+		}
+	}
+
 	if (!includeReplies) {
 		q.reply_id = null;
 	}
@@ -122,7 +137,7 @@ async function byNative(res, rej, me, text, userId, includeReplies, withMedia, s
 }
 
 // Search by Elasticsearch
-async function byElasticsearch(res, rej, me, text, userId, includeReplies, withMedia, sinceDate, untilDate, offset, max) {
+async function byElasticsearch(res, rej, me, text, userId, following, includeReplies, withMedia, sinceDate, untilDate, offset, max) {
 	const es = require('../../db/elasticsearch');
 
 	es.search({
diff --git a/src/web/app/common/scripts/parse-search-query.ts b/src/web/app/common/scripts/parse-search-query.ts
index adcbfbb8f..62b2cf51b 100644
--- a/src/web/app/common/scripts/parse-search-query.ts
+++ b/src/web/app/common/scripts/parse-search-query.ts
@@ -10,6 +10,9 @@ export default function(qs: string) {
 				case 'user':
 					q['username'] = value;
 					break;
+				case 'follow':
+					q['following'] = value == 'null' ? null : value == 'true';
+					break;
 				case 'reply':
 					q['include_replies'] = value == 'true';
 					break;
diff --git a/src/web/docs/search.ja.pug b/src/web/docs/search.ja.pug
index f7ec9519f..7d4d23fb6 100644
--- a/src/web/docs/search.ja.pug
+++ b/src/web/docs/search.ja.pug
@@ -21,6 +21,9 @@ section
 			tr
 				td user
 				td ユーザー名。投稿者を限定します。
+			tr
+				td follow
+				td フォローしているユーザーのみに限定。(trueかfalse)
 			tr
 				td reply
 				td 返信を含めるか否か。(trueかfalse)

From 5ac31b440ad3f0fb56fc8237af7bf9b5b1236b62 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, 21 Dec 2017 06:33:03 +0900
Subject: [PATCH 0080/1250] v3422

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c8687534b..4159d025e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3422 (2017/12/21)
+-----------------
+* 検索にfollow追加 #1023
+
 3420 (2017/12/21)
 -----------------
 * 検索機能を大幅に強化
diff --git a/package.json b/package.json
index 6ab49852b..54fbd9e99 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3420",
+	"version": "0.0.3422",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 92c85e0bda07d5ad338957a9842012d6a94102a2 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, 21 Dec 2017 07:35:16 +0900
Subject: [PATCH 0081/1250] :v:

---
 src/api/endpoints/posts/search.ts             | 130 ++++++++++++++----
 .../app/common/scripts/parse-search-query.ts  |   7 +-
 src/web/docs/search.ja.pug                    |  29 +++-
 3 files changed, 131 insertions(+), 35 deletions(-)

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index 88cdd32da..21e9134d3 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -34,13 +34,17 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [following = null, followingErr] = $(params.following).optional.nullable.boolean().$;
 	if (followingErr) return rej('invalid following param');
 
-	// Get 'include_replies' parameter
-	const [includeReplies = true, includeRepliesErr] = $(params.include_replies).optional.boolean().$;
-	if (includeRepliesErr) return rej('invalid include_replies param');
+	// Get 'reply' parameter
+	const [reply = null, replyErr] = $(params.reply).optional.nullable.boolean().$;
+	if (replyErr) return rej('invalid reply param');
 
-	// Get 'with_media' parameter
-	const [withMedia = false, withMediaErr] = $(params.with_media).optional.boolean().$;
-	if (withMediaErr) return rej('invalid with_media param');
+	// Get 'repost' parameter
+	const [repost = null, repostErr] = $(params.repost).optional.nullable.boolean().$;
+	if (repostErr) return rej('invalid repost param');
+
+	// Get 'media' parameter
+	const [media = null, mediaErr] = $(params.media).optional.nullable.boolean().$;
+	if (mediaErr) return rej('invalid media param');
 
 	// Get 'since_date' parameter
 	const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$;
@@ -72,53 +76,119 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	// If Elasticsearch is available, search by it
 	// If not, search by MongoDB
 	(config.elasticsearch.enable ? byElasticsearch : byNative)
-		(res, rej, me, text, user, following, includeReplies, withMedia, sinceDate, untilDate, offset, limit);
+		(res, rej, me, text, user, following, reply, repost, media, sinceDate, untilDate, offset, limit);
 });
 
 // Search by MongoDB
-async function byNative(res, rej, me, text, userId, following, includeReplies, withMedia, sinceDate, untilDate, offset, max) {
-	const q: any = {};
+async function byNative(res, rej, me, text, userId, following, reply, repost, media, sinceDate, untilDate, offset, max) {
+	const q: any = {
+		$and: []
+	};
+
+	const push = q.$and.push;
 
 	if (text) {
-		q.$and = text.split(' ').map(x => ({
-			text: new RegExp(escapeRegexp(x))
-		}));
+		push({
+			$and: text.split(' ').map(x => ({
+				text: new RegExp(escapeRegexp(x))
+			}))
+		});
 	}
 
 	if (userId) {
-		q.user_id = userId;
+		push({
+			user_id: userId
+		});
 	}
 
 	if (following != null) {
 		const ids = await getFriends(me._id, false);
-		q.user_id = {};
-		if (following) {
-			q.user_id.$in = ids;
+		push({
+			user_id: following ? {
+				$in: ids
+			} : {
+				$nin: ids
+			}
+		});
+	}
+
+	if (reply != null) {
+		if (reply) {
+			push({
+				reply_id: {
+					$exists: true,
+					$ne: null
+				}
+			});
 		} else {
-			q.user_id.$nin = ids;
+			push({
+				$or: [{
+					reply_id: {
+						$exists: false
+					}
+				}, {
+					reply_id: null
+				}]
+			});
 		}
 	}
 
-	if (!includeReplies) {
-		q.reply_id = null;
+	if (repost != null) {
+		if (repost) {
+			push({
+				repost_id: {
+					$exists: true,
+					$ne: null
+				}
+			});
+		} else {
+			push({
+				$or: [{
+					repost_id: {
+						$exists: false
+					}
+				}, {
+					repost_id: null
+				}]
+			});
+		}
 	}
 
-	if (withMedia) {
-		q.media_ids = {
-			$exists: true,
-			$ne: null
-		};
+	if (media != null) {
+		if (media) {
+			push({
+				media_ids: {
+					$exists: true,
+					$ne: null
+				}
+			});
+		} else {
+			push({
+				$or: [{
+					media_ids: {
+						$exists: false
+					}
+				}, {
+					media_ids: null
+				}]
+			});
+		}
 	}
 
 	if (sinceDate) {
-		q.created_at = {
-			$gt: new Date(sinceDate)
-		};
+		push({
+			created_at: {
+				$gt: new Date(sinceDate)
+			}
+		});
 	}
 
 	if (untilDate) {
-		if (q.created_at == undefined) q.created_at = {};
-		q.created_at.$lt = new Date(untilDate);
+		push({
+			created_at: {
+				$lt: new Date(untilDate)
+			}
+		});
 	}
 
 	// Search posts
@@ -137,7 +207,7 @@ async function byNative(res, rej, me, text, userId, following, includeReplies, w
 }
 
 // Search by Elasticsearch
-async function byElasticsearch(res, rej, me, text, userId, following, includeReplies, withMedia, sinceDate, untilDate, offset, max) {
+async function byElasticsearch(res, rej, me, text, userId, following, reply, repost, media, sinceDate, untilDate, offset, max) {
 	const es = require('../../db/elasticsearch');
 
 	es.search({
diff --git a/src/web/app/common/scripts/parse-search-query.ts b/src/web/app/common/scripts/parse-search-query.ts
index 62b2cf51b..f65e4683a 100644
--- a/src/web/app/common/scripts/parse-search-query.ts
+++ b/src/web/app/common/scripts/parse-search-query.ts
@@ -14,10 +14,13 @@ export default function(qs: string) {
 					q['following'] = value == 'null' ? null : value == 'true';
 					break;
 				case 'reply':
-					q['include_replies'] = value == 'true';
+					q['reply'] = value == 'null' ? null : value == 'true';
+					break;
+				case 'repost':
+					q['repost'] = value == 'null' ? null : value == 'true';
 					break;
 				case 'media':
-					q['with_media'] = value == 'true';
+					q['media'] = value == 'null' ? null : value == 'true';
 					break;
 				case 'until':
 				case 'since':
diff --git a/src/web/docs/search.ja.pug b/src/web/docs/search.ja.pug
index 7d4d23fb6..d46e5f4a0 100644
--- a/src/web/docs/search.ja.pug
+++ b/src/web/docs/search.ja.pug
@@ -23,13 +23,36 @@ section
 				td ユーザー名。投稿者を限定します。
 			tr
 				td follow
-				td フォローしているユーザーのみに限定。(trueかfalse)
+				td
+					| true ... フォローしているユーザーに限定。
+					br
+					| false ... フォローしていないユーザーに限定。
+					br
+					| null ... 特に限定しない(デフォルト)
 			tr
 				td reply
-				td 返信を含めるか否か。(trueかfalse)
+				td
+					| true ... 返信に限定。
+					br
+					| false ... 返信でない投稿に限定。
+					br
+					| null ... 特に限定しない(デフォルト)
+			tr
+				td repost
+				td
+					| true ... Repostに限定。
+					br
+					| false ... Repostでない投稿に限定。
+					br
+					| null ... 特に限定しない(デフォルト)
 			tr
 				td media
-				td メディアが添付されているか。(trueかfalse)
+				td
+					| true ... メディアが添付されている投稿に限定。
+					br
+					| false ... メディアが添付されていない投稿に限定。
+					br
+					| null ... 特に限定しない(デフォルト)
 			tr
 				td until
 				td 上限の日時。(YYYY-MM-DD)

From 11192a0fa222041aedf726d8986ce1737cc7fda6 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, 21 Dec 2017 07:35:23 +0900
Subject: [PATCH 0082/1250] v3424

---
 CHANGELOG.md | 5 +++++
 package.json | 2 +-
 2 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4159d025e..c33ecddb1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,11 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3424 (2017/12/21)
+-----------------
+* 検索にrepost追加
+* など
+
 3422 (2017/12/21)
 -----------------
 * 検索にfollow追加 #1023
diff --git a/package.json b/package.json
index 54fbd9e99..a85ffded0 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3422",
+	"version": "0.0.3424",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From bf400bf1f244f108045952c74f91a746e01e9ffa 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, 21 Dec 2017 07:45:45 +0900
Subject: [PATCH 0083/1250] Fix bug

---
 src/api/endpoints/posts/search.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index 21e9134d3..a3c44d09c 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -85,7 +85,7 @@ async function byNative(res, rej, me, text, userId, following, reply, repost, me
 		$and: []
 	};
 
-	const push = q.$and.push;
+	const push = x => q.$and.push(x);
 
 	if (text) {
 		push({

From c460ac3738b532ac2f0831cc7ff5eec7e710b2b7 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, 21 Dec 2017 07:57:31 +0900
Subject: [PATCH 0084/1250] :v:

---
 src/api/endpoints/posts/search.ts             | 31 +++++++++++++++++--
 .../app/common/scripts/parse-search-query.ts  |  3 ++
 src/web/docs/search.ja.pug                    |  8 +++++
 3 files changed, 39 insertions(+), 3 deletions(-)

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index a3c44d09c..777cd7909 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -46,6 +46,10 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [media = null, mediaErr] = $(params.media).optional.nullable.boolean().$;
 	if (mediaErr) return rej('invalid media param');
 
+	// Get 'poll' parameter
+	const [poll = null, pollErr] = $(params.poll).optional.nullable.boolean().$;
+	if (pollErr) return rej('invalid poll param');
+
 	// Get 'since_date' parameter
 	const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$;
 	if (sinceDateErr) throw 'invalid since_date param';
@@ -76,11 +80,11 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	// If Elasticsearch is available, search by it
 	// If not, search by MongoDB
 	(config.elasticsearch.enable ? byElasticsearch : byNative)
-		(res, rej, me, text, user, following, reply, repost, media, sinceDate, untilDate, offset, limit);
+		(res, rej, me, text, user, following, reply, repost, media, poll, sinceDate, untilDate, offset, limit);
 });
 
 // Search by MongoDB
-async function byNative(res, rej, me, text, userId, following, reply, repost, media, sinceDate, untilDate, offset, max) {
+async function byNative(res, rej, me, text, userId, following, reply, repost, media, poll, sinceDate, untilDate, offset, max) {
 	const q: any = {
 		$and: []
 	};
@@ -175,6 +179,27 @@ async function byNative(res, rej, me, text, userId, following, reply, repost, me
 		}
 	}
 
+	if (poll != null) {
+		if (poll) {
+			push({
+				poll: {
+					$exists: true,
+					$ne: null
+				}
+			});
+		} else {
+			push({
+				$or: [{
+					poll: {
+						$exists: false
+					}
+				}, {
+					poll: null
+				}]
+			});
+		}
+	}
+
 	if (sinceDate) {
 		push({
 			created_at: {
@@ -207,7 +232,7 @@ async function byNative(res, rej, me, text, userId, following, reply, repost, me
 }
 
 // Search by Elasticsearch
-async function byElasticsearch(res, rej, me, text, userId, following, reply, repost, media, sinceDate, untilDate, offset, max) {
+async function byElasticsearch(res, rej, me, text, userId, following, reply, repost, media, poll, sinceDate, untilDate, offset, max) {
 	const es = require('../../db/elasticsearch');
 
 	es.search({
diff --git a/src/web/app/common/scripts/parse-search-query.ts b/src/web/app/common/scripts/parse-search-query.ts
index f65e4683a..c021ee641 100644
--- a/src/web/app/common/scripts/parse-search-query.ts
+++ b/src/web/app/common/scripts/parse-search-query.ts
@@ -22,6 +22,9 @@ export default function(qs: string) {
 				case 'media':
 					q['media'] = value == 'null' ? null : value == 'true';
 					break;
+				case 'poll':
+					q['poll'] = value == 'null' ? null : value == 'true';
+					break;
 				case 'until':
 				case 'since':
 					// YYYY-MM-DD
diff --git a/src/web/docs/search.ja.pug b/src/web/docs/search.ja.pug
index d46e5f4a0..41e443d74 100644
--- a/src/web/docs/search.ja.pug
+++ b/src/web/docs/search.ja.pug
@@ -53,6 +53,14 @@ section
 					| false ... メディアが添付されていない投稿に限定。
 					br
 					| null ... 特に限定しない(デフォルト)
+			tr
+				td poll
+				td
+					| true ... 投票が添付されている投稿に限定。
+					br
+					| false ... 投票が添付されていない投稿に限定。
+					br
+					| null ... 特に限定しない(デフォルト)
 			tr
 				td until
 				td 上限の日時。(YYYY-MM-DD)

From 1bcf8e27ddb5e28027986f1b62d8cf136aeb7def 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, 21 Dec 2017 07:58:04 +0900
Subject: [PATCH 0085/1250] v3426

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c33ecddb1..ff417fde9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3426 (2017/12/21)
+-----------------
+* 検索にpoll追加
+
 3424 (2017/12/21)
 -----------------
 * 検索にrepost追加
diff --git a/package.json b/package.json
index a85ffded0..d3960472d 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3424",
+	"version": "0.0.3426",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From a2aba67da5c0c17a661ebadef82bf950e32235cf 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, 21 Dec 2017 11:08:17 +0900
Subject: [PATCH 0086/1250] Fix #1025

---
 src/web/app/desktop/tags/search-posts.tag | 13 +++++++------
 src/web/app/mobile/tags/search-posts.tag  | 10 ++++------
 2 files changed, 11 insertions(+), 12 deletions(-)

diff --git a/src/web/app/desktop/tags/search-posts.tag b/src/web/app/desktop/tags/search-posts.tag
index c6b24837d..2acb675d4 100644
--- a/src/web/app/desktop/tags/search-posts.tag
+++ b/src/web/app/desktop/tags/search-posts.tag
@@ -41,7 +41,8 @@
 		this.isLoading = true;
 		this.isEmpty = false;
 		this.moreLoading = false;
-		this.page = 0;
+		this.limit = 30;
+		this.offset = 0;
 
 		this.on('mount', () => {
 			document.addEventListener('keydown', this.onDocumentKeydown);
@@ -72,16 +73,16 @@
 
 		this.more = () => {
 			if (this.moreLoading || this.isLoading || this.timeline.posts.length == 0) return;
+			this.offset += this.limit;
 			this.update({
 				moreLoading: true
 			});
-			this.api('posts/search', {
-				query: this.query,
-				page: this.page + 1
+			return this.api('posts/search', Object.assign({}, parse(this.query), {
+				limit: this.limit,
+				offset: this.offset
 			}).then(posts => {
 				this.update({
-					moreLoading: false,
-					page: page + 1
+					moreLoading: false
 				});
 				this.refs.timeline.prependPosts(posts);
 			});
diff --git a/src/web/app/mobile/tags/search-posts.tag b/src/web/app/mobile/tags/search-posts.tag
index 023a35bf6..b37ba69e8 100644
--- a/src/web/app/mobile/tags/search-posts.tag
+++ b/src/web/app/mobile/tags/search-posts.tag
@@ -19,11 +19,10 @@
 
 		this.mixin('api');
 
-		this.max = 30;
+		this.limit = 30;
 		this.offset = 0;
 
 		this.query = this.opts.query;
-		this.withMedia = this.opts.withMedia;
 
 		this.init = new Promise((res, rej) => {
 			this.api('posts/search', parse(this.query)).then(posts => {
@@ -33,10 +32,9 @@
 		});
 
 		this.more = () => {
-			this.offset += this.max;
-			return this.api('posts/search', {
-				query: this.query,
-				max: this.max,
+			this.offset += this.limit;
+			return this.api('posts/search', Object.assign({}, parse(this.query), {
+				limit: this.limit,
 				offset: this.offset
 			});
 		};

From e7691676bc91c06f121a60e6b4d896c4be81bf29 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, 21 Dec 2017 11:09:39 +0900
Subject: [PATCH 0087/1250] v3428

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ff417fde9..db21a15d4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3428 (2017/12/21)
+-----------------
+* バグ修正
+
 3426 (2017/12/21)
 -----------------
 * 検索にpoll追加
diff --git a/package.json b/package.json
index d3960472d..c3dc9da45 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3426",
+	"version": "0.0.3428",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 15f31d453b163a04df26d921af50d561091aeab8 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, 21 Dec 2017 11:13:49 +0900
Subject: [PATCH 0088/1250] oops

---
 src/web/app/desktop/tags/search-posts.tag | 2 +-
 src/web/app/mobile/tags/search-posts.tag  | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/web/app/desktop/tags/search-posts.tag b/src/web/app/desktop/tags/search-posts.tag
index 2acb675d4..f7ec85a4f 100644
--- a/src/web/app/desktop/tags/search-posts.tag
+++ b/src/web/app/desktop/tags/search-posts.tag
@@ -80,7 +80,7 @@
 			return this.api('posts/search', Object.assign({}, parse(this.query), {
 				limit: this.limit,
 				offset: this.offset
-			}).then(posts => {
+			})).then(posts => {
 				this.update({
 					moreLoading: false
 				});
diff --git a/src/web/app/mobile/tags/search-posts.tag b/src/web/app/mobile/tags/search-posts.tag
index b37ba69e8..3e3c034f2 100644
--- a/src/web/app/mobile/tags/search-posts.tag
+++ b/src/web/app/mobile/tags/search-posts.tag
@@ -36,7 +36,7 @@
 			return this.api('posts/search', Object.assign({}, parse(this.query), {
 				limit: this.limit,
 				offset: this.offset
-			});
+			}));
 		};
 	</script>
 </mk-search-posts>

From b251f81ec1c0fadbd603164642f6977e38a879c3 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, 21 Dec 2017 11:14:16 +0900
Subject: [PATCH 0089/1250] v3430

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index db21a15d4..c253d1f11 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3430 (2017/12/21)
+-----------------
+* oops
+
 3428 (2017/12/21)
 -----------------
 * バグ修正
diff --git a/package.json b/package.json
index c3dc9da45..b43f5be70 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3428",
+	"version": "0.0.3430",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 89b65fd16c9fcba3d6e6c8d7839747cc08b9b44e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 21 Dec 2017 15:28:50 +0900
Subject: [PATCH 0090/1250] Fix bug

---
 src/api/endpoints/posts/search.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index 777cd7909..bd3bbfc12 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -105,7 +105,7 @@ async function byNative(res, rej, me, text, userId, following, reply, repost, me
 		});
 	}
 
-	if (following != null) {
+	if (following != null && me != null) {
 		const ids = await getFriends(me._id, false);
 		push({
 			user_id: following ? {

From 93dafa205e520cc22ee99e556fcec37f0b2a85c9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 21 Dec 2017 15:30:15 +0900
Subject: [PATCH 0091/1250] #1026

---
 src/api/endpoints/posts/search.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index bd3bbfc12..16d54f729 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -111,7 +111,7 @@ async function byNative(res, rej, me, text, userId, following, reply, repost, me
 			user_id: following ? {
 				$in: ids
 			} : {
-				$nin: ids
+				$nin: ids.concat(me._id)
 			}
 		});
 	}

From 89c8d88b64619e93dc18a408543e9fc6852b3f65 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 21 Dec 2017 15:37:26 +0900
Subject: [PATCH 0092/1250] Fix bug

---
 src/api/endpoints/posts/search.ts | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index 16d54f729..ac25652a0 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -85,7 +85,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 // Search by MongoDB
 async function byNative(res, rej, me, text, userId, following, reply, repost, media, poll, sinceDate, untilDate, offset, max) {
-	const q: any = {
+	let q: any = {
 		$and: []
 	};
 
@@ -216,6 +216,10 @@ async function byNative(res, rej, me, text, userId, following, reply, repost, me
 		});
 	}
 
+	if (q.$and.length == 0) {
+		q = {};
+	}
+
 	// Search posts
 	const posts = await Post
 		.find(q, {

From dddc1f6624f7caa42e2e7379267b3f9691107f5c 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, 22 Dec 2017 04:50:50 +0900
Subject: [PATCH 0093/1250] wip

---
 src/api/endpoints/posts/timeline.ts | 12 +++++++++++-
 src/api/models/mute.ts              |  3 +++
 2 files changed, 14 insertions(+), 1 deletion(-)
 create mode 100644 src/api/models/mute.ts

diff --git a/src/api/endpoints/posts/timeline.ts b/src/api/endpoints/posts/timeline.ts
index 91cba0a04..6cc7825e6 100644
--- a/src/api/endpoints/posts/timeline.ts
+++ b/src/api/endpoints/posts/timeline.ts
@@ -77,7 +77,17 @@ module.exports = async (params, user, app) => {
 			channel_id: {
 				$in: watchingChannelIds
 			}
-		}]
+		}],
+		// mute
+		user_id: {
+			$nin: mutes
+		},
+		'_reply.user_id': {
+			$nin: mutes
+		},
+		'_repost.user_id': {
+			$nin: mutes
+		},
 	} as any;
 
 	if (sinceId) {
diff --git a/src/api/models/mute.ts b/src/api/models/mute.ts
new file mode 100644
index 000000000..16018b82f
--- /dev/null
+++ b/src/api/models/mute.ts
@@ -0,0 +1,3 @@
+import db from '../../db/mongodb';
+
+export default db.get('mute') as any; // fuck type definition

From 19e52bf1c8cf0198ffb388987739d4e2895975f2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 05:41:21 +0900
Subject: [PATCH 0094/1250] wip

---
 src/api/endpoints/posts/create.ts            |  6 +-
 tools/migration/node.2017-12-22.hiseikika.js | 67 ++++++++++++++++++++
 2 files changed, 72 insertions(+), 1 deletion(-)
 create mode 100644 tools/migration/node.2017-12-22.hiseikika.js

diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index 7270efaf7..9d791538f 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -215,7 +215,11 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		poll: poll,
 		text: text,
 		user_id: user._id,
-		app_id: app ? app._id : null
+		app_id: app ? app._id : null,
+
+		// 以下非正規化データ
+		_reply: reply ? { user_id: reply.user_id } : undefined,
+		_repost: repost ? { user_id: repost.user_id } : undefined,
 	});
 
 	// Serialize
diff --git a/tools/migration/node.2017-12-22.hiseikika.js b/tools/migration/node.2017-12-22.hiseikika.js
new file mode 100644
index 000000000..ff8294c8d
--- /dev/null
+++ b/tools/migration/node.2017-12-22.hiseikika.js
@@ -0,0 +1,67 @@
+// for Node.js interpret
+
+const { default: Post } = require('../../built/api/models/post')
+const { default: zip } = require('@prezzemolo/zip')
+
+const migrate = async (post) => {
+	const x = {};
+	if (post.reply_id != null) {
+		const reply = await Post.findOne({
+			_id: post.reply_id
+		});
+		x['_reply.user_id'] = reply.user_id;
+	}
+	if (post.repost_id != null) {
+		const repost = await Post.findOne({
+			_id: post.repost_id
+		});
+		x['_repost.user_id'] = repost.user_id;
+	}
+	if (post.reply_id != null || post.repost_id != null) {
+		const result = await Post.update(post._id, {
+			$set: x,
+		});
+		return result.ok === 1;
+	} else {
+		return true;
+	}
+}
+
+async function main() {
+	const query = {
+		$or: [{
+			reply_id: {
+				$exists: true,
+				$ne: null
+			}
+		}, {
+			repost_id: {
+				$exists: true,
+				$ne: null
+			}
+		}]
+	}
+
+	const count = await Post.count(query);
+
+	const dop = Number.parseInt(process.argv[2]) || 5
+	const idop = ((count - (count % dop)) / dop) + 1
+
+	return zip(
+		1,
+		async (time) => {
+			console.log(`${time} / ${idop}`)
+			const doc = await Post.find(query, {
+				limit: dop, skip: time * dop
+			})
+			return Promise.all(doc.map(migrate))
+		},
+		idop
+	).then(a => {
+		const rv = []
+		a.forEach(e => rv.push(...e))
+		return rv
+	})
+}
+
+main().then(console.dir).catch(console.error)

From 5f5c186215975841f3727bb8d1a3ca80d8f20e32 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 06:03:54 +0900
Subject: [PATCH 0095/1250] wip

---
 src/api/endpoints.ts                | 17 +++++++
 src/api/endpoints/mute/create.ts    | 61 ++++++++++++++++++++++++
 src/api/endpoints/mute/delete.ts    | 63 +++++++++++++++++++++++++
 src/api/endpoints/mute/list.ts      | 73 +++++++++++++++++++++++++++++
 src/api/endpoints/posts/timeline.ts | 19 ++++++--
 5 files changed, 228 insertions(+), 5 deletions(-)
 create mode 100644 src/api/endpoints/mute/create.ts
 create mode 100644 src/api/endpoints/mute/delete.ts
 create mode 100644 src/api/endpoints/mute/list.ts

diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts
index 1138df193..e84638157 100644
--- a/src/api/endpoints.ts
+++ b/src/api/endpoints.ts
@@ -222,6 +222,23 @@ const endpoints: Endpoint[] = [
 		withCredential: true,
 		kind: 'notification-read'
 	},
+
+	{
+		name: 'mute/create',
+		withCredential: true,
+		kind: 'account/write'
+	},
+	{
+		name: 'mute/delete',
+		withCredential: true,
+		kind: 'account/write'
+	},
+	{
+		name: 'mute/list',
+		withCredential: true,
+		kind: 'account/read'
+	},
+
 	{
 		name: 'notifications/get_unread_count',
 		withCredential: true,
diff --git a/src/api/endpoints/mute/create.ts b/src/api/endpoints/mute/create.ts
new file mode 100644
index 000000000..f44854ab5
--- /dev/null
+++ b/src/api/endpoints/mute/create.ts
@@ -0,0 +1,61 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import User from '../../models/user';
+import Mute from '../../models/mute';
+
+/**
+ * Mute a user
+ *
+ * @param {any} params
+ * @param {any} user
+ * @return {Promise<any>}
+ */
+module.exports = (params, user) => new Promise(async (res, rej) => {
+	const muter = user;
+
+	// Get 'user_id' parameter
+	const [userId, userIdErr] = $(params.user_id).id().$;
+	if (userIdErr) return rej('invalid user_id param');
+
+	// 自分自身
+	if (user._id.equals(userId)) {
+		return rej('mutee is yourself');
+	}
+
+	// Get mutee
+	const mutee = await User.findOne({
+		_id: userId
+	}, {
+		fields: {
+			data: false,
+			profile: false
+		}
+	});
+
+	if (mutee === null) {
+		return rej('user not found');
+	}
+
+	// Check if already muting
+	const exist = await Mute.findOne({
+		muter_id: muter._id,
+		mutee_id: mutee._id,
+		deleted_at: { $exists: false }
+	});
+
+	if (exist !== null) {
+		return rej('already muting');
+	}
+
+	// Create mute
+	await Mute.insert({
+		created_at: new Date(),
+		muter_id: muter._id,
+		mutee_id: mutee._id,
+	});
+
+	// Send response
+	res();
+});
diff --git a/src/api/endpoints/mute/delete.ts b/src/api/endpoints/mute/delete.ts
new file mode 100644
index 000000000..d6bff3353
--- /dev/null
+++ b/src/api/endpoints/mute/delete.ts
@@ -0,0 +1,63 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import User from '../../models/user';
+import Mute from '../../models/mute';
+
+/**
+ * Unmute a user
+ *
+ * @param {any} params
+ * @param {any} user
+ * @return {Promise<any>}
+ */
+module.exports = (params, user) => new Promise(async (res, rej) => {
+	const muter = user;
+
+	// Get 'user_id' parameter
+	const [userId, userIdErr] = $(params.user_id).id().$;
+	if (userIdErr) return rej('invalid user_id param');
+
+	// Check if the mutee is yourself
+	if (user._id.equals(userId)) {
+		return rej('mutee is yourself');
+	}
+
+	// Get mutee
+	const mutee = await User.findOne({
+		_id: userId
+	}, {
+		fields: {
+			data: false,
+			profile: false
+		}
+	});
+
+	if (mutee === null) {
+		return rej('user not found');
+	}
+
+	// Check not muting
+	const exist = await Mute.findOne({
+		muter_id: muter._id,
+		mutee_id: mutee._id,
+		deleted_at: { $exists: false }
+	});
+
+	if (exist === null) {
+		return rej('already not muting');
+	}
+
+	// Delete mute
+	await Mute.update({
+		_id: exist._id
+	}, {
+		$set: {
+			deleted_at: new Date()
+		}
+	});
+
+	// Send response
+	res();
+});
diff --git a/src/api/endpoints/mute/list.ts b/src/api/endpoints/mute/list.ts
new file mode 100644
index 000000000..740e19f0b
--- /dev/null
+++ b/src/api/endpoints/mute/list.ts
@@ -0,0 +1,73 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import Mute from '../../models/mute';
+import serialize from '../../serializers/user';
+import getFriends from '../../common/get-friends';
+
+/**
+ * Get muted users of a user
+ *
+ * @param {any} params
+ * @param {any} me
+ * @return {Promise<any>}
+ */
+module.exports = (params, me) => new Promise(async (res, rej) => {
+	// Get 'iknow' parameter
+	const [iknow = false, iknowErr] = $(params.iknow).optional.boolean().$;
+	if (iknowErr) return rej('invalid iknow param');
+
+	// Get 'limit' parameter
+	const [limit = 30, limitErr] = $(params.limit).optional.number().range(1, 100).$;
+	if (limitErr) return rej('invalid limit param');
+
+	// Get 'cursor' parameter
+	const [cursor = null, cursorErr] = $(params.cursor).optional.id().$;
+	if (cursorErr) return rej('invalid cursor param');
+
+	// Construct query
+	const query = {
+		muter_id: me._id,
+		deleted_at: { $exists: false }
+	} as any;
+
+	if (iknow) {
+		// Get my friends
+		const myFriends = await getFriends(me._id);
+
+		query.mutee_id = {
+			$in: myFriends
+		};
+	}
+
+	// カーソルが指定されている場合
+	if (cursor) {
+		query._id = {
+			$lt: cursor
+		};
+	}
+
+	// Get mutes
+	const mutes = await Mute
+		.find(query, {
+			limit: limit + 1,
+			sort: { _id: -1 }
+		});
+
+	// 「次のページ」があるかどうか
+	const inStock = mutes.length === limit + 1;
+	if (inStock) {
+		mutes.pop();
+	}
+
+	// Serialize
+	const users = await Promise.all(mutes.map(async m =>
+		await serialize(m.mutee_id, me, { detail: true })));
+
+	// Response
+	res({
+		users: users,
+		next: inStock ? mutes[mutes.length - 1]._id : null,
+	});
+});
diff --git a/src/api/endpoints/posts/timeline.ts b/src/api/endpoints/posts/timeline.ts
index 6cc7825e6..da7ffd0c1 100644
--- a/src/api/endpoints/posts/timeline.ts
+++ b/src/api/endpoints/posts/timeline.ts
@@ -4,6 +4,7 @@
 import $ from 'cafy';
 import rap from '@prezzemolo/rap';
 import Post from '../../models/post';
+import Mute from '../../models/mute';
 import ChannelWatching from '../../models/channel-watching';
 import getFriends from '../../common/get-friends';
 import serialize from '../../serializers/post';
@@ -42,15 +43,23 @@ module.exports = async (params, user, app) => {
 		throw 'only one of since_id, until_id, since_date, until_date can be specified';
 	}
 
-	const { followingIds, watchingChannelIds } = await rap({
+	const { followingIds, watchingChannelIds, mutedUserIds } = await rap({
 		// ID list of the user itself and other users who the user follows
 		followingIds: getFriends(user._id),
+
 		// Watchしているチャンネルを取得
 		watchingChannelIds: ChannelWatching.find({
 			user_id: user._id,
 			// 削除されたドキュメントは除く
 			deleted_at: { $exists: false }
-		}).then(watches => watches.map(w => w.channel_id))
+		}).then(watches => watches.map(w => w.channel_id)),
+
+		// ミュートしているユーザーを取得
+		mutedUserIds: Mute.find({
+			muter_id: user._id,
+			// 削除されたドキュメントは除く
+			deleted_at: { $exists: false }
+		}).then(ms => ms.map(m => m.mutee_id))
 	});
 
 	//#region Construct query
@@ -80,13 +89,13 @@ module.exports = async (params, user, app) => {
 		}],
 		// mute
 		user_id: {
-			$nin: mutes
+			$nin: mutedUserIds
 		},
 		'_reply.user_id': {
-			$nin: mutes
+			$nin: mutedUserIds
 		},
 		'_repost.user_id': {
-			$nin: mutes
+			$nin: mutedUserIds
 		},
 	} as any;
 

From b50cb8f8aa3297fbf8e8e760ed1a40e22ab161a5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 06:38:48 +0900
Subject: [PATCH 0096/1250] wip

---
 src/api/endpoints/posts/search.ts | 89 +++++++++++++++++++++++++++++--
 src/web/docs/search.ja.pug        | 16 ++++++
 2 files changed, 102 insertions(+), 3 deletions(-)

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index ac25652a0..f722231d4 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -6,6 +6,7 @@ import $ from 'cafy';
 const escapeRegexp = require('escape-regexp');
 import Post from '../../models/post';
 import User from '../../models/user';
+import Mute from '../../models/mute';
 import getFriends from '../../common/get-friends';
 import serialize from '../../serializers/post';
 import config from '../../../conf';
@@ -34,6 +35,10 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [following = null, followingErr] = $(params.following).optional.nullable.boolean().$;
 	if (followingErr) return rej('invalid following param');
 
+	// Get 'mute' parameter
+	const [mute = 'mute_all', muteErr] = $(params.mute).optional.string().$;
+	if (muteErr) return rej('invalid mute param');
+
 	// Get 'reply' parameter
 	const [reply = null, replyErr] = $(params.reply).optional.nullable.boolean().$;
 	if (replyErr) return rej('invalid reply param');
@@ -80,11 +85,11 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	// If Elasticsearch is available, search by it
 	// If not, search by MongoDB
 	(config.elasticsearch.enable ? byElasticsearch : byNative)
-		(res, rej, me, text, user, following, reply, repost, media, poll, sinceDate, untilDate, offset, limit);
+		(res, rej, me, text, user, following, mute, reply, repost, media, poll, sinceDate, untilDate, offset, limit);
 });
 
 // Search by MongoDB
-async function byNative(res, rej, me, text, userId, following, reply, repost, media, poll, sinceDate, untilDate, offset, max) {
+async function byNative(res, rej, me, text, userId, following, mute, reply, repost, media, poll, sinceDate, untilDate, offset, max) {
 	let q: any = {
 		$and: []
 	};
@@ -116,6 +121,84 @@ async function byNative(res, rej, me, text, userId, following, reply, repost, me
 		});
 	}
 
+	if (me != null) {
+		const mutes = await Mute.find({
+			muter_id: me._id,
+			deleted_at: { $exists: false }
+		});
+		const mutedUserIds = mutes.map(m => m.mutee_id);
+
+		switch (mute) {
+			case 'mute_all':
+				push({
+					user_id: {
+						$nin: mutedUserIds
+					},
+					'_reply.user_id': {
+						$nin: mutedUserIds
+					},
+					'_repost.user_id': {
+						$nin: mutedUserIds
+					}
+				});
+				break;
+			case 'mute_related':
+				push({
+					'_reply.user_id': {
+						$nin: mutedUserIds
+					},
+					'_repost.user_id': {
+						$nin: mutedUserIds
+					}
+				});
+				break;
+			case 'mute_direct':
+				push({
+					user_id: {
+						$nin: mutedUserIds
+					}
+				});
+				break;
+			case 'direct_only':
+				push({
+					user_id: {
+						$in: mutedUserIds
+					}
+				});
+				break;
+			case 'related_only':
+				push({
+					$or: [{
+						'_reply.user_id': {
+							$in: mutedUserIds
+						}
+					}, {
+						'_repost.user_id': {
+							$in: mutedUserIds
+						}
+					}]
+				});
+				break;
+			case 'all_only':
+				push({
+					$or: [{
+						user_id: {
+							$in: mutedUserIds
+						}
+					}, {
+						'_reply.user_id': {
+							$in: mutedUserIds
+						}
+					}, {
+						'_repost.user_id': {
+							$in: mutedUserIds
+						}
+					}]
+				});
+				break;
+		}
+	}
+
 	if (reply != null) {
 		if (reply) {
 			push({
@@ -236,7 +319,7 @@ async function byNative(res, rej, me, text, userId, following, reply, repost, me
 }
 
 // Search by Elasticsearch
-async function byElasticsearch(res, rej, me, text, userId, following, reply, repost, media, poll, sinceDate, untilDate, offset, max) {
+async function byElasticsearch(res, rej, me, text, userId, following, mute, reply, repost, media, poll, sinceDate, untilDate, offset, max) {
 	const es = require('../../db/elasticsearch');
 
 	es.search({
diff --git a/src/web/docs/search.ja.pug b/src/web/docs/search.ja.pug
index 41e443d74..552f95c60 100644
--- a/src/web/docs/search.ja.pug
+++ b/src/web/docs/search.ja.pug
@@ -29,6 +29,22 @@ section
 					| false ... フォローしていないユーザーに限定。
 					br
 					| null ... 特に限定しない(デフォルト)
+			tr
+				td mute
+				td
+					| mute_all ... ミュートしているユーザーの投稿とその投稿に対する返信やRepostを除外する(デフォルト)
+					br
+					| mute_related ... ミュートしているユーザーの投稿に対する返信やRepostだけ除外する
+					br
+					| mute_direct ... ミュートしているユーザーの投稿だけ除外する
+					br
+					| disabled ... ミュートしているユーザーの投稿とその投稿に対する返信やRepostも含める
+					br
+					| direct_only ... ミュートしているユーザーの投稿だけに限定
+					br
+					| related_only ... ミュートしているユーザーの投稿に対する返信やRepostだけに限定
+					br
+					| all_only ... ミュートしているユーザーの投稿とその投稿に対する返信やRepostに限定
 			tr
 				td reply
 				td

From cb14fbadc030e59561f2344bbd88e9236f5a0272 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 06:56:37 +0900
Subject: [PATCH 0097/1250] wip

---
 locales/en.yml                      |  5 +++++
 locales/ja.yml                      |  5 +++++
 src/api/serializers/user.ts         | 15 +++++++++++++--
 src/web/app/desktop/tags/user.tag   | 27 ++++++++++++++++++++++++++-
 src/web/docs/api/entities/user.yaml |  6 ++++++
 src/web/docs/mute.ja.pug            |  3 +++
 6 files changed, 58 insertions(+), 3 deletions(-)
 create mode 100644 src/web/docs/mute.ja.pug

diff --git a/locales/en.yml b/locales/en.yml
index 57e0c4116..dd3ee2a2a 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -473,6 +473,11 @@ desktop:
     mk-user:
       last-used-at: "Last used at"
 
+      follows-you: "Follows you"
+      mute: "Mute"
+      muted: "Muting"
+      unmute: "Unmute"
+
       photos:
         title: "Photos"
         loading: "Loading"
diff --git a/locales/ja.yml b/locales/ja.yml
index ee52f0716..d12eec86d 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -473,6 +473,11 @@ desktop:
     mk-user:
       last-used-at: "最終アクセス"
 
+      follows-you: "フォローされています"
+      mute: "ミュートする"
+      muted: "ミュートしています"
+      unmute: "ミュート解除"
+
       photos:
         title: "フォト"
         loading: "読み込み中"
diff --git a/src/api/serializers/user.ts b/src/api/serializers/user.ts
index fe924911c..ac157097a 100644
--- a/src/api/serializers/user.ts
+++ b/src/api/serializers/user.ts
@@ -6,6 +6,7 @@ import deepcopy = require('deepcopy');
 import { default as User, IUser } from '../models/user';
 import serializePost from './post';
 import Following from '../models/following';
+import Mute from '../models/mute';
 import getFriends from '../common/get-friends';
 import config from '../../conf';
 import rap from '@prezzemolo/rap';
@@ -113,7 +114,7 @@ export default (
 	}
 
 	if (meId && !meId.equals(_user.id)) {
-		// If the user is following
+		// Whether the user is following
 		_user.is_following = (async () => {
 			const follow = await Following.findOne({
 				follower_id: meId,
@@ -123,7 +124,7 @@ export default (
 			return follow !== null;
 		})();
 
-		// If the user is followed
+		// Whether the user is followed
 		_user.is_followed = (async () => {
 			const follow2 = await Following.findOne({
 				follower_id: _user.id,
@@ -132,6 +133,16 @@ export default (
 			});
 			return follow2 !== null;
 		})();
+
+		// Whether the user is muted
+		_user.is_muted = (async () => {
+			const mute = await Mute.findOne({
+				muter_id: meId,
+				mutee_id: _user.id,
+				deleted_at: { $exists: false }
+			});
+			return mute !== null;
+		})();
 	}
 
 	if (opts.detail) {
diff --git a/src/web/app/desktop/tags/user.tag b/src/web/app/desktop/tags/user.tag
index b4db47f9d..b29d1eaeb 100644
--- a/src/web/app/desktop/tags/user.tag
+++ b/src/web/app/desktop/tags/user.tag
@@ -226,7 +226,9 @@
 <mk-user-profile>
 	<div class="friend-form" if={ SIGNIN && I.id != user.id }>
 		<mk-big-follow-button user={ user }/>
-		<p class="followed" if={ user.is_followed }>フォローされています</p>
+		<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>
 	</div>
 	<div class="description" if={ user.description }>{ user.description }</div>
 	<div class="birthday" if={ user.profile.birthday }>
@@ -311,6 +313,7 @@
 		this.age = require('s-age');
 
 		this.mixin('i');
+		this.mixin('api');
 
 		this.user = this.opts.user;
 
@@ -325,6 +328,28 @@
 				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>
 
diff --git a/src/web/docs/api/entities/user.yaml b/src/web/docs/api/entities/user.yaml
index abc3f300d..e62ad84db 100644
--- a/src/web/docs/api/entities/user.yaml
+++ b/src/web/docs/api/entities/user.yaml
@@ -75,6 +75,12 @@ props:
     optional: true
     desc:
       ja: "自分がこのユーザーにフォローされているか"
+  - name: "is_muted"
+    type: "boolean"
+    optional: true
+    desc:
+      ja: "自分がこのユーザーをミュートしているか"
+      en: "Whether you muted this user"
   - name: "last_used_at"
     type: "date"
     optional: false
diff --git a/src/web/docs/mute.ja.pug b/src/web/docs/mute.ja.pug
new file mode 100644
index 000000000..4f5fad8b6
--- /dev/null
+++ b/src/web/docs/mute.ja.pug
@@ -0,0 +1,3 @@
+h1 ミュート
+
+p ユーザーをミュートすると、タイムラインや検索結果に対象のユーザーの投稿(およびそれらの投稿に対する返信やRepost)が表示されなくなります。

From 429135fe4b236531ffccebcefedb98f0dcead415 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 07:26:23 +0900
Subject: [PATCH 0098/1250] wip

---
 src/api/endpoints/i/notifications.ts | 23 ++++++++++++++-----
 src/api/stream/home.ts               | 33 ++++++++++++++++++++++++++--
 2 files changed, 49 insertions(+), 7 deletions(-)

diff --git a/src/api/endpoints/i/notifications.ts b/src/api/endpoints/i/notifications.ts
index 48254e5e6..fb9be7f61 100644
--- a/src/api/endpoints/i/notifications.ts
+++ b/src/api/endpoints/i/notifications.ts
@@ -3,6 +3,7 @@
  */
 import $ from 'cafy';
 import Notification from '../../models/notification';
+import Mute from '../../models/mute';
 import serialize from '../../serializers/notification';
 import getFriends from '../../common/get-friends';
 import read from '../../common/read-notification';
@@ -45,8 +46,18 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		return rej('cannot set since_id and until_id');
 	}
 
+	const mute = await Mute.find({
+		muter_id: user._id,
+		deleted_at: { $exists: false }
+	});
+
 	const query = {
-		notifiee_id: user._id
+		notifiee_id: user._id,
+		$and: [{
+			notifier_id: {
+				$nin: mute.map(m => m.mutee_id)
+			}
+		}]
 	} as any;
 
 	const sort = {
@@ -54,12 +65,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	};
 
 	if (following) {
-		// ID list of the user $self and other users who the user follows
+		// ID list of the user itself and other users who the user follows
 		const followingIds = await getFriends(user._id);
 
-		query.notifier_id = {
-			$in: followingIds
-		};
+		query.$and.push({
+			notifier_id: {
+				$in: followingIds
+			}
+		});
 	}
 
 	if (type) {
diff --git a/src/api/stream/home.ts b/src/api/stream/home.ts
index 7c8f3bfec..7dcdb5ed7 100644
--- a/src/api/stream/home.ts
+++ b/src/api/stream/home.ts
@@ -3,19 +3,48 @@ import * as redis from 'redis';
 import * as debug from 'debug';
 
 import User from '../models/user';
+import Mute from '../models/mute';
 import serializePost from '../serializers/post';
 import readNotification from '../common/read-notification';
 
 const log = debug('misskey');
 
-export default function homeStream(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void {
+export default async function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any) {
 	// Subscribe Home stream channel
 	subscriber.subscribe(`misskey:user-stream:${user._id}`);
 
+	const mute = await Mute.find({
+		muter_id: user._id,
+		deleted_at: { $exists: false }
+	});
+	const mutedUserIds = mute.map(m => m.mutee_id.toString());
+
 	subscriber.on('message', async (channel, data) => {
 		switch (channel.split(':')[1]) {
 			case 'user-stream':
-				connection.send(data);
+				try {
+					const x = JSON.parse(data);
+
+					if (x.type == 'post') {
+						if (mutedUserIds.indexOf(x.body.user_id) != -1) {
+							return;
+						}
+						if (x.body.reply != null && mutedUserIds.indexOf(x.body.reply.user_id) != -1) {
+							return;
+						}
+						if (x.body.repost != null && mutedUserIds.indexOf(x.body.repost.user_id) != -1) {
+							return;
+						}
+					} else if (x.type == 'notification') {
+						if (mutedUserIds.indexOf(x.body.user_id) != -1) {
+							return;
+						}
+					}
+
+					connection.send(data);
+				} catch (e) {
+					connection.send(data);
+				}
 				break;
 			case 'post-stream':
 				const postId = channel.split(':')[2];

From f1166616168dd687a7c2a8a3f7223d2eb612390b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 07:38:57 +0900
Subject: [PATCH 0099/1250] wip

---
 src/api/common/notify.ts                            | 12 ++++++++++++
 src/api/endpoints/notifications/get_unread_count.ts | 10 ++++++++++
 2 files changed, 22 insertions(+)

diff --git a/src/api/common/notify.ts b/src/api/common/notify.ts
index 4b3e6a5d5..f06622f91 100644
--- a/src/api/common/notify.ts
+++ b/src/api/common/notify.ts
@@ -1,5 +1,6 @@
 import * as mongo from 'mongodb';
 import Notification from '../models/notification';
+import Mute from '../models/mute';
 import event from '../event';
 import serialize from '../serializers/notification';
 
@@ -32,6 +33,17 @@ export default (
 	setTimeout(async () => {
 		const fresh = await Notification.findOne({ _id: notification._id }, { is_read: true });
 		if (!fresh.is_read) {
+			//#region ただしミュートしているユーザーからの通知なら無視
+			const mute = await Mute.find({
+				muter_id: notifiee,
+				deleted_at: { $exists: false }
+			});
+			const mutedUserIds = mute.map(m => m.mutee_id.toString());
+			if (mutedUserIds.indexOf(notifier.toHexString()) != -1) {
+				return;
+			}
+			//#endregion
+
 			event(notifiee, 'unread_notification', await serialize(notification));
 		}
 	}, 3000);
diff --git a/src/api/endpoints/notifications/get_unread_count.ts b/src/api/endpoints/notifications/get_unread_count.ts
index 9514e7871..845d6b29c 100644
--- a/src/api/endpoints/notifications/get_unread_count.ts
+++ b/src/api/endpoints/notifications/get_unread_count.ts
@@ -2,6 +2,7 @@
  * Module dependencies
  */
 import Notification from '../../models/notification';
+import Mute from '../../models/mute';
 
 /**
  * Get count of unread notifications
@@ -11,9 +12,18 @@ import Notification from '../../models/notification';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
+	const mute = await Mute.find({
+		muter_id: user._id,
+		deleted_at: { $exists: false }
+	});
+	const mutedUserIds = mute.map(m => m.mutee_id);
+
 	const count = await Notification
 		.count({
 			notifiee_id: user._id,
+			notifier_id: {
+				$nin: mutedUserIds
+			},
 			is_read: false
 		});
 

From f37be3ef209fdc93916a922a1453784c4d2dff4a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 07:43:56 +0900
Subject: [PATCH 0100/1250] wip

---
 src/api/common/notify.ts          |  2 +-
 src/api/endpoints/posts/create.ts | 14 +++++++++++---
 2 files changed, 12 insertions(+), 4 deletions(-)

diff --git a/src/api/common/notify.ts b/src/api/common/notify.ts
index f06622f91..2b79416a3 100644
--- a/src/api/common/notify.ts
+++ b/src/api/common/notify.ts
@@ -39,7 +39,7 @@ export default (
 				deleted_at: { $exists: false }
 			});
 			const mutedUserIds = mute.map(m => m.mutee_id.toString());
-			if (mutedUserIds.indexOf(notifier.toHexString()) != -1) {
+			if (mutedUserIds.indexOf(notifier.toString()) != -1) {
 				return;
 			}
 			//#endregion
diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index 9d791538f..a1d05c67c 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -8,6 +8,7 @@ import { default as Post, IPost, isValidText } from '../../models/post';
 import { default as User, IUser } from '../../models/user';
 import { default as Channel, IChannel } from '../../models/channel';
 import Following from '../../models/following';
+import Mute from '../../models/mute';
 import DriveFile from '../../models/drive-file';
 import Watching from '../../models/post-watching';
 import ChannelWatching from '../../models/channel-watching';
@@ -240,7 +241,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 
 	const mentions = [];
 
-	function addMention(mentionee, reason) {
+	async function addMention(mentionee, reason) {
 		// Reject if already added
 		if (mentions.some(x => x.equals(mentionee))) return;
 
@@ -249,8 +250,15 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 
 		// Publish event
 		if (!user._id.equals(mentionee)) {
-			event(mentionee, reason, postObj);
-			pushSw(mentionee, reason, postObj);
+			const mentioneeMutes = await Mute.find({
+				muter_id: mentionee,
+				deleted_at: { $exists: false }
+			});
+			const mentioneesMutedUserIds = mentioneeMutes.map(m => m.mutee_id.toString());
+			if (mentioneesMutedUserIds.indexOf(user._id.toString()) == -1) {
+				event(mentionee, reason, postObj);
+				pushSw(mentionee, reason, postObj);
+			}
 		}
 	}
 

From 0f7d64899aced774c03aaa3b14f846536c12b19e Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 07:57:46 +0900
Subject: [PATCH 0101/1250] Update mute.ja.pug

---
 src/web/docs/mute.ja.pug | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/web/docs/mute.ja.pug b/src/web/docs/mute.ja.pug
index 4f5fad8b6..a1f396006 100644
--- a/src/web/docs/mute.ja.pug
+++ b/src/web/docs/mute.ja.pug
@@ -1,3 +1,7 @@
 h1 ミュート
 
-p ユーザーをミュートすると、タイムラインや検索結果に対象のユーザーの投稿(およびそれらの投稿に対する返信やRepost)が表示されなくなります。
+p ユーザーをミュートすると、タイムラインや検索結果に対象のユーザーの投稿(およびそれらの投稿に対する返信やRepost)が表示されなくなります。また、ミュートしているユーザーからの通知も表示されなくなります。
+
+p ユーザーページからそのユーザーをミュートすることができます。
+
+p ミュートを行ったことは相手に通知されず、ミュートされていることを知ることもできません。

From d12da6b2bfb282da1aa6697c0db20a4533052f64 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, 22 Dec 2017 10:38:59 +0900
Subject: [PATCH 0102/1250] wip

---
 locales/en.yml                        |  4 +++
 locales/ja.yml                        |  4 +++
 src/web/app/desktop/tags/settings.tag | 38 +++++++++++++++++++++++++++
 3 files changed, 46 insertions(+)

diff --git a/locales/en.yml b/locales/en.yml
index dd3ee2a2a..e55984677 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -346,6 +346,9 @@ desktop:
       failed: "Failed to setup. please ensure that the token is correct."
       info: "From the next sign in, enter the token that is displayed on the device in addition to the password."
 
+    mk-mute-setting:
+      no-users: "No muted users"
+
     mk-post-form:
       post-placeholder: "What's happening?"
       reply-placeholder: "Reply to this post..."
@@ -379,6 +382,7 @@ desktop:
 
     mk-settings:
       profile: "Profile"
+      mute: "Mute"
       drive: "Drive"
       security: "Security"
       password: "Password"
diff --git a/locales/ja.yml b/locales/ja.yml
index d12eec86d..70ff8739f 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -346,6 +346,9 @@ desktop:
       failed: "設定に失敗しました。トークンに誤りがないかご確認ください。"
       info: "次回サインインからは、同様にパスワードに加えてデバイスに表示されているトークンを入力します。"
 
+    mk-mute-setting:
+      no-users: "ミュートしているユーザーはいません"
+
     mk-post-form:
       post-placeholder: "いまどうしてる?"
       reply-placeholder: "この投稿への返信..."
@@ -379,6 +382,7 @@ desktop:
 
     mk-settings:
       profile: "プロフィール"
+      mute: "ミュート"
       drive: "ドライブ"
       security: "セキュリティ"
       password: "パスワード"
diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag
index 2f36d9b3e..457b7e227 100644
--- a/src/web/app/desktop/tags/settings.tag
+++ b/src/web/app/desktop/tags/settings.tag
@@ -4,6 +4,7 @@
 		<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>
@@ -26,6 +27,11 @@
 			<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/>
@@ -386,3 +392,35 @@
 		});
 	</script>
 </mk-drive-setting>
+
+<mk-mute-setting>
+	<div class="none ui info" 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 each={ user in users }>
+			<p><b>{ user.name }</b> @{ user.username }</p>
+		</div>
+	</div>
+
+	<style>
+		:scope
+			display block
+
+	</style>
+	<script>
+		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>

From 0ecc1aef9797a7a1fd16f0d1c142c186489b762f Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 12:59:37 +0900
Subject: [PATCH 0103/1250] Update mute.ja.pug

---
 src/web/docs/mute.ja.pug | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/src/web/docs/mute.ja.pug b/src/web/docs/mute.ja.pug
index a1f396006..176ace5e5 100644
--- a/src/web/docs/mute.ja.pug
+++ b/src/web/docs/mute.ja.pug
@@ -1,7 +1,14 @@
 h1 ミュート
 
-p ユーザーをミュートすると、タイムラインや検索結果に対象のユーザーの投稿(およびそれらの投稿に対する返信やRepost)が表示されなくなります。また、ミュートしているユーザーからの通知も表示されなくなります。
+p ユーザーページから、そのユーザーをミュートすることができます。
 
-p ユーザーページからそのユーザーをミュートすることができます。
+p ユーザーをミュートすると、そのユーザーに関する次のコンテンツがMisskeyに表示されなくなります:
+ul
+	li タイムラインや投稿の検索結果内の、そのユーザーの
+投稿(およびそれらの投稿に対する返信やRepost)
+	li そのユーザーからの通知
+	li メッセージ履歴一覧内の、そのユーザーとのメッセージ履歴
 
 p ミュートを行ったことは相手に通知されず、ミュートされていることを知ることもできません。
+
+p 設定>ミュート から、自分がミュートしているユーザー一覧を確認することができます。

From 12dd2bf6c766542e330cf596a016d21d80fc1a40 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 12:59:59 +0900
Subject: [PATCH 0104/1250] oops

---
 src/web/docs/mute.ja.pug | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/web/docs/mute.ja.pug b/src/web/docs/mute.ja.pug
index 176ace5e5..5e79af5f8 100644
--- a/src/web/docs/mute.ja.pug
+++ b/src/web/docs/mute.ja.pug
@@ -4,8 +4,7 @@ p ユーザーページから、そのユーザーをミュートすることが
 
 p ユーザーをミュートすると、そのユーザーに関する次のコンテンツがMisskeyに表示されなくなります:
 ul
-	li タイムラインや投稿の検索結果内の、そのユーザーの
-投稿(およびそれらの投稿に対する返信やRepost)
+	li タイムラインや投稿の検索結果内の、そのユーザーの投稿(およびそれらの投稿に対する返信やRepost)
 	li そのユーザーからの通知
 	li メッセージ履歴一覧内の、そのユーザーとのメッセージ履歴
 

From 04ab810ed19c3d5a6e29222ea96baf603e4cf9a0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 14:21:40 +0900
Subject: [PATCH 0105/1250] wip

---
 src/api/endpoints/messaging/history.ts         | 11 ++++++++++-
 src/api/endpoints/messaging/messages/create.ts | 12 ++++++++++++
 2 files changed, 22 insertions(+), 1 deletion(-)

diff --git a/src/api/endpoints/messaging/history.ts b/src/api/endpoints/messaging/history.ts
index 5f7c9276d..f14740dff 100644
--- a/src/api/endpoints/messaging/history.ts
+++ b/src/api/endpoints/messaging/history.ts
@@ -3,6 +3,7 @@
  */
 import $ from 'cafy';
 import History from '../../models/messaging-history';
+import Mute from '../../models/mute';
 import serialize from '../../serializers/messaging-message';
 
 /**
@@ -17,10 +18,18 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) return rej('invalid limit param');
 
+	const mute = await Mute.find({
+		muter_id: user._id,
+		deleted_at: { $exists: false }
+	});
+
 	// Get history
 	const history = await History
 		.find({
-			user_id: user._id
+			user_id: user._id,
+			partner: {
+				$nin: mute.map(m => m.mutee_id)
+			}
 		}, {
 			limit: limit,
 			sort: {
diff --git a/src/api/endpoints/messaging/messages/create.ts b/src/api/endpoints/messaging/messages/create.ts
index 3c7689f96..f69f2e0fb 100644
--- a/src/api/endpoints/messaging/messages/create.ts
+++ b/src/api/endpoints/messaging/messages/create.ts
@@ -6,6 +6,7 @@ import Message from '../../../models/messaging-message';
 import { isValidText } from '../../../models/messaging-message';
 import History from '../../../models/messaging-history';
 import User from '../../../models/user';
+import Mute from '../../../models/mute';
 import DriveFile from '../../../models/drive-file';
 import serialize from '../../../serializers/messaging-message';
 import publishUserStream from '../../../event';
@@ -97,6 +98,17 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	setTimeout(async () => {
 		const freshMessage = await Message.findOne({ _id: message._id }, { is_read: true });
 		if (!freshMessage.is_read) {
+			//#region ただしミュートしているユーザーからの通知なら無視
+			const mute = await Mute.find({
+				muter_id: recipient._id,
+				deleted_at: { $exists: false }
+			});
+			const mutedUserIds = mute.map(m => m.mutee_id.toString());
+			if (mutedUserIds.indexOf(user._id.toString()) != -1) {
+				return;
+			}
+			//#endregion
+
 			publishUserStream(message.recipient_id, 'unread_messaging_message', messageObj);
 			pushSw(message.recipient_id, 'unread_messaging_message', messageObj);
 		}

From 4bc5a152f4d3c4df2b8b916e396b2eae1cdef05b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 14:35:47 +0900
Subject: [PATCH 0106/1250] wip

---
 src/api/endpoints/messaging/unread.ts | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/src/api/endpoints/messaging/unread.ts b/src/api/endpoints/messaging/unread.ts
index 40bc83fe1..c4326e1d2 100644
--- a/src/api/endpoints/messaging/unread.ts
+++ b/src/api/endpoints/messaging/unread.ts
@@ -2,6 +2,7 @@
  * Module dependencies
  */
 import Message from '../../models/messaging-message';
+import Mute from '../../models/mute';
 
 /**
  * Get count of unread messages
@@ -11,8 +12,17 @@ import Message from '../../models/messaging-message';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
+	const mute = await Mute.find({
+		muter_id: user._id,
+		deleted_at: { $exists: false }
+	});
+	const mutedUserIds = mute.map(m => m.mutee_id);
+
 	const count = await Message
 		.count({
+			user_id: {
+				$nin: mutedUserIds
+			},
 			recipient_id: user._id,
 			is_read: false
 		});

From 1b1871589fe2fdf5a01f1b6675ad44494b2d3804 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 16:22:33 +0900
Subject: [PATCH 0107/1250] Update create.ts

---
 src/api/endpoints/messaging/messages/create.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/api/endpoints/messaging/messages/create.ts b/src/api/endpoints/messaging/messages/create.ts
index f69f2e0fb..4e9d10197 100644
--- a/src/api/endpoints/messaging/messages/create.ts
+++ b/src/api/endpoints/messaging/messages/create.ts
@@ -98,7 +98,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	setTimeout(async () => {
 		const freshMessage = await Message.findOne({ _id: message._id }, { is_read: true });
 		if (!freshMessage.is_read) {
-			//#region ただしミュートしているユーザーからの通知なら無視
+			//#region ただしミュートされているなら発行しない
 			const mute = await Mute.find({
 				muter_id: recipient._id,
 				deleted_at: { $exists: false }

From 48b6dac98ee8c3623681877f75a7d769652d3fc4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 22 Dec 2017 17:44:31 +0900
Subject: [PATCH 0108/1250] v3451

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c253d1f11..bf3a52ccd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3451 (2017/12/22)
+-----------------
+* ミュート機能
+
 3430 (2017/12/21)
 -----------------
 * oops
diff --git a/package.json b/package.json
index b43f5be70..eb79e8149 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3430",
+	"version": "0.0.3451",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 8b556e78d5ca4e97558b164dbe07ffc1c87ca97c Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 23 Dec 2017 00:39:00 +0900
Subject: [PATCH 0109/1250] Update search.ja.pug

---
 src/web/docs/search.ja.pug | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/docs/search.ja.pug b/src/web/docs/search.ja.pug
index 552f95c60..e94990205 100644
--- a/src/web/docs/search.ja.pug
+++ b/src/web/docs/search.ja.pug
@@ -8,7 +8,7 @@ p
 section
 	h2 オプション
 	p
-		| オプションを使用して、より高度な検索をすることもできます。
+		| オプションを使用して、より高度な検索を行えます。
 		| オプションを指定するには、「オプション名:値」という形式でクエリに含めます。
 	p 利用可能なオプション一覧です:
 

From b794b027a0d0d6de155612163bbaae6d05d4c3cd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 23 Dec 2017 04:21:15 +0900
Subject: [PATCH 0110/1250] #1035

---
 src/api/endpoints/posts/search.ts | 4 +++-
 src/web/docs/search.ja.pug        | 6 ++++++
 2 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index f722231d4..4697e6ed0 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -99,7 +99,9 @@ async function byNative(res, rej, me, text, userId, following, mute, reply, repo
 	if (text) {
 		push({
 			$and: text.split(' ').map(x => ({
-				text: new RegExp(escapeRegexp(x))
+				text: x[0] == '-' ? {
+					$ne: new RegExp(escapeRegexp(x))
+				} : new RegExp(escapeRegexp(x))
 			}))
 		});
 	}
diff --git a/src/web/docs/search.ja.pug b/src/web/docs/search.ja.pug
index e94990205..5baac9d40 100644
--- a/src/web/docs/search.ja.pug
+++ b/src/web/docs/search.ja.pug
@@ -5,6 +5,12 @@ p
 	| キーワードを半角スペースで区切ると、and検索になります。
 	| 例えば、「git コミット」と検索すると、「gitで編集したファイルの特定の行だけコミットする方法がわからない」などがマッチします。
 
+section
+	h2 キーワードの除外
+	p キーワードの前に「-」(ハイフン)をプリフィクスすると、そのキーワードを含まない投稿に限定します。
+	p 例えば、「gitというキーワードを含むが、コミットというキーワードは含まない投稿」を検索したい場合、クエリは以下のようになります:
+	code git -コミット
+
 section
 	h2 オプション
 	p

From 3efe4cbad1e9059f66eb6f848316ed6fde95cde6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 23 Dec 2017 04:21:48 +0900
Subject: [PATCH 0111/1250] oops

---
 src/api/endpoints/posts/search.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index 4697e6ed0..33ef2a0a0 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -100,7 +100,7 @@ async function byNative(res, rej, me, text, userId, following, mute, reply, repo
 		push({
 			$and: text.split(' ').map(x => ({
 				text: x[0] == '-' ? {
-					$ne: new RegExp(escapeRegexp(x))
+					$ne: new RegExp(escapeRegexp(x.substr(1)))
 				} : new RegExp(escapeRegexp(x))
 			}))
 		});

From 5ec5ce92f2c39344857192d2254531f41507ae49 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 23 Dec 2017 04:22:45 +0900
Subject: [PATCH 0112/1250] oops

---
 src/web/docs/search.ja.pug | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/docs/search.ja.pug b/src/web/docs/search.ja.pug
index 5baac9d40..09173a350 100644
--- a/src/web/docs/search.ja.pug
+++ b/src/web/docs/search.ja.pug
@@ -7,7 +7,7 @@ p
 
 section
 	h2 キーワードの除外
-	p キーワードの前に「-」(ハイフン)をプリフィクスすると、そのキーワードを含まない投稿に限定します。
+	p キーワードの前に「-」(ハイフン)をプリフィクスすると、そのキーワードを含まない投稿に限定します。
 	p 例えば、「gitというキーワードを含むが、コミットというキーワードは含まない投稿」を検索したい場合、クエリは以下のようになります:
 	code git -コミット
 

From 218229939374cd398db8816d0fe4b235da63c19c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 23 Dec 2017 04:26:37 +0900
Subject: [PATCH 0113/1250] oops

---
 src/api/endpoints/posts/search.ts | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index 33ef2a0a0..6cea5bdf5 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -99,8 +99,9 @@ async function byNative(res, rej, me, text, userId, following, mute, reply, repo
 	if (text) {
 		push({
 			$and: text.split(' ').map(x => ({
+				// キーワードが-で始まる場合そのキーワードを除外する
 				text: x[0] == '-' ? {
-					$ne: new RegExp(escapeRegexp(x.substr(1)))
+					$not: new RegExp(escapeRegexp(x.substr(1)))
 				} : new RegExp(escapeRegexp(x))
 			}))
 		});

From b8b122cd4d60593f468a581d4a079624733928e0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 23 Dec 2017 04:38:56 +0900
Subject: [PATCH 0114/1250] #1034

---
 src/api/endpoints/posts/search.ts | 24 ++++++++++++++++--------
 src/web/docs/search.ja.pug        |  5 +++++
 2 files changed, 21 insertions(+), 8 deletions(-)

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index 6cea5bdf5..26675989d 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -97,14 +97,22 @@ async function byNative(res, rej, me, text, userId, following, mute, reply, repo
 	const push = x => q.$and.push(x);
 
 	if (text) {
-		push({
-			$and: text.split(' ').map(x => ({
-				// キーワードが-で始まる場合そのキーワードを除外する
-				text: x[0] == '-' ? {
-					$not: new RegExp(escapeRegexp(x.substr(1)))
-				} : new RegExp(escapeRegexp(x))
-			}))
-		});
+		// 完全一致検索
+		if (/"""(.+?)"""/.test(text)) {
+			const x = text.match(/"""(.+?)"""/)[1];
+			push({
+				text: x
+			});
+		} else {
+			push({
+				$and: text.split(' ').map(x => ({
+					// キーワードが-で始まる場合そのキーワードを除外する
+					text: x[0] == '-' ? {
+						$not: new RegExp(escapeRegexp(x.substr(1)))
+					} : new RegExp(escapeRegexp(x))
+				}))
+			});
+		}
 	}
 
 	if (userId) {
diff --git a/src/web/docs/search.ja.pug b/src/web/docs/search.ja.pug
index 09173a350..9e6478948 100644
--- a/src/web/docs/search.ja.pug
+++ b/src/web/docs/search.ja.pug
@@ -11,6 +11,11 @@ section
 	p 例えば、「gitというキーワードを含むが、コミットというキーワードは含まない投稿」を検索したい場合、クエリは以下のようになります:
 	code git -コミット
 
+section
+	h2 完全一致
+	p テキストを「"""」で囲むと、そのテキストと完全に一致する投稿を検索します。
+	p 例えば、「"""にゃーん"""」と検索すると、「にゃーん」という投稿のみがヒットし、「にゃーん…」という投稿はヒットしません。
+
 section
 	h2 オプション
 	p

From 94700214f596498d1474336d329084461b6d7b97 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 23 Dec 2017 07:21:52 +0900
Subject: [PATCH 0115/1250] #1037 #1038

---
 src/api/endpoints/posts/search.ts             | 138 +++++++-----------
 .../app/common/scripts/parse-search-query.ts  |   5 +-
 src/web/docs/search.ja.pug                    |  19 ++-
 3 files changed, 72 insertions(+), 90 deletions(-)

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index 26675989d..31c9a8d3c 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -1,7 +1,6 @@
 /**
  * Module dependencies
  */
-import * as mongo from 'mongodb';
 import $ from 'cafy';
 const escapeRegexp = require('escape-regexp');
 import Post from '../../models/post';
@@ -9,7 +8,6 @@ import User from '../../models/user';
 import Mute from '../../models/mute';
 import getFriends from '../../common/get-friends';
 import serialize from '../../serializers/post';
-import config from '../../../conf';
 
 /**
  * Search a post
@@ -23,13 +21,21 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [text, textError] = $(params.text).optional.string().$;
 	if (textError) return rej('invalid text param');
 
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).optional.id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'include_user_ids' parameter
+	const [includeUserIds = [], includeUserIdsErr] = $(params.include_user_ids).optional.array('id').$;
+	if (includeUserIdsErr) return rej('invalid include_user_ids param');
 
-	// Get 'username' parameter
-	const [username, usernameErr] = $(params.username).optional.string().$;
-	if (usernameErr) return rej('invalid username param');
+	// Get 'exclude_user_ids' parameter
+	const [excludeUserIds = [], excludeUserIdsErr] = $(params.exclude_user_ids).optional.array('id').$;
+	if (excludeUserIdsErr) return rej('invalid exclude_user_ids param');
+
+	// Get 'include_user_usernames' parameter
+	const [includeUserUsernames = [], includeUserUsernamesErr] = $(params.include_user_usernames).optional.array('string').$;
+	if (includeUserUsernamesErr) return rej('invalid include_user_usernames param');
+
+	// Get 'exclude_user_usernames' parameter
+	const [excludeUserUsernames = [], excludeUserUsernamesErr] = $(params.exclude_user_usernames).optional.array('string').$;
+	if (excludeUserUsernamesErr) return rej('invalid exclude_user_usernames param');
 
 	// Get 'following' parameter
 	const [following = null, followingErr] = $(params.following).optional.nullable.boolean().$;
@@ -71,25 +77,36 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 30).$;
 	if (limitErr) return rej('invalid limit param');
 
-	let user = userId;
-
-	if (user == null && username != null) {
-		const _user = await User.findOne({
-			username_lower: username.toLowerCase()
-		});
-		if (_user) {
-			user = _user._id;
-		}
+	let includeUsers = includeUserIds;
+	if (includeUserUsernames != null) {
+		const ids = (await Promise.all(includeUserUsernames.map(async (username) => {
+			const _user = await User.findOne({
+				username_lower: username.toLowerCase()
+			});
+			return _user ? _user._id : null;
+		}))).filter(id => id != null);
+		includeUsers = includeUsers.concat(ids);
 	}
 
-	// If Elasticsearch is available, search by it
-	// If not, search by MongoDB
-	(config.elasticsearch.enable ? byElasticsearch : byNative)
-		(res, rej, me, text, user, following, mute, reply, repost, media, poll, sinceDate, untilDate, offset, limit);
+	let excludeUsers = excludeUserIds;
+	if (excludeUserUsernames != null) {
+		const ids = (await Promise.all(excludeUserUsernames.map(async (username) => {
+			const _user = await User.findOne({
+				username_lower: username.toLowerCase()
+			});
+			return _user ? _user._id : null;
+		}))).filter(id => id != null);
+		excludeUsers = excludeUsers.concat(ids);
+	}
+
+	search(res, rej, me, text, includeUsers, excludeUsers, following,
+			mute, reply, repost, media, poll, sinceDate, untilDate, offset, limit);
 });
 
-// Search by MongoDB
-async function byNative(res, rej, me, text, userId, following, mute, reply, repost, media, poll, sinceDate, untilDate, offset, max) {
+async function search(
+	res, rej, me, text, includeUserIds, excludeUserIds, following,
+	mute, reply, repost, media, poll, sinceDate, untilDate, offset, max) {
+
 	let q: any = {
 		$and: []
 	};
@@ -115,9 +132,17 @@ async function byNative(res, rej, me, text, userId, following, mute, reply, repo
 		}
 	}
 
-	if (userId) {
+	if (includeUserIds && includeUserIds.length != 0) {
 		push({
-			user_id: userId
+			user_id: {
+				$in: includeUserIds
+			}
+		});
+	} else if (excludeUserIds && excludeUserIds.length != 0) {
+		push({
+			user_id: {
+				$nin: excludeUserIds
+			}
 		});
 	}
 
@@ -328,66 +353,3 @@ async function byNative(res, rej, me, text, userId, following, mute, reply, repo
 	res(await Promise.all(posts.map(async post =>
 		await serialize(post, me))));
 }
-
-// Search by Elasticsearch
-async function byElasticsearch(res, rej, me, text, userId, following, mute, reply, repost, media, poll, sinceDate, untilDate, offset, max) {
-	const es = require('../../db/elasticsearch');
-
-	es.search({
-		index: 'misskey',
-		type: 'post',
-		body: {
-			size: max,
-			from: offset,
-			query: {
-				simple_query_string: {
-					fields: ['text'],
-					query: text,
-					default_operator: 'and'
-				}
-			},
-			sort: [
-				{ _doc: 'desc' }
-			],
-			highlight: {
-				pre_tags: ['<mark>'],
-				post_tags: ['</mark>'],
-				encoder: 'html',
-				fields: {
-					text: {}
-				}
-			}
-		}
-	}, async (error, response) => {
-		if (error) {
-			console.error(error);
-			return res(500);
-		}
-
-		if (response.hits.total === 0) {
-			return res([]);
-		}
-
-		const hits = response.hits.hits.map(hit => new mongo.ObjectID(hit._id));
-
-		// Fetch found posts
-		const posts = await Post
-			.find({
-				_id: {
-					$in: hits
-				}
-			}, {
-				sort: {
-					_id: -1
-				}
-			});
-
-		posts.map(post => {
-			post._highlight = response.hits.hits.filter(hit => post._id.equals(hit._id))[0].highlight.text[0];
-		});
-
-		// Serialize
-		res(await Promise.all(posts.map(async post =>
-			await serialize(post, me))));
-	});
-}
diff --git a/src/web/app/common/scripts/parse-search-query.ts b/src/web/app/common/scripts/parse-search-query.ts
index c021ee641..512791ecb 100644
--- a/src/web/app/common/scripts/parse-search-query.ts
+++ b/src/web/app/common/scripts/parse-search-query.ts
@@ -8,7 +8,10 @@ export default function(qs: string) {
 			const [key, value] = x.split(':');
 			switch (key) {
 				case 'user':
-					q['username'] = value;
+					q['include_user_usernames'] = value.split(',');
+					break;
+				case 'exclude_user':
+					q['exclude_user_usernames'] = value.split(',');
 					break;
 				case 'follow':
 					q['following'] = value == 'null' ? null : value == 'true';
diff --git a/src/web/docs/search.ja.pug b/src/web/docs/search.ja.pug
index 9e6478948..f33091ee6 100644
--- a/src/web/docs/search.ja.pug
+++ b/src/web/docs/search.ja.pug
@@ -31,7 +31,24 @@ section
 		tbody
 			tr
 				td user
-				td ユーザー名。投稿者を限定します。
+				td
+					| 指定されたユーザー名のユーザーの投稿に限定します。
+					| 「,」(カンマ)で区切って、複数ユーザーを指定することもできます。
+					br
+					| 例えば、
+					code user:himawari,sakurako
+					| と検索すると「@himawariまたは@sakurakoの投稿」だけに限定します。
+					| (つまりユーザーのホワイトリストです)
+			tr
+				td exclude_user
+				td
+					| 指定されたユーザー名のユーザーの投稿を除外します。
+					| 「,」(カンマ)で区切って、複数ユーザーを指定することもできます。
+					br
+					| 例えば、
+					code exclude_user:akari,chinatsu
+					| と検索すると「@akariまたは@chinatsu以外の投稿」に限定します。
+					| (つまりユーザーのブラックリストです)
 			tr
 				td follow
 				td

From 1e8e78d4dd2e4db30e9db0faaef35c6a067e67be Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 23 Dec 2017 07:21:57 +0900
Subject: [PATCH 0116/1250] :art:

---
 src/web/docs/style.styl | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/web/docs/style.styl b/src/web/docs/style.styl
index 3dcb3e169..a726d49b1 100644
--- a/src/web/docs/style.styl
+++ b/src/web/docs/style.styl
@@ -106,6 +106,7 @@ table
 		min-width 128px
 
 code
+	display inline-block
 	padding 8px 10px
 	font-family Consolas, 'Courier New', Courier, Monaco, monospace
 	color #295c92

From e74cf7d2e1c0d0d20336e524ee5f098c7582efe5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 23 Dec 2017 07:22:58 +0900
Subject: [PATCH 0117/1250] v3460

---
 CHANGELOG.md | 6 ++++++
 package.json | 2 +-
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index bf3a52ccd..a05097ff0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,12 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3460 (2017/12/23)
+-----------------
+* 検索で複数のユーザーを指定できるように
+* 検索でユーザーを除外できるように
+* など
+
 3451 (2017/12/22)
 -----------------
 * ミュート機能
diff --git a/package.json b/package.json
index eb79e8149..b236051e7 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3451",
+	"version": "0.0.3460",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From b4eb428909d3ac17727d372e7a17f08fcf83dc23 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sat, 23 Dec 2017 02:52:15 +0000
Subject: [PATCH 0118/1250] fix(package): update ts-node to version 4.1.0

Closes #985
---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index b236051e7..81588c66f 100644
--- a/package.json
+++ b/package.json
@@ -163,7 +163,7 @@
 		"tcp-port-used": "0.1.2",
 		"textarea-caret": "3.0.2",
 		"tmp": "0.0.33",
-		"ts-node": "3.3.0",
+		"ts-node": "4.1.0",
 		"tslint": "5.8.0",
 		"typescript": "2.6.2",
 		"uglify-es": "3.2.0",

From beb2c5bd18dd2da8a11b6ae6e7b24b8a9e98bd72 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sat, 23 Dec 2017 17:55:00 +0000
Subject: [PATCH 0119/1250] fix(package): update riot-tag-loader to version
 2.0.0

Closes #1042
---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 81588c66f..2607d33a2 100644
--- a/package.json
+++ b/package.json
@@ -146,7 +146,7 @@
 		"request": "2.83.0",
 		"rimraf": "2.6.2",
 		"riot": "3.7.4",
-		"riot-tag-loader": "1.0.0",
+		"riot-tag-loader": "2.0.0",
 		"rndstr": "1.0.0",
 		"s-age": "1.1.0",
 		"seedrandom": "2.4.3",

From bb95ea796ed293064f69f9e63d3e41da82328a39 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sun, 24 Dec 2017 13:50:30 +0000
Subject: [PATCH 0120/1250] fix(package): update gulp-htmlmin to version 4.0.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 2607d33a2..33ffb53b4 100644
--- a/package.json
+++ b/package.json
@@ -106,7 +106,7 @@
 		"gm": "1.23.0",
 		"gulp": "3.9.1",
 		"gulp-cssnano": "2.1.2",
-		"gulp-htmlmin": "3.0.0",
+		"gulp-htmlmin": "4.0.0",
 		"gulp-imagemin": "4.0.0",
 		"gulp-mocha": "4.3.1",
 		"gulp-pug": "3.3.0",

From 7bd54eef232adb3c48a48cddd30ebebbb86577bf Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sun, 24 Dec 2017 23:57:04 +0000
Subject: [PATCH 0121/1250] fix(package): update mongodb to version 3.0.1

Closes #1046
---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 2607d33a2..9649e0b09 100644
--- a/package.json
+++ b/package.json
@@ -126,7 +126,7 @@
 		"mkdirp": "^0.5.1",
 		"mocha": "4.0.1",
 		"moji": "0.5.1",
-		"mongodb": "2.2.33",
+		"mongodb": "3.0.1",
 		"monk": "6.0.5",
 		"morgan": "1.9.0",
 		"ms": "2.1.1",

From a6febb184878167830355f533d1717b1b13862ef Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 25 Dec 2017 11:02:15 +0000
Subject: [PATCH 0122/1250] fix(package): update riot-tag-loader to version
 2.0.1

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 2607d33a2..d43e967eb 100644
--- a/package.json
+++ b/package.json
@@ -146,7 +146,7 @@
 		"request": "2.83.0",
 		"rimraf": "2.6.2",
 		"riot": "3.7.4",
-		"riot-tag-loader": "2.0.0",
+		"riot-tag-loader": "2.0.1",
 		"rndstr": "1.0.0",
 		"s-age": "1.1.0",
 		"seedrandom": "2.4.3",

From e15f8a9b6f03a2a3a758ae2d0389202860c036e3 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 27 Dec 2017 10:33:26 +0000
Subject: [PATCH 0123/1250] fix(package): update uglifyjs-webpack-plugin to
 version 1.1.5

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 2607d33a2..c8ab350d4 100644
--- a/package.json
+++ b/package.json
@@ -167,7 +167,7 @@
 		"tslint": "5.8.0",
 		"typescript": "2.6.2",
 		"uglify-es": "3.2.0",
-		"uglifyjs-webpack-plugin": "1.1.4",
+		"uglifyjs-webpack-plugin": "1.1.5",
 		"uuid": "3.1.0",
 		"vhost": "3.0.2",
 		"web-push": "3.2.5",

From c9125337d6117c6bb7e479383aed40c75c8e3456 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 27 Dec 2017 17:50:23 +0000
Subject: [PATCH 0124/1250] fix(package): update qrcode to version 1.0.1

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 2607d33a2..ddb18a2e7 100644
--- a/package.json
+++ b/package.json
@@ -138,7 +138,7 @@
 		"prominence": "0.2.0",
 		"proxy-addr": "2.0.2",
 		"pug": "2.0.0-rc.4",
-		"qrcode": "1.0.0",
+		"qrcode": "1.0.1",
 		"ratelimiter": "3.0.3",
 		"recaptcha-promise": "0.1.3",
 		"reconnecting-websocket": "3.2.2",

From 08c4a87936c6bfd75a5a560a5f323dce205fe27b Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 28 Dec 2017 09:49:30 +0900
Subject: [PATCH 0125/1250] Update .travis.yml

---
 .travis.yml | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/.travis.yml b/.travis.yml
index ed53af9e2..6e33a2d12 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,9 @@
 # travis file
 # https://docs.travis-ci.com/user/customizing-the-build
 
+notifications:
+  email: false
+
 branches:
   except:
     - release

From 1086b6ca6029b8cbad2037d9bd922eae87af5742 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 28 Dec 2017 04:49:49 +0000
Subject: [PATCH 0126/1250] fix(package): update gm to version 1.23.1

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index c3be5cdae..3fbb3f85b 100644
--- a/package.json
+++ b/package.json
@@ -103,7 +103,7 @@
 		"express": "4.16.2",
 		"file-type": "7.4.0",
 		"fuckadblock": "3.2.1",
-		"gm": "1.23.0",
+		"gm": "1.23.1",
 		"gulp": "3.9.1",
 		"gulp-cssnano": "2.1.2",
 		"gulp-htmlmin": "4.0.0",

From e58b535cbac4e6ef255e1fc7129e516f0234dbac Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 28 Dec 2017 14:01:33 +0000
Subject: [PATCH 0127/1250] fix(package): update riot to version 3.8.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index c3be5cdae..68d5c2726 100644
--- a/package.json
+++ b/package.json
@@ -145,7 +145,7 @@
 		"redis": "2.8.0",
 		"request": "2.83.0",
 		"rimraf": "2.6.2",
-		"riot": "3.7.4",
+		"riot": "3.8.0",
 		"riot-tag-loader": "2.0.1",
 		"rndstr": "1.0.0",
 		"s-age": "1.1.0",

From 312275bf9010d8b03a4f6e0f4359ed2182490314 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 28 Dec 2017 20:40:53 +0000
Subject: [PATCH 0128/1250] fix(package): update riot to version 3.8.1

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 68d5c2726..a2d5e3ce1 100644
--- a/package.json
+++ b/package.json
@@ -145,7 +145,7 @@
 		"redis": "2.8.0",
 		"request": "2.83.0",
 		"rimraf": "2.6.2",
-		"riot": "3.8.0",
+		"riot": "3.8.1",
 		"riot-tag-loader": "2.0.1",
 		"rndstr": "1.0.0",
 		"s-age": "1.1.0",

From 2f0fbb87f6e9517849af34f0bf5eeb15127b9bfb Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 28 Dec 2017 20:50:28 +0000
Subject: [PATCH 0129/1250] fix(package): update qrcode to version 1.2.0

Closes #1053
---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 68d5c2726..605f24bf0 100644
--- a/package.json
+++ b/package.json
@@ -138,7 +138,7 @@
 		"prominence": "0.2.0",
 		"proxy-addr": "2.0.2",
 		"pug": "2.0.0-rc.4",
-		"qrcode": "1.0.1",
+		"qrcode": "1.2.0",
 		"ratelimiter": "3.0.3",
 		"recaptcha-promise": "0.1.3",
 		"reconnecting-websocket": "3.2.2",

From f08e4c117a66c03287a3d7e3fa75ac6efeda4f18 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Fri, 29 Dec 2017 09:50:12 +0000
Subject: [PATCH 0130/1250] fix(package): update riot-tag-loader to version
 2.0.2

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index dcd880fbc..95759fa31 100644
--- a/package.json
+++ b/package.json
@@ -146,7 +146,7 @@
 		"request": "2.83.0",
 		"rimraf": "2.6.2",
 		"riot": "3.8.1",
-		"riot-tag-loader": "2.0.1",
+		"riot-tag-loader": "2.0.2",
 		"rndstr": "1.0.0",
 		"s-age": "1.1.0",
 		"seedrandom": "2.4.3",

From aec2cbdd9218575bf95e1ea814b739c9f4bc01d5 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sat, 30 Dec 2017 21:27:41 +0000
Subject: [PATCH 0131/1250] fix(package): update gulp-imagemin to version 4.1.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index dcd880fbc..80bfa5f4c 100644
--- a/package.json
+++ b/package.json
@@ -107,7 +107,7 @@
 		"gulp": "3.9.1",
 		"gulp-cssnano": "2.1.2",
 		"gulp-htmlmin": "4.0.0",
-		"gulp-imagemin": "4.0.0",
+		"gulp-imagemin": "4.1.0",
 		"gulp-mocha": "4.3.1",
 		"gulp-pug": "3.3.0",
 		"gulp-rename": "1.2.2",

From 3831e36589426807bee9303df25179fda6da5c39 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 31 Dec 2017 14:38:41 +0900
Subject: [PATCH 0132/1250] Improve readability

---
 src/db/mongodb.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/db/mongodb.ts b/src/db/mongodb.ts
index c978e6460..be234b365 100644
--- a/src/db/mongodb.ts
+++ b/src/db/mongodb.ts
@@ -1,8 +1,8 @@
 import config from '../conf';
 
 const uri = config.mongodb.user && config.mongodb.pass
-? `mongodb://${config.mongodb.user}:${config.mongodb.pass}@${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`
-: `mongodb://${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`;
+	? `mongodb://${config.mongodb.user}:${config.mongodb.pass}@${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`
+	: `mongodb://${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`;
 
 /**
  * monk

From f1416f785a95c3cb45e268753e2c77377d799c63 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 31 Dec 2017 15:15:19 +0900
Subject: [PATCH 0133/1250] Fix bug

SEE:
https://github.com/mongodb/node-mongodb-native/blob/3.0.0/CHANGES_3.0.0.md
---
 src/db/mongodb.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/db/mongodb.ts b/src/db/mongodb.ts
index be234b365..1263ccaac 100644
--- a/src/db/mongodb.ts
+++ b/src/db/mongodb.ts
@@ -24,9 +24,9 @@ const nativeDbConn = async (): Promise<mongodb.Db> => {
 	if (mdb) return mdb;
 
 	const db = await ((): Promise<mongodb.Db> => new Promise((resolve, reject) => {
-		mongodb.MongoClient.connect(uri, (e, db) => {
+		(mongodb as any).MongoClient.connect(uri, (e, client) => {
 			if (e) return reject(e);
-			resolve(db);
+			resolve(client.db(config.mongodb.db));
 		});
 	}))();
 

From 4be21efc0f4e40bfdb565d14cf1af3e43acebb92 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, 1 Jan 2018 00:02:58 +0900
Subject: [PATCH 0134/1250] LICENSE: Update year to 2018

---
 LICENSE                   | 2 +-
 src/const.json            | 2 +-
 webpack/plugins/banner.ts | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/LICENSE b/LICENSE
index e3733b396..0b6e30e45 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 The MIT License (MIT)
 
-Copyright (c) 2014-2017 syuilo
+Copyright (c) 2014-2018 syuilo
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/src/const.json b/src/const.json
index 0ee6ac206..d8fe4fe6c 100644
--- a/src/const.json
+++ b/src/const.json
@@ -1,5 +1,5 @@
 {
-	"copyright": "Copyright (c) 2014-2017 syuilo",
+	"copyright": "Copyright (c) 2014-2018 syuilo",
 	"themeColor": "#ff4e45",
 	"themeColorForeground": "#fff"
 }
diff --git a/webpack/plugins/banner.ts b/webpack/plugins/banner.ts
index 47b8cd355..a8774e0a3 100644
--- a/webpack/plugins/banner.ts
+++ b/webpack/plugins/banner.ts
@@ -3,7 +3,7 @@ import * as webpack from 'webpack';
 
 export default version => new webpack.BannerPlugin({
 	banner:
-		`Misskey v${version} | MIT Licensed, (c) syuilo 2014-2017\n` +
+		`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]'

From df876c8904a1d646d01f210574d619955a2eccee Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 1 Jan 2018 01:58:12 +0900
Subject: [PATCH 0135/1250] Update dependencies :rocket:

---
 package.json | 30 +++++++++++++++---------------
 1 file changed, 15 insertions(+), 15 deletions(-)

diff --git a/package.json b/package.json
index 4df15d421..ff9069a42 100644
--- a/package.json
+++ b/package.json
@@ -23,9 +23,9 @@
 	},
 	"dependencies": {
 		"@fortawesome/fontawesome": "1.0.1",
-		"@fortawesome/fontawesome-free-brands": "5.0.1",
-		"@fortawesome/fontawesome-free-regular": "5.0.1",
-		"@fortawesome/fontawesome-free-solid": "5.0.1",
+		"@fortawesome/fontawesome-free-brands": "5.0.2",
+		"@fortawesome/fontawesome-free-regular": "5.0.2",
+		"@fortawesome/fontawesome-free-solid": "5.0.2",
 		"@prezzemolo/rap": "0.1.2",
 		"@prezzemolo/zip": "0.0.3",
 		"@types/bcryptjs": "2.4.1",
@@ -39,9 +39,9 @@
 		"@types/deep-equal": "1.0.1",
 		"@types/elasticsearch": "5.0.19",
 		"@types/eventemitter3": "2.0.2",
-		"@types/express": "4.0.39",
+		"@types/express": "4.11.0",
 		"@types/gm": "1.17.33",
-		"@types/gulp": "4.0.3",
+		"@types/gulp": "3.8.36",
 		"@types/gulp-htmlmin": "1.3.31",
 		"@types/gulp-mocha": "0.0.31",
 		"@types/gulp-rename": "0.0.33",
@@ -52,17 +52,17 @@
 		"@types/is-root": "1.0.0",
 		"@types/is-url": "1.2.28",
 		"@types/js-yaml": "3.10.1",
-		"@types/mkdirp": "^0.5.2",
-		"@types/mocha": "2.2.44",
-		"@types/mongodb": "2.2.17",
+		"@types/mkdirp": "0.5.2",
+		"@types/mocha": "2.2.45",
+		"@types/mongodb": "2.2.18",
 		"@types/monk": "1.0.6",
 		"@types/morgan": "1.7.35",
 		"@types/ms": "0.7.30",
 		"@types/multer": "1.3.6",
-		"@types/node": "8.5.1",
+		"@types/node": "8.5.2",
 		"@types/page": "1.5.32",
 		"@types/proxy-addr": "2.0.0",
-		"@types/pug": "^2.0.4",
+		"@types/pug": "2.0.4",
 		"@types/qrcode": "0.8.0",
 		"@types/ratelimiter": "2.1.28",
 		"@types/redis": "2.8.3",
@@ -108,11 +108,11 @@
 		"gulp-cssnano": "2.1.2",
 		"gulp-htmlmin": "4.0.0",
 		"gulp-imagemin": "4.1.0",
-		"gulp-mocha": "4.3.1",
+		"gulp-mocha": "5.0.0",
 		"gulp-pug": "3.3.0",
 		"gulp-rename": "1.2.2",
 		"gulp-replace": "0.6.1",
-		"gulp-stylus": "^2.6.0",
+		"gulp-stylus": "2.6.0",
 		"gulp-tslint": "8.1.2",
 		"gulp-typescript": "3.2.3",
 		"gulp-uglify": "3.0.0",
@@ -123,8 +123,8 @@
 		"is-url": "1.2.2",
 		"js-yaml": "3.10.0",
 		"mecab-async": "0.1.2",
-		"mkdirp": "^0.5.1",
-		"mocha": "4.0.1",
+		"mkdirp": "0.5.1",
+		"mocha": "4.1.0",
 		"moji": "0.5.1",
 		"mongodb": "3.0.1",
 		"monk": "6.0.5",
@@ -166,7 +166,7 @@
 		"ts-node": "4.1.0",
 		"tslint": "5.8.0",
 		"typescript": "2.6.2",
-		"uglify-es": "3.2.0",
+		"uglify-es": "3.3.4",
 		"uglifyjs-webpack-plugin": "1.1.5",
 		"uuid": "3.1.0",
 		"vhost": "3.0.2",

From 018b45c6576d4ea40b47bc7d8ab9a06fd788490f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 1 Jan 2018 02:08:41 +0900
Subject: [PATCH 0136/1250] :v:

---
 src/common/build/license.ts     | 13 +++++++++++++
 src/web/docs/license.en.pug     |  3 +++
 src/web/docs/license.ja.pug     |  3 +++
 src/web/docs/vars.ts            |  3 +++
 webpack/module/rules/license.ts |  9 ++-------
 5 files changed, 24 insertions(+), 7 deletions(-)
 create mode 100644 src/common/build/license.ts
 create mode 100644 src/web/docs/license.en.pug
 create mode 100644 src/web/docs/license.ja.pug

diff --git a/src/common/build/license.ts b/src/common/build/license.ts
new file mode 100644
index 000000000..e5c264df8
--- /dev/null
+++ b/src/common/build/license.ts
@@ -0,0 +1,13 @@
+import * as fs from 'fs';
+
+const license = fs.readFileSync(__dirname + '/../../../LICENSE', 'utf-8');
+
+const licenseHtml = license
+	.replace(/\r\n/g, '\n')
+	.replace(/(.)\n(.)/g, '$1 $2')
+	.replace(/(^|\n)(.*?)($|\n)/g, '<p>$2</p>');
+
+export {
+	license,
+	licenseHtml
+};
diff --git a/src/web/docs/license.en.pug b/src/web/docs/license.en.pug
new file mode 100644
index 000000000..240756e7e
--- /dev/null
+++ b/src/web/docs/license.en.pug
@@ -0,0 +1,3 @@
+h1 License
+
+div!= common.license
diff --git a/src/web/docs/license.ja.pug b/src/web/docs/license.ja.pug
new file mode 100644
index 000000000..1f44f3f5e
--- /dev/null
+++ b/src/web/docs/license.ja.pug
@@ -0,0 +1,3 @@
+h1 ライセンス
+
+div!= common.license
diff --git a/src/web/docs/vars.ts b/src/web/docs/vars.ts
index 65b224fbf..95ae9ee62 100644
--- a/src/web/docs/vars.ts
+++ b/src/web/docs/vars.ts
@@ -4,6 +4,7 @@ import * as yaml from 'js-yaml';
 
 import { fa } from '../../common/build/fa';
 import config from '../../conf';
+import { licenseHtml } from '../../common/build/license';
 const constants = require('../../const.json');
 
 export default function(): { [key: string]: any } {
@@ -42,5 +43,7 @@ export default function(): { [key: string]: any } {
 
 	vars['facss'] = fa.dom.css();
 
+	vars['license'] = licenseHtml;
+
 	return vars;
 }
diff --git a/webpack/module/rules/license.ts b/webpack/module/rules/license.ts
index 1795af960..de8b7d79f 100644
--- a/webpack/module/rules/license.ts
+++ b/webpack/module/rules/license.ts
@@ -2,13 +2,8 @@
  * Inject license
  */
 
-import * as fs from 'fs';
 const StringReplacePlugin = require('string-replace-webpack-plugin');
-
-const license = fs.readFileSync(__dirname + '/../../../LICENSE', 'utf-8')
-	.replace(/\r\n/g, '\n')
-	.replace(/(.)\n(.)/g, '$1 $2')
-	.replace(/(^|\n)(.*?)($|\n)/g, '<p>$2</p>');
+import { licenseHtml } from '../../../src/common/build/license';
 
 export default () => ({
 	enforce: 'pre',
@@ -16,7 +11,7 @@ export default () => ({
 	exclude: /node_modules/,
 	loader: StringReplacePlugin.replace({
 		replacements: [{
-			pattern: '%license%', replacement: () => license
+			pattern: '%license%', replacement: () => licenseHtml
 		}]
 	})
 });

From c4b306b3f1ef1cf3af4f921e6794d00805b7a57f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 1 Jan 2018 02:09:12 +0900
Subject: [PATCH 0137/1250] v3493

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index a05097ff0..6e69a7319 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+3493 (2018/01/01)
+-----------------
+* なんか
+
 3460 (2017/12/23)
 -----------------
 * 検索で複数のユーザーを指定できるように
diff --git a/package.json b/package.json
index ff9069a42..9245a6e2a 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3460",
+	"version": "0.0.3493",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 26e449b7a2cefbd939054b4db0119b822fd1c448 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 1 Jan 2018 02:14:00 +0900
Subject: [PATCH 0138/1250] Update backup.md

---
 docs/backup.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/backup.md b/docs/backup.md
index 484564b31..74ec2678e 100644
--- a/docs/backup.md
+++ b/docs/backup.md
@@ -7,7 +7,7 @@ Make sure **mongodb-tools** installed.
 
 In your shell:
 ``` shell
-$ mongodump --archive=db-backup
+$ mongodump --archive=db-backup -u <YourUserName> -p <YourPassword>
 ```
 
 For details, plese see [mongodump docs](https://docs.mongodb.com/manual/reference/program/mongodump/).

From ef371d9bf4886223f8f233f850079cd1ff6a7c5e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 1 Jan 2018 02:26:25 +0900
Subject: [PATCH 0139/1250] Fix bug

---
 src/db/mongodb.ts | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/src/db/mongodb.ts b/src/db/mongodb.ts
index 1263ccaac..bbbe70c34 100644
--- a/src/db/mongodb.ts
+++ b/src/db/mongodb.ts
@@ -1,7 +1,10 @@
 import config from '../conf';
 
-const uri = config.mongodb.user && config.mongodb.pass
-	? `mongodb://${config.mongodb.user}:${config.mongodb.pass}@${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`
+const u = config.mongodb.user ? encodeURIComponent(config.mongodb.user) : null;
+const p = config.mongodb.pass ? encodeURIComponent(config.mongodb.pass) : null;
+
+const uri = u && p
+	? `mongodb://${u}:${p}@${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`
 	: `mongodb://${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`;
 
 /**

From b430d4f2341a5711091bd9db29a310b4dd7f25da Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 8 Jan 2018 01:47:56 +0900
Subject: [PATCH 0140/1250] Show the licenses in the doc

---
 package.json                 |  2 ++
 src/web/docs/api/gulpfile.ts |  8 ++++----
 src/web/docs/gulpfile.ts     |  4 ++--
 src/web/docs/license.en.pug  | 14 ++++++++++++++
 src/web/docs/license.ja.pug  | 14 ++++++++++++++
 src/web/docs/style.styl      |  2 ++
 src/web/docs/vars.ts         | 17 ++++++++++++++++-
 7 files changed, 54 insertions(+), 7 deletions(-)

diff --git a/package.json b/package.json
index 9245a6e2a..69c92efdf 100644
--- a/package.json
+++ b/package.json
@@ -52,6 +52,7 @@
 		"@types/is-root": "1.0.0",
 		"@types/is-url": "1.2.28",
 		"@types/js-yaml": "3.10.1",
+		"@types/license-checker": "^15.0.0",
 		"@types/mkdirp": "0.5.2",
 		"@types/mocha": "2.2.45",
 		"@types/mongodb": "2.2.18",
@@ -122,6 +123,7 @@
 		"is-root": "1.0.0",
 		"is-url": "1.2.2",
 		"js-yaml": "3.10.0",
+		"license-checker": "^15.0.0",
 		"mecab-async": "0.1.2",
 		"mkdirp": "0.5.1",
 		"mocha": "4.1.0",
diff --git a/src/web/docs/api/gulpfile.ts b/src/web/docs/api/gulpfile.ts
index 4c30871a0..cd1bf1530 100644
--- a/src/web/docs/api/gulpfile.ts
+++ b/src/web/docs/api/gulpfile.ts
@@ -17,8 +17,6 @@ import config from './../../../conf';
 
 import generateVars from '../vars';
 
-const commonVars = generateVars();
-
 const langs = Object.keys(locales);
 
 const kebab = string => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase();
@@ -94,7 +92,8 @@ gulp.task('doc:api', [
 	'doc:api:entities'
 ]);
 
-gulp.task('doc:api:endpoints', () => {
+gulp.task('doc:api:endpoints', async () => {
+	const commonVars = await generateVars();
 	glob('./src/web/docs/api/endpoints/**/*.yaml', (globErr, files) => {
 		if (globErr) {
 			console.error(globErr);
@@ -144,7 +143,8 @@ gulp.task('doc:api:endpoints', () => {
 	});
 });
 
-gulp.task('doc:api:entities', () => {
+gulp.task('doc:api:entities', async () => {
+	const commonVars = await generateVars();
 	glob('./src/web/docs/api/entities/**/*.yaml', (globErr, files) => {
 		if (globErr) {
 			console.error(globErr);
diff --git a/src/web/docs/gulpfile.ts b/src/web/docs/gulpfile.ts
index 71033e1bc..d5ddda108 100644
--- a/src/web/docs/gulpfile.ts
+++ b/src/web/docs/gulpfile.ts
@@ -23,9 +23,9 @@ gulp.task('doc', [
 	'doc:styles'
 ]);
 
-const commonVars = generateVars();
+gulp.task('doc:docs', async () => {
+	const commonVars = await generateVars();
 
-gulp.task('doc:docs', () => {
 	glob('./src/web/docs/**/*.*.pug', (globErr, files) => {
 		if (globErr) {
 			console.error(globErr);
diff --git a/src/web/docs/license.en.pug b/src/web/docs/license.en.pug
index 240756e7e..45d8b7647 100644
--- a/src/web/docs/license.en.pug
+++ b/src/web/docs/license.en.pug
@@ -1,3 +1,17 @@
 h1 License
 
 div!= common.license
+
+details
+	summary Libraries
+
+	section
+		h2 Libraries
+
+		each dependency, name in common.dependencies
+			details
+				summary= name
+
+				section
+					h3= name
+					pre= dependency.licenseText
diff --git a/src/web/docs/license.ja.pug b/src/web/docs/license.ja.pug
index 1f44f3f5e..7bd9a6294 100644
--- a/src/web/docs/license.ja.pug
+++ b/src/web/docs/license.ja.pug
@@ -1,3 +1,17 @@
 h1 ライセンス
 
 div!= common.license
+
+details
+	summary ライブラリ
+
+	section
+		h2 ライブラリ
+
+		each dependency, name in common.dependencies
+			details
+				summary= name
+
+				section
+					h3= name
+					pre= dependency.licenseText
diff --git a/src/web/docs/style.styl b/src/web/docs/style.styl
index a726d49b1..bc165f872 100644
--- a/src/web/docs/style.styl
+++ b/src/web/docs/style.styl
@@ -114,5 +114,7 @@ code
 	border-radius 4px
 
 pre
+	overflow auto
+
 	> code
 		display block
diff --git a/src/web/docs/vars.ts b/src/web/docs/vars.ts
index 95ae9ee62..6f713f21d 100644
--- a/src/web/docs/vars.ts
+++ b/src/web/docs/vars.ts
@@ -1,13 +1,16 @@
 import * as fs from 'fs';
+import * as util from 'util';
 import * as glob from 'glob';
 import * as yaml from 'js-yaml';
+import * as licenseChecker from 'license-checker';
+import * as tmp from 'tmp';
 
 import { fa } from '../../common/build/fa';
 import config from '../../conf';
 import { licenseHtml } from '../../common/build/license';
 const constants = require('../../const.json');
 
-export default function(): { [key: string]: any } {
+export default async function(): Promise<{ [key: string]: any }> {
 	const vars = {} as { [key: string]: any };
 
 	const endpoints = glob.sync('./src/web/docs/api/endpoints/**/*.yaml');
@@ -45,5 +48,17 @@ export default function(): { [key: string]: any } {
 
 	vars['license'] = licenseHtml;
 
+	const tmpObj = tmp.fileSync();
+	fs.writeFileSync(tmpObj.name, JSON.stringify({
+		licenseText: ''
+	}), 'utf-8');
+	const dependencies = await util.promisify(licenseChecker.init).bind(licenseChecker)({
+		start: __dirname + '/../../../',
+		customPath: tmpObj.name
+	});
+	tmpObj.removeCallback();
+
+	vars['dependencies'] = dependencies;
+
 	return vars;
 }

From b14a395b502af4e982e3c75fac3a42be7f6e3e44 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 19 Jan 2018 09:22:30 +0900
Subject: [PATCH 0141/1250] Update api.ts

---
 src/web/app/common/scripts/api.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/app/common/scripts/api.ts b/src/web/app/common/scripts/api.ts
index 2008e6f5a..bba838f56 100644
--- a/src/web/app/common/scripts/api.ts
+++ b/src/web/app/common/scripts/api.ts
@@ -40,7 +40,7 @@ export default (i, endpoint, data = {}): Promise<{ [x: string]: any }> => {
 			} else {
 				res.json().then(err => {
 					reject(err.error);
-				});
+				}, reject);
 			}
 		}).catch(reject);
 	});

From 3e28c7eaa8e455d4ca98a0b8faf086b81b326b48 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 19 Jan 2018 20:34:01 +0900
Subject: [PATCH 0142/1250] Refactor

---
 src/api/common/add-file-to-drive.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/api/common/add-file-to-drive.ts b/src/api/common/add-file-to-drive.ts
index 427b54d72..23cbc44e6 100644
--- a/src/api/common/add-file-to-drive.ts
+++ b/src/api/common/add-file-to-drive.ts
@@ -266,11 +266,11 @@ export default (user: any, file: string | stream.Readable, ...args) => new Promi
 		}
 		rej(new Error('un-compatible file.'));
 	})
-	.then(([path, remove]): Promise<any> => new Promise((res, rej) => {
+	.then(([path, shouldCleanup]): Promise<any> => new Promise((res, rej) => {
 		addFile(user, path, ...args)
 			.then(file => {
 				res(file);
-				if (remove) {
+				if (shouldCleanup) {
 					fs.unlink(path, (e) => {
 						if (e) log(e.stack);
 					});

From a4d658a891a1b28b88ba9f6195548e22e196bc43 Mon Sep 17 00:00:00 2001
From: Aya Morisawa <AyaMorisawa4869@gmail.com>
Date: Sun, 21 Jan 2018 15:49:31 +0900
Subject: [PATCH 0143/1250] Update text.js

---
 test/text.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/test/text.js b/test/text.js
index 49e2f02b5..24800ac44 100644
--- a/test/text.js
+++ b/test/text.js
@@ -8,7 +8,7 @@ const analyze = require('../built/api/common/text').default;
 const syntaxhighlighter = require('../built/api/common/text/core/syntax-highlighter').default;
 
 describe('Text', () => {
-	it('is correctly analyzed', () => {
+	it('can be analyzed', () => {
 		const tokens = analyze('@himawari お腹ペコい :cat: #yryr');
 		assert.deepEqual([
 			{ type: 'mention', content: '@himawari', username: 'himawari' },
@@ -19,7 +19,7 @@ describe('Text', () => {
 		], tokens);
 	});
 
-	it('逆関数で正しく復元できる', () => {
+	it('can be inverted', () => {
 		const text = '@himawari お腹ペコい :cat: #yryr';
 		assert.equal(analyze(text).map(x => x.content).join(''), text);
 	});

From cef8a3a7bbabde411e39b4207956db7c12c57b57 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 2 Feb 2018 07:34:51 +0900
Subject: [PATCH 0144/1250] Update dependencies :rocket:

---
 package.json      | 60 +++++++++++++++++++++++------------------------
 src/db/mongodb.ts |  2 +-
 2 files changed, 31 insertions(+), 31 deletions(-)

diff --git a/package.json b/package.json
index 69c92efdf..bd5114480 100644
--- a/package.json
+++ b/package.json
@@ -30,7 +30,7 @@
 		"@prezzemolo/zip": "0.0.3",
 		"@types/bcryptjs": "2.4.1",
 		"@types/body-parser": "1.16.8",
-		"@types/chai": "4.0.10",
+		"@types/chai": "4.1.2",
 		"@types/chai-http": "3.0.3",
 		"@types/compression": "0.0.35",
 		"@types/cookie": "0.3.1",
@@ -39,7 +39,7 @@
 		"@types/deep-equal": "1.0.1",
 		"@types/elasticsearch": "5.0.19",
 		"@types/eventemitter3": "2.0.2",
-		"@types/express": "4.11.0",
+		"@types/express": "4.11.1",
 		"@types/gm": "1.17.33",
 		"@types/gulp": "3.8.36",
 		"@types/gulp-htmlmin": "1.3.31",
@@ -54,55 +54,55 @@
 		"@types/js-yaml": "3.10.1",
 		"@types/license-checker": "^15.0.0",
 		"@types/mkdirp": "0.5.2",
-		"@types/mocha": "2.2.45",
-		"@types/mongodb": "2.2.18",
-		"@types/monk": "1.0.6",
+		"@types/mocha": "2.2.47",
+		"@types/mongodb": "3.0.5",
+		"@types/monk": "6.0.0",
 		"@types/morgan": "1.7.35",
 		"@types/ms": "0.7.30",
 		"@types/multer": "1.3.6",
-		"@types/node": "8.5.2",
+		"@types/node": "9.4.0",
 		"@types/page": "1.5.32",
 		"@types/proxy-addr": "2.0.0",
 		"@types/pug": "2.0.4",
 		"@types/qrcode": "0.8.0",
 		"@types/ratelimiter": "2.1.28",
-		"@types/redis": "2.8.3",
-		"@types/request": "2.0.9",
+		"@types/redis": "2.8.5",
+		"@types/request": "2.47.0",
 		"@types/rimraf": "2.0.2",
 		"@types/riot": "3.6.1",
 		"@types/seedrandom": "2.4.27",
 		"@types/serve-favicon": "2.2.30",
-		"@types/speakeasy": "2.0.1",
+		"@types/speakeasy": "2.0.2",
 		"@types/tmp": "0.0.33",
 		"@types/uuid": "3.4.3",
-		"@types/webpack": "3.8.1",
+		"@types/webpack": "3.8.4",
 		"@types/webpack-stream": "3.2.8",
-		"@types/websocket": "0.0.35",
+		"@types/websocket": "0.0.36",
 		"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.0",
+		"cafy": "3.2.1",
 		"chai": "4.1.2",
 		"chai-http": "3.0.0",
 		"chalk": "2.3.0",
 		"compression": "1.7.1",
 		"cookie": "0.3.1",
 		"cors": "2.8.4",
-		"cropperjs": "1.2.1",
-		"css-loader": "0.28.7",
+		"cropperjs": "1.2.2",
+		"css-loader": "0.28.9",
 		"debug": "3.1.0",
 		"deep-equal": "1.0.1",
 		"deepcopy": "0.6.3",
 		"diskusage": "0.2.4",
-		"elasticsearch": "14.0.0",
+		"elasticsearch": "14.1.0",
 		"escape-regexp": "0.0.1",
 		"eventemitter3": "3.0.0",
 		"exif-js": "2.3.0",
 		"express": "4.16.2",
-		"file-type": "7.4.0",
+		"file-type": "7.5.0",
 		"fuckadblock": "3.2.1",
 		"gm": "1.23.1",
 		"gulp": "3.9.1",
@@ -113,29 +113,29 @@
 		"gulp-pug": "3.3.0",
 		"gulp-rename": "1.2.2",
 		"gulp-replace": "0.6.1",
-		"gulp-stylus": "2.6.0",
+		"gulp-stylus": "2.7.0",
 		"gulp-tslint": "8.1.2",
-		"gulp-typescript": "3.2.3",
+		"gulp-typescript": "3.2.4",
 		"gulp-uglify": "3.0.0",
 		"gulp-util": "3.0.8",
 		"highlight.js": "9.12.0",
-		"inquirer": "4.0.1",
+		"inquirer": "5.0.1",
 		"is-root": "1.0.0",
 		"is-url": "1.2.2",
 		"js-yaml": "3.10.0",
-		"license-checker": "^15.0.0",
+		"license-checker": "16.0.0",
 		"mecab-async": "0.1.2",
 		"mkdirp": "0.5.1",
-		"mocha": "4.1.0",
+		"mocha": "5.0.0",
 		"moji": "0.5.1",
-		"mongodb": "3.0.1",
+		"mongodb": "3.0.2",
 		"monk": "6.0.5",
 		"morgan": "1.9.0",
 		"ms": "2.1.1",
 		"multer": "1.3.0",
 		"nprogress": "0.2.0",
 		"os-utils": "0.0.14",
-		"page": "1.7.1",
+		"page": "1.8.3",
 		"pictograph": "2.1.5",
 		"prominence": "0.2.0",
 		"proxy-addr": "2.0.2",
@@ -150,13 +150,13 @@
 		"riot": "3.8.1",
 		"riot-tag-loader": "2.0.2",
 		"rndstr": "1.0.0",
-		"s-age": "1.1.0",
+		"s-age": "1.1.2",
 		"seedrandom": "2.4.3",
 		"serve-favicon": "2.4.5",
 		"sortablejs": "1.7.0",
 		"speakeasy": "2.0.0",
 		"string-replace-webpack-plugin": "0.1.3",
-		"style-loader": "0.19.1",
+		"style-loader": "0.20.1",
 		"stylus": "0.54.5",
 		"stylus-loader": "3.0.1",
 		"summaly": "2.0.3",
@@ -166,11 +166,11 @@
 		"textarea-caret": "3.0.2",
 		"tmp": "0.0.33",
 		"ts-node": "4.1.0",
-		"tslint": "5.8.0",
-		"typescript": "2.6.2",
-		"uglify-es": "3.3.4",
-		"uglifyjs-webpack-plugin": "1.1.5",
-		"uuid": "3.1.0",
+		"tslint": "5.9.1",
+		"typescript": "2.7.1",
+		"uglify-es": "3.3.9",
+		"uglifyjs-webpack-plugin": "1.1.8",
+		"uuid": "3.2.1",
 		"vhost": "3.0.2",
 		"web-push": "3.2.5",
 		"webpack": "3.10.0",
diff --git a/src/db/mongodb.ts b/src/db/mongodb.ts
index bbbe70c34..233f2f3d7 100644
--- a/src/db/mongodb.ts
+++ b/src/db/mongodb.ts
@@ -10,7 +10,7 @@ const uri = u && p
 /**
  * monk
  */
-import * as mongo from 'monk';
+import mongo from 'monk';
 
 const db = mongo(uri);
 

From 2f645d59db613d4284209c70cc83402facc52fb4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 2 Feb 2018 08:06:01 +0900
Subject: [PATCH 0145/1250] wip

---
 src/api/models/app.ts                    |  95 ++++++++++-
 src/api/models/auth-session.ts           |  44 ++++-
 src/api/models/channel.ts                |  66 +++++++-
 src/api/models/drive-file.ts             |  84 +++++++++-
 src/api/models/drive-folder.ts           |  67 +++++++-
 src/api/models/messaging-message.ts      |  66 +++++++-
 src/api/models/notification.ts           |  64 +++++++-
 src/api/models/post-reaction.ts          |  47 +++++-
 src/api/models/post.ts                   | 189 +++++++++++++++++++++-
 src/api/models/signin.ts                 |  28 +++-
 src/api/models/user.ts                   | 196 ++++++++++++++++++++++-
 src/api/serializers/app.ts               |  83 ----------
 src/api/serializers/auth-session.ts      |  40 -----
 src/api/serializers/channel.ts           |  66 --------
 src/api/serializers/drive-file.ts        |  78 ---------
 src/api/serializers/drive-folder.ts      |  64 --------
 src/api/serializers/drive-tag.ts         |  35 ----
 src/api/serializers/messaging-message.ts |  68 --------
 src/api/serializers/notification.ts      |  65 --------
 src/api/serializers/post-reaction.ts     |  43 -----
 src/api/serializers/post.ts              | 192 ----------------------
 src/api/serializers/signin.ts            |  23 ---
 src/api/serializers/user.ts              | 190 ----------------------
 23 files changed, 920 insertions(+), 973 deletions(-)
 delete mode 100644 src/api/serializers/app.ts
 delete mode 100644 src/api/serializers/auth-session.ts
 delete mode 100644 src/api/serializers/channel.ts
 delete mode 100644 src/api/serializers/drive-file.ts
 delete mode 100644 src/api/serializers/drive-folder.ts
 delete mode 100644 src/api/serializers/drive-tag.ts
 delete mode 100644 src/api/serializers/messaging-message.ts
 delete mode 100644 src/api/serializers/notification.ts
 delete mode 100644 src/api/serializers/post-reaction.ts
 delete mode 100644 src/api/serializers/post.ts
 delete mode 100644 src/api/serializers/signin.ts
 delete mode 100644 src/api/serializers/user.ts

diff --git a/src/api/models/app.ts b/src/api/models/app.ts
index 68f2f448b..fe9d49ff6 100644
--- a/src/api/models/app.ts
+++ b/src/api/models/app.ts
@@ -1,13 +1,96 @@
+import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
+import AccessToken from './access-token';
 import db from '../../db/mongodb';
+import config from '../../conf';
 
-const collection = db.get('apps');
+const App = db.get<IApp>('apps');
+App.createIndex('name_id');
+App.createIndex('name_id_lower');
+App.createIndex('secret');
+export default App;
 
-(collection as any).createIndex('name_id'); // fuck type definition
-(collection as any).createIndex('name_id_lower'); // fuck type definition
-(collection as any).createIndex('secret'); // fuck type definition
-
-export default collection as any; // fuck type definition
+export type IApp = {
+	_id: mongo.ObjectID;
+	created_at: Date;
+	user_id: mongo.ObjectID;
+};
 
 export function isValidNameId(nameId: string): boolean {
 	return typeof nameId == 'string' && /^[a-zA-Z0-9\-]{3,30}$/.test(nameId);
 }
+
+/**
+ * Pack an app for API response
+ *
+ * @param {any} app
+ * @param {any} me?
+ * @param {any} options?
+ * @return {Promise<any>}
+ */
+export const pack = (
+	app: any,
+	me?: any,
+	options?: {
+		includeSecret?: boolean,
+		includeProfileImageIds?: boolean
+	}
+) => new Promise<any>(async (resolve, reject) => {
+	const opts = options || {
+		includeSecret: false,
+		includeProfileImageIds: false
+	};
+
+	let _app: any;
+
+	// Populate the app if 'app' is ID
+	if (mongo.ObjectID.prototype.isPrototypeOf(app)) {
+		_app = await App.findOne({
+			_id: app
+		});
+	} else if (typeof app === 'string') {
+		_app = await App.findOne({
+			_id: new mongo.ObjectID(app)
+		});
+	} else {
+		_app = deepcopy(app);
+	}
+
+	// Me
+	if (me && !mongo.ObjectID.prototype.isPrototypeOf(me)) {
+		if (typeof me === 'string') {
+			me = new mongo.ObjectID(me);
+		} else {
+			me = me._id;
+		}
+	}
+
+	// Rename _id to id
+	_app.id = _app._id;
+	delete _app._id;
+
+	delete _app.name_id_lower;
+
+	// Visible by only owner
+	if (!opts.includeSecret) {
+		delete _app.secret;
+	}
+
+	_app.icon_url = _app.icon != null
+		? `${config.drive_url}/${_app.icon}`
+		: `${config.drive_url}/app-default.jpg`;
+
+	if (me) {
+		// 既に連携しているか
+		const exist = await AccessToken.count({
+			app_id: _app.id,
+			user_id: me,
+		}, {
+				limit: 1
+			});
+
+		_app.is_authorized = exist === 1;
+	}
+
+	resolve(_app);
+});
diff --git a/src/api/models/auth-session.ts b/src/api/models/auth-session.ts
index b264a133e..997ec61c2 100644
--- a/src/api/models/auth-session.ts
+++ b/src/api/models/auth-session.ts
@@ -1,3 +1,45 @@
+import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
 import db from '../../db/mongodb';
+import { pack as packApp } from './app';
 
-export default db.get('auth_sessions') as any; // fuck type definition
+const AuthSession = db.get('auth_sessions');
+export default AuthSession;
+
+export interface IAuthSession {
+	_id: mongo.ObjectID;
+}
+
+/**
+ * Pack an auth session for API response
+ *
+ * @param {any} session
+ * @param {any} me?
+ * @return {Promise<any>}
+ */
+export const pack = (
+	session: any,
+	me?: any
+) => new Promise<any>(async (resolve, reject) => {
+	let _session: any;
+
+	// TODO: Populate session if it ID
+
+	_session = deepcopy(session);
+
+	// Me
+	if (me && !mongo.ObjectID.prototype.isPrototypeOf(me)) {
+		if (typeof me === 'string') {
+			me = new mongo.ObjectID(me);
+		} else {
+			me = me._id;
+		}
+	}
+
+	delete _session._id;
+
+	// Populate app
+	_session.app = await packApp(_session.app_id, me);
+
+	resolve(_session);
+});
diff --git a/src/api/models/channel.ts b/src/api/models/channel.ts
index c80e84dbc..815d53593 100644
--- a/src/api/models/channel.ts
+++ b/src/api/models/channel.ts
@@ -1,9 +1,11 @@
 import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
+import { IUser } from './user';
+import Watching from './channel-watching';
 import db from '../../db/mongodb';
 
-const collection = db.get('channels');
-
-export default collection as any; // fuck type definition
+const Channel = db.get<IChannel>('channels');
+export default Channel;
 
 export type IChannel = {
 	_id: mongo.ObjectID;
@@ -12,3 +14,61 @@ export type IChannel = {
 	user_id: mongo.ObjectID;
 	index: number;
 };
+
+/**
+ * Pack a channel for API response
+ *
+ * @param channel target
+ * @param me? serializee
+ * @return response
+ */
+export const pack = (
+	channel: string | mongo.ObjectID | IChannel,
+	me?: string | mongo.ObjectID | IUser
+) => new Promise<any>(async (resolve, reject) => {
+
+	let _channel: any;
+
+	// Populate the channel if 'channel' is ID
+	if (mongo.ObjectID.prototype.isPrototypeOf(channel)) {
+		_channel = await Channel.findOne({
+			_id: channel
+		});
+	} else if (typeof channel === 'string') {
+		_channel = await Channel.findOne({
+			_id: new mongo.ObjectID(channel)
+		});
+	} else {
+		_channel = deepcopy(channel);
+	}
+
+	// Rename _id to id
+	_channel.id = _channel._id;
+	delete _channel._id;
+
+	// Remove needless properties
+	delete _channel.user_id;
+
+	// Me
+	const meId: mongo.ObjectID = me
+	? mongo.ObjectID.prototype.isPrototypeOf(me)
+		? me as mongo.ObjectID
+		: typeof me === 'string'
+			? new mongo.ObjectID(me)
+			: (me as IUser)._id
+	: null;
+
+	if (me) {
+		//#region Watchしているかどうか
+		const watch = await Watching.findOne({
+			user_id: meId,
+			channel_id: _channel.id,
+			deleted_at: { $exists: false }
+		});
+
+		_channel.is_watching = watch !== null;
+		//#endregion
+	}
+
+	resolve(_channel);
+});
diff --git a/src/api/models/drive-file.ts b/src/api/models/drive-file.ts
index 802ee5a5f..6a8db3ad4 100644
--- a/src/api/models/drive-file.ts
+++ b/src/api/models/drive-file.ts
@@ -1,9 +1,12 @@
 import * as mongodb from 'mongodb';
+import deepcopy = require('deepcopy');
+import { pack as packFolder } from './drive-folder';
+import config from '../../conf';
 import monkDb, { nativeDbConn } from '../../db/mongodb';
 
-const collection = monkDb.get('drive_files.files');
+const DriveFile = monkDb.get<IDriveFile>('drive_files.files');
 
-export default collection as any; // fuck type definition
+export default DriveFile;
 
 const getGridFSBucket = async (): Promise<mongodb.GridFSBucket> => {
 	const db = await nativeDbConn();
@@ -15,6 +18,12 @@ const getGridFSBucket = async (): Promise<mongodb.GridFSBucket> => {
 
 export { getGridFSBucket };
 
+export type IDriveFile = {
+	_id: mongodb.ObjectID;
+	created_at: Date;
+	user_id: mongodb.ObjectID;
+};
+
 export function validateFileName(name: string): boolean {
 	return (
 		(name.trim().length > 0) &&
@@ -24,3 +33,74 @@ export function validateFileName(name: string): boolean {
 		(name.indexOf('..') === -1)
 	);
 }
+
+/**
+ * Pack a drive file for API response
+ *
+ * @param {any} file
+ * @param {any} options?
+ * @return {Promise<any>}
+ */
+export const pack = (
+	file: any,
+	options?: {
+		detail: boolean
+	}
+) => new Promise<any>(async (resolve, reject) => {
+	const opts = Object.assign({
+		detail: false
+	}, options);
+
+	let _file: any;
+
+	// Populate the file if 'file' is ID
+	if (mongodb.ObjectID.prototype.isPrototypeOf(file)) {
+		_file = await DriveFile.findOne({
+			_id: file
+		});
+	} else if (typeof file === 'string') {
+		_file = await DriveFile.findOne({
+			_id: new mongodb.ObjectID(file)
+		});
+	} else {
+		_file = deepcopy(file);
+	}
+
+	if (!_file) return reject('invalid file arg.');
+
+	// rendered target
+	let _target: any = {};
+
+	_target.id = _file._id;
+	_target.created_at = _file.uploadDate;
+	_target.name = _file.filename;
+	_target.type = _file.contentType;
+	_target.datasize = _file.length;
+	_target.md5 = _file.md5;
+
+	_target = Object.assign(_target, _file.metadata);
+
+	_target.url = `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`;
+
+	if (_target.properties == null) _target.properties = {};
+
+	if (opts.detail) {
+		if (_target.folder_id) {
+			// Populate folder
+			_target.folder = await packFolder(_target.folder_id, {
+				detail: true
+			});
+		}
+
+		/*
+		if (_target.tags) {
+			// Populate tags
+			_target.tags = await _target.tags.map(async (tag: any) =>
+				await serializeDriveTag(tag)
+			);
+		}
+		*/
+	}
+
+	resolve(_target);
+});
diff --git a/src/api/models/drive-folder.ts b/src/api/models/drive-folder.ts
index f81ffe855..48b26c2bd 100644
--- a/src/api/models/drive-folder.ts
+++ b/src/api/models/drive-folder.ts
@@ -1,6 +1,16 @@
+import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
 import db from '../../db/mongodb';
+import DriveFile from './drive-file';
 
-export default db.get('drive_folders') as any; // fuck type definition
+const DriveFolder = db.get<IDriveFolder>('drive_folders');
+export default DriveFolder;
+
+export type IDriveFolder = {
+	_id: mongo.ObjectID;
+	created_at: Date;
+	user_id: mongo.ObjectID;
+};
 
 export function isValidFolderName(name: string): boolean {
 	return (
@@ -8,3 +18,58 @@ export function isValidFolderName(name: string): boolean {
 		(name.length <= 200)
 	);
 }
+
+/**
+ * Pack a drive folder for API response
+ *
+ * @param {any} folder
+ * @param {any} options?
+ * @return {Promise<any>}
+ */
+export const pack = (
+	folder: any,
+	options?: {
+		detail: boolean
+	}
+) => new Promise<any>(async (resolve, reject) => {
+	const opts = Object.assign({
+		detail: false
+	}, options);
+
+	let _folder: any;
+
+	// Populate the folder if 'folder' is ID
+	if (mongo.ObjectID.prototype.isPrototypeOf(folder)) {
+		_folder = await DriveFolder.findOne({ _id: folder });
+	} else if (typeof folder === 'string') {
+		_folder = await DriveFolder.findOne({ _id: new mongo.ObjectID(folder) });
+	} else {
+		_folder = deepcopy(folder);
+	}
+
+	// Rename _id to id
+	_folder.id = _folder._id;
+	delete _folder._id;
+
+	if (opts.detail) {
+		const childFoldersCount = await DriveFolder.count({
+			parent_id: _folder.id
+		});
+
+		const childFilesCount = await DriveFile.count({
+			'metadata.folder_id': _folder.id
+		});
+
+		_folder.folders_count = childFoldersCount;
+		_folder.files_count = childFilesCount;
+	}
+
+	if (opts.detail && _folder.parent_id) {
+		// Populate parent folder
+		_folder.parent = await pack(_folder.parent_id, {
+			detail: true
+		});
+	}
+
+	resolve(_folder);
+});
diff --git a/src/api/models/messaging-message.ts b/src/api/models/messaging-message.ts
index 18afa57e4..ffdda1db2 100644
--- a/src/api/models/messaging-message.ts
+++ b/src/api/models/messaging-message.ts
@@ -1,7 +1,12 @@
 import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
+import { pack as packUser } from './user';
+import { pack as packFile } from './drive-file';
 import db from '../../db/mongodb';
+import parse from '../common/text';
 
-export default db.get('messaging_messages') as any; // fuck type definition
+const MessagingMessage = db.get<IMessagingMessage>('messaging_messages');
+export default MessagingMessage;
 
 export interface IMessagingMessage {
 	_id: mongo.ObjectID;
@@ -10,3 +15,62 @@ export interface IMessagingMessage {
 export function isValidText(text: string): boolean {
 	return text.length <= 1000 && text.trim() != '';
 }
+
+/**
+ * Pack a messaging message for API response
+ *
+ * @param {any} message
+ * @param {any} me?
+ * @param {any} options?
+ * @return {Promise<any>}
+ */
+export const pack = (
+	message: any,
+	me?: any,
+	options?: {
+		populateRecipient: boolean
+	}
+) => new Promise<any>(async (resolve, reject) => {
+	const opts = options || {
+		populateRecipient: true
+	};
+
+	let _message: any;
+
+	// Populate the message if 'message' is ID
+	if (mongo.ObjectID.prototype.isPrototypeOf(message)) {
+		_message = await MessagingMessage.findOne({
+			_id: message
+		});
+	} else if (typeof message === 'string') {
+		_message = await MessagingMessage.findOne({
+			_id: new mongo.ObjectID(message)
+		});
+	} else {
+		_message = deepcopy(message);
+	}
+
+	// Rename _id to id
+	_message.id = _message._id;
+	delete _message._id;
+
+	// Parse text
+	if (_message.text) {
+		_message.ast = parse(_message.text);
+	}
+
+	// Populate user
+	_message.user = await packUser(_message.user_id, me);
+
+	if (_message.file) {
+		// Populate file
+		_message.file = await packFile(_message.file_id);
+	}
+
+	if (opts.populateRecipient) {
+		// Populate recipient
+		_message.recipient = await packUser(_message.recipient_id, me);
+	}
+
+	resolve(_message);
+});
diff --git a/src/api/models/notification.ts b/src/api/models/notification.ts
index e3dc6c70a..fa7049d31 100644
--- a/src/api/models/notification.ts
+++ b/src/api/models/notification.ts
@@ -1,8 +1,11 @@
 import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
 import db from '../../db/mongodb';
-import { IUser } from './user';
+import { IUser, pack as packUser } from './user';
+import { pack as packPost } from './post';
 
-export default db.get('notifications') as any; // fuck type definition
+const Notification = db.get<INotification>('notifications');
+export default Notification;
 
 export interface INotification {
 	_id: mongo.ObjectID;
@@ -45,3 +48,60 @@ export interface INotification {
 	 */
 	is_read: Boolean;
 }
+
+/**
+ * Pack a notification for API response
+ *
+ * @param {any} notification
+ * @return {Promise<any>}
+ */
+export const pack = (notification: any) => new Promise<any>(async (resolve, reject) => {
+	let _notification: any;
+
+	// Populate the notification if 'notification' is ID
+	if (mongo.ObjectID.prototype.isPrototypeOf(notification)) {
+		_notification = await Notification.findOne({
+			_id: notification
+		});
+	} else if (typeof notification === 'string') {
+		_notification = await Notification.findOne({
+			_id: new mongo.ObjectID(notification)
+		});
+	} else {
+		_notification = deepcopy(notification);
+	}
+
+	// Rename _id to id
+	_notification.id = _notification._id;
+	delete _notification._id;
+
+	// Rename notifier_id to user_id
+	_notification.user_id = _notification.notifier_id;
+	delete _notification.notifier_id;
+
+	const me = _notification.notifiee_id;
+	delete _notification.notifiee_id;
+
+	// Populate notifier
+	_notification.user = await packUser(_notification.user_id, me);
+
+	switch (_notification.type) {
+		case 'follow':
+			// nope
+			break;
+		case 'mention':
+		case 'reply':
+		case 'repost':
+		case 'quote':
+		case 'reaction':
+		case 'poll_vote':
+			// Populate post
+			_notification.post = await packPost(_notification.post_id, me);
+			break;
+		default:
+			console.error(`Unknown type: ${_notification.type}`);
+			break;
+	}
+
+	resolve(_notification);
+});
diff --git a/src/api/models/post-reaction.ts b/src/api/models/post-reaction.ts
index 282ae5bd2..568bfc89a 100644
--- a/src/api/models/post-reaction.ts
+++ b/src/api/models/post-reaction.ts
@@ -1,3 +1,48 @@
+import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
 import db from '../../db/mongodb';
+import Reaction from './post-reaction';
+import { pack as packUser } from './user';
 
-export default db.get('post_reactions') as any; // fuck type definition
+const PostReaction = db.get<IPostReaction>('post_reactions');
+export default PostReaction;
+
+export interface IPostReaction {
+	_id: mongo.ObjectID;
+}
+
+/**
+ * Pack a reaction for API response
+ *
+ * @param {any} reaction
+ * @param {any} me?
+ * @return {Promise<any>}
+ */
+export const pack = (
+	reaction: any,
+	me?: any
+) => new Promise<any>(async (resolve, reject) => {
+	let _reaction: any;
+
+	// Populate the reaction if 'reaction' is ID
+	if (mongo.ObjectID.prototype.isPrototypeOf(reaction)) {
+		_reaction = await Reaction.findOne({
+			_id: reaction
+		});
+	} else if (typeof reaction === 'string') {
+		_reaction = await Reaction.findOne({
+			_id: new mongo.ObjectID(reaction)
+		});
+	} else {
+		_reaction = deepcopy(reaction);
+	}
+
+	// Rename _id to id
+	_reaction.id = _reaction._id;
+	delete _reaction._id;
+
+	// Populate user
+	_reaction.user = await packUser(_reaction.user_id, me);
+
+	resolve(_reaction);
+});
diff --git a/src/api/models/post.ts b/src/api/models/post.ts
index 7584ce182..ecc5e1a5e 100644
--- a/src/api/models/post.ts
+++ b/src/api/models/post.ts
@@ -1,8 +1,18 @@
 import * as mongo from 'mongodb';
-
+import deepcopy = require('deepcopy');
+import rap from '@prezzemolo/rap';
 import db from '../../db/mongodb';
+import { IUser, pack as packUser } from './user';
+import { pack as packApp } from './app';
+import { pack as packChannel } from './channel';
+import Vote from './poll-vote';
+import Reaction from './post-reaction';
+import { pack as packFile } from './drive-file';
+import parse from '../common/text';
 
-export default db.get('posts') as any; // fuck type definition
+const Post = db.get<IPost>('posts');
+
+export default Post;
 
 export function isValidText(text: string): boolean {
 	return text.length <= 1000 && text.trim() != '';
@@ -20,3 +30,178 @@ export type IPost = {
 	user_id: mongo.ObjectID;
 	app_id: mongo.ObjectID;
 };
+
+/**
+ * Pack a post for API response
+ *
+ * @param post target
+ * @param me? serializee
+ * @param options? serialize options
+ * @return response
+ */
+export const pack = async (
+	post: string | mongo.ObjectID | IPost,
+	me?: string | mongo.ObjectID | IUser,
+	options?: {
+		detail: boolean
+	}
+) => {
+	const opts = options || {
+		detail: true,
+	};
+
+	// Me
+	const meId: mongo.ObjectID = me
+		? mongo.ObjectID.prototype.isPrototypeOf(me)
+			? me as mongo.ObjectID
+			: typeof me === 'string'
+				? new mongo.ObjectID(me)
+				: (me as IUser)._id
+		: null;
+
+	let _post: any;
+
+	// Populate the post if 'post' is ID
+	if (mongo.ObjectID.prototype.isPrototypeOf(post)) {
+		_post = await Post.findOne({
+			_id: post
+		});
+	} else if (typeof post === 'string') {
+		_post = await Post.findOne({
+			_id: new mongo.ObjectID(post)
+		});
+	} else {
+		_post = deepcopy(post);
+	}
+
+	if (!_post) throw 'invalid post arg.';
+
+	const id = _post._id;
+
+	// Rename _id to id
+	_post.id = _post._id;
+	delete _post._id;
+
+	delete _post.mentions;
+
+	// Parse text
+	if (_post.text) {
+		_post.ast = parse(_post.text);
+	}
+
+	// Populate user
+	_post.user = packUser(_post.user_id, meId);
+
+	// Populate app
+	if (_post.app_id) {
+		_post.app = packApp(_post.app_id);
+	}
+
+	// Populate channel
+	if (_post.channel_id) {
+		_post.channel = packChannel(_post.channel_id);
+	}
+
+	// Populate media
+	if (_post.media_ids) {
+		_post.media = Promise.all(_post.media_ids.map(fileId =>
+			packFile(fileId)
+		));
+	}
+
+	// When requested a detailed post data
+	if (opts.detail) {
+		// Get previous post info
+		_post.prev = (async () => {
+			const prev = await Post.findOne({
+				user_id: _post.user_id,
+				_id: {
+					$lt: id
+				}
+			}, {
+				fields: {
+					_id: true
+				},
+				sort: {
+					_id: -1
+				}
+			});
+			return prev ? prev._id : null;
+		})();
+
+		// Get next post info
+		_post.next = (async () => {
+			const next = await Post.findOne({
+				user_id: _post.user_id,
+				_id: {
+					$gt: id
+				}
+			}, {
+				fields: {
+					_id: true
+				},
+				sort: {
+					_id: 1
+				}
+			});
+			return next ? next._id : null;
+		})();
+
+		if (_post.reply_id) {
+			// Populate reply to post
+			_post.reply = pack(_post.reply_id, meId, {
+				detail: false
+			});
+		}
+
+		if (_post.repost_id) {
+			// Populate repost
+			_post.repost = pack(_post.repost_id, meId, {
+				detail: _post.text == null
+			});
+		}
+
+		// Poll
+		if (meId && _post.poll) {
+			_post.poll = (async (poll) => {
+				const vote = await Vote
+					.findOne({
+						user_id: meId,
+						post_id: id
+					});
+
+				if (vote != null) {
+					const myChoice = poll.choices
+						.filter(c => c.id == vote.choice)[0];
+
+					myChoice.is_voted = true;
+				}
+
+				return poll;
+			})(_post.poll);
+		}
+
+		// Fetch my reaction
+		if (meId) {
+			_post.my_reaction = (async () => {
+				const reaction = await Reaction
+					.findOne({
+						user_id: meId,
+						post_id: id,
+						deleted_at: { $exists: false }
+					});
+
+				if (reaction) {
+					return reaction.reaction;
+				}
+
+				return null;
+			})();
+		}
+	}
+
+	// resolve promises in _post object
+	_post = await rap(_post);
+
+	return _post;
+};
diff --git a/src/api/models/signin.ts b/src/api/models/signin.ts
index 385a348f2..262c8707e 100644
--- a/src/api/models/signin.ts
+++ b/src/api/models/signin.ts
@@ -1,3 +1,29 @@
+import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
 import db from '../../db/mongodb';
 
-export default db.get('signin') as any; // fuck type definition
+const Signin = db.get<ISignin>('signin');
+export default Signin;
+
+export interface ISignin {
+	_id: mongo.ObjectID;
+}
+
+/**
+ * Pack a signin record for API response
+ *
+ * @param {any} record
+ * @return {Promise<any>}
+ */
+export const pack = (
+	record: any
+) => new Promise<any>(async (resolve, reject) => {
+
+	const _record = deepcopy(record);
+
+	// Rename _id to id
+	_record.id = _record._id;
+	delete _record._id;
+
+	resolve(_record);
+});
diff --git a/src/api/models/user.ts b/src/api/models/user.ts
index 018979158..48a45ac2f 100644
--- a/src/api/models/user.ts
+++ b/src/api/models/user.ts
@@ -1,14 +1,19 @@
 import * as mongo from 'mongodb';
-
+import deepcopy = require('deepcopy');
+import rap from '@prezzemolo/rap';
 import db from '../../db/mongodb';
-import { IPost } from './post';
+import { IPost, pack as packPost } from './post';
+import Following from './following';
+import Mute from './mute';
+import getFriends from '../common/get-friends';
+import config from '../../conf';
 
-const collection = db.get('users');
+const User = db.get<IUser>('users');
 
-(collection as any).createIndex('username'); // fuck type definition
-(collection as any).createIndex('token'); // fuck type definition
+User.createIndex('username');
+User.createIndex('token');
 
-export default collection as any; // fuck type definition
+export default User;
 
 export function validateUsername(username: string): boolean {
 	return typeof username == 'string' && /^[a-zA-Z0-9\-]{3,20}$/.test(username);
@@ -83,3 +88,182 @@ export function init(user): IUser {
 	user.pinned_post_id = new mongo.ObjectID(user.pinned_post_id);
 	return user;
 }
+
+/**
+ * Pack a user for API response
+ *
+ * @param user target
+ * @param me? serializee
+ * @param options? serialize options
+ * @return Packed user
+ */
+export const pack = (
+	user: string | mongo.ObjectID | IUser,
+	me?: string | mongo.ObjectID | IUser,
+	options?: {
+		detail?: boolean,
+		includeSecrets?: boolean
+	}
+) => new Promise<any>(async (resolve, reject) => {
+
+	const opts = Object.assign({
+		detail: false,
+		includeSecrets: false
+	}, options);
+
+	let _user: any;
+
+	const fields = opts.detail ? {
+		settings: false
+	} : {
+		settings: false,
+		client_settings: false,
+		profile: false,
+		keywords: false,
+		domains: false
+	};
+
+	// Populate the user if 'user' is ID
+	if (mongo.ObjectID.prototype.isPrototypeOf(user)) {
+		_user = await User.findOne({
+			_id: user
+		}, { fields });
+	} else if (typeof user === 'string') {
+		_user = await User.findOne({
+			_id: new mongo.ObjectID(user)
+		}, { fields });
+	} else {
+		_user = deepcopy(user);
+	}
+
+	if (!_user) return reject('invalid user arg.');
+
+	// Me
+	const meId: mongo.ObjectID = me
+		? mongo.ObjectID.prototype.isPrototypeOf(me)
+			? me as mongo.ObjectID
+			: typeof me === 'string'
+				? new mongo.ObjectID(me)
+				: (me as IUser)._id
+		: null;
+
+	// Rename _id to id
+	_user.id = _user._id;
+	delete _user._id;
+
+	// Remove needless properties
+	delete _user.latest_post;
+
+	// Remove private properties
+	delete _user.password;
+	delete _user.token;
+	delete _user.two_factor_temp_secret;
+	delete _user.two_factor_secret;
+	delete _user.username_lower;
+	if (_user.twitter) {
+		delete _user.twitter.access_token;
+		delete _user.twitter.access_token_secret;
+	}
+	delete _user.line;
+
+	// Visible via only the official client
+	if (!opts.includeSecrets) {
+		delete _user.email;
+		delete _user.client_settings;
+	}
+
+	if (!opts.detail) {
+		delete _user.two_factor_enabled;
+	}
+
+	_user.avatar_url = _user.avatar_id != null
+		? `${config.drive_url}/${_user.avatar_id}`
+		: `${config.drive_url}/default-avatar.jpg`;
+
+	_user.banner_url = _user.banner_id != null
+		? `${config.drive_url}/${_user.banner_id}`
+		: null;
+
+	if (!meId || !meId.equals(_user.id) || !opts.detail) {
+		delete _user.avatar_id;
+		delete _user.banner_id;
+
+		delete _user.drive_capacity;
+	}
+
+	if (meId && !meId.equals(_user.id)) {
+		// Whether the user is following
+		_user.is_following = (async () => {
+			const follow = await Following.findOne({
+				follower_id: meId,
+				followee_id: _user.id,
+				deleted_at: { $exists: false }
+			});
+			return follow !== null;
+		})();
+
+		// Whether the user is followed
+		_user.is_followed = (async () => {
+			const follow2 = await Following.findOne({
+				follower_id: _user.id,
+				followee_id: meId,
+				deleted_at: { $exists: false }
+			});
+			return follow2 !== null;
+		})();
+
+		// Whether the user is muted
+		_user.is_muted = (async () => {
+			const mute = await Mute.findOne({
+				muter_id: meId,
+				mutee_id: _user.id,
+				deleted_at: { $exists: false }
+			});
+			return mute !== null;
+		})();
+	}
+
+	if (opts.detail) {
+		if (_user.pinned_post_id) {
+			// Populate pinned post
+			_user.pinned_post = packPost(_user.pinned_post_id, meId, {
+				detail: true
+			});
+		}
+
+		if (meId && !meId.equals(_user.id)) {
+			const myFollowingIds = await getFriends(meId);
+
+			// Get following you know count
+			_user.following_you_know_count = Following.count({
+				followee_id: { $in: myFollowingIds },
+				follower_id: _user.id,
+				deleted_at: { $exists: false }
+			});
+
+			// Get followers you know count
+			_user.followers_you_know_count = Following.count({
+				followee_id: _user.id,
+				follower_id: { $in: myFollowingIds },
+				deleted_at: { $exists: false }
+			});
+		}
+	}
+
+	// resolve promises in _user object
+	_user = await rap(_user);
+
+	resolve(_user);
+});
+
+/*
+function img(url) {
+	return {
+		thumbnail: {
+			large: `${url}`,
+			medium: '',
+			small: ''
+		}
+	};
+}
+*/
diff --git a/src/api/serializers/app.ts b/src/api/serializers/app.ts
deleted file mode 100644
index 9d1c46dca..000000000
--- a/src/api/serializers/app.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import deepcopy = require('deepcopy');
-import App from '../models/app';
-import AccessToken from '../models/access-token';
-import config from '../../conf';
-
-/**
- * Serialize an app
- *
- * @param {any} app
- * @param {any} me?
- * @param {any} options?
- * @return {Promise<any>}
- */
-export default (
-	app: any,
-	me?: any,
-	options?: {
-		includeSecret?: boolean,
-		includeProfileImageIds?: boolean
-	}
-) => new Promise<any>(async (resolve, reject) => {
-	const opts = options || {
-		includeSecret: false,
-		includeProfileImageIds: false
-	};
-
-	let _app: any;
-
-	// Populate the app if 'app' is ID
-	if (mongo.ObjectID.prototype.isPrototypeOf(app)) {
-		_app = await App.findOne({
-			_id: app
-		});
-	} else if (typeof app === 'string') {
-		_app = await App.findOne({
-			_id: new mongo.ObjectID(app)
-		});
-	} else {
-		_app = deepcopy(app);
-	}
-
-	// Me
-	if (me && !mongo.ObjectID.prototype.isPrototypeOf(me)) {
-		if (typeof me === 'string') {
-			me = new mongo.ObjectID(me);
-		} else {
-			me = me._id;
-		}
-	}
-
-	// Rename _id to id
-	_app.id = _app._id;
-	delete _app._id;
-
-	delete _app.name_id_lower;
-
-	// Visible by only owner
-	if (!opts.includeSecret) {
-		delete _app.secret;
-	}
-
-	_app.icon_url = _app.icon != null
-		? `${config.drive_url}/${_app.icon}`
-		: `${config.drive_url}/app-default.jpg`;
-
-	if (me) {
-		// 既に連携しているか
-		const exist = await AccessToken.count({
-			app_id: _app.id,
-			user_id: me,
-		}, {
-				limit: 1
-			});
-
-		_app.is_authorized = exist === 1;
-	}
-
-	resolve(_app);
-});
diff --git a/src/api/serializers/auth-session.ts b/src/api/serializers/auth-session.ts
deleted file mode 100644
index a9acf1243..000000000
--- a/src/api/serializers/auth-session.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import deepcopy = require('deepcopy');
-import serializeApp from './app';
-
-/**
- * Serialize an auth session
- *
- * @param {any} session
- * @param {any} me?
- * @return {Promise<any>}
- */
-export default (
-	session: any,
-	me?: any
-) => new Promise<any>(async (resolve, reject) => {
-	let _session: any;
-
-	// TODO: Populate session if it ID
-
-	_session = deepcopy(session);
-
-	// Me
-	if (me && !mongo.ObjectID.prototype.isPrototypeOf(me)) {
-		if (typeof me === 'string') {
-			me = new mongo.ObjectID(me);
-		} else {
-			me = me._id;
-		}
-	}
-
-	delete _session._id;
-
-	// Populate app
-	_session.app = await serializeApp(_session.app_id, me);
-
-	resolve(_session);
-});
diff --git a/src/api/serializers/channel.ts b/src/api/serializers/channel.ts
deleted file mode 100644
index 3cba39aa1..000000000
--- a/src/api/serializers/channel.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import deepcopy = require('deepcopy');
-import { IUser } from '../models/user';
-import { default as Channel, IChannel } from '../models/channel';
-import Watching from '../models/channel-watching';
-
-/**
- * Serialize a channel
- *
- * @param channel target
- * @param me? serializee
- * @return response
- */
-export default (
-	channel: string | mongo.ObjectID | IChannel,
-	me?: string | mongo.ObjectID | IUser
-) => new Promise<any>(async (resolve, reject) => {
-
-	let _channel: any;
-
-	// Populate the channel if 'channel' is ID
-	if (mongo.ObjectID.prototype.isPrototypeOf(channel)) {
-		_channel = await Channel.findOne({
-			_id: channel
-		});
-	} else if (typeof channel === 'string') {
-		_channel = await Channel.findOne({
-			_id: new mongo.ObjectID(channel)
-		});
-	} else {
-		_channel = deepcopy(channel);
-	}
-
-	// Rename _id to id
-	_channel.id = _channel._id;
-	delete _channel._id;
-
-	// Remove needless properties
-	delete _channel.user_id;
-
-	// Me
-	const meId: mongo.ObjectID = me
-	? mongo.ObjectID.prototype.isPrototypeOf(me)
-		? me as mongo.ObjectID
-		: typeof me === 'string'
-			? new mongo.ObjectID(me)
-			: (me as IUser)._id
-	: null;
-
-	if (me) {
-		//#region Watchしているかどうか
-		const watch = await Watching.findOne({
-			user_id: meId,
-			channel_id: _channel.id,
-			deleted_at: { $exists: false }
-		});
-
-		_channel.is_watching = watch !== null;
-		//#endregion
-	}
-
-	resolve(_channel);
-});
diff --git a/src/api/serializers/drive-file.ts b/src/api/serializers/drive-file.ts
deleted file mode 100644
index 003e09ee7..000000000
--- a/src/api/serializers/drive-file.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import DriveFile from '../models/drive-file';
-import serializeDriveFolder from './drive-folder';
-import serializeDriveTag from './drive-tag';
-import deepcopy = require('deepcopy');
-import config from '../../conf';
-
-/**
- * Serialize a drive file
- *
- * @param {any} file
- * @param {any} options?
- * @return {Promise<any>}
- */
-export default (
-	file: any,
-	options?: {
-		detail: boolean
-	}
-) => new Promise<any>(async (resolve, reject) => {
-	const opts = Object.assign({
-		detail: false
-	}, options);
-
-	let _file: any;
-
-	// Populate the file if 'file' is ID
-	if (mongo.ObjectID.prototype.isPrototypeOf(file)) {
-		_file = await DriveFile.findOne({
-			_id: file
-		});
-	} else if (typeof file === 'string') {
-		_file = await DriveFile.findOne({
-			_id: new mongo.ObjectID(file)
-		});
-	} else {
-		_file = deepcopy(file);
-	}
-
-	if (!_file) return reject('invalid file arg.');
-
-	// rendered target
-	let _target: any = {};
-
-	_target.id = _file._id;
-	_target.created_at = _file.uploadDate;
-	_target.name = _file.filename;
-	_target.type = _file.contentType;
-	_target.datasize = _file.length;
-	_target.md5 = _file.md5;
-
-	_target = Object.assign(_target, _file.metadata);
-
-	_target.url = `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`;
-
-	if (_target.properties == null) _target.properties = {};
-
-	if (opts.detail) {
-		if (_target.folder_id) {
-			// Populate folder
-			_target.folder = await serializeDriveFolder(_target.folder_id, {
-				detail: true
-			});
-		}
-
-		if (_target.tags) {
-			// Populate tags
-			_target.tags = await _target.tags.map(async (tag: any) =>
-				await serializeDriveTag(tag)
-			);
-		}
-	}
-
-	resolve(_target);
-});
diff --git a/src/api/serializers/drive-folder.ts b/src/api/serializers/drive-folder.ts
deleted file mode 100644
index 6ebf454a2..000000000
--- a/src/api/serializers/drive-folder.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import DriveFolder from '../models/drive-folder';
-import DriveFile from '../models/drive-file';
-import deepcopy = require('deepcopy');
-
-/**
- * Serialize a drive folder
- *
- * @param {any} folder
- * @param {any} options?
- * @return {Promise<any>}
- */
-const self = (
-	folder: any,
-	options?: {
-		detail: boolean
-	}
-) => new Promise<any>(async (resolve, reject) => {
-	const opts = Object.assign({
-		detail: false
-	}, options);
-
-	let _folder: any;
-
-	// Populate the folder if 'folder' is ID
-	if (mongo.ObjectID.prototype.isPrototypeOf(folder)) {
-		_folder = await DriveFolder.findOne({ _id: folder });
-	} else if (typeof folder === 'string') {
-		_folder = await DriveFolder.findOne({ _id: new mongo.ObjectID(folder) });
-	} else {
-		_folder = deepcopy(folder);
-	}
-
-	// Rename _id to id
-	_folder.id = _folder._id;
-	delete _folder._id;
-
-	if (opts.detail) {
-		const childFoldersCount = await DriveFolder.count({
-			parent_id: _folder.id
-		});
-
-		const childFilesCount = await DriveFile.count({
-			'metadata.folder_id': _folder.id
-		});
-
-		_folder.folders_count = childFoldersCount;
-		_folder.files_count = childFilesCount;
-	}
-
-	if (opts.detail && _folder.parent_id) {
-		// Populate parent folder
-		_folder.parent = await self(_folder.parent_id, {
-			detail: true
-		});
-	}
-
-	resolve(_folder);
-});
-
-export default self;
diff --git a/src/api/serializers/drive-tag.ts b/src/api/serializers/drive-tag.ts
deleted file mode 100644
index 2f152381b..000000000
--- a/src/api/serializers/drive-tag.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import DriveTag from '../models/drive-tag';
-import deepcopy = require('deepcopy');
-
-/**
- * Serialize a drive tag
- *
- * @param {any} tag
- * @return {Promise<any>}
- */
-const self = (
-	tag: any
-) => new Promise<any>(async (resolve, reject) => {
-	let _tag: any;
-
-	// Populate the tag if 'tag' is ID
-	if (mongo.ObjectID.prototype.isPrototypeOf(tag)) {
-		_tag = await DriveTag.findOne({ _id: tag });
-	} else if (typeof tag === 'string') {
-		_tag = await DriveTag.findOne({ _id: new mongo.ObjectID(tag) });
-	} else {
-		_tag = deepcopy(tag);
-	}
-
-	// Rename _id to id
-	_tag.id = _tag._id;
-	delete _tag._id;
-
-	resolve(_tag);
-});
-
-export default self;
diff --git a/src/api/serializers/messaging-message.ts b/src/api/serializers/messaging-message.ts
deleted file mode 100644
index 4ab95e42a..000000000
--- a/src/api/serializers/messaging-message.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import deepcopy = require('deepcopy');
-import Message from '../models/messaging-message';
-import serializeUser from './user';
-import serializeDriveFile from './drive-file';
-import parse from '../common/text';
-
-/**
- * Serialize a message
- *
- * @param {any} message
- * @param {any} me?
- * @param {any} options?
- * @return {Promise<any>}
- */
-export default (
-	message: any,
-	me?: any,
-	options?: {
-		populateRecipient: boolean
-	}
-) => new Promise<any>(async (resolve, reject) => {
-	const opts = options || {
-		populateRecipient: true
-	};
-
-	let _message: any;
-
-	// Populate the message if 'message' is ID
-	if (mongo.ObjectID.prototype.isPrototypeOf(message)) {
-		_message = await Message.findOne({
-			_id: message
-		});
-	} else if (typeof message === 'string') {
-		_message = await Message.findOne({
-			_id: new mongo.ObjectID(message)
-		});
-	} else {
-		_message = deepcopy(message);
-	}
-
-	// Rename _id to id
-	_message.id = _message._id;
-	delete _message._id;
-
-	// Parse text
-	if (_message.text) {
-		_message.ast = parse(_message.text);
-	}
-
-	// Populate user
-	_message.user = await serializeUser(_message.user_id, me);
-
-	if (_message.file) {
-		// Populate file
-		_message.file = await serializeDriveFile(_message.file_id);
-	}
-
-	if (opts.populateRecipient) {
-		// Populate recipient
-		_message.recipient = await serializeUser(_message.recipient_id, me);
-	}
-
-	resolve(_message);
-});
diff --git a/src/api/serializers/notification.ts b/src/api/serializers/notification.ts
deleted file mode 100644
index ac919dc8b..000000000
--- a/src/api/serializers/notification.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import Notification from '../models/notification';
-import serializeUser from './user';
-import serializePost from './post';
-import deepcopy = require('deepcopy');
-
-/**
- * Serialize a notification
- *
- * @param {any} notification
- * @return {Promise<any>}
- */
-export default (notification: any) => new Promise<any>(async (resolve, reject) => {
-	let _notification: any;
-
-	// Populate the notification if 'notification' is ID
-	if (mongo.ObjectID.prototype.isPrototypeOf(notification)) {
-		_notification = await Notification.findOne({
-			_id: notification
-		});
-	} else if (typeof notification === 'string') {
-		_notification = await Notification.findOne({
-			_id: new mongo.ObjectID(notification)
-		});
-	} else {
-		_notification = deepcopy(notification);
-	}
-
-	// Rename _id to id
-	_notification.id = _notification._id;
-	delete _notification._id;
-
-	// Rename notifier_id to user_id
-	_notification.user_id = _notification.notifier_id;
-	delete _notification.notifier_id;
-
-	const me = _notification.notifiee_id;
-	delete _notification.notifiee_id;
-
-	// Populate notifier
-	_notification.user = await serializeUser(_notification.user_id, me);
-
-	switch (_notification.type) {
-		case 'follow':
-			// nope
-			break;
-		case 'mention':
-		case 'reply':
-		case 'repost':
-		case 'quote':
-		case 'reaction':
-		case 'poll_vote':
-			// Populate post
-			_notification.post = await serializePost(_notification.post_id, me);
-			break;
-		default:
-			console.error(`Unknown type: ${_notification.type}`);
-			break;
-	}
-
-	resolve(_notification);
-});
diff --git a/src/api/serializers/post-reaction.ts b/src/api/serializers/post-reaction.ts
deleted file mode 100644
index b8807a741..000000000
--- a/src/api/serializers/post-reaction.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import deepcopy = require('deepcopy');
-import Reaction from '../models/post-reaction';
-import serializeUser from './user';
-
-/**
- * Serialize a reaction
- *
- * @param {any} reaction
- * @param {any} me?
- * @return {Promise<any>}
- */
-export default (
-	reaction: any,
-	me?: any
-) => new Promise<any>(async (resolve, reject) => {
-	let _reaction: any;
-
-	// Populate the reaction if 'reaction' is ID
-	if (mongo.ObjectID.prototype.isPrototypeOf(reaction)) {
-		_reaction = await Reaction.findOne({
-			_id: reaction
-		});
-	} else if (typeof reaction === 'string') {
-		_reaction = await Reaction.findOne({
-			_id: new mongo.ObjectID(reaction)
-		});
-	} else {
-		_reaction = deepcopy(reaction);
-	}
-
-	// Rename _id to id
-	_reaction.id = _reaction._id;
-	delete _reaction._id;
-
-	// Populate user
-	_reaction.user = await serializeUser(_reaction.user_id, me);
-
-	resolve(_reaction);
-});
diff --git a/src/api/serializers/post.ts b/src/api/serializers/post.ts
deleted file mode 100644
index 03fd12077..000000000
--- a/src/api/serializers/post.ts
+++ /dev/null
@@ -1,192 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import deepcopy = require('deepcopy');
-import { default as Post, IPost } from '../models/post';
-import Reaction from '../models/post-reaction';
-import { IUser } from '../models/user';
-import Vote from '../models/poll-vote';
-import serializeApp from './app';
-import serializeChannel from './channel';
-import serializeUser from './user';
-import serializeDriveFile from './drive-file';
-import parse from '../common/text';
-import rap from '@prezzemolo/rap';
-
-/**
- * Serialize a post
- *
- * @param post target
- * @param me? serializee
- * @param options? serialize options
- * @return response
- */
-const self = async (
-	post: string | mongo.ObjectID | IPost,
-	me?: string | mongo.ObjectID | IUser,
-	options?: {
-		detail: boolean
-	}
-) => {
-	const opts = options || {
-		detail: true,
-	};
-
-	// Me
-	const meId: mongo.ObjectID = me
-		? mongo.ObjectID.prototype.isPrototypeOf(me)
-			? me as mongo.ObjectID
-			: typeof me === 'string'
-				? new mongo.ObjectID(me)
-				: (me as IUser)._id
-		: null;
-
-	let _post: any;
-
-	// Populate the post if 'post' is ID
-	if (mongo.ObjectID.prototype.isPrototypeOf(post)) {
-		_post = await Post.findOne({
-			_id: post
-		});
-	} else if (typeof post === 'string') {
-		_post = await Post.findOne({
-			_id: new mongo.ObjectID(post)
-		});
-	} else {
-		_post = deepcopy(post);
-	}
-
-	if (!_post) throw 'invalid post arg.';
-
-	const id = _post._id;
-
-	// Rename _id to id
-	_post.id = _post._id;
-	delete _post._id;
-
-	delete _post.mentions;
-
-	// Parse text
-	if (_post.text) {
-		_post.ast = parse(_post.text);
-	}
-
-	// Populate user
-	_post.user = serializeUser(_post.user_id, meId);
-
-	// Populate app
-	if (_post.app_id) {
-		_post.app = serializeApp(_post.app_id);
-	}
-
-	// Populate channel
-	if (_post.channel_id) {
-		_post.channel = serializeChannel(_post.channel_id);
-	}
-
-	// Populate media
-	if (_post.media_ids) {
-		_post.media = Promise.all(_post.media_ids.map(fileId =>
-			serializeDriveFile(fileId)
-		));
-	}
-
-	// When requested a detailed post data
-	if (opts.detail) {
-		// Get previous post info
-		_post.prev = (async () => {
-			const prev = await Post.findOne({
-				user_id: _post.user_id,
-				_id: {
-					$lt: id
-				}
-			}, {
-				fields: {
-					_id: true
-				},
-				sort: {
-					_id: -1
-				}
-			});
-			return prev ? prev._id : null;
-		})();
-
-		// Get next post info
-		_post.next = (async () => {
-			const next = await Post.findOne({
-				user_id: _post.user_id,
-				_id: {
-					$gt: id
-				}
-			}, {
-				fields: {
-					_id: true
-				},
-				sort: {
-					_id: 1
-				}
-			});
-			return next ? next._id : null;
-		})();
-
-		if (_post.reply_id) {
-			// Populate reply to post
-			_post.reply = self(_post.reply_id, meId, {
-				detail: false
-			});
-		}
-
-		if (_post.repost_id) {
-			// Populate repost
-			_post.repost = self(_post.repost_id, meId, {
-				detail: _post.text == null
-			});
-		}
-
-		// Poll
-		if (meId && _post.poll) {
-			_post.poll = (async (poll) => {
-				const vote = await Vote
-					.findOne({
-						user_id: meId,
-						post_id: id
-					});
-
-				if (vote != null) {
-					const myChoice = poll.choices
-						.filter(c => c.id == vote.choice)[0];
-
-					myChoice.is_voted = true;
-				}
-
-				return poll;
-			})(_post.poll);
-		}
-
-		// Fetch my reaction
-		if (meId) {
-			_post.my_reaction = (async () => {
-				const reaction = await Reaction
-					.findOne({
-						user_id: meId,
-						post_id: id,
-						deleted_at: { $exists: false }
-					});
-
-				if (reaction) {
-					return reaction.reaction;
-				}
-
-				return null;
-			})();
-		}
-	}
-
-	// resolve promises in _post object
-	_post = await rap(_post);
-
-	return _post;
-};
-
-export default self;
diff --git a/src/api/serializers/signin.ts b/src/api/serializers/signin.ts
deleted file mode 100644
index 406806767..000000000
--- a/src/api/serializers/signin.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Module dependencies
- */
-import deepcopy = require('deepcopy');
-
-/**
- * Serialize a signin record
- *
- * @param {any} record
- * @return {Promise<any>}
- */
-export default (
-	record: any
-) => new Promise<any>(async (resolve, reject) => {
-
-	const _record = deepcopy(record);
-
-	// Rename _id to id
-	_record.id = _record._id;
-	delete _record._id;
-
-	resolve(_record);
-});
diff --git a/src/api/serializers/user.ts b/src/api/serializers/user.ts
deleted file mode 100644
index ac157097a..000000000
--- a/src/api/serializers/user.ts
+++ /dev/null
@@ -1,190 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import deepcopy = require('deepcopy');
-import { default as User, IUser } from '../models/user';
-import serializePost from './post';
-import Following from '../models/following';
-import Mute from '../models/mute';
-import getFriends from '../common/get-friends';
-import config from '../../conf';
-import rap from '@prezzemolo/rap';
-
-/**
- * Serialize a user
- *
- * @param user target
- * @param me? serializee
- * @param options? serialize options
- * @return response
- */
-export default (
-	user: string | mongo.ObjectID | IUser,
-	me?: string | mongo.ObjectID | IUser,
-	options?: {
-		detail?: boolean,
-		includeSecrets?: boolean
-	}
-) => new Promise<any>(async (resolve, reject) => {
-
-	const opts = Object.assign({
-		detail: false,
-		includeSecrets: false
-	}, options);
-
-	let _user: any;
-
-	const fields = opts.detail ? {
-		settings: false
-	} : {
-		settings: false,
-		client_settings: false,
-		profile: false,
-		keywords: false,
-		domains: false
-	};
-
-	// Populate the user if 'user' is ID
-	if (mongo.ObjectID.prototype.isPrototypeOf(user)) {
-		_user = await User.findOne({
-			_id: user
-		}, { fields });
-	} else if (typeof user === 'string') {
-		_user = await User.findOne({
-			_id: new mongo.ObjectID(user)
-		}, { fields });
-	} else {
-		_user = deepcopy(user);
-	}
-
-	if (!_user) return reject('invalid user arg.');
-
-	// Me
-	const meId: mongo.ObjectID = me
-		? mongo.ObjectID.prototype.isPrototypeOf(me)
-			? me as mongo.ObjectID
-			: typeof me === 'string'
-				? new mongo.ObjectID(me)
-				: (me as IUser)._id
-		: null;
-
-	// Rename _id to id
-	_user.id = _user._id;
-	delete _user._id;
-
-	// Remove needless properties
-	delete _user.latest_post;
-
-	// Remove private properties
-	delete _user.password;
-	delete _user.token;
-	delete _user.two_factor_temp_secret;
-	delete _user.two_factor_secret;
-	delete _user.username_lower;
-	if (_user.twitter) {
-		delete _user.twitter.access_token;
-		delete _user.twitter.access_token_secret;
-	}
-	delete _user.line;
-
-	// Visible via only the official client
-	if (!opts.includeSecrets) {
-		delete _user.email;
-		delete _user.client_settings;
-	}
-
-	if (!opts.detail) {
-		delete _user.two_factor_enabled;
-	}
-
-	_user.avatar_url = _user.avatar_id != null
-		? `${config.drive_url}/${_user.avatar_id}`
-		: `${config.drive_url}/default-avatar.jpg`;
-
-	_user.banner_url = _user.banner_id != null
-		? `${config.drive_url}/${_user.banner_id}`
-		: null;
-
-	if (!meId || !meId.equals(_user.id) || !opts.detail) {
-		delete _user.avatar_id;
-		delete _user.banner_id;
-
-		delete _user.drive_capacity;
-	}
-
-	if (meId && !meId.equals(_user.id)) {
-		// Whether the user is following
-		_user.is_following = (async () => {
-			const follow = await Following.findOne({
-				follower_id: meId,
-				followee_id: _user.id,
-				deleted_at: { $exists: false }
-			});
-			return follow !== null;
-		})();
-
-		// Whether the user is followed
-		_user.is_followed = (async () => {
-			const follow2 = await Following.findOne({
-				follower_id: _user.id,
-				followee_id: meId,
-				deleted_at: { $exists: false }
-			});
-			return follow2 !== null;
-		})();
-
-		// Whether the user is muted
-		_user.is_muted = (async () => {
-			const mute = await Mute.findOne({
-				muter_id: meId,
-				mutee_id: _user.id,
-				deleted_at: { $exists: false }
-			});
-			return mute !== null;
-		})();
-	}
-
-	if (opts.detail) {
-		if (_user.pinned_post_id) {
-			// Populate pinned post
-			_user.pinned_post = serializePost(_user.pinned_post_id, meId, {
-				detail: true
-			});
-		}
-
-		if (meId && !meId.equals(_user.id)) {
-			const myFollowingIds = await getFriends(meId);
-
-			// Get following you know count
-			_user.following_you_know_count = Following.count({
-				followee_id: { $in: myFollowingIds },
-				follower_id: _user.id,
-				deleted_at: { $exists: false }
-			});
-
-			// Get followers you know count
-			_user.followers_you_know_count = Following.count({
-				followee_id: _user.id,
-				follower_id: { $in: myFollowingIds },
-				deleted_at: { $exists: false }
-			});
-		}
-	}
-
-	// resolve promises in _user object
-	_user = await rap(_user);
-
-	resolve(_user);
-});
-/*
-function img(url) {
-	return {
-		thumbnail: {
-			large: `${url}`,
-			medium: '',
-			small: ''
-		}
-	};
-}
-*/

From 8462a7493802b711da7e4cb58b3bf055937b651c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 2 Feb 2018 08:21:30 +0900
Subject: [PATCH 0146/1250] wip

---
 src/api/common/add-file-to-drive.ts                    |  4 ++--
 src/api/common/notify.ts                               |  6 +++---
 src/api/endpoints/app/create.ts                        |  4 ++--
 src/api/endpoints/app/show.ts                          |  4 ++--
 src/api/endpoints/auth/session/show.ts                 |  4 ++--
 src/api/endpoints/auth/session/userkey.ts              |  4 ++--
 src/api/endpoints/channels.ts                          |  4 ++--
 src/api/endpoints/channels/create.ts                   |  4 ++--
 src/api/endpoints/channels/posts.ts                    |  4 ++--
 src/api/endpoints/channels/show.ts                     |  4 ++--
 src/api/endpoints/drive/files.ts                       |  4 ++--
 src/api/endpoints/drive/files/create.ts                |  4 ++--
 src/api/endpoints/drive/files/find.ts                  |  4 ++--
 src/api/endpoints/drive/files/show.ts                  |  4 ++--
 src/api/endpoints/drive/files/update.ts                |  4 ++--
 src/api/endpoints/drive/files/upload_from_url.ts       |  4 ++--
 src/api/endpoints/drive/folders.ts                     |  4 ++--
 src/api/endpoints/drive/folders/create.ts              |  4 ++--
 src/api/endpoints/drive/folders/find.ts                |  4 ++--
 src/api/endpoints/drive/folders/show.ts                |  4 ++--
 src/api/endpoints/drive/folders/update.ts              |  4 ++--
 src/api/endpoints/drive/stream.ts                      |  4 ++--
 src/api/endpoints/i.ts                                 |  4 ++--
 src/api/endpoints/i/authorized_apps.ts                 |  4 ++--
 src/api/endpoints/i/favorites.ts                       |  4 ++--
 src/api/endpoints/i/notifications.ts                   |  4 ++--
 src/api/endpoints/i/pin.ts                             |  4 ++--
 src/api/endpoints/i/signin_history.ts                  |  4 ++--
 src/api/endpoints/i/update.ts                          |  4 ++--
 src/api/endpoints/messaging/history.ts                 |  4 ++--
 src/api/endpoints/messaging/messages.ts                |  4 ++--
 src/api/endpoints/messaging/messages/create.ts         |  4 ++--
 src/api/endpoints/mute/list.ts                         |  4 ++--
 src/api/endpoints/my/apps.ts                           |  4 ++--
 src/api/endpoints/posts.ts                             |  4 ++--
 src/api/endpoints/posts/context.ts                     |  4 ++--
 src/api/endpoints/posts/create.ts                      |  4 ++--
 src/api/endpoints/posts/mentions.ts                    |  4 ++--
 src/api/endpoints/posts/polls/recommendation.ts        |  4 ++--
 src/api/endpoints/posts/reactions.ts                   |  4 ++--
 src/api/endpoints/posts/replies.ts                     |  4 ++--
 src/api/endpoints/posts/reposts.ts                     |  4 ++--
 src/api/endpoints/posts/search.ts                      |  4 ++--
 src/api/endpoints/posts/show.ts                        |  4 ++--
 src/api/endpoints/posts/timeline.ts                    |  4 ++--
 src/api/endpoints/posts/trend.ts                       |  4 ++--
 src/api/endpoints/users.ts                             |  4 ++--
 src/api/endpoints/users/followers.ts                   |  4 ++--
 src/api/endpoints/users/following.ts                   |  4 ++--
 .../endpoints/users/get_frequently_replied_users.ts    |  4 ++--
 src/api/endpoints/users/posts.ts                       |  4 ++--
 src/api/endpoints/users/recommendation.ts              |  4 ++--
 src/api/endpoints/users/search.ts                      |  4 ++--
 src/api/endpoints/users/search_by_username.ts          |  4 ++--
 src/api/endpoints/users/show.ts                        |  4 ++--
 src/api/models/drive-file.ts                           | 10 ++++++++--
 src/api/models/drive-folder.ts                         |  2 ++
 src/api/models/messaging-message.ts                    |  5 +++++
 src/api/private/signin.ts                              |  4 ++--
 src/api/private/signup.ts                              |  4 ++--
 src/api/service/twitter.ts                             |  4 ++--
 61 files changed, 132 insertions(+), 119 deletions(-)

diff --git a/src/api/common/add-file-to-drive.ts b/src/api/common/add-file-to-drive.ts
index 23cbc44e6..1ee455c09 100644
--- a/src/api/common/add-file-to-drive.ts
+++ b/src/api/common/add-file-to-drive.ts
@@ -12,7 +12,7 @@ import prominence = require('prominence');
 
 import DriveFile, { getGridFSBucket } from '../models/drive-file';
 import DriveFolder from '../models/drive-folder';
-import serialize from '../serializers/drive-file';
+import { pack } from '../models/drive-file';
 import event, { publishDriveStream } from '../event';
 import config from '../../conf';
 
@@ -282,7 +282,7 @@ export default (user: any, file: string | stream.Readable, ...args) => new Promi
 		log(`drive file has been created ${file._id}`);
 		resolve(file);
 
-		serialize(file).then(serializedFile => {
+		pack(file).then(serializedFile => {
 			// Publish drive_file_created event
 			event(user._id, 'drive_file_created', serializedFile);
 			publishDriveStream(user._id, 'file_created', serializedFile);
diff --git a/src/api/common/notify.ts b/src/api/common/notify.ts
index 2b79416a3..ae5669b84 100644
--- a/src/api/common/notify.ts
+++ b/src/api/common/notify.ts
@@ -2,7 +2,7 @@ import * as mongo from 'mongodb';
 import Notification from '../models/notification';
 import Mute from '../models/mute';
 import event from '../event';
-import serialize from '../serializers/notification';
+import { pack } from '../models/notification';
 
 export default (
 	notifiee: mongo.ObjectID,
@@ -27,7 +27,7 @@ export default (
 
 	// Publish notification event
 	event(notifiee, 'notification',
-		await serialize(notification));
+		await pack(notification));
 
 	// 3秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
 	setTimeout(async () => {
@@ -44,7 +44,7 @@ export default (
 			}
 			//#endregion
 
-			event(notifiee, 'unread_notification', await serialize(notification));
+			event(notifiee, 'unread_notification', await pack(notification));
 		}
 	}, 3000);
 });
diff --git a/src/api/endpoints/app/create.ts b/src/api/endpoints/app/create.ts
index ca684de02..320163ebd 100644
--- a/src/api/endpoints/app/create.ts
+++ b/src/api/endpoints/app/create.ts
@@ -5,7 +5,7 @@ import rndstr from 'rndstr';
 import $ from 'cafy';
 import App from '../../models/app';
 import { isValidNameId } from '../../models/app';
-import serialize from '../../serializers/app';
+import { pack } from '../../models/app';
 
 /**
  * @swagger
@@ -106,5 +106,5 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	});
 
 	// Response
-	res(await serialize(app));
+	res(await pack(app));
 });
diff --git a/src/api/endpoints/app/show.ts b/src/api/endpoints/app/show.ts
index 054aab859..a3ef24717 100644
--- a/src/api/endpoints/app/show.ts
+++ b/src/api/endpoints/app/show.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import App from '../../models/app';
-import serialize from '../../serializers/app';
+import { pack } from '../../models/app';
 
 /**
  * @swagger
@@ -67,7 +67,7 @@ module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) =>
 	}
 
 	// Send response
-	res(await serialize(app, user, {
+	res(await pack(app, user, {
 		includeSecret: isSecure && app.user_id.equals(user._id)
 	}));
 });
diff --git a/src/api/endpoints/auth/session/show.ts b/src/api/endpoints/auth/session/show.ts
index ede8a6763..1fe3b873f 100644
--- a/src/api/endpoints/auth/session/show.ts
+++ b/src/api/endpoints/auth/session/show.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import AuthSess from '../../../models/auth-session';
-import serialize from '../../../serializers/auth-session';
+import { pack } from '../../../models/auth-session';
 
 /**
  * @swagger
@@ -67,5 +67,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}
 
 	// Response
-	res(await serialize(session, user));
+	res(await pack(session, user));
 });
diff --git a/src/api/endpoints/auth/session/userkey.ts b/src/api/endpoints/auth/session/userkey.ts
index afd3250b0..fc989bf8c 100644
--- a/src/api/endpoints/auth/session/userkey.ts
+++ b/src/api/endpoints/auth/session/userkey.ts
@@ -5,7 +5,7 @@ import $ from 'cafy';
 import App from '../../../models/app';
 import AuthSess from '../../../models/auth-session';
 import AccessToken from '../../../models/access-token';
-import serialize from '../../../serializers/user';
+import { pack } from '../../../models/user';
 
 /**
  * @swagger
@@ -102,7 +102,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
 	// Response
 	res({
 		access_token: accessToken.token,
-		user: await serialize(session.user_id, null, {
+		user: await pack(session.user_id, null, {
 			detail: true
 		})
 	});
diff --git a/src/api/endpoints/channels.ts b/src/api/endpoints/channels.ts
index 14817d9bd..92dcee83d 100644
--- a/src/api/endpoints/channels.ts
+++ b/src/api/endpoints/channels.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import Channel from '../models/channel';
-import serialize from '../serializers/channel';
+import { pack } from '../models/channel';
 
 /**
  * Get all channels
@@ -55,5 +55,5 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(channels.map(async channel =>
-		await serialize(channel, me))));
+		await pack(channel, me))));
 });
diff --git a/src/api/endpoints/channels/create.ts b/src/api/endpoints/channels/create.ts
index a8d7c29dc..695b4515b 100644
--- a/src/api/endpoints/channels/create.ts
+++ b/src/api/endpoints/channels/create.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import Channel from '../../models/channel';
 import Watching from '../../models/channel-watching';
-import serialize from '../../serializers/channel';
+import { pack } from '../../models/channel';
 
 /**
  * Create a channel
@@ -28,7 +28,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	});
 
 	// Response
-	res(await serialize(channel));
+	res(await pack(channel));
 
 	// Create Watching
 	await Watching.insert({
diff --git a/src/api/endpoints/channels/posts.ts b/src/api/endpoints/channels/posts.ts
index 9c2d607ed..3feee51f7 100644
--- a/src/api/endpoints/channels/posts.ts
+++ b/src/api/endpoints/channels/posts.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import { default as Channel, IChannel } from '../../models/channel';
 import Post from '../../models/post';
-import serialize from '../../serializers/post';
+import { pack } from '../../models/post';
 
 /**
  * Show a posts of a channel
@@ -74,6 +74,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(posts.map(async (post) =>
-		await serialize(post, user)
+		await pack(post, user)
 	)));
 });
diff --git a/src/api/endpoints/channels/show.ts b/src/api/endpoints/channels/show.ts
index 8861e5459..89c48379a 100644
--- a/src/api/endpoints/channels/show.ts
+++ b/src/api/endpoints/channels/show.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import { default as Channel, IChannel } from '../../models/channel';
-import serialize from '../../serializers/channel';
+import { pack } from '../../models/channel';
 
 /**
  * Show a channel
@@ -27,5 +27,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}
 
 	// Serialize
-	res(await serialize(channel, user));
+	res(await pack(channel, user));
 });
diff --git a/src/api/endpoints/drive/files.ts b/src/api/endpoints/drive/files.ts
index 3d5f81339..3bd80e728 100644
--- a/src/api/endpoints/drive/files.ts
+++ b/src/api/endpoints/drive/files.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import DriveFile from '../../models/drive-file';
-import serialize from '../../serializers/drive-file';
+import { pack } from '../../models/drive-file';
 
 /**
  * Get drive files
@@ -69,6 +69,6 @@ module.exports = async (params, user, app) => {
 		});
 
 	// Serialize
-	const _files = await Promise.all(files.map(file => serialize(file)));
+	const _files = await Promise.all(files.map(file => pack(file)));
 	return _files;
 };
diff --git a/src/api/endpoints/drive/files/create.ts b/src/api/endpoints/drive/files/create.ts
index 437348a1e..6fa76d7e9 100644
--- a/src/api/endpoints/drive/files/create.ts
+++ b/src/api/endpoints/drive/files/create.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import { validateFileName } from '../../../models/drive-file';
-import serialize from '../../../serializers/drive-file';
+import { pack } from '../../../models/drive-file';
 import create from '../../../common/add-file-to-drive';
 
 /**
@@ -43,7 +43,7 @@ module.exports = async (file, params, user): Promise<any> => {
 		const driveFile = await create(user, file.path, name, null, folderId);
 
 		// Serialize
-		return serialize(driveFile);
+		return pack(driveFile);
 	} catch (e) {
 		console.error(e);
 
diff --git a/src/api/endpoints/drive/files/find.ts b/src/api/endpoints/drive/files/find.ts
index a1cdf1643..571aba81f 100644
--- a/src/api/endpoints/drive/files/find.ts
+++ b/src/api/endpoints/drive/files/find.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import DriveFile from '../../../models/drive-file';
-import serialize from '../../../serializers/drive-file';
+import { pack } from '../../../models/drive-file';
 
 /**
  * Find a file(s)
@@ -31,5 +31,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(files.map(async file =>
-		await serialize(file))));
+		await pack(file))));
 });
diff --git a/src/api/endpoints/drive/files/show.ts b/src/api/endpoints/drive/files/show.ts
index 3c7cf774f..00f69f141 100644
--- a/src/api/endpoints/drive/files/show.ts
+++ b/src/api/endpoints/drive/files/show.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import DriveFile from '../../../models/drive-file';
-import serialize from '../../../serializers/drive-file';
+import { pack } from '../../../models/drive-file';
 
 /**
  * Show a file
@@ -29,7 +29,7 @@ module.exports = async (params, user) => {
 	}
 
 	// Serialize
-	const _file = await serialize(file, {
+	const _file = await pack(file, {
 		detail: true
 	});
 
diff --git a/src/api/endpoints/drive/files/update.ts b/src/api/endpoints/drive/files/update.ts
index f39a420d6..9ef8215b1 100644
--- a/src/api/endpoints/drive/files/update.ts
+++ b/src/api/endpoints/drive/files/update.ts
@@ -5,7 +5,7 @@ import $ from 'cafy';
 import DriveFolder from '../../../models/drive-folder';
 import DriveFile from '../../../models/drive-file';
 import { validateFileName } from '../../../models/drive-file';
-import serialize from '../../../serializers/drive-file';
+import { pack } from '../../../models/drive-file';
 import { publishDriveStream } from '../../../event';
 
 /**
@@ -67,7 +67,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	});
 
 	// Serialize
-	const fileObj = await serialize(file);
+	const fileObj = await pack(file);
 
 	// Response
 	res(fileObj);
diff --git a/src/api/endpoints/drive/files/upload_from_url.ts b/src/api/endpoints/drive/files/upload_from_url.ts
index 519e0bdf6..f0398bfc5 100644
--- a/src/api/endpoints/drive/files/upload_from_url.ts
+++ b/src/api/endpoints/drive/files/upload_from_url.ts
@@ -4,7 +4,7 @@
 import * as URL from 'url';
 import $ from 'cafy';
 import { validateFileName } from '../../../models/drive-file';
-import serialize from '../../../serializers/drive-file';
+import { pack } from '../../../models/drive-file';
 import create from '../../../common/add-file-to-drive';
 import * as debug from 'debug';
 import * as tmp from 'tmp';
@@ -63,5 +63,5 @@ module.exports = async (params, user): Promise<any> => {
 		if (e) log(e.stack);
 	});
 
-	return serialize(driveFile);
+	return pack(driveFile);
 };
diff --git a/src/api/endpoints/drive/folders.ts b/src/api/endpoints/drive/folders.ts
index 7944e2c6a..e650fb74a 100644
--- a/src/api/endpoints/drive/folders.ts
+++ b/src/api/endpoints/drive/folders.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import DriveFolder from '../../models/drive-folder';
-import serialize from '../../serializers/drive-folder';
+import { pack } from '../../models/drive-folder';
 
 /**
  * Get drive folders
@@ -63,5 +63,5 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(folders.map(async folder =>
-		await serialize(folder))));
+		await pack(folder))));
 });
diff --git a/src/api/endpoints/drive/folders/create.ts b/src/api/endpoints/drive/folders/create.ts
index be847b215..1953c09ee 100644
--- a/src/api/endpoints/drive/folders/create.ts
+++ b/src/api/endpoints/drive/folders/create.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import DriveFolder from '../../../models/drive-folder';
 import { isValidFolderName } from '../../../models/drive-folder';
-import serialize from '../../../serializers/drive-folder';
+import { pack } from '../../../models/drive-folder';
 import { publishDriveStream } from '../../../event';
 
 /**
@@ -47,7 +47,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	});
 
 	// Serialize
-	const folderObj = await serialize(folder);
+	const folderObj = await pack(folder);
 
 	// Response
 	res(folderObj);
diff --git a/src/api/endpoints/drive/folders/find.ts b/src/api/endpoints/drive/folders/find.ts
index a5eb8e015..caad45d74 100644
--- a/src/api/endpoints/drive/folders/find.ts
+++ b/src/api/endpoints/drive/folders/find.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import DriveFolder from '../../../models/drive-folder';
-import serialize from '../../../serializers/drive-folder';
+import { pack } from '../../../models/drive-folder';
 
 /**
  * Find a folder(s)
@@ -30,5 +30,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		});
 
 	// Serialize
-	res(await Promise.all(folders.map(folder => serialize(folder))));
+	res(await Promise.all(folders.map(folder => pack(folder))));
 });
diff --git a/src/api/endpoints/drive/folders/show.ts b/src/api/endpoints/drive/folders/show.ts
index 9b1c04ca3..fd3061ca5 100644
--- a/src/api/endpoints/drive/folders/show.ts
+++ b/src/api/endpoints/drive/folders/show.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import DriveFolder from '../../../models/drive-folder';
-import serialize from '../../../serializers/drive-folder';
+import { pack } from '../../../models/drive-folder';
 
 /**
  * Show a folder
@@ -29,7 +29,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}
 
 	// Serialize
-	res(await serialize(folder, {
+	res(await pack(folder, {
 		detail: true
 	}));
 });
diff --git a/src/api/endpoints/drive/folders/update.ts b/src/api/endpoints/drive/folders/update.ts
index ff673402a..8f50a9d00 100644
--- a/src/api/endpoints/drive/folders/update.ts
+++ b/src/api/endpoints/drive/folders/update.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import DriveFolder from '../../../models/drive-folder';
 import { isValidFolderName } from '../../../models/drive-folder';
-import serialize from '../../../serializers/drive-folder';
+import { pack } from '../../../models/drive-folder';
 import { publishDriveStream } from '../../../event';
 
 /**
@@ -91,7 +91,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	});
 
 	// Serialize
-	const folderObj = await serialize(folder);
+	const folderObj = await pack(folder);
 
 	// Response
 	res(folderObj);
diff --git a/src/api/endpoints/drive/stream.ts b/src/api/endpoints/drive/stream.ts
index 5b0eb0a0d..3527d7050 100644
--- a/src/api/endpoints/drive/stream.ts
+++ b/src/api/endpoints/drive/stream.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import DriveFile from '../../models/drive-file';
-import serialize from '../../serializers/drive-file';
+import { pack } from '../../models/drive-file';
 
 /**
  * Get drive stream
@@ -64,5 +64,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(files.map(async file =>
-		await serialize(file))));
+		await pack(file))));
 });
diff --git a/src/api/endpoints/i.ts b/src/api/endpoints/i.ts
index ae75f11d5..1b6c1e58d 100644
--- a/src/api/endpoints/i.ts
+++ b/src/api/endpoints/i.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import User from '../models/user';
-import serialize from '../serializers/user';
+import { pack } from '../models/user';
 
 /**
  * Show myself
@@ -15,7 +15,7 @@ import serialize from '../serializers/user';
  */
 module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) => {
 	// Serialize
-	res(await serialize(user, user, {
+	res(await pack(user, user, {
 		detail: true,
 		includeSecrets: isSecure
 	}));
diff --git a/src/api/endpoints/i/authorized_apps.ts b/src/api/endpoints/i/authorized_apps.ts
index 807ca5b1e..40ce7a68c 100644
--- a/src/api/endpoints/i/authorized_apps.ts
+++ b/src/api/endpoints/i/authorized_apps.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import AccessToken from '../../models/access-token';
-import serialize from '../../serializers/app';
+import { pack } from '../../models/app';
 
 /**
  * Get authorized apps of my account
@@ -39,5 +39,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(tokens.map(async token =>
-		await serialize(token.app_id))));
+		await pack(token.app_id))));
 });
diff --git a/src/api/endpoints/i/favorites.ts b/src/api/endpoints/i/favorites.ts
index a66eaa754..eb464cf0f 100644
--- a/src/api/endpoints/i/favorites.ts
+++ b/src/api/endpoints/i/favorites.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import Favorite from '../../models/favorite';
-import serialize from '../../serializers/post';
+import { pack } from '../../models/post';
 
 /**
  * Get followers of a user
@@ -39,6 +39,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(favorites.map(async favorite =>
-		await serialize(favorite.post)
+		await pack(favorite.post)
 	)));
 });
diff --git a/src/api/endpoints/i/notifications.ts b/src/api/endpoints/i/notifications.ts
index fb9be7f61..688039a0d 100644
--- a/src/api/endpoints/i/notifications.ts
+++ b/src/api/endpoints/i/notifications.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import Notification from '../../models/notification';
 import Mute from '../../models/mute';
-import serialize from '../../serializers/notification';
+import { pack } from '../../models/notification';
 import getFriends from '../../common/get-friends';
 import read from '../../common/read-notification';
 
@@ -101,7 +101,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(notifications.map(async notification =>
-		await serialize(notification))));
+		await pack(notification))));
 
 	// Mark as read all
 	if (notifications.length > 0 && markAsRead) {
diff --git a/src/api/endpoints/i/pin.ts b/src/api/endpoints/i/pin.ts
index a94950d22..ff546fc2b 100644
--- a/src/api/endpoints/i/pin.ts
+++ b/src/api/endpoints/i/pin.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import User from '../../models/user';
 import Post from '../../models/post';
-import serialize from '../../serializers/user';
+import { pack } from '../../models/user';
 
 /**
  * Pin post
@@ -35,7 +35,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	});
 
 	// Serialize
-	const iObj = await serialize(user, user, {
+	const iObj = await pack(user, user, {
 		detail: true
 	});
 
diff --git a/src/api/endpoints/i/signin_history.ts b/src/api/endpoints/i/signin_history.ts
index e38bfa4d9..3ab59b694 100644
--- a/src/api/endpoints/i/signin_history.ts
+++ b/src/api/endpoints/i/signin_history.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import Signin from '../../models/signin';
-import serialize from '../../serializers/signin';
+import { pack } from '../../models/signin';
 
 /**
  * Get signin history of my account
@@ -58,5 +58,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(history.map(async record =>
-		await serialize(record))));
+		await pack(record))));
 });
diff --git a/src/api/endpoints/i/update.ts b/src/api/endpoints/i/update.ts
index c484c51a9..a138832e5 100644
--- a/src/api/endpoints/i/update.ts
+++ b/src/api/endpoints/i/update.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import User from '../../models/user';
 import { isValidName, isValidDescription, isValidLocation, isValidBirthday } from '../../models/user';
-import serialize from '../../serializers/user';
+import { pack } from '../../models/user';
 import event from '../../event';
 import config from '../../../conf';
 
@@ -65,7 +65,7 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re
 	});
 
 	// Serialize
-	const iObj = await serialize(user, user, {
+	const iObj = await pack(user, user, {
 		detail: true,
 		includeSecrets: isSecure
 	});
diff --git a/src/api/endpoints/messaging/history.ts b/src/api/endpoints/messaging/history.ts
index f14740dff..1683ca7a8 100644
--- a/src/api/endpoints/messaging/history.ts
+++ b/src/api/endpoints/messaging/history.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import History from '../../models/messaging-history';
 import Mute from '../../models/mute';
-import serialize from '../../serializers/messaging-message';
+import { pack } from '../../models/messaging-message';
 
 /**
  * Show messaging history
@@ -39,5 +39,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(history.map(async h =>
-		await serialize(h.message, user))));
+		await pack(h.message, user))));
 });
diff --git a/src/api/endpoints/messaging/messages.ts b/src/api/endpoints/messaging/messages.ts
index 3d3c6950a..67ba5e9d6 100644
--- a/src/api/endpoints/messaging/messages.ts
+++ b/src/api/endpoints/messaging/messages.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import Message from '../../models/messaging-message';
 import User from '../../models/user';
-import serialize from '../../serializers/messaging-message';
+import { pack } from '../../models/messaging-message';
 import read from '../../common/read-messaging-message';
 
 /**
@@ -87,7 +87,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(messages.map(async message =>
-		await serialize(message, user, {
+		await pack(message, user, {
 			populateRecipient: false
 		}))));
 
diff --git a/src/api/endpoints/messaging/messages/create.ts b/src/api/endpoints/messaging/messages/create.ts
index 4e9d10197..1b8a5f59e 100644
--- a/src/api/endpoints/messaging/messages/create.ts
+++ b/src/api/endpoints/messaging/messages/create.ts
@@ -8,7 +8,7 @@ import History from '../../../models/messaging-history';
 import User from '../../../models/user';
 import Mute from '../../../models/mute';
 import DriveFile from '../../../models/drive-file';
-import serialize from '../../../serializers/messaging-message';
+import { pack } from '../../../models/messaging-message';
 import publishUserStream from '../../../event';
 import { publishMessagingStream, publishMessagingIndexStream, pushSw } from '../../../event';
 import config from '../../../../conf';
@@ -79,7 +79,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	});
 
 	// Serialize
-	const messageObj = await serialize(message);
+	const messageObj = await pack(message);
 
 	// Reponse
 	res(messageObj);
diff --git a/src/api/endpoints/mute/list.ts b/src/api/endpoints/mute/list.ts
index 740e19f0b..19e3b157e 100644
--- a/src/api/endpoints/mute/list.ts
+++ b/src/api/endpoints/mute/list.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import Mute from '../../models/mute';
-import serialize from '../../serializers/user';
+import { pack } from '../../models/user';
 import getFriends from '../../common/get-friends';
 
 /**
@@ -63,7 +63,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Serialize
 	const users = await Promise.all(mutes.map(async m =>
-		await serialize(m.mutee_id, me, { detail: true })));
+		await pack(m.mutee_id, me, { detail: true })));
 
 	// Response
 	res({
diff --git a/src/api/endpoints/my/apps.ts b/src/api/endpoints/my/apps.ts
index eb9c75876..fe583db86 100644
--- a/src/api/endpoints/my/apps.ts
+++ b/src/api/endpoints/my/apps.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import App from '../../models/app';
-import serialize from '../../serializers/app';
+import { pack } from '../../models/app';
 
 /**
  * Get my apps
@@ -37,5 +37,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Reply
 	res(await Promise.all(apps.map(async app =>
-		await serialize(app))));
+		await pack(app))));
 });
diff --git a/src/api/endpoints/posts.ts b/src/api/endpoints/posts.ts
index db166cd67..d10c6ab40 100644
--- a/src/api/endpoints/posts.ts
+++ b/src/api/endpoints/posts.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import Post from '../models/post';
-import serialize from '../serializers/post';
+import { pack } from '../models/post';
 
 /**
  * Lists all posts
@@ -85,5 +85,5 @@ module.exports = (params) => new Promise(async (res, rej) => {
 		});
 
 	// Serialize
-	res(await Promise.all(posts.map(async post => await serialize(post))));
+	res(await Promise.all(posts.map(async post => await pack(post))));
 });
diff --git a/src/api/endpoints/posts/context.ts b/src/api/endpoints/posts/context.ts
index bad59a6be..3051e7af1 100644
--- a/src/api/endpoints/posts/context.ts
+++ b/src/api/endpoints/posts/context.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import Post from '../../models/post';
-import serialize from '../../serializers/post';
+import { pack } from '../../models/post';
 
 /**
  * Show a context of a post
@@ -60,5 +60,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(context.map(async post =>
-		await serialize(post, user))));
+		await pack(post, user))));
 });
diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index a1d05c67c..0fa52221f 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -12,7 +12,7 @@ import Mute from '../../models/mute';
 import DriveFile from '../../models/drive-file';
 import Watching from '../../models/post-watching';
 import ChannelWatching from '../../models/channel-watching';
-import serialize from '../../serializers/post';
+import { pack } from '../../models/post';
 import notify from '../../common/notify';
 import watch from '../../common/watch-post';
 import event, { pushSw, publishChannelStream } from '../../event';
@@ -224,7 +224,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 	});
 
 	// Serialize
-	const postObj = await serialize(post);
+	const postObj = await pack(post);
 
 	// Reponse
 	res({
diff --git a/src/api/endpoints/posts/mentions.ts b/src/api/endpoints/posts/mentions.ts
index 3bb4ec3fa..7127db0ad 100644
--- a/src/api/endpoints/posts/mentions.ts
+++ b/src/api/endpoints/posts/mentions.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import Post from '../../models/post';
 import getFriends from '../../common/get-friends';
-import serialize from '../../serializers/post';
+import { pack } from '../../models/post';
 
 /**
  * Get mentions of myself
@@ -73,6 +73,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(mentions.map(async mention =>
-		await serialize(mention, user)
+		await pack(mention, user)
 	)));
 });
diff --git a/src/api/endpoints/posts/polls/recommendation.ts b/src/api/endpoints/posts/polls/recommendation.ts
index 9c92d6cac..5ccb75449 100644
--- a/src/api/endpoints/posts/polls/recommendation.ts
+++ b/src/api/endpoints/posts/polls/recommendation.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import Vote from '../../../models/poll-vote';
 import Post from '../../../models/post';
-import serialize from '../../../serializers/post';
+import { pack } from '../../../models/post';
 
 /**
  * Get recommended polls
@@ -56,5 +56,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(posts.map(async post =>
-		await serialize(post, user, { detail: true }))));
+		await pack(post, user, { detail: true }))));
 });
diff --git a/src/api/endpoints/posts/reactions.ts b/src/api/endpoints/posts/reactions.ts
index eab5d9b25..f60334df8 100644
--- a/src/api/endpoints/posts/reactions.ts
+++ b/src/api/endpoints/posts/reactions.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import Post from '../../models/post';
 import Reaction from '../../models/post-reaction';
-import serialize from '../../serializers/post-reaction';
+import { pack } from '../../models/post-reaction';
 
 /**
  * Show reactions of a post
@@ -54,5 +54,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(reactions.map(async reaction =>
-		await serialize(reaction, user))));
+		await pack(reaction, user))));
 });
diff --git a/src/api/endpoints/posts/replies.ts b/src/api/endpoints/posts/replies.ts
index 3fd6a4676..1442b8a4c 100644
--- a/src/api/endpoints/posts/replies.ts
+++ b/src/api/endpoints/posts/replies.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import Post from '../../models/post';
-import serialize from '../../serializers/post';
+import { pack } from '../../models/post';
 
 /**
  * Show a replies of a post
@@ -50,5 +50,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(replies.map(async post =>
-		await serialize(post, user))));
+		await pack(post, user))));
 });
diff --git a/src/api/endpoints/posts/reposts.ts b/src/api/endpoints/posts/reposts.ts
index bcc6163a1..0fbb0687b 100644
--- a/src/api/endpoints/posts/reposts.ts
+++ b/src/api/endpoints/posts/reposts.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import Post from '../../models/post';
-import serialize from '../../serializers/post';
+import { pack } from '../../models/post';
 
 /**
  * Show a reposts of a post
@@ -70,5 +70,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(reposts.map(async post =>
-		await serialize(post, user))));
+		await pack(post, user))));
 });
diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index 31c9a8d3c..6e26f5539 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -7,7 +7,7 @@ import Post from '../../models/post';
 import User from '../../models/user';
 import Mute from '../../models/mute';
 import getFriends from '../../common/get-friends';
-import serialize from '../../serializers/post';
+import { pack } from '../../models/post';
 
 /**
  * Search a post
@@ -351,5 +351,5 @@ async function search(
 
 	// Serialize
 	res(await Promise.all(posts.map(async post =>
-		await serialize(post, me))));
+		await pack(post, me))));
 }
diff --git a/src/api/endpoints/posts/show.ts b/src/api/endpoints/posts/show.ts
index 5bfe4f660..c31244971 100644
--- a/src/api/endpoints/posts/show.ts
+++ b/src/api/endpoints/posts/show.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import Post from '../../models/post';
-import serialize from '../../serializers/post';
+import { pack } from '../../models/post';
 
 /**
  * Show a post
@@ -27,7 +27,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}
 
 	// Serialize
-	res(await serialize(post, user, {
+	res(await pack(post, user, {
 		detail: true
 	}));
 });
diff --git a/src/api/endpoints/posts/timeline.ts b/src/api/endpoints/posts/timeline.ts
index da7ffd0c1..c41cfdb8b 100644
--- a/src/api/endpoints/posts/timeline.ts
+++ b/src/api/endpoints/posts/timeline.ts
@@ -7,7 +7,7 @@ import Post from '../../models/post';
 import Mute from '../../models/mute';
 import ChannelWatching from '../../models/channel-watching';
 import getFriends from '../../common/get-friends';
-import serialize from '../../serializers/post';
+import { pack } from '../../models/post';
 
 /**
  * Get timeline of myself
@@ -128,5 +128,5 @@ module.exports = async (params, user, app) => {
 		});
 
 	// Serialize
-	return await Promise.all(timeline.map(post => serialize(post, user)));
+	return await Promise.all(timeline.map(post => pack(post, user)));
 };
diff --git a/src/api/endpoints/posts/trend.ts b/src/api/endpoints/posts/trend.ts
index 64a195dff..b2b1d327a 100644
--- a/src/api/endpoints/posts/trend.ts
+++ b/src/api/endpoints/posts/trend.ts
@@ -4,7 +4,7 @@
 const ms = require('ms');
 import $ from 'cafy';
 import Post from '../../models/post';
-import serialize from '../../serializers/post';
+import { pack } from '../../models/post';
 
 /**
  * Get trend posts
@@ -76,5 +76,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(posts.map(async post =>
-		await serialize(post, user, { detail: true }))));
+		await pack(post, user, { detail: true }))));
 });
diff --git a/src/api/endpoints/users.ts b/src/api/endpoints/users.ts
index f3c9b66a5..ba33b1aeb 100644
--- a/src/api/endpoints/users.ts
+++ b/src/api/endpoints/users.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import User from '../models/user';
-import serialize from '../serializers/user';
+import { pack } from '../models/user';
 
 /**
  * Lists all users
@@ -55,5 +55,5 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(users.map(async user =>
-		await serialize(user, me))));
+		await pack(user, me))));
 });
diff --git a/src/api/endpoints/users/followers.ts b/src/api/endpoints/users/followers.ts
index 4905323ba..b0fb83c68 100644
--- a/src/api/endpoints/users/followers.ts
+++ b/src/api/endpoints/users/followers.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import User from '../../models/user';
 import Following from '../../models/following';
-import serialize from '../../serializers/user';
+import { pack } from '../../models/user';
 import getFriends from '../../common/get-friends';
 
 /**
@@ -82,7 +82,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Serialize
 	const users = await Promise.all(following.map(async f =>
-		await serialize(f.follower_id, me, { detail: true })));
+		await pack(f.follower_id, me, { detail: true })));
 
 	// Response
 	res({
diff --git a/src/api/endpoints/users/following.ts b/src/api/endpoints/users/following.ts
index dc2ff49bb..8e88431e9 100644
--- a/src/api/endpoints/users/following.ts
+++ b/src/api/endpoints/users/following.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import User from '../../models/user';
 import Following from '../../models/following';
-import serialize from '../../serializers/user';
+import { pack } from '../../models/user';
 import getFriends from '../../common/get-friends';
 
 /**
@@ -82,7 +82,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Serialize
 	const users = await Promise.all(following.map(async f =>
-		await serialize(f.followee_id, me, { detail: true })));
+		await pack(f.followee_id, me, { detail: true })));
 
 	// Response
 	res({
diff --git a/src/api/endpoints/users/get_frequently_replied_users.ts b/src/api/endpoints/users/get_frequently_replied_users.ts
index a8add623d..3cbc76132 100644
--- a/src/api/endpoints/users/get_frequently_replied_users.ts
+++ b/src/api/endpoints/users/get_frequently_replied_users.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import Post from '../../models/post';
 import User from '../../models/user';
-import serialize from '../../serializers/user';
+import { pack } from '../../models/user';
 
 module.exports = (params, me) => new Promise(async (res, rej) => {
 	// Get 'user_id' parameter
@@ -91,7 +91,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Make replies object (includes weights)
 	const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({
-		user: await serialize(user, me, { detail: true }),
+		user: await pack(user, me, { detail: true }),
 		weight: repliedUsers[user] / peak
 	})));
 
diff --git a/src/api/endpoints/users/posts.ts b/src/api/endpoints/users/posts.ts
index 0d8384a43..1f3db3cf7 100644
--- a/src/api/endpoints/users/posts.ts
+++ b/src/api/endpoints/users/posts.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import Post from '../../models/post';
 import User from '../../models/user';
-import serialize from '../../serializers/post';
+import { pack } from '../../models/post';
 
 /**
  * Get posts of a user
@@ -124,6 +124,6 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(posts.map(async (post) =>
-		await serialize(post, me)
+		await pack(post, me)
 	)));
 });
diff --git a/src/api/endpoints/users/recommendation.ts b/src/api/endpoints/users/recommendation.ts
index 731d68a7b..b80fd63ce 100644
--- a/src/api/endpoints/users/recommendation.ts
+++ b/src/api/endpoints/users/recommendation.ts
@@ -4,7 +4,7 @@
 const ms = require('ms');
 import $ from 'cafy';
 import User from '../../models/user';
-import serialize from '../../serializers/user';
+import { pack } from '../../models/user';
 import getFriends from '../../common/get-friends';
 
 /**
@@ -44,5 +44,5 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(users.map(async user =>
-		await serialize(user, me, { detail: true }))));
+		await pack(user, me, { detail: true }))));
 });
diff --git a/src/api/endpoints/users/search.ts b/src/api/endpoints/users/search.ts
index 73a5db47e..213038403 100644
--- a/src/api/endpoints/users/search.ts
+++ b/src/api/endpoints/users/search.ts
@@ -4,7 +4,7 @@
 import * as mongo from 'mongodb';
 import $ from 'cafy';
 import User from '../../models/user';
-import serialize from '../../serializers/user';
+import { pack } from '../../models/user';
 import config from '../../../conf';
 const escapeRegexp = require('escape-regexp');
 
@@ -94,6 +94,6 @@ async function byElasticsearch(res, rej, me, query, offset, max) {
 
 		// Serialize
 		res(await Promise.all(users.map(async user =>
-			await serialize(user, me, { detail: true }))));
+			await pack(user, me, { detail: true }))));
 	});
 }
diff --git a/src/api/endpoints/users/search_by_username.ts b/src/api/endpoints/users/search_by_username.ts
index 7f2f42f0a..63e206b1f 100644
--- a/src/api/endpoints/users/search_by_username.ts
+++ b/src/api/endpoints/users/search_by_username.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import User from '../../models/user';
-import serialize from '../../serializers/user';
+import { pack } from '../../models/user';
 
 /**
  * Search a user by username
@@ -35,5 +35,5 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(users.map(async user =>
-		await serialize(user, me, { detail: true }))));
+		await pack(user, me, { detail: true }))));
 });
diff --git a/src/api/endpoints/users/show.ts b/src/api/endpoints/users/show.ts
index 8e74b0fe3..a51cb619d 100644
--- a/src/api/endpoints/users/show.ts
+++ b/src/api/endpoints/users/show.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import User from '../../models/user';
-import serialize from '../../serializers/user';
+import { pack } from '../../models/user';
 
 /**
  * Show a user
@@ -41,7 +41,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	}
 
 	// Send response
-	res(await serialize(user, me, {
+	res(await pack(user, me, {
 		detail: true
 	}));
 });
diff --git a/src/api/models/drive-file.ts b/src/api/models/drive-file.ts
index 6a8db3ad4..9b9df1dac 100644
--- a/src/api/models/drive-file.ts
+++ b/src/api/models/drive-file.ts
@@ -20,8 +20,14 @@ export { getGridFSBucket };
 
 export type IDriveFile = {
 	_id: mongodb.ObjectID;
-	created_at: Date;
-	user_id: mongodb.ObjectID;
+	uploadDate: Date;
+	md5: string;
+	filename: string;
+	metadata: {
+		properties: any;
+		user_id: mongodb.ObjectID;
+		folder_id: mongodb.ObjectID;
+	}
 };
 
 export function validateFileName(name: string): boolean {
diff --git a/src/api/models/drive-folder.ts b/src/api/models/drive-folder.ts
index 48b26c2bd..54b45049b 100644
--- a/src/api/models/drive-folder.ts
+++ b/src/api/models/drive-folder.ts
@@ -9,7 +9,9 @@ export default DriveFolder;
 export type IDriveFolder = {
 	_id: mongo.ObjectID;
 	created_at: Date;
+	name: string;
 	user_id: mongo.ObjectID;
+	parent_id: mongo.ObjectID;
 };
 
 export function isValidFolderName(name: string): boolean {
diff --git a/src/api/models/messaging-message.ts b/src/api/models/messaging-message.ts
index ffdda1db2..90cf1cd71 100644
--- a/src/api/models/messaging-message.ts
+++ b/src/api/models/messaging-message.ts
@@ -10,6 +10,11 @@ export default MessagingMessage;
 
 export interface IMessagingMessage {
 	_id: mongo.ObjectID;
+	created_at: Date;
+	text: string;
+	user_id: mongo.ObjectID;
+	recipient_id: mongo.ObjectID;
+	is_read: boolean;
 }
 
 export function isValidText(text: string): boolean {
diff --git a/src/api/private/signin.ts b/src/api/private/signin.ts
index a26c8f6c5..ab6e93562 100644
--- a/src/api/private/signin.ts
+++ b/src/api/private/signin.ts
@@ -3,7 +3,7 @@ import * as bcrypt from 'bcryptjs';
 import * as speakeasy from 'speakeasy';
 import { default as User, IUser } from '../models/user';
 import Signin from '../models/signin';
-import serialize from '../serializers/signin';
+import { pack } from '../models/signin';
 import event from '../event';
 import signin from '../common/signin';
 import config from '../../conf';
@@ -85,5 +85,5 @@ export default async (req: express.Request, res: express.Response) => {
 	});
 
 	// Publish signin event
-	event(user._id, 'signin', await serialize(record));
+	event(user._id, 'signin', await pack(record));
 };
diff --git a/src/api/private/signup.ts b/src/api/private/signup.ts
index 466c6a489..105fe319a 100644
--- a/src/api/private/signup.ts
+++ b/src/api/private/signup.ts
@@ -4,7 +4,7 @@ import * as bcrypt from 'bcryptjs';
 import recaptcha = require('recaptcha-promise');
 import { default as User, IUser } from '../models/user';
 import { validateUsername, validatePassword } from '../models/user';
-import serialize from '../serializers/user';
+import { pack } from '../models/user';
 import generateUserToken from '../common/generate-native-user-token';
 import config from '../../conf';
 
@@ -142,7 +142,7 @@ export default async (req: express.Request, res: express.Response) => {
 	});
 
 	// Response
-	res.send(await serialize(account));
+	res.send(await pack(account));
 
 	// Create search index
 	if (config.elasticsearch.enable) {
diff --git a/src/api/service/twitter.ts b/src/api/service/twitter.ts
index 0e75ee0bd..ca4f8abcc 100644
--- a/src/api/service/twitter.ts
+++ b/src/api/service/twitter.ts
@@ -6,7 +6,7 @@ import * as uuid from 'uuid';
 import autwh from 'autwh';
 import redis from '../../db/redis';
 import User from '../models/user';
-import serialize from '../serializers/user';
+import { pack } from '../models/user';
 import event from '../event';
 import config from '../../conf';
 import signin from '../common/signin';
@@ -50,7 +50,7 @@ module.exports = (app: express.Application) => {
 		res.send(`Twitterの連携を解除しました :v:`);
 
 		// Publish i updated event
-		event(user._id, 'i_updated', await serialize(user, user, {
+		event(user._id, 'i_updated', await pack(user, user, {
 			detail: true,
 			includeSecrets: true
 		}));

From b815dc929fd5ab4f22d5fd64234f1fa85d563022 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, 2 Feb 2018 10:31:17 +0900
Subject: [PATCH 0147/1250] wip

---
 src/api/endpoints/app/create.ts                         | 3 +--
 src/api/endpoints/app/show.ts                           | 3 +--
 src/api/endpoints/auth/session/show.ts                  | 3 +--
 src/api/endpoints/channels.ts                           | 3 +--
 src/api/endpoints/channels/posts.ts                     | 3 +--
 src/api/endpoints/channels/show.ts                      | 3 +--
 src/api/endpoints/drive/files.ts                        | 3 +--
 src/api/endpoints/drive/files/create.ts                 | 3 +--
 src/api/endpoints/drive/files/find.ts                   | 3 +--
 src/api/endpoints/drive/files/show.ts                   | 3 +--
 src/api/endpoints/drive/files/update.ts                 | 3 +--
 src/api/endpoints/drive/files/upload_from_url.ts        | 3 +--
 src/api/endpoints/drive/folders.ts                      | 3 +--
 src/api/endpoints/drive/folders/create.ts               | 3 +--
 src/api/endpoints/drive/folders/find.ts                 | 3 +--
 src/api/endpoints/drive/folders/show.ts                 | 3 +--
 src/api/endpoints/drive/folders/update.ts               | 3 +--
 src/api/endpoints/drive/stream.ts                       | 3 +--
 src/api/endpoints/i.ts                                  | 3 +--
 src/api/endpoints/i/signin_history.ts                   | 3 +--
 src/api/endpoints/i/update.ts                           | 3 +--
 src/api/endpoints/my/apps.ts                            | 3 +--
 src/api/endpoints/posts.ts                              | 3 +--
 src/api/endpoints/posts/context.ts                      | 3 +--
 src/api/endpoints/posts/polls/recommendation.ts         | 3 +--
 src/api/endpoints/posts/reactions.ts                    | 3 +--
 src/api/endpoints/posts/replies.ts                      | 3 +--
 src/api/endpoints/posts/reposts.ts                      | 3 +--
 src/api/endpoints/posts/show.ts                         | 3 +--
 src/api/endpoints/posts/trend.ts                        | 3 +--
 src/api/endpoints/users.ts                              | 3 +--
 src/api/endpoints/users/get_frequently_replied_users.ts | 3 +--
 src/api/endpoints/users/posts.ts                        | 3 +--
 src/api/endpoints/users/recommendation.ts               | 3 +--
 src/api/endpoints/users/search.ts                       | 3 +--
 src/api/endpoints/users/search_by_username.ts           | 3 +--
 src/api/endpoints/users/show.ts                         | 3 +--
 src/api/private/signin.ts                               | 3 +--
 src/api/private/signup.ts                               | 3 +--
 src/api/service/twitter.ts                              | 3 +--
 40 files changed, 40 insertions(+), 80 deletions(-)

diff --git a/src/api/endpoints/app/create.ts b/src/api/endpoints/app/create.ts
index 320163ebd..71633f7de 100644
--- a/src/api/endpoints/app/create.ts
+++ b/src/api/endpoints/app/create.ts
@@ -4,8 +4,7 @@
 import rndstr from 'rndstr';
 import $ from 'cafy';
 import App from '../../models/app';
-import { isValidNameId } from '../../models/app';
-import { pack } from '../../models/app';
+import { isValidNameId }, { pack } from '../../models/app';
 
 /**
  * @swagger
diff --git a/src/api/endpoints/app/show.ts b/src/api/endpoints/app/show.ts
index a3ef24717..8bc3dda42 100644
--- a/src/api/endpoints/app/show.ts
+++ b/src/api/endpoints/app/show.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import App from '../../models/app';
-import { pack } from '../../models/app';
+import App, { pack } from '../../models/app';
 
 /**
  * @swagger
diff --git a/src/api/endpoints/auth/session/show.ts b/src/api/endpoints/auth/session/show.ts
index 1fe3b873f..73ac3185f 100644
--- a/src/api/endpoints/auth/session/show.ts
+++ b/src/api/endpoints/auth/session/show.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import AuthSess from '../../../models/auth-session';
-import { pack } from '../../../models/auth-session';
+import AuthSess, { pack } from '../../../models/auth-session';
 
 /**
  * @swagger
diff --git a/src/api/endpoints/channels.ts b/src/api/endpoints/channels.ts
index 92dcee83d..b9a7d1b78 100644
--- a/src/api/endpoints/channels.ts
+++ b/src/api/endpoints/channels.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Channel from '../models/channel';
-import { pack } from '../models/channel';
+import Channel, { pack } from '../models/channel';
 
 /**
  * Get all channels
diff --git a/src/api/endpoints/channels/posts.ts b/src/api/endpoints/channels/posts.ts
index 3feee51f7..d722589c2 100644
--- a/src/api/endpoints/channels/posts.ts
+++ b/src/api/endpoints/channels/posts.ts
@@ -3,8 +3,7 @@
  */
 import $ from 'cafy';
 import { default as Channel, IChannel } from '../../models/channel';
-import Post from '../../models/post';
-import { pack } from '../../models/post';
+import Post, { pack } from '../../models/post';
 
 /**
  * Show a posts of a channel
diff --git a/src/api/endpoints/channels/show.ts b/src/api/endpoints/channels/show.ts
index 89c48379a..3238616fa 100644
--- a/src/api/endpoints/channels/show.ts
+++ b/src/api/endpoints/channels/show.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import { default as Channel, IChannel } from '../../models/channel';
-import { pack } from '../../models/channel';
+import { default as Channel, IChannel }, { pack } from '../../models/channel';
 
 /**
  * Show a channel
diff --git a/src/api/endpoints/drive/files.ts b/src/api/endpoints/drive/files.ts
index 3bd80e728..89915331e 100644
--- a/src/api/endpoints/drive/files.ts
+++ b/src/api/endpoints/drive/files.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import DriveFile from '../../models/drive-file';
-import { pack } from '../../models/drive-file';
+import DriveFile, { pack } from '../../models/drive-file';
 
 /**
  * Get drive files
diff --git a/src/api/endpoints/drive/files/create.ts b/src/api/endpoints/drive/files/create.ts
index 6fa76d7e9..7b424f3f5 100644
--- a/src/api/endpoints/drive/files/create.ts
+++ b/src/api/endpoints/drive/files/create.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import { validateFileName } from '../../../models/drive-file';
-import { pack } from '../../../models/drive-file';
+import { validateFileName }, { pack } from '../../../models/drive-file';
 import create from '../../../common/add-file-to-drive';
 
 /**
diff --git a/src/api/endpoints/drive/files/find.ts b/src/api/endpoints/drive/files/find.ts
index 571aba81f..e026afe93 100644
--- a/src/api/endpoints/drive/files/find.ts
+++ b/src/api/endpoints/drive/files/find.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import DriveFile from '../../../models/drive-file';
-import { pack } from '../../../models/drive-file';
+import DriveFile, { pack } from '../../../models/drive-file';
 
 /**
  * Find a file(s)
diff --git a/src/api/endpoints/drive/files/show.ts b/src/api/endpoints/drive/files/show.ts
index 00f69f141..21664f7ba 100644
--- a/src/api/endpoints/drive/files/show.ts
+++ b/src/api/endpoints/drive/files/show.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import DriveFile from '../../../models/drive-file';
-import { pack } from '../../../models/drive-file';
+import DriveFile, { pack } from '../../../models/drive-file';
 
 /**
  * Show a file
diff --git a/src/api/endpoints/drive/files/update.ts b/src/api/endpoints/drive/files/update.ts
index 9ef8215b1..ff65a48f7 100644
--- a/src/api/endpoints/drive/files/update.ts
+++ b/src/api/endpoints/drive/files/update.ts
@@ -4,8 +4,7 @@
 import $ from 'cafy';
 import DriveFolder from '../../../models/drive-folder';
 import DriveFile from '../../../models/drive-file';
-import { validateFileName } from '../../../models/drive-file';
-import { pack } from '../../../models/drive-file';
+import { validateFileName }, { pack } from '../../../models/drive-file';
 import { publishDriveStream } from '../../../event';
 
 /**
diff --git a/src/api/endpoints/drive/files/upload_from_url.ts b/src/api/endpoints/drive/files/upload_from_url.ts
index f0398bfc5..009f06aaa 100644
--- a/src/api/endpoints/drive/files/upload_from_url.ts
+++ b/src/api/endpoints/drive/files/upload_from_url.ts
@@ -3,8 +3,7 @@
  */
 import * as URL from 'url';
 import $ from 'cafy';
-import { validateFileName } from '../../../models/drive-file';
-import { pack } from '../../../models/drive-file';
+import { validateFileName }, { pack } from '../../../models/drive-file';
 import create from '../../../common/add-file-to-drive';
 import * as debug from 'debug';
 import * as tmp from 'tmp';
diff --git a/src/api/endpoints/drive/folders.ts b/src/api/endpoints/drive/folders.ts
index e650fb74a..428bde350 100644
--- a/src/api/endpoints/drive/folders.ts
+++ b/src/api/endpoints/drive/folders.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import DriveFolder from '../../models/drive-folder';
-import { pack } from '../../models/drive-folder';
+import DriveFolder, { pack } from '../../models/drive-folder';
 
 /**
  * Get drive folders
diff --git a/src/api/endpoints/drive/folders/create.ts b/src/api/endpoints/drive/folders/create.ts
index 1953c09ee..6543b1127 100644
--- a/src/api/endpoints/drive/folders/create.ts
+++ b/src/api/endpoints/drive/folders/create.ts
@@ -3,8 +3,7 @@
  */
 import $ from 'cafy';
 import DriveFolder from '../../../models/drive-folder';
-import { isValidFolderName } from '../../../models/drive-folder';
-import { pack } from '../../../models/drive-folder';
+import { isValidFolderName }, { pack } from '../../../models/drive-folder';
 import { publishDriveStream } from '../../../event';
 
 /**
diff --git a/src/api/endpoints/drive/folders/find.ts b/src/api/endpoints/drive/folders/find.ts
index caad45d74..fc84766bc 100644
--- a/src/api/endpoints/drive/folders/find.ts
+++ b/src/api/endpoints/drive/folders/find.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import DriveFolder from '../../../models/drive-folder';
-import { pack } from '../../../models/drive-folder';
+import DriveFolder, { pack } from '../../../models/drive-folder';
 
 /**
  * Find a folder(s)
diff --git a/src/api/endpoints/drive/folders/show.ts b/src/api/endpoints/drive/folders/show.ts
index fd3061ca5..e07d14d20 100644
--- a/src/api/endpoints/drive/folders/show.ts
+++ b/src/api/endpoints/drive/folders/show.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import DriveFolder from '../../../models/drive-folder';
-import { pack } from '../../../models/drive-folder';
+import DriveFolder, { pack } from '../../../models/drive-folder';
 
 /**
  * Show a folder
diff --git a/src/api/endpoints/drive/folders/update.ts b/src/api/endpoints/drive/folders/update.ts
index 8f50a9d00..2adcadcb0 100644
--- a/src/api/endpoints/drive/folders/update.ts
+++ b/src/api/endpoints/drive/folders/update.ts
@@ -3,8 +3,7 @@
  */
 import $ from 'cafy';
 import DriveFolder from '../../../models/drive-folder';
-import { isValidFolderName } from '../../../models/drive-folder';
-import { pack } from '../../../models/drive-folder';
+import { isValidFolderName }, { pack } from '../../../models/drive-folder';
 import { publishDriveStream } from '../../../event';
 
 /**
diff --git a/src/api/endpoints/drive/stream.ts b/src/api/endpoints/drive/stream.ts
index 3527d7050..8352c7dd4 100644
--- a/src/api/endpoints/drive/stream.ts
+++ b/src/api/endpoints/drive/stream.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import DriveFile from '../../models/drive-file';
-import { pack } from '../../models/drive-file';
+import DriveFile, { pack } from '../../models/drive-file';
 
 /**
  * Get drive stream
diff --git a/src/api/endpoints/i.ts b/src/api/endpoints/i.ts
index 1b6c1e58d..7efdbcd7c 100644
--- a/src/api/endpoints/i.ts
+++ b/src/api/endpoints/i.ts
@@ -1,8 +1,7 @@
 /**
  * Module dependencies
  */
-import User from '../models/user';
-import { pack } from '../models/user';
+import User, { pack } from '../models/user';
 
 /**
  * Show myself
diff --git a/src/api/endpoints/i/signin_history.ts b/src/api/endpoints/i/signin_history.ts
index 3ab59b694..859e81653 100644
--- a/src/api/endpoints/i/signin_history.ts
+++ b/src/api/endpoints/i/signin_history.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Signin from '../../models/signin';
-import { pack } from '../../models/signin';
+import Signin, { pack } from '../../models/signin';
 
 /**
  * Get signin history of my account
diff --git a/src/api/endpoints/i/update.ts b/src/api/endpoints/i/update.ts
index a138832e5..cd4b1a13f 100644
--- a/src/api/endpoints/i/update.ts
+++ b/src/api/endpoints/i/update.ts
@@ -3,8 +3,7 @@
  */
 import $ from 'cafy';
 import User from '../../models/user';
-import { isValidName, isValidDescription, isValidLocation, isValidBirthday } from '../../models/user';
-import { pack } from '../../models/user';
+import { isValidName, isValidDescription, isValidLocation, isValidBirthday }, { pack } from '../../models/user';
 import event from '../../event';
 import config from '../../../conf';
 
diff --git a/src/api/endpoints/my/apps.ts b/src/api/endpoints/my/apps.ts
index fe583db86..b23619050 100644
--- a/src/api/endpoints/my/apps.ts
+++ b/src/api/endpoints/my/apps.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import App from '../../models/app';
-import { pack } from '../../models/app';
+import App, { pack } from '../../models/app';
 
 /**
  * Get my apps
diff --git a/src/api/endpoints/posts.ts b/src/api/endpoints/posts.ts
index d10c6ab40..3b2942592 100644
--- a/src/api/endpoints/posts.ts
+++ b/src/api/endpoints/posts.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post from '../models/post';
-import { pack } from '../models/post';
+import Post, { pack } from '../models/post';
 
 /**
  * Lists all posts
diff --git a/src/api/endpoints/posts/context.ts b/src/api/endpoints/posts/context.ts
index 3051e7af1..5ba375897 100644
--- a/src/api/endpoints/posts/context.ts
+++ b/src/api/endpoints/posts/context.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post from '../../models/post';
-import { pack } from '../../models/post';
+import Post, { pack } from '../../models/post';
 
 /**
  * Show a context of a post
diff --git a/src/api/endpoints/posts/polls/recommendation.ts b/src/api/endpoints/posts/polls/recommendation.ts
index 5ccb75449..4a3fa3f55 100644
--- a/src/api/endpoints/posts/polls/recommendation.ts
+++ b/src/api/endpoints/posts/polls/recommendation.ts
@@ -3,8 +3,7 @@
  */
 import $ from 'cafy';
 import Vote from '../../../models/poll-vote';
-import Post from '../../../models/post';
-import { pack } from '../../../models/post';
+import Post, { pack } from '../../../models/post';
 
 /**
  * Get recommended polls
diff --git a/src/api/endpoints/posts/reactions.ts b/src/api/endpoints/posts/reactions.ts
index f60334df8..feb140ab4 100644
--- a/src/api/endpoints/posts/reactions.ts
+++ b/src/api/endpoints/posts/reactions.ts
@@ -3,8 +3,7 @@
  */
 import $ from 'cafy';
 import Post from '../../models/post';
-import Reaction from '../../models/post-reaction';
-import { pack } from '../../models/post-reaction';
+import Reaction, { pack } from '../../models/post-reaction';
 
 /**
  * Show reactions of a post
diff --git a/src/api/endpoints/posts/replies.ts b/src/api/endpoints/posts/replies.ts
index 1442b8a4c..613c4fa24 100644
--- a/src/api/endpoints/posts/replies.ts
+++ b/src/api/endpoints/posts/replies.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post from '../../models/post';
-import { pack } from '../../models/post';
+import Post, { pack } from '../../models/post';
 
 /**
  * Show a replies of a post
diff --git a/src/api/endpoints/posts/reposts.ts b/src/api/endpoints/posts/reposts.ts
index 0fbb0687b..89ab0e3d5 100644
--- a/src/api/endpoints/posts/reposts.ts
+++ b/src/api/endpoints/posts/reposts.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post from '../../models/post';
-import { pack } from '../../models/post';
+import Post, { pack } from '../../models/post';
 
 /**
  * Show a reposts of a post
diff --git a/src/api/endpoints/posts/show.ts b/src/api/endpoints/posts/show.ts
index c31244971..383949059 100644
--- a/src/api/endpoints/posts/show.ts
+++ b/src/api/endpoints/posts/show.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post from '../../models/post';
-import { pack } from '../../models/post';
+import Post, { pack } from '../../models/post';
 
 /**
  * Show a post
diff --git a/src/api/endpoints/posts/trend.ts b/src/api/endpoints/posts/trend.ts
index b2b1d327a..caded92bf 100644
--- a/src/api/endpoints/posts/trend.ts
+++ b/src/api/endpoints/posts/trend.ts
@@ -3,8 +3,7 @@
  */
 const ms = require('ms');
 import $ from 'cafy';
-import Post from '../../models/post';
-import { pack } from '../../models/post';
+import Post, { pack } from '../../models/post';
 
 /**
  * Get trend posts
diff --git a/src/api/endpoints/users.ts b/src/api/endpoints/users.ts
index ba33b1aeb..095b9fe40 100644
--- a/src/api/endpoints/users.ts
+++ b/src/api/endpoints/users.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User from '../models/user';
-import { pack } from '../models/user';
+import User, { pack } from '../models/user';
 
 /**
  * Lists all users
diff --git a/src/api/endpoints/users/get_frequently_replied_users.ts b/src/api/endpoints/users/get_frequently_replied_users.ts
index 3cbc76132..87f4f77a5 100644
--- a/src/api/endpoints/users/get_frequently_replied_users.ts
+++ b/src/api/endpoints/users/get_frequently_replied_users.ts
@@ -3,8 +3,7 @@
  */
 import $ from 'cafy';
 import Post from '../../models/post';
-import User from '../../models/user';
-import { pack } from '../../models/user';
+import User, { pack } from '../../models/user';
 
 module.exports = (params, me) => new Promise(async (res, rej) => {
 	// Get 'user_id' parameter
diff --git a/src/api/endpoints/users/posts.ts b/src/api/endpoints/users/posts.ts
index 1f3db3cf7..285e5bc46 100644
--- a/src/api/endpoints/users/posts.ts
+++ b/src/api/endpoints/users/posts.ts
@@ -3,8 +3,7 @@
  */
 import $ from 'cafy';
 import Post from '../../models/post';
-import User from '../../models/user';
-import { pack } from '../../models/post';
+import User, { pack } from '../../models/user';
 
 /**
  * Get posts of a user
diff --git a/src/api/endpoints/users/recommendation.ts b/src/api/endpoints/users/recommendation.ts
index b80fd63ce..736233b34 100644
--- a/src/api/endpoints/users/recommendation.ts
+++ b/src/api/endpoints/users/recommendation.ts
@@ -3,8 +3,7 @@
  */
 const ms = require('ms');
 import $ from 'cafy';
-import User from '../../models/user';
-import { pack } from '../../models/user';
+import User, { pack } from '../../models/user';
 import getFriends from '../../common/get-friends';
 
 /**
diff --git a/src/api/endpoints/users/search.ts b/src/api/endpoints/users/search.ts
index 213038403..1142db9e9 100644
--- a/src/api/endpoints/users/search.ts
+++ b/src/api/endpoints/users/search.ts
@@ -3,8 +3,7 @@
  */
 import * as mongo from 'mongodb';
 import $ from 'cafy';
-import User from '../../models/user';
-import { pack } from '../../models/user';
+import User, { pack } from '../../models/user';
 import config from '../../../conf';
 const escapeRegexp = require('escape-regexp');
 
diff --git a/src/api/endpoints/users/search_by_username.ts b/src/api/endpoints/users/search_by_username.ts
index 63e206b1f..9c5e1905a 100644
--- a/src/api/endpoints/users/search_by_username.ts
+++ b/src/api/endpoints/users/search_by_username.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User from '../../models/user';
-import { pack } from '../../models/user';
+import User, { pack } from '../../models/user';
 
 /**
  * Search a user by username
diff --git a/src/api/endpoints/users/show.ts b/src/api/endpoints/users/show.ts
index a51cb619d..7aea59296 100644
--- a/src/api/endpoints/users/show.ts
+++ b/src/api/endpoints/users/show.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User from '../../models/user';
-import { pack } from '../../models/user';
+import User, { pack } from '../../models/user';
 
 /**
  * Show a user
diff --git a/src/api/private/signin.ts b/src/api/private/signin.ts
index ab6e93562..b49d25d99 100644
--- a/src/api/private/signin.ts
+++ b/src/api/private/signin.ts
@@ -2,8 +2,7 @@ import * as express from 'express';
 import * as bcrypt from 'bcryptjs';
 import * as speakeasy from 'speakeasy';
 import { default as User, IUser } from '../models/user';
-import Signin from '../models/signin';
-import { pack } from '../models/signin';
+import Signin, { pack } from '../models/signin';
 import event from '../event';
 import signin from '../common/signin';
 import config from '../../conf';
diff --git a/src/api/private/signup.ts b/src/api/private/signup.ts
index 105fe319a..392f3b1fc 100644
--- a/src/api/private/signup.ts
+++ b/src/api/private/signup.ts
@@ -3,8 +3,7 @@ import * as express from 'express';
 import * as bcrypt from 'bcryptjs';
 import recaptcha = require('recaptcha-promise');
 import { default as User, IUser } from '../models/user';
-import { validateUsername, validatePassword } from '../models/user';
-import { pack } from '../models/user';
+import { validateUsername, validatePassword }, { pack } from '../models/user';
 import generateUserToken from '../common/generate-native-user-token';
 import config from '../../conf';
 
diff --git a/src/api/service/twitter.ts b/src/api/service/twitter.ts
index ca4f8abcc..7d4964eba 100644
--- a/src/api/service/twitter.ts
+++ b/src/api/service/twitter.ts
@@ -5,8 +5,7 @@ import * as uuid from 'uuid';
 // const Twitter = require('twitter');
 import autwh from 'autwh';
 import redis from '../../db/redis';
-import User from '../models/user';
-import { pack } from '../models/user';
+import User, { pack } from '../models/user';
 import event from '../event';
 import config from '../../conf';
 import signin from '../common/signin';

From 7e478fd4b65b294e293f1046e6557e9a347dab11 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 4 Feb 2018 14:52:33 +0900
Subject: [PATCH 0148/1250] wip

---
 src/api/endpoints/aggregation/posts/reactions.ts | 11 +++++++----
 src/api/endpoints/aggregation/users.ts           | 13 ++++++++-----
 src/api/endpoints/app/create.ts                  |  3 +--
 src/api/endpoints/channels/show.ts               |  2 +-
 src/api/endpoints/drive/files/create.ts          |  2 +-
 src/api/endpoints/drive/files/update.ts          |  3 +--
 src/api/endpoints/drive/files/upload_from_url.ts |  2 +-
 src/api/endpoints/drive/folders/create.ts        |  3 +--
 src/api/endpoints/drive/folders/update.ts        |  3 +--
 src/api/endpoints/following/create.ts            |  7 +++----
 src/api/endpoints/following/delete.ts            |  5 ++---
 src/api/endpoints/i/update.ts                    |  3 +--
 src/api/endpoints/posts/reactions/create.ts      |  9 ++++-----
 src/api/endpoints/users/posts.ts                 |  4 ++--
 src/api/endpoints/users/search.ts                |  2 +-
 src/api/models/app.ts                            |  1 +
 src/api/models/drive-file.ts                     |  1 +
 src/api/models/post-reaction.ts                  |  3 +++
 src/api/models/post.ts                           |  4 +++-
 src/api/models/user.ts                           |  1 +
 src/api/private/signup.ts                        |  3 +--
 src/api/service/twitter.ts                       |  2 +-
 src/api/stream/home.ts                           |  4 ++--
 23 files changed, 48 insertions(+), 43 deletions(-)

diff --git a/src/api/endpoints/aggregation/posts/reactions.ts b/src/api/endpoints/aggregation/posts/reactions.ts
index 2cd4588ae..790b523be 100644
--- a/src/api/endpoints/aggregation/posts/reactions.ts
+++ b/src/api/endpoints/aggregation/posts/reactions.ts
@@ -35,10 +35,13 @@ module.exports = (params) => new Promise(async (res, rej) => {
 				{ deleted_at: { $gt: startTime } }
 			]
 		}, {
-			_id: false,
-			post_id: false
-		}, {
-			sort: { created_at: -1 }
+			sort: {
+				_id: -1
+			},
+			fields: {
+				_id: false,
+				post_id: false
+			}
 		});
 
 	const graph = [];
diff --git a/src/api/endpoints/aggregation/users.ts b/src/api/endpoints/aggregation/users.ts
index 9eb2d035e..e38ce92ff 100644
--- a/src/api/endpoints/aggregation/users.ts
+++ b/src/api/endpoints/aggregation/users.ts
@@ -17,11 +17,14 @@ module.exports = params => new Promise(async (res, rej) => {
 
 	const users = await User
 		.find({}, {
-			_id: false,
-			created_at: true,
-			deleted_at: true
-		}, {
-			sort: { created_at: -1 }
+			sort: {
+				_id: -1
+			},
+			fields: {
+				_id: false,
+				created_at: true,
+				deleted_at: true
+			}
 		});
 
 	const graph = [];
diff --git a/src/api/endpoints/app/create.ts b/src/api/endpoints/app/create.ts
index 71633f7de..0f688792a 100644
--- a/src/api/endpoints/app/create.ts
+++ b/src/api/endpoints/app/create.ts
@@ -3,8 +3,7 @@
  */
 import rndstr from 'rndstr';
 import $ from 'cafy';
-import App from '../../models/app';
-import { isValidNameId }, { pack } from '../../models/app';
+import App, { isValidNameId, pack } from '../../models/app';
 
 /**
  * @swagger
diff --git a/src/api/endpoints/channels/show.ts b/src/api/endpoints/channels/show.ts
index 3238616fa..332da6467 100644
--- a/src/api/endpoints/channels/show.ts
+++ b/src/api/endpoints/channels/show.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import { default as Channel, IChannel }, { pack } from '../../models/channel';
+import Channel, { IChannel, pack } from '../../models/channel';
 
 /**
  * Show a channel
diff --git a/src/api/endpoints/drive/files/create.ts b/src/api/endpoints/drive/files/create.ts
index 7b424f3f5..96bcace88 100644
--- a/src/api/endpoints/drive/files/create.ts
+++ b/src/api/endpoints/drive/files/create.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import { validateFileName }, { pack } from '../../../models/drive-file';
+import { validateFileName, pack } from '../../../models/drive-file';
 import create from '../../../common/add-file-to-drive';
 
 /**
diff --git a/src/api/endpoints/drive/files/update.ts b/src/api/endpoints/drive/files/update.ts
index ff65a48f7..83da46211 100644
--- a/src/api/endpoints/drive/files/update.ts
+++ b/src/api/endpoints/drive/files/update.ts
@@ -3,8 +3,7 @@
  */
 import $ from 'cafy';
 import DriveFolder from '../../../models/drive-folder';
-import DriveFile from '../../../models/drive-file';
-import { validateFileName }, { pack } from '../../../models/drive-file';
+import DriveFile, { validateFileName, pack } from '../../../models/drive-file';
 import { publishDriveStream } from '../../../event';
 
 /**
diff --git a/src/api/endpoints/drive/files/upload_from_url.ts b/src/api/endpoints/drive/files/upload_from_url.ts
index 009f06aaa..68428747e 100644
--- a/src/api/endpoints/drive/files/upload_from_url.ts
+++ b/src/api/endpoints/drive/files/upload_from_url.ts
@@ -3,7 +3,7 @@
  */
 import * as URL from 'url';
 import $ from 'cafy';
-import { validateFileName }, { pack } from '../../../models/drive-file';
+import { validateFileName, pack } from '../../../models/drive-file';
 import create from '../../../common/add-file-to-drive';
 import * as debug from 'debug';
 import * as tmp from 'tmp';
diff --git a/src/api/endpoints/drive/folders/create.ts b/src/api/endpoints/drive/folders/create.ts
index 6543b1127..03f396ddc 100644
--- a/src/api/endpoints/drive/folders/create.ts
+++ b/src/api/endpoints/drive/folders/create.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import DriveFolder from '../../../models/drive-folder';
-import { isValidFolderName }, { pack } from '../../../models/drive-folder';
+import DriveFolder, { isValidFolderName, pack } from '../../../models/drive-folder';
 import { publishDriveStream } from '../../../event';
 
 /**
diff --git a/src/api/endpoints/drive/folders/update.ts b/src/api/endpoints/drive/folders/update.ts
index 2adcadcb0..d3df8bdae 100644
--- a/src/api/endpoints/drive/folders/update.ts
+++ b/src/api/endpoints/drive/folders/update.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import DriveFolder from '../../../models/drive-folder';
-import { isValidFolderName }, { pack } from '../../../models/drive-folder';
+import DriveFolder, { isValidFolderName, pack } from '../../../models/drive-folder';
 import { publishDriveStream } from '../../../event';
 
 /**
diff --git a/src/api/endpoints/following/create.ts b/src/api/endpoints/following/create.ts
index b4a2217b1..8e1aa3471 100644
--- a/src/api/endpoints/following/create.ts
+++ b/src/api/endpoints/following/create.ts
@@ -2,11 +2,10 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User from '../../models/user';
+import User, { pack as packUser } from '../../models/user';
 import Following from '../../models/following';
 import notify from '../../common/notify';
 import event from '../../event';
-import serializeUser from '../../serializers/user';
 
 /**
  * Follow a user
@@ -77,8 +76,8 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	});
 
 	// Publish follow event
-	event(follower._id, 'follow', await serializeUser(followee, follower));
-	event(followee._id, 'followed', await serializeUser(follower, followee));
+	event(follower._id, 'follow', await packUser(followee, follower));
+	event(followee._id, 'followed', await packUser(follower, followee));
 
 	// Notify
 	notify(followee._id, follower._id, 'follow');
diff --git a/src/api/endpoints/following/delete.ts b/src/api/endpoints/following/delete.ts
index aa1639ef6..b68cec09d 100644
--- a/src/api/endpoints/following/delete.ts
+++ b/src/api/endpoints/following/delete.ts
@@ -2,10 +2,9 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User from '../../models/user';
+import User, { pack as packUser } from '../../models/user';
 import Following from '../../models/following';
 import event from '../../event';
-import serializeUser from '../../serializers/user';
 
 /**
  * Unfollow a user
@@ -78,5 +77,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	});
 
 	// Publish follow event
-	event(follower._id, 'unfollow', await serializeUser(followee, follower));
+	event(follower._id, 'unfollow', await packUser(followee, follower));
 });
diff --git a/src/api/endpoints/i/update.ts b/src/api/endpoints/i/update.ts
index cd4b1a13f..7bbbf9590 100644
--- a/src/api/endpoints/i/update.ts
+++ b/src/api/endpoints/i/update.ts
@@ -2,8 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User from '../../models/user';
-import { isValidName, isValidDescription, isValidLocation, isValidBirthday }, { pack } from '../../models/user';
+import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../models/user';
 import event from '../../event';
 import config from '../../../conf';
 
diff --git a/src/api/endpoints/posts/reactions/create.ts b/src/api/endpoints/posts/reactions/create.ts
index d537463df..0b0e0e294 100644
--- a/src/api/endpoints/posts/reactions/create.ts
+++ b/src/api/endpoints/posts/reactions/create.ts
@@ -3,13 +3,12 @@
  */
 import $ from 'cafy';
 import Reaction from '../../../models/post-reaction';
-import Post from '../../../models/post';
+import Post, { pack as packPost } from '../../../models/post';
+import { pack as packUser } from '../../../models/user';
 import Watching from '../../../models/post-watching';
 import notify from '../../../common/notify';
 import watch from '../../../common/watch-post';
 import { publishPostStream, pushSw } from '../../../event';
-import serializePost from '../../../serializers/post';
-import serializeUser from '../../../serializers/user';
 
 /**
  * React to a post
@@ -90,8 +89,8 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	});
 
 	pushSw(post.user_id, 'reaction', {
-		user: await serializeUser(user, post.user_id),
-		post: await serializePost(post, post.user_id),
+		user: await packUser(user, post.user_id),
+		post: await packPost(post, post.user_id),
 		reaction: reaction
 	});
 
diff --git a/src/api/endpoints/users/posts.ts b/src/api/endpoints/users/posts.ts
index 285e5bc46..0c8bceee3 100644
--- a/src/api/endpoints/users/posts.ts
+++ b/src/api/endpoints/users/posts.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post from '../../models/post';
-import User, { pack } from '../../models/user';
+import Post, { pack } from '../../models/post';
+import User from '../../models/user';
 
 /**
  * Get posts of a user
diff --git a/src/api/endpoints/users/search.ts b/src/api/endpoints/users/search.ts
index 1142db9e9..39e2ff989 100644
--- a/src/api/endpoints/users/search.ts
+++ b/src/api/endpoints/users/search.ts
@@ -51,7 +51,7 @@ async function byNative(res, rej, me, query, offset, max) {
 
 	// Serialize
 	res(await Promise.all(users.map(async user =>
-		await serialize(user, me, { detail: true }))));
+		await pack(user, me, { detail: true }))));
 }
 
 // Search by Elasticsearch
diff --git a/src/api/models/app.ts b/src/api/models/app.ts
index fe9d49ff6..34e9867db 100644
--- a/src/api/models/app.ts
+++ b/src/api/models/app.ts
@@ -14,6 +14,7 @@ export type IApp = {
 	_id: mongo.ObjectID;
 	created_at: Date;
 	user_id: mongo.ObjectID;
+	secret: string;
 };
 
 export function isValidNameId(nameId: string): boolean {
diff --git a/src/api/models/drive-file.ts b/src/api/models/drive-file.ts
index 9b9df1dac..2a46d8dc4 100644
--- a/src/api/models/drive-file.ts
+++ b/src/api/models/drive-file.ts
@@ -23,6 +23,7 @@ export type IDriveFile = {
 	uploadDate: Date;
 	md5: string;
 	filename: string;
+	contentType: string;
 	metadata: {
 		properties: any;
 		user_id: mongodb.ObjectID;
diff --git a/src/api/models/post-reaction.ts b/src/api/models/post-reaction.ts
index 568bfc89a..639a70e00 100644
--- a/src/api/models/post-reaction.ts
+++ b/src/api/models/post-reaction.ts
@@ -9,6 +9,9 @@ export default PostReaction;
 
 export interface IPostReaction {
 	_id: mongo.ObjectID;
+	created_at: Date;
+	deleted_at: Date;
+	reaction: string;
 }
 
 /**
diff --git a/src/api/models/post.ts b/src/api/models/post.ts
index ecc5e1a5e..0bbacebf6 100644
--- a/src/api/models/post.ts
+++ b/src/api/models/post.ts
@@ -25,10 +25,12 @@ export type IPost = {
 	media_ids: mongo.ObjectID[];
 	reply_id: mongo.ObjectID;
 	repost_id: mongo.ObjectID;
-	poll: {}; // todo
+	poll: any; // todo
 	text: string;
 	user_id: mongo.ObjectID;
 	app_id: mongo.ObjectID;
+	category: string;
+	is_category_verified: boolean;
 };
 
 /**
diff --git a/src/api/models/user.ts b/src/api/models/user.ts
index 48a45ac2f..e92f244dd 100644
--- a/src/api/models/user.ts
+++ b/src/api/models/user.ts
@@ -42,6 +42,7 @@ export function isValidBirthday(birthday: string): boolean {
 export type IUser = {
 	_id: mongo.ObjectID;
 	created_at: Date;
+	deleted_at: Date;
 	email: string;
 	followers_count: number;
 	following_count: number;
diff --git a/src/api/private/signup.ts b/src/api/private/signup.ts
index 392f3b1fc..8efdb6db4 100644
--- a/src/api/private/signup.ts
+++ b/src/api/private/signup.ts
@@ -2,8 +2,7 @@ import * as uuid from 'uuid';
 import * as express from 'express';
 import * as bcrypt from 'bcryptjs';
 import recaptcha = require('recaptcha-promise');
-import { default as User, IUser } from '../models/user';
-import { validateUsername, validatePassword }, { pack } from '../models/user';
+import User, { IUser, validateUsername, validatePassword, pack } from '../models/user';
 import generateUserToken from '../common/generate-native-user-token';
 import config from '../../conf';
 
diff --git a/src/api/service/twitter.ts b/src/api/service/twitter.ts
index 7d4964eba..adcd5ac49 100644
--- a/src/api/service/twitter.ts
+++ b/src/api/service/twitter.ts
@@ -163,7 +163,7 @@ module.exports = (app: express.Application) => {
 				res.send(`Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`);
 
 				// Publish i updated event
-				event(user._id, 'i_updated', await serialize(user, user, {
+				event(user._id, 'i_updated', await pack(user, user, {
 					detail: true,
 					includeSecrets: true
 				}));
diff --git a/src/api/stream/home.ts b/src/api/stream/home.ts
index 7dcdb5ed7..10078337c 100644
--- a/src/api/stream/home.ts
+++ b/src/api/stream/home.ts
@@ -4,7 +4,7 @@ import * as debug from 'debug';
 
 import User from '../models/user';
 import Mute from '../models/mute';
-import serializePost from '../serializers/post';
+import { pack as packPost } from '../models/post';
 import readNotification from '../common/read-notification';
 
 const log = debug('misskey');
@@ -49,7 +49,7 @@ export default async function(request: websocket.request, connection: websocket.
 			case 'post-stream':
 				const postId = channel.split(':')[2];
 				log(`RECEIVED: ${postId} ${data} by @${user.username}`);
-				const post = await serializePost(postId, user, {
+				const post = await packPost(postId, user, {
 					detail: true
 				});
 				connection.send(JSON.stringify({

From 4b02eedde209ff3b97f44225dd32b17e3365f31d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Feb 2018 04:29:09 +0900
Subject: [PATCH 0149/1250] 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 99bb5272953afd34f7be0305b0c1382b529e50b8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Feb 2018 14:25:19 +0900
Subject: [PATCH 0150/1250] 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 aa3e66be61708b8aa33b222536c2896b7da30025 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 7 Feb 2018 15:16:01 +0900
Subject: [PATCH 0151/1250] 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 17e3bf214e94ea4effa940891c8c6dcf4db2adfc Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 7 Feb 2018 15:34:08 +0900
Subject: [PATCH 0152/1250] 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 84cdb52d42e1c5416f24c9f8c410392e3b25de57 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 0153/1250] 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 ab751459b0690974491a4a8db4f456b223acdf00 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 0154/1250] 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 6fbba53704b006a913a66bedcd6490e27cc270d1 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 0155/1250] 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 def160f1c5c8f32ddf29a77450f483a9cf29cb94 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 0156/1250] 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 6e3d34b900f81ccb6edd0f95a905bd23f966de9f 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 0157/1250] 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 9e5fd1c0202164ac00c5e5896182e3c5a2e13d2e 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 0158/1250] 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 7fa93b737c64ffa6d14eade9aa96c0eb77901d74 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 0159/1250] 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 7fcaa5b929aefa68d1eaddbf4a675ce3bb4c8d0b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 8 Feb 2018 14:02:08 +0900
Subject: [PATCH 0160/1250] 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 700f6356620751a4c2c32ddacf77f83080789144 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 8 Feb 2018 14:03:43 +0900
Subject: [PATCH 0161/1250] 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 0b7d14cc5fa8f3663584c310a5f6fbf9f74b2835 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 8 Feb 2018 14:25:49 +0900
Subject: [PATCH 0162/1250] 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 ab40642681f8abde8e6bbe95a26a9338a759ba9a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 8 Feb 2018 14:50:18 +0900
Subject: [PATCH 0163/1250] 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 fec44ef1bac651c3fe367692e1923de97f645f74 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 8 Feb 2018 14:54:16 +0900
Subject: [PATCH 0164/1250] 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 3f5541b16406bf7f6a7631e9dbbc108463e834c6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 8 Feb 2018 15:07:55 +0900
Subject: [PATCH 0165/1250] 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 d787afbabcffcb4de866dc61e060615164a62b5c 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 0166/1250] 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 dd870d3b847c01a528d2122ba994d8494ceec5a2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Feb 2018 13:11:30 +0900
Subject: [PATCH 0167/1250] 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 2afd658b0fab8e796c9756220b8de2e39de542a1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Feb 2018 13:20:45 +0900
Subject: [PATCH 0168/1250] 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 b5c948004c59c30dc18b0a6c429e7be88d497520 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Feb 2018 13:32:41 +0900
Subject: [PATCH 0169/1250] 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 2f3ae4b1ff1f98fb06f0e9a7b2b4cc51ef78d94c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Feb 2018 18:28:06 +0900
Subject: [PATCH 0170/1250] 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 6da9e01a758867a9e380fa7688d170ff09d30fff Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Feb 2018 18:57:42 +0900
Subject: [PATCH 0171/1250] 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 b6d71c3f4fba2d0009b5dbbf6cb364519ffbad53 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Feb 2018 10:27:05 +0900
Subject: [PATCH 0172/1250] 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 ebcba5eb0493e8d1043d16334c27393ae539a8dc Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Feb 2018 10:32:59 +0900
Subject: [PATCH 0173/1250] 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 e64e6e9117cdd887f65e41560ef14d5b262ab0ed Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Feb 2018 10:52:26 +0900
Subject: [PATCH 0174/1250] 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 97b367f4331a5092efe5448981a1f873008921c3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Feb 2018 14:56:33 +0900
Subject: [PATCH 0175/1250] 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 3912a3ae8440ba3c95e96ba39cca12de9b949cd3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Feb 2018 15:06:11 +0900
Subject: [PATCH 0176/1250] 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 fa126ad038ad6beb304fc9aa717871a7fdf0e8d4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Feb 2018 16:22:14 +0900
Subject: [PATCH 0177/1250] 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 1e070ff4d3f622ce3324f931c1c9855cb3c4699b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Feb 2018 17:01:32 +0900
Subject: [PATCH 0178/1250] 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 2b47b10a317607d75ac63f46ac68d90bcf451fcf Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Feb 2018 19:57:37 +0900
Subject: [PATCH 0179/1250] 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 36794f24e98035dabf9e1033cedbf306b71079bc Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 11 Feb 2018 12:08:43 +0900
Subject: [PATCH 0180/1250] 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 cf2dab03100ad17418d72f562f3b0f5a0563cce7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 11 Feb 2018 12:42:02 +0900
Subject: [PATCH 0181/1250] 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 e68f5e38340c67f3d9063df2dfcf9e83bcf49a4a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 11 Feb 2018 13:02:35 +0900
Subject: [PATCH 0182/1250] 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 245b3f374026d96ab9184b86d57d57860aec89cf Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 11 Feb 2018 13:09:46 +0900
Subject: [PATCH 0183/1250] 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 e27ca176a3b4d2c01d5384a1fc5578609bf382e7 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 0184/1250] 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 13d0d8977bb511ec356065af22da7e258990c422 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 0185/1250] 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 2cd49e658d1b7bfd090f45f22a1befc4004fcb83 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 0186/1250] 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 061f099a9fdb858af54da55a3958454834bb8015 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 0187/1250] 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 1b8468fcb7263ac6c855b7ef6be99555ec0d6d14 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 0188/1250] 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 157c9d0044c2277193a8d3b775c3b55f08dc01cb 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 0189/1250] 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 b94cdf52dd3e313282545f758b8046a20b44da95 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 0190/1250] 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 e09bc57ad1e904867572a803bae64016bcee7171 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 0191/1250] 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 74b80c551b49a193412b988bc887f6fb93ff378e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 11 Feb 2018 23:26:35 +0900
Subject: [PATCH 0192/1250] 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 05bf1d77b57a65a707113982c9ef3c5daebfddf1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 11 Feb 2018 23:35:32 +0900
Subject: [PATCH 0193/1250] 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 e6f3e3c6137f2a9aa5137ec1332eab92a4f7c929 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 12 Feb 2018 00:17:51 +0900
Subject: [PATCH 0194/1250] 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 154975b5a4a73a3ed0d9440197f972989512d76b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 12 Feb 2018 00:23:05 +0900
Subject: [PATCH 0195/1250] 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 88ef55822198ad7064ae54c8d4dec0965c7bdbc3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 12 Feb 2018 00:29:10 +0900
Subject: [PATCH 0196/1250] 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 594bbd65f1c7c6c613fb0a7d72b609cdc83be8bd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 12 Feb 2018 00:41:48 +0900
Subject: [PATCH 0197/1250] 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 b2e7b6ae95277b8f154dbe52f87a16e9c4f0e4b4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 12 Feb 2018 00:58:02 +0900
Subject: [PATCH 0198/1250] 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 fdec160b368479130eafc0b18fb279bf46d59d92 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 12 Feb 2018 01:06:17 +0900
Subject: [PATCH 0199/1250] 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 69374e797327b46db02269c6b093b16499048cfd 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 0200/1250] 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 9c51f1211a7a3e8a11ea0963e17055d7cad8cdac Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 12 Feb 2018 12:21:26 +0900
Subject: [PATCH 0201/1250] 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 16f319fadd3f7a7f920972b65d047cc82b230f30 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 12 Feb 2018 18:49:06 +0900
Subject: [PATCH 0202/1250] 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 40fdd7183b7365cd5281e3321bed9282292f2358 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 12 Feb 2018 19:02:19 +0900
Subject: [PATCH 0203/1250] 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 615062c100d01ee612a5f37c855e06c432eb7b2c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 12 Feb 2018 19:17:47 +0900
Subject: [PATCH 0204/1250] 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 9aea92e7af4c0a69201bcc14908d4b5a4f002cbc 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 0205/1250] 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 55383ed7151a03e933f72b9ef34fc5fb05706574 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 0206/1250] 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 2f7b2be29ea4139e7d5df54e82ba1878d7841a06 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 0207/1250] 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 53750ed72e42d64b0f1d89c1a1f866a18a4e0b36 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 0208/1250] 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 fd7f05ce1dd9c8cef7a1e2689e2e2adb8ed7b537 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 0209/1250] 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 50f10cae641950ab9d1cf4c789db38ce26f0ae64 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 0210/1250] 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 8202dca642eaf93827c39f01bdbd81f809ea9b2e 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 0211/1250] 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 25c0caf64656a13b7b57103633d831fddccc3cb4 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 0212/1250] 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 7358990110b5b1e88afceb2f6470b2556039ad94 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 08:11:10 +0900
Subject: [PATCH 0213/1250] 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 67fe781b5cf93128eb1315c4aae5a399da737a55 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 08:24:44 +0900
Subject: [PATCH 0214/1250] 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 711c606e2a40552c2e9cac689979d4684fffe41c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 08:26:07 +0900
Subject: [PATCH 0215/1250] 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 2c5020bc0e5ac32a4b793eab3293d165c57f7f89 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 08:31:03 +0900
Subject: [PATCH 0216/1250] 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 68dcd12feda26df8dc54b0acbd709363c502cc89 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 09:12:54 +0900
Subject: [PATCH 0217/1250] 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 e159876d3deb690ad253e790d5080a6739160efb Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 09:27:57 +0900
Subject: [PATCH 0218/1250] 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 c1614da6534ea67f62c8001edfdc19f11e049882 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 0219/1250] 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 f7aa0ff8868a4fbb38c4b21e00fd3f09335fa33a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 12:21:02 +0900
Subject: [PATCH 0220/1250] 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 d2bc32b6fd4c2a8b6d4f5fe0a896c63ec8dce563 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 12:32:00 +0900
Subject: [PATCH 0221/1250] 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 367f8973f22a8dc4e2912c4f9a368e2083b61d03 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 12:50:42 +0900
Subject: [PATCH 0222/1250] 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 37e4719d7d252c5fcb9e65576e0461c0533d5276 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 13:18:03 +0900
Subject: [PATCH 0223/1250] 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 3ce5803bb0ef29da0aabe3f2e8660c99754822b2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 13:49:48 +0900
Subject: [PATCH 0224/1250] 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 0a4897f5e7445dcd29748b4852027b3953aafbf4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 14:05:41 +0900
Subject: [PATCH 0225/1250] 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 ac397bf9f9c654de2ea4c92cb153c0e85e774176 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 0226/1250] 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 7305f2fa4bf00f543707cd3d311228887681e008 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 0227/1250] 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 1afa20f751342dd931fc02614f8f987e26179098 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 0228/1250] 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 bd48648dac13f0b641690f4e9554f8a1af07e289 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 0229/1250] 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 e7fd7fda716c08e32c845c0d487dff2dba8f06fd 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 0230/1250] 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 3a71ffe2568ffc8fb0d2eefd45cb560ec3ce1771 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 0231/1250] 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 9ee81d618066603a51add1ff25f2c8af6ec48c56 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 0232/1250] 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 8db9b3e69d3e262e1de72f2147c221b2d52e54cd 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 0233/1250] 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 869aec05d31f60be569621fa090249b39da15a71 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 0234/1250] 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 333b9909ee56040cc107f382a578e11e73350ada 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 0235/1250] 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 15b192abce5523da0cca16c164d5d44954002b4f 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 0236/1250] 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 fe6cc1bb249d3d83e88cd85ffb2da011de058cd7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 20:53:45 +0900
Subject: [PATCH 0237/1250] 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 75349a7f0b7b1617be0f072234001589669fb961 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 21:01:39 +0900
Subject: [PATCH 0238/1250] 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 640da1904276e86e6a69bd77aca1c58d1dcbca42 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 21:04:08 +0900
Subject: [PATCH 0239/1250] 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 cbca7808a93a61a74635c3d28739f69d68ce8d74 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 0240/1250] 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 a5b0fcf738c77cc8d7cc5e94d1cef69daea4034c 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 0241/1250] 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 df0728b61a8e03fd0a5feb45ee3714af7bb83008 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 0242/1250] 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 5ad2d811aaeb75249ecaa709ca080e05a111216e 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 0243/1250] 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 925eb9c1d51ebe17242415fda0929acb55e1b49a 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 0244/1250] 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 3135d92e4f66a89997d84703609647eaf6109973 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 0245/1250] 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 1403a3a19aa518c211bc448fce223e668cdc0fe6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 14 Feb 2018 15:54:18 +0900
Subject: [PATCH 0246/1250] 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 978786983a4131d7d776610f71c54f77c21347a1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 14 Feb 2018 18:01:15 +0900
Subject: [PATCH 0247/1250] 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 9140190931f9bb1b2a7b394ac4da8701413c9d77 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 14 Feb 2018 19:03:48 +0900
Subject: [PATCH 0248/1250] 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 b36d2c68e6954488da723c18c3dfe1e84e2f2187 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 14 Feb 2018 19:21:15 +0900
Subject: [PATCH 0249/1250] 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 a5bfab4792c6c82fe5d313d2e5ebf70e94f87906 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 14 Feb 2018 19:30:35 +0900
Subject: [PATCH 0250/1250] 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 c6d4e052fc07b38f170adaadc5cacaa72675ef3e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 14 Feb 2018 19:33:39 +0900
Subject: [PATCH 0251/1250] 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 edfd968c8e5ac270ac5eb89905eb04e3eaa83240 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 0252/1250] 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 ee6e68caf2fec520a255f1bc3357c9090d975ede 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 0253/1250] 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 291b9404912d2de1a5a17a1aba693c9e64f7b432 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 0254/1250] 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 414cdac1a1f8a53a14747b9ad18243b219408478 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 0255/1250] 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 c7bdf0a9e105b219c346b664b44f1d05d8483866 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 0256/1250] 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 aacdd4697dbb577ce1d1cd31c3c3d761b1a0a174 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 0257/1250] 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 a08c9a6dccea46c5ba68e828fb08b8e56ddc1040 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 0258/1250] 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 666a57f9184e78753f167b4134bfe099dedae327 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 00:32:13 +0900
Subject: [PATCH 0259/1250] 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 3d9376a7e6b804741d5ce78cafff010b9cce635c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 00:33:07 +0900
Subject: [PATCH 0260/1250] 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 8353432bc95404c47594afb59f394ec12031aaad Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 01:07:09 +0900
Subject: [PATCH 0261/1250] 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 84d7e09071ee8d377628fc11e01e443c01e93614 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 01:41:31 +0900
Subject: [PATCH 0262/1250] 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 b3422ba3d3467659f636d2bce193ae3b5f7d9675 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 01:42:11 +0900
Subject: [PATCH 0263/1250] 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 22178da73febb7272c75fd1c9b2b71042fd77731 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 12:36:42 +0900
Subject: [PATCH 0264/1250] 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 0ccd111355d30dcba84bc650c0e328a1e8930015 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 12:44:42 +0900
Subject: [PATCH 0265/1250] 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 6065851537cac759919037b29712c61238d24a03 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 0266/1250] 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 854a8b2dc0c68ec6ce6797563fbd8bb00bdf6ff0 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 0267/1250] 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 37912766967b3ef99ccef3cd525aa193081daa7e 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 0268/1250] 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 4a3c0265b6e18f878c476af4f5bb1309b9b43fef 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 0269/1250] 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 7a4e9fc48ef022d8be9598729dc0d00ff37f896d 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 0270/1250] 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 7f3878453b1d640761517c29d580885d571a0ba9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 15:14:28 +0900
Subject: [PATCH 0271/1250] 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 e46bd0ebdee04eed50a1fbdf58444f2a2b815c48 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 0272/1250] 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 e716efa310697facd9abc75f28d240b36d7205c9 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 0273/1250] 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 5923bf185a5017c57af0bdf263b970142a3b01a5 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 0274/1250] 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 076552fada519ff0aed054a7daf0f24627187375 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 0275/1250] 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 9da73d6c56646e92fa101ca863c06a821742035b 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 0276/1250] 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 5c349bdfa6999116fcee439658e273e195f7ebc8 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 0277/1250] 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 0e06bbe634ff26f5b341ec4e670ed727fb260338 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 17:50:19 +0900
Subject: [PATCH 0278/1250] 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 fa37d73b2230d78a1f7c90f73e9935837e1bbe83 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 18:33:34 +0900
Subject: [PATCH 0279/1250] 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 471bc9555d57c4ba03ff9deb5f329664df93f566 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 18:39:05 +0900
Subject: [PATCH 0280/1250] 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 30640263b1a7c0c1d39f351e48d6ec0bd84f6c73 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 19:59:07 +0900
Subject: [PATCH 0281/1250] 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 1f42df8e69f824cefd6d7d79fb1f362b34be5f57 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 20:00:23 +0900
Subject: [PATCH 0282/1250] 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 7c68263bf08ec0b85808b1cfcf88e18301f2e9f5 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 0283/1250] 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 f6edfa26dde9de5cd1a7d26a5f9aa50cfec9b829 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 23:07:19 +0900
Subject: [PATCH 0284/1250] 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 539944d6f3241b0da0f0a4c6f489b7770d960214 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 02:53:54 +0900
Subject: [PATCH 0285/1250] 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 2c5c9e8ca859691ee72f3b38dbb7853d11d2c6d4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 03:23:10 +0900
Subject: [PATCH 0286/1250] 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 54c21577c9224b407b5a92f4a6cfac503ce9ed3a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 03:26:59 +0900
Subject: [PATCH 0287/1250] 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 05b31ef6b54d0f3e3558063b844b7d34b6dd8f85 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 0288/1250] 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 53534946c42d6e4ff32468845c17920a6852a526 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 0289/1250] 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 cfc228e3b4129b13255a4b4d9b6f23a1cdb7a800 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 0290/1250] 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 052b6f10fb7ec76b7b7b0e8ed5d831867fbdcce0 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 0291/1250] 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 7ac6e8b4c1654cb801df662f2f6b13d045b0e484 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 0292/1250] 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 7a6b8e067f0aa6e2c495308e1fa95bb30ef1ca17 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 0293/1250] 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 39c3c455136072a8e0311b03d4d998c5495ccca3 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 0294/1250] 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 5149eca4bc8fe57c4b91e273471f6e9d48639e05 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 0295/1250] 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 384b2cbe14619e757b366e21be6330bf5767fd4c 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 0296/1250] 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 8fa8eb2a39d52701cae6751b3716287bc92d186f 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 0297/1250] 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 64098554980c5632c838b4140f1f5be8c4fd7500 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 0298/1250] 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 5ac643bb41c9bd383ab7348cab94174bbebd0ffd 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 0299/1250] 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 ce5e1c1917c10c1627ae0c1f442b8dd9979c4f01 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 0300/1250] 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 f4749d236bb313a66d15f19167fdf308e896dcb3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 15:38:12 +0900
Subject: [PATCH 0301/1250] 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 b2d4eeb43bc75b09155d9a4a8d8109c109a5b16c 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 0302/1250] 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 9cd844bf1766b08ae789e3d38ce3894fbe57cb99 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 0303/1250] 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 ff4a18da6ed25450c07b5caad82750f7910611e2 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 0304/1250] 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 5d2d9c000c52cf47e39846c50810e1701a0c8423 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 0305/1250] 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 91f475bb04ae8573761da9926a60dc6398c87791 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 0306/1250] 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 5d346685feb829285bf9bc82c7faae793a00aa9f 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 0307/1250] 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 3c540728d050ec6d09575502b0f427b0bbbb0171 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 0308/1250] 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 be9feccb4f238353359d8036a25ce79fd690d6e1 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 0309/1250] 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 60a44f4e5eb59dd080d3f1dd29a684eaad6d7388 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 0310/1250] 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 4d95a9ffe69e924cc2222090ae2701778bf71590 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 0311/1250] 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 c32ca37376ca33c660a18359a9f148f806d5af00 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 0312/1250] 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 51eae4a86ba96b3778d63285622bbda96607b6cb 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 0313/1250] 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 e0c43393651fc9bf3fcbb2a2d8e8b190b9fa013e 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 0314/1250] 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 d1db2512da8f4fcc42ec0f946c85d98f5be92146 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 0315/1250] 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 9098389794e27a5cd969b5b2097b4aeecf8825a2 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 0316/1250] 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 9387f408b729bc0872dab9b2f3f27c47e5ec9902 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 17 Feb 2018 02:24:10 +0900
Subject: [PATCH 0317/1250] 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 9e9a72232452f79ee0ee78e2dee89d4c98a5dba4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 17 Feb 2018 03:01:00 +0900
Subject: [PATCH 0318/1250] 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 17d0ab17b8e5a92d5bb657e2fea7a8138b7bbaf0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 17 Feb 2018 03:18:48 +0900
Subject: [PATCH 0319/1250] 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 dc6796b07c0ea51c33b1d9915174775c50a11e35 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 17 Feb 2018 03:53:21 +0900
Subject: [PATCH 0320/1250] 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 94a16c5189b6cc3a3608735ecfc81de7aa3800c3 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 0321/1250] 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 e298805ace559997eb00a59c3de54421998b81ae 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 0322/1250] 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 fe908c9762f66a8ea348dac8caf7b8416d7d324f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 17 Feb 2018 18:14:23 +0900
Subject: [PATCH 0323/1250] 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 b730e33187019397dea44e5b116e95c18aeb2000 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Feb 2018 12:35:18 +0900
Subject: [PATCH 0324/1250] 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 6c5bd67c49c10af49e03871a0f96bb6ae53cf009 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Feb 2018 12:37:18 +0900
Subject: [PATCH 0325/1250] 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 7ba0026935deb4175b9920a35489fcdcdb4e5ece Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Feb 2018 13:48:40 +0900
Subject: [PATCH 0326/1250] 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 ab7d98830885cde759823a17e3e2dca16331021f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Feb 2018 15:27:06 +0900
Subject: [PATCH 0327/1250] 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 c0a7cefe4659428adfeb0bfc7ab8e93986543405 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 18 Feb 2018 16:38:07 +0900
Subject: [PATCH 0328/1250] 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 51a0cc6435e836db08b28ff92142d78fcdb727fe Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Feb 2018 18:40:24 +0900
Subject: [PATCH 0329/1250] 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 1c72c109ce23c75a925e9ad8932c50032b95423e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Feb 2018 18:46:33 +0900
Subject: [PATCH 0330/1250] 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 aefb0f3833a8a34b495e087a7d04a79ee3b825b5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Feb 2018 18:49:44 +0900
Subject: [PATCH 0331/1250] 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 af0c4b3daf18d7c7a365324a7975ea839a3e9fff Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Feb 2018 22:14:51 +0900
Subject: [PATCH 0332/1250] 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 4bbe6efb7105e7aed1e1f827ee6359a3a02b1ddd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Feb 2018 22:16:36 +0900
Subject: [PATCH 0333/1250] 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 f0593a357c0f715c32cb8f615e6b087a3323fbba Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Feb 2018 23:51:41 +0900
Subject: [PATCH 0334/1250] 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 856d9470eb3fa4aa71fd23ee414ee515b04a5685 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Feb 2018 23:56:25 +0900
Subject: [PATCH 0335/1250] 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 1e53407bde26dedabdab1f32f6cdc537680e7710 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Feb 2018 00:18:01 +0900
Subject: [PATCH 0336/1250] 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 cedb4aa07f0409f7fc81f7bd6cb00e3d08249dfe Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Feb 2018 00:21:18 +0900
Subject: [PATCH 0337/1250] 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 177988783f4ce0dd27fdb27914cb4b40050517f7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Feb 2018 00:38:49 +0900
Subject: [PATCH 0338/1250] wip

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

diff --git a/.gitattributes b/.gitattributes
index 139529262..e76630ea9 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,8 +1,6 @@
 *.svg -diff -text
 *.psd -diff -text
 *.ai -diff -text
-
-*.tag linguist-language=HTML
 *.zip filter=lfs diff=lfs merge=lfs -text
 *.xcf filter=lfs diff=lfs merge=lfs -text
 *.ai filter=lfs diff=lfs merge=lfs -text

From 451c3d537841a734dcf1e3ba1dcbdd87ef3a3689 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Feb 2018 14:29:42 +0900
Subject: [PATCH 0339/1250] 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 0bb7f979907dc8b9545ae7d3ff0cce893d9e2c4c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Feb 2018 15:55:17 +0900
Subject: [PATCH 0340/1250] 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 1637984308f212968fc911cd5870977a3e32aae2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Feb 2018 16:08:41 +0900
Subject: [PATCH 0341/1250] 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 32993e12e37bcf8bf6a4a42d49c2820bc9a7d877 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Feb 2018 16:18:18 +0900
Subject: [PATCH 0342/1250] 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 11bb87dd337ec16e9a03dd6214755ffb715cc4d5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Feb 2018 16:58:37 +0900
Subject: [PATCH 0343/1250] 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 2c179e4e024c112ddcbc02de57d278ff6254c947 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Feb 2018 17:03:22 +0900
Subject: [PATCH 0344/1250] 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 c8e203b909d27b7836d0fe035511db75601ee7a9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Feb 2018 18:26:20 +0900
Subject: [PATCH 0345/1250] 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 f40a8739d785e24b4499e73efb35846ee329f6a3 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 19 Feb 2018 19:46:31 +0900
Subject: [PATCH 0346/1250] 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 0998043909c3012dca90872b4d1f9ae8fb619c13 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Feb 2018 23:37:09 +0900
Subject: [PATCH 0347/1250] 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 c249c7fea226da08039046ac55ecc041236d27e7 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 0348/1250] 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 70a793dadd3823c550f88d7c348cbc40a1a1be7c 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 0349/1250] 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 523069f82ee5fca12361fb9358e9b2e1909ac1b0 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 0350/1250] 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 ad23d49ab76d4c056eb6f782c7703930b3266f62 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 0351/1250] 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 50e59878b6a25f78073cbeb9d2b0a3c8a84a0803 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 0352/1250] 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 f3460a3a2db9f623721565e9b8965eb7ea9778b2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 07:56:39 +0900
Subject: [PATCH 0353/1250] 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 7db938be69f5bed52f3891ab2dffd3f9670e3f00 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 07:58:21 +0900
Subject: [PATCH 0354/1250] 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 c45c2505c6fdb0b4e641d9f6d5b456a09e7015e7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 08:05:41 +0900
Subject: [PATCH 0355/1250] 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 a3e1faa79d89c364c399a6d0728a8398aaab8a24 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 08:14:44 +0900
Subject: [PATCH 0356/1250] 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 d16ad6ddca5a861b8a2a260b62cf76b95baabdb6 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 0357/1250] 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 762af99d5f4b3805c1d0a74741325712a0f80808 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 0358/1250] 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 a168037ef2b5863d30929170a64637f63405e65f 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 0359/1250] 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 9d9e4b93f7a9da99478ebc0a4fa6953d423393f0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 12:38:45 +0900
Subject: [PATCH 0360/1250] 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 228e21ec2ec26e05988cf7eba4eccab1fea7c07a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 13:02:53 +0900
Subject: [PATCH 0361/1250] 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 61dcbac17704ccd5108a7b8a0a689761fddf77f8 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 0362/1250] 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 cc48ffdd8e7e509c88a0e4a5afb8e4a8304dbed1 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 0363/1250] 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 e0e3629afec8dd61031c8b277c2d0edc39d393b9 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 0364/1250] 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 ebba62e7f39a44855dfcdc7694eaa461efb1b8a7 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 0365/1250] 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 5b24bc7fcc7e024dab8da580c40175a4107a559e 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 0366/1250] 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 5d4083cf39e36a2c74bdf3f3bb2e6a988b137582 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 0367/1250] 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 f1958f8b33b41284d75dac4b02a3dfde4a7600fb Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 22:31:12 +0900
Subject: [PATCH 0368/1250] 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 1029a93f54c137938c38fed248bc290dcbba67c0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 22:53:34 +0900
Subject: [PATCH 0369/1250] 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 b108ba31d4c78eded8c209c3e4303ad3d936e391 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 23:02:24 +0900
Subject: [PATCH 0370/1250] 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 a35575438c7b2a093b088e86c8a38982ae07f278 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 23:12:29 +0900
Subject: [PATCH 0371/1250] 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 2f3cbd236c2e8cc583d1f9bb907c677d30953902 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 23:17:04 +0900
Subject: [PATCH 0372/1250] 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 821e8e22a22f831886fa22ab818adee115d358fe Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 23:22:19 +0900
Subject: [PATCH 0373/1250] 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 8baf46fc34716443232bd56ec2312bd0baebca0f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 23:26:41 +0900
Subject: [PATCH 0374/1250] 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 6cbfa924ff01eff1e59525e67d0e83e6c05c129c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 23:37:35 +0900
Subject: [PATCH 0375/1250] 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 29670194b09d680642139c49e5e20d3a09fbe74e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 21 Feb 2018 01:12:18 +0900
Subject: [PATCH 0376/1250] 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 bc40f3286b9411da13cafab456acf7c1d69f32d2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 21 Feb 2018 01:14:25 +0900
Subject: [PATCH 0377/1250] 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 3b8608da40e62db9672518316ab83d20adebf6c8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 21 Feb 2018 01:23:25 +0900
Subject: [PATCH 0378/1250] 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 4553ab84b447622f07a126ee74cc5e46a0c4c02c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 21 Feb 2018 01:39:51 +0900
Subject: [PATCH 0379/1250] 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 1a7a70c89e087dfff34ac4bba1619d94b1297190 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 21 Feb 2018 02:53:34 +0900
Subject: [PATCH 0380/1250] 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 4d6a2b42259ed779f79b3f2d7d4d41252b93b93c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 21 Feb 2018 02:55:22 +0900
Subject: [PATCH 0381/1250] 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 ebad9dec46b3be0f368676f24d1aee8984705745 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 21 Feb 2018 04:52:52 +0900
Subject: [PATCH 0382/1250] 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 91d8a0d22e3df9beffe0ccaf6b6ffe2744983bdd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 21 Feb 2018 05:55:19 +0900
Subject: [PATCH 0383/1250] 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 73820245f6260c4bfb4ef7e6b708a35598f9f184 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 0384/1250] 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 ff5b9a46d3a60087cebc83e339cb907e41a963e6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 21 Feb 2018 15:30:03 +0900
Subject: [PATCH 0385/1250] 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 1222e9f91108ed4eb39bc8ed7194f1cfa7dfc423 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 21 Feb 2018 15:30:56 +0900
Subject: [PATCH 0386/1250] 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 dddf37e400b6459c496de0773e214da6860c2205 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 0387/1250] 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 a9ee85b12da29e3d218073a02f8e9f392015ef76 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 0388/1250] 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 f39a777af1d90ed5d89a0ce8e0b355a2e226c2d5 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 0389/1250] 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 9cd086ce2f3ee233935365117ec36ea7c5cf4588 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 0390/1250] 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 7f0c66af3c220cd05fcea2ef698fc296e60d186b 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 0391/1250] 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 bc5e11ce083d77e59b34cbc4a489ab7db67f67b5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 00:07:37 +0900
Subject: [PATCH 0392/1250] 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 5252be0998d40f711d2695a19fa93d72e94835b0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 00:14:20 +0900
Subject: [PATCH 0393/1250] 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 2563e769a615967cbdffe41b74c626df987ca7ab Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 00:18:19 +0900
Subject: [PATCH 0394/1250] 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 12614eb649d2b23ce3f5c09716c71813eeaa1f09 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 00:21:07 +0900
Subject: [PATCH 0395/1250] 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 8840ef7c616cc0556e045be93d99ade1a2429908 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 01:08:49 +0900
Subject: [PATCH 0396/1250] 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 c2bf638a8c27e5c9ef3d5435f56d2f0e330d8dae Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 01:25:57 +0900
Subject: [PATCH 0397/1250] 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 77c8257f05a46153158e59fa8b1a638ee4e6d8df Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 02:00:30 +0900
Subject: [PATCH 0398/1250] 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 ed24decdaba57187c52138f2fd8c85c405b469a9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 02:15:46 +0900
Subject: [PATCH 0399/1250] 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 e55917d0667bd5ec15dc466e57e9e24c7b5bd21e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 02:22:10 +0900
Subject: [PATCH 0400/1250] 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 bbd36ba4edd28d06fab4e584c7801b4138f1ee47 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 02:37:04 +0900
Subject: [PATCH 0401/1250] 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 93314b48a2f5336094682b0c388150bf4602239a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 02:41:29 +0900
Subject: [PATCH 0402/1250] 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 f415e07386be4a22bb13c261a2c80603d07bc560 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 03:11:24 +0900
Subject: [PATCH 0403/1250] 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 4b5ddf4d4f57a443c41b6f5fe8b1f09a8040c046 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 05:05:19 +0900
Subject: [PATCH 0404/1250] 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 55da30778a916f427fcc287aa0d223ca182b009b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 05:16:38 +0900
Subject: [PATCH 0405/1250] 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 681e609833b83d3e15a421f3a1751f1c0e14cfb8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 05:30:37 +0900
Subject: [PATCH 0406/1250] 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 1a968fd997141596f95a9df94f6ba14b1efca74f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 05:57:24 +0900
Subject: [PATCH 0407/1250] 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 60acc386c0c00da0519e4878b62d672268266701 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 06:13:38 +0900
Subject: [PATCH 0408/1250] 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 d3bff0c0ffad6706c5b63214b2bf974fb37447a9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 06:17:02 +0900
Subject: [PATCH 0409/1250] 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 cd1802c2d78bd5a72e32573804add346c7f697a7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 07:06:47 +0900
Subject: [PATCH 0410/1250] 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 77a268a20ddb0236210059b660ad4c4fe96bba4c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 17:06:19 +0900
Subject: [PATCH 0411/1250] 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 ef679409b4243a5455524861da1118717769015f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 17:32:58 +0900
Subject: [PATCH 0412/1250] 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 1414a43d9b4f4f1967830e5e66757844b7502736 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 17:37:40 +0900
Subject: [PATCH 0413/1250] 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 84c69ffb9b379d47eacb36812bfbf7534707d5a1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 17:38:48 +0900
Subject: [PATCH 0414/1250] 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 5a320c580a9216ac987d8addf2ace492932a2f85 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 17:51:08 +0900
Subject: [PATCH 0415/1250] 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 e4ebda0b5345dc633ce463f7921adc3d9f75e887 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 17:57:14 +0900
Subject: [PATCH 0416/1250] 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 09d8e2d90ead962abace55469a4d30c871bb09b9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 18:06:32 +0900
Subject: [PATCH 0417/1250] 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 e18fdd854a5fde30a8041db5dfcc1502fe3da29b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 18:37:47 +0900
Subject: [PATCH 0418/1250] 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 1e158599946cdf28e2ebb9bc7d80c190566b6fea Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 20:52:39 +0900
Subject: [PATCH 0419/1250] 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 bb2b30e6c433abb29a479e10bcaca145294f3cc7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 21:15:24 +0900
Subject: [PATCH 0420/1250] 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 adff8423d3b52625676661d3a54cb3b96168eb9a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 21:23:10 +0900
Subject: [PATCH 0421/1250] 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 0314de31f4e519ebb87b0f3e17e2b32b0a2e272c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 21:29:04 +0900
Subject: [PATCH 0422/1250] 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 ee62919035f98f7eabc9e6c221fef7428173e5b7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 21:39:36 +0900
Subject: [PATCH 0423/1250] 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 bbcdb0725e0ee3ef8d77a9ae1ee2e4acf83ac55b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 22:03:44 +0900
Subject: [PATCH 0424/1250] 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 0ac099ba33b7d1fd93b33973391f794a4f8a305a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 22:27:28 +0900
Subject: [PATCH 0425/1250] 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 101a98f61ef62c9bde0904c96d49b19aa4520dca Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 22:38:14 +0900
Subject: [PATCH 0426/1250] 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 2dd30657e9ab1ebb88229c5f873832fd51c34765 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 22:51:33 +0900
Subject: [PATCH 0427/1250] 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 067159723e2742e4cf663adb210fcebf7ed9962f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 23:05:05 +0900
Subject: [PATCH 0428/1250] 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 f5cee1658c57a7f44ec718147fa91de3fd401842 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 23:53:07 +0900
Subject: [PATCH 0429/1250] 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 7335197eee48fb144964aba6985850d181694672 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 01:11:09 +0900
Subject: [PATCH 0430/1250] 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 e62d9876069a383d0fcc478ed7a6a0451175c654 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 01:19:51 +0900
Subject: [PATCH 0431/1250] 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 1214827ed44f25a77176ec980805b9b36be1ddf1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 01:27:02 +0900
Subject: [PATCH 0432/1250] 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 65edf859035cfa8f903f1e364ef5a3acb7083f68 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 02:06:35 +0900
Subject: [PATCH 0433/1250] 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 ee3de0c32500f0b7eb50c51f706c229d712f9060 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 02:09:48 +0900
Subject: [PATCH 0434/1250] 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>

From f5ae8ba88ad6051fb03142cf4009905bcf92417e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 02:16:17 +0900
Subject: [PATCH 0435/1250] clean

---
 package.json | 34 +++++++++++++++++-----------------
 1 file changed, 17 insertions(+), 17 deletions(-)

diff --git a/package.json b/package.json
index 4521b0ceb..8924391e4 100644
--- a/package.json
+++ b/package.json
@@ -52,7 +52,7 @@
 		"@types/is-root": "1.0.0",
 		"@types/is-url": "1.2.28",
 		"@types/js-yaml": "3.10.1",
-		"@types/license-checker": "^15.0.0",
+		"@types/license-checker": "15.0.0",
 		"@types/mkdirp": "0.5.2",
 		"@types/mocha": "2.2.47",
 		"@types/mongodb": "3.0.5",
@@ -83,7 +83,7 @@
 		"autwh": "0.0.1",
 		"bcryptjs": "2.4.3",
 		"body-parser": "1.18.2",
-		"cache-loader": "^1.2.0",
+		"cache-loader": "1.2.0",
 		"cafy": "3.2.1",
 		"chai": "4.1.2",
 		"chai-http": "3.0.0",
@@ -99,8 +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",
+		"eslint": "4.18.0",
+		"eslint-plugin-vue": "4.2.2",
 		"eventemitter3": "3.0.0",
 		"exif-js": "2.3.0",
 		"express": "4.16.2",
@@ -122,13 +122,13 @@
 		"gulp-util": "3.0.8",
 		"hard-source-webpack-plugin": "0.6.0-alpha.8",
 		"highlight.js": "9.12.0",
-		"html-minifier": "^3.5.9",
+		"html-minifier": "3.5.9",
 		"inquirer": "5.0.1",
 		"is-root": "1.0.0",
 		"is-url": "1.2.2",
 		"js-yaml": "3.10.0",
 		"license-checker": "16.0.0",
-		"loader-utils": "^1.1.0",
+		"loader-utils": "1.1.0",
 		"mecab-async": "0.1.2",
 		"mkdirp": "0.5.1",
 		"mocha": "5.0.0",
@@ -161,7 +161,7 @@
 		"serve-favicon": "2.4.5",
 		"sortablejs": "1.7.0",
 		"speakeasy": "2.0.0",
-		"string-replace-loader": "^1.3.0",
+		"string-replace-loader": "1.3.0",
 		"string-replace-webpack-plugin": "0.1.3",
 		"style-loader": "0.20.1",
 		"stylus": "0.54.5",
@@ -172,25 +172,25 @@
 		"tcp-port-used": "0.1.2",
 		"textarea-caret": "3.0.2",
 		"tmp": "0.0.33",
-		"ts-loader": "^3.5.0",
+		"ts-loader": "3.5.0",
 		"ts-node": "4.1.0",
 		"tslint": "5.9.1",
 		"typescript": "2.7.1",
-		"typescript-eslint-parser": "^13.0.0",
+		"typescript-eslint-parser": "13.0.0",
 		"uglify-es": "3.3.9",
 		"uglifyjs-webpack-plugin": "1.1.8",
 		"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",
-		"vue-template-compiler": "^2.5.13",
-		"vuedraggable": "^2.16.0",
+		"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",
+		"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",
+		"webpack-replace-loader": "1.3.0",
 		"websocket": "1.0.25",
 		"xev": "2.0.0"
 	}

From 9c6f4f94db60420d04efc31602e215cbaa5749b1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 02:24:01 +0900
Subject: [PATCH 0436/1250] Update dependencises :rocket:

---
 package.json | 42 +++++++++++++++++++++---------------------
 1 file changed, 21 insertions(+), 21 deletions(-)

diff --git a/package.json b/package.json
index 8924391e4..bae86f9e5 100644
--- a/package.json
+++ b/package.json
@@ -31,7 +31,7 @@
 		"@types/bcryptjs": "2.4.1",
 		"@types/body-parser": "1.16.8",
 		"@types/chai": "4.1.2",
-		"@types/chai-http": "3.0.3",
+		"@types/chai-http": "3.0.4",
 		"@types/compression": "0.0.35",
 		"@types/cookie": "0.3.1",
 		"@types/cors": "2.8.3",
@@ -54,17 +54,17 @@
 		"@types/js-yaml": "3.10.1",
 		"@types/license-checker": "15.0.0",
 		"@types/mkdirp": "0.5.2",
-		"@types/mocha": "2.2.47",
+		"@types/mocha": "2.2.48",
 		"@types/mongodb": "3.0.5",
 		"@types/monk": "6.0.0",
 		"@types/morgan": "1.7.35",
 		"@types/ms": "0.7.30",
 		"@types/multer": "1.3.6",
-		"@types/node": "9.4.0",
+		"@types/node": "9.4.6",
 		"@types/page": "1.5.32",
 		"@types/proxy-addr": "2.0.0",
 		"@types/pug": "2.0.4",
-		"@types/qrcode": "0.8.0",
+		"@types/qrcode": "0.8.1",
 		"@types/ratelimiter": "2.1.28",
 		"@types/redis": "2.8.5",
 		"@types/request": "2.47.0",
@@ -75,9 +75,9 @@
 		"@types/speakeasy": "2.0.2",
 		"@types/tmp": "0.0.33",
 		"@types/uuid": "3.4.3",
-		"@types/webpack": "3.8.4",
-		"@types/webpack-stream": "3.2.8",
-		"@types/websocket": "0.0.36",
+		"@types/webpack": "3.8.8",
+		"@types/webpack-stream": "3.2.9",
+		"@types/websocket": "0.0.37",
 		"accesses": "2.5.0",
 		"animejs": "2.2.0",
 		"autwh": "0.0.1",
@@ -87,8 +87,8 @@
 		"cafy": "3.2.1",
 		"chai": "4.1.2",
 		"chai-http": "3.0.0",
-		"chalk": "2.3.0",
-		"compression": "1.7.1",
+		"chalk": "2.3.1",
+		"compression": "1.7.2",
 		"cookie": "0.3.1",
 		"cors": "2.8.4",
 		"cropperjs": "1.2.2",
@@ -101,10 +101,10 @@
 		"escape-regexp": "0.0.1",
 		"eslint": "4.18.0",
 		"eslint-plugin-vue": "4.2.2",
-		"eventemitter3": "3.0.0",
+		"eventemitter3": "3.0.1",
 		"exif-js": "2.3.0",
 		"express": "4.16.2",
-		"file-type": "7.5.0",
+		"file-type": "7.6.0",
 		"fuckadblock": "3.2.1",
 		"gm": "1.23.1",
 		"gulp": "3.9.1",
@@ -116,14 +116,14 @@
 		"gulp-rename": "1.2.2",
 		"gulp-replace": "0.6.1",
 		"gulp-stylus": "2.7.0",
-		"gulp-tslint": "8.1.2",
+		"gulp-tslint": "8.1.3",
 		"gulp-typescript": "3.2.4",
 		"gulp-uglify": "3.0.0",
 		"gulp-util": "3.0.8",
 		"hard-source-webpack-plugin": "0.6.0-alpha.8",
 		"highlight.js": "9.12.0",
 		"html-minifier": "3.5.9",
-		"inquirer": "5.0.1",
+		"inquirer": "5.1.0",
 		"is-root": "1.0.0",
 		"is-url": "1.2.2",
 		"js-yaml": "3.10.0",
@@ -131,7 +131,7 @@
 		"loader-utils": "1.1.0",
 		"mecab-async": "0.1.2",
 		"mkdirp": "0.5.1",
-		"mocha": "5.0.0",
+		"mocha": "5.0.1",
 		"moji": "0.5.1",
 		"mongodb": "3.0.2",
 		"monk": "6.0.5",
@@ -141,9 +141,9 @@
 		"nprogress": "0.2.0",
 		"os-utils": "0.0.14",
 		"page": "1.8.3",
-		"pictograph": "2.1.5",
+		"pictograph": "2.2.0",
 		"prominence": "0.2.0",
-		"proxy-addr": "2.0.2",
+		"proxy-addr": "2.0.3",
 		"pug": "2.0.0-rc.4",
 		"qrcode": "1.2.0",
 		"ratelimiter": "3.0.3",
@@ -163,22 +163,22 @@
 		"speakeasy": "2.0.0",
 		"string-replace-loader": "1.3.0",
 		"string-replace-webpack-plugin": "0.1.3",
-		"style-loader": "0.20.1",
+		"style-loader": "0.20.2",
 		"stylus": "0.54.5",
 		"stylus-loader": "3.0.1",
 		"summaly": "2.0.3",
 		"swagger-jsdoc": "1.9.7",
 		"syuilo-password-strength": "0.0.1",
 		"tcp-port-used": "0.1.2",
-		"textarea-caret": "3.0.2",
+		"textarea-caret": "3.1.0",
 		"tmp": "0.0.33",
 		"ts-loader": "3.5.0",
 		"ts-node": "4.1.0",
 		"tslint": "5.9.1",
-		"typescript": "2.7.1",
+		"typescript": "2.7.2",
 		"typescript-eslint-parser": "13.0.0",
 		"uglify-es": "3.3.9",
-		"uglifyjs-webpack-plugin": "1.1.8",
+		"uglifyjs-webpack-plugin": "1.2.0",
 		"uuid": "3.2.1",
 		"vhost": "3.0.2",
 		"vue": "2.5.13",
@@ -189,7 +189,7 @@
 		"vue-template-compiler": "2.5.13",
 		"vuedraggable": "2.16.0",
 		"web-push": "3.2.5",
-		"webpack": "3.10.0",
+		"webpack": "3.11.0",
 		"webpack-replace-loader": "1.3.0",
 		"websocket": "1.0.25",
 		"xev": "2.0.0"

From 4586dde9b8169faba30ca8369776f628b3d0928d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 02:28:55 +0900
Subject: [PATCH 0437/1250] Clean up

---
 package.json             | 8 --------
 webpack/plugins/index.ts | 3 ---
 2 files changed, 11 deletions(-)

diff --git a/package.json b/package.json
index bae86f9e5..6ec851249 100644
--- a/package.json
+++ b/package.json
@@ -61,7 +61,6 @@
 		"@types/ms": "0.7.30",
 		"@types/multer": "1.3.6",
 		"@types/node": "9.4.6",
-		"@types/page": "1.5.32",
 		"@types/proxy-addr": "2.0.0",
 		"@types/pug": "2.0.4",
 		"@types/qrcode": "0.8.1",
@@ -91,7 +90,6 @@
 		"compression": "1.7.2",
 		"cookie": "0.3.1",
 		"cors": "2.8.4",
-		"cropperjs": "1.2.2",
 		"css-loader": "0.28.9",
 		"debug": "3.1.0",
 		"deep-equal": "1.0.1",
@@ -120,7 +118,6 @@
 		"gulp-typescript": "3.2.4",
 		"gulp-uglify": "3.0.0",
 		"gulp-util": "3.0.8",
-		"hard-source-webpack-plugin": "0.6.0-alpha.8",
 		"highlight.js": "9.12.0",
 		"html-minifier": "3.5.9",
 		"inquirer": "5.1.0",
@@ -140,7 +137,6 @@
 		"multer": "1.3.0",
 		"nprogress": "0.2.0",
 		"os-utils": "0.0.14",
-		"page": "1.8.3",
 		"pictograph": "2.2.0",
 		"prominence": "0.2.0",
 		"proxy-addr": "2.0.3",
@@ -150,7 +146,6 @@
 		"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",
@@ -159,10 +154,7 @@
 		"s-age": "1.1.2",
 		"seedrandom": "2.4.3",
 		"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.2",
 		"stylus": "0.54.5",
 		"stylus-loader": "3.0.1",
diff --git a/webpack/plugins/index.ts b/webpack/plugins/index.ts
index 027f60224..56e73e504 100644
--- a/webpack/plugins/index.ts
+++ b/webpack/plugins/index.ts
@@ -1,5 +1,3 @@
-const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
-
 import consts from './consts';
 import hoist from './hoist';
 import minify from './minify';
@@ -9,7 +7,6 @@ const isProduction = env === 'production';
 
 export default (version, lang) => {
 	const plugins = [
-		//new HardSourceWebpackPlugin(),
 		consts(lang)
 	];
 

From 8a5e416f2dd628ea1b6f79b87b1ececebcfb672e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 02:30:52 +0900
Subject: [PATCH 0438/1250] :v:

---
 src/web/app/common/scripts/check-for-update.ts | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/src/web/app/common/scripts/check-for-update.ts b/src/web/app/common/scripts/check-for-update.ts
index 0b58c0a67..0855676a4 100644
--- a/src/web/app/common/scripts/check-for-update.ts
+++ b/src/web/app/common/scripts/check-for-update.ts
@@ -1,11 +1,10 @@
 import MiOS from '../mios';
-
-declare const _VERSION_: string;
+import { version } from '../../config';
 
 export default async function(mios: MiOS) {
 	const meta = await mios.getMeta();
 
-	if (meta.version != _VERSION_) {
+	if (meta.version != version) {
 		localStorage.setItem('should-refresh', 'true');
 
 		// Clear cache (serive worker)
@@ -19,6 +18,6 @@ export default async function(mios: MiOS) {
 			console.error(e);
 		}
 
-		alert('%i18n:common.update-available%'.replace('{newer}', meta.version).replace('{current}', _VERSION_));
+		alert('%i18n:common.update-available%'.replace('{newer}', meta.version).replace('{current}', version));
 	}
 }

From 6525025fcc21a4b4e9082b614971592f263d0b96 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 02:53:36 +0900
Subject: [PATCH 0439/1250] :v:

---
 src/web/app/ch/router.ts  | 32 -----------------------------
 src/web/app/ch/script.ts  |  3 ---
 src/web/app/dev/router.ts | 42 ---------------------------------------
 src/web/app/dev/script.ts |  3 ---
 4 files changed, 80 deletions(-)
 delete mode 100644 src/web/app/ch/router.ts
 delete mode 100644 src/web/app/dev/router.ts

diff --git a/src/web/app/ch/router.ts b/src/web/app/ch/router.ts
deleted file mode 100644
index f10c4acdf..000000000
--- a/src/web/app/ch/router.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import * as riot from 'riot';
-import * as route from 'page';
-let page = null;
-
-export default () => {
-	route('/',         index);
-	route('/:channel', channel);
-	route('*',         notFound);
-
-	function index() {
-		mount(document.createElement('mk-index'));
-	}
-
-	function channel(ctx) {
-		const el = document.createElement('mk-channel');
-		el.setAttribute('id', ctx.params.channel);
-		mount(el);
-	}
-
-	function notFound() {
-		mount(document.createElement('mk-not-found'));
-	}
-
-	// EXEC
-	(route as any)();
-};
-
-function mount(content) {
-	if (page) page.unmount();
-	const body = document.getElementById('app');
-	page = riot.mount(body.appendChild(content))[0];
-}
diff --git a/src/web/app/ch/script.ts b/src/web/app/ch/script.ts
index e23558037..4c6b6dfd1 100644
--- a/src/web/app/ch/script.ts
+++ b/src/web/app/ch/script.ts
@@ -7,12 +7,9 @@ import './style.styl';
 
 require('./tags');
 import init from '../init';
-import route from './router';
 
 /**
  * init
  */
 init(() => {
-	// Start routing
-	route();
 });
diff --git a/src/web/app/dev/router.ts b/src/web/app/dev/router.ts
deleted file mode 100644
index fcd2b1f76..000000000
--- a/src/web/app/dev/router.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import * as riot from 'riot';
-import * as route from 'page';
-let page = null;
-
-export default () => {
-	route('/',         index);
-	route('/apps',     apps);
-	route('/app/new',  newApp);
-	route('/app/:app', app);
-	route('*',         notFound);
-
-	function index() {
-		mount(document.createElement('mk-index'));
-	}
-
-	function apps() {
-		mount(document.createElement('mk-apps-page'));
-	}
-
-	function newApp() {
-		mount(document.createElement('mk-new-app-page'));
-	}
-
-	function app(ctx) {
-		const el = document.createElement('mk-app-page');
-		el.setAttribute('app', ctx.params.app);
-		mount(el);
-	}
-
-	function notFound() {
-		mount(document.createElement('mk-not-found'));
-	}
-
-	// EXEC
-	(route as any)();
-};
-
-function mount(content) {
-	if (page) page.unmount();
-	const body = document.getElementById('app');
-	page = riot.mount(body.appendChild(content))[0];
-}
diff --git a/src/web/app/dev/script.ts b/src/web/app/dev/script.ts
index b115c5be4..bb4341119 100644
--- a/src/web/app/dev/script.ts
+++ b/src/web/app/dev/script.ts
@@ -7,12 +7,9 @@ import './style.styl';
 
 require('./tags');
 import init from '../init';
-import route from './router';
 
 /**
  * init
  */
 init(() => {
-	// Start routing
-	route();
 });

From f20cb7880f1caf7b5f313259f014d6e1edb69122 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 22 Feb 2018 17:54:21 +0000
Subject: [PATCH 0440/1250] fix(package): update css-loader to version 0.28.10

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 6ec851249..37ef587f1 100644
--- a/package.json
+++ b/package.json
@@ -90,7 +90,7 @@
 		"compression": "1.7.2",
 		"cookie": "0.3.1",
 		"cors": "2.8.4",
-		"css-loader": "0.28.9",
+		"css-loader": "0.28.10",
 		"debug": "3.1.0",
 		"deep-equal": "1.0.1",
 		"deepcopy": "0.6.3",

From 442e0507d504c35ab478cda57a51621ba229b084 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 03:33:12 +0900
Subject: [PATCH 0441/1250] :v:

---
 src/web/app/animation.styl                    | 12 ++++
 src/web/app/app.styl                          |  1 +
 .../views/components/ui.header.account.vue    | 56 +++++++++++--------
 3 files changed, 46 insertions(+), 23 deletions(-)
 create mode 100644 src/web/app/animation.styl

diff --git a/src/web/app/animation.styl b/src/web/app/animation.styl
new file mode 100644
index 000000000..8f121b313
--- /dev/null
+++ b/src/web/app/animation.styl
@@ -0,0 +1,12 @@
+.zoom-in-top-enter-active,
+.zoom-in-top-leave-active {
+	opacity: 1;
+	transform: scaleY(1);
+	transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
+	transform-origin: center top;
+}
+.zoom-in-top-enter,
+.zoom-in-top-leave-active {
+	opacity: 0;
+	transform: scaleY(0);
+}
diff --git a/src/web/app/app.styl b/src/web/app/app.styl
index 22043b883..c441a445f 100644
--- a/src/web/app/app.styl
+++ b/src/web/app/app.styl
@@ -1,4 +1,5 @@
 @import "../style"
+@import "../animation"
 
 html
 	&.progress
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 af58e81a0..b55333ecc 100644
--- a/src/web/app/desktop/views/components/ui.header.account.vue
+++ b/src/web/app/desktop/views/components/ui.header.account.vue
@@ -4,29 +4,31 @@
 		<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="`/${ 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>
+	<transition name="zoom-in-top">
+		<div class="menu" v-if="isOpen">
+			<ul>
+				<li>
+					<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>
+				</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>
+	</transition>
 </div>
 </template>
 
@@ -209,4 +211,12 @@ export default Vue.extend({
 						background $theme-color
 						color $theme-color-foreground
 
+					&:active
+						background darken($theme-color, 10%)
+
+.zoom-in-top-enter-active,
+.zoom-in-top-leave-active {
+	transform-origin: center -16px;
+}
+
 </style>

From 6c3961b07831411e26fb7f3ebeeda03ac5dc1e2b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 04:01:22 +0900
Subject: [PATCH 0442/1250] :v:

---
 .../app/common/views/components/messaging-room.vue  | 13 +++++++++++--
 src/web/app/desktop/views/pages/messaging-room.vue  |  4 ++++
 src/web/app/mobile/style.styl                       |  8 ++++++++
 src/web/app/mobile/views/components/ui.vue          |  7 +++++++
 src/web/app/mobile/views/pages/messaging-room.vue   |  2 +-
 5 files changed, 31 insertions(+), 3 deletions(-)

diff --git a/src/web/app/common/views/components/messaging-room.vue b/src/web/app/common/views/components/messaging-room.vue
index cfb1e23ac..7af6b3fae 100644
--- a/src/web/app/common/views/components/messaging-room.vue
+++ b/src/web/app/common/views/components/messaging-room.vue
@@ -116,7 +116,9 @@ export default Vue.extend({
 
 			if (isBottom) {
 				// Scroll to bottom
-				this.scrollToBottom();
+				this.$nextTick(() => {
+					this.scrollToBottom();
+				});
 			} else if (message.user_id != (this as any).os.i.id) {
 				// Notify
 				this.notify('%i18n:common.tags.mk-messaging-room.new-message%');
@@ -132,7 +134,7 @@ export default Vue.extend({
 			});
 		},
 		isBottom() {
-			const asobi = 32;
+			const asobi = 64;
 			const current = this.isNaked
 				? window.scrollY + window.innerHeight
 				: this.$el.scrollTop + this.$el.offsetHeight;
@@ -179,9 +181,16 @@ export default Vue.extend({
 
 <style lang="stylus" scoped>
 .mk-messaging-room
+	display flex
+	flex 1
+	flex-direction column
+	height 100%
+
 	> .stream
+		width 100%
 		max-width 600px
 		margin 0 auto
+		flex 1
 
 		> .init
 			width 100%
diff --git a/src/web/app/desktop/views/pages/messaging-room.vue b/src/web/app/desktop/views/pages/messaging-room.vue
index d71a93b24..99279dc07 100644
--- a/src/web/app/desktop/views/pages/messaging-room.vue
+++ b/src/web/app/desktop/views/pages/messaging-room.vue
@@ -46,6 +46,10 @@ export default Vue.extend({
 
 <style lang="stylus" scoped>
 .mk-messaging-room-page
+	display flex
+	flex 1
+	flex-direction column
+	min-height 100%
 	background #fff
 
 </style>
diff --git a/src/web/app/mobile/style.styl b/src/web/app/mobile/style.styl
index 63e4f2349..81912a248 100644
--- a/src/web/app/mobile/style.styl
+++ b/src/web/app/mobile/style.styl
@@ -5,3 +5,11 @@
 	top auto
 	bottom 15px
 	left 15px
+
+html
+	height 100%
+
+body
+	display flex
+	flex-direction column
+	min-height 100%
diff --git a/src/web/app/mobile/views/components/ui.vue b/src/web/app/mobile/views/components/ui.vue
index 54b8a2d0d..fbe80e8c2 100644
--- a/src/web/app/mobile/views/components/ui.vue
+++ b/src/web/app/mobile/views/components/ui.vue
@@ -63,5 +63,12 @@ export default Vue.extend({
 
 <style lang="stylus" scoped>
 .mk-ui
+	display flex
+	flex 1
 	padding-top 48px
+
+	> .content
+		display flex
+		flex 1
+		flex-direction column
 </style>
diff --git a/src/web/app/mobile/views/pages/messaging-room.vue b/src/web/app/mobile/views/pages/messaging-room.vue
index a653145c1..eb5439915 100644
--- a/src/web/app/mobile/views/pages/messaging-room.vue
+++ b/src/web/app/mobile/views/pages/messaging-room.vue
@@ -4,7 +4,7 @@
 		<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-messaging-room v-if="!fetching" :user="user" :is-naked="true"/>
 </mk-ui>
 </template>
 

From 3215432a9d3dd724cc6e27b44dc62ce04fb96afc Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 04:07:22 +0900
Subject: [PATCH 0443/1250] :v:

---
 src/web/app/desktop/views/components/posts.post.vue | 13 +++++++++----
 src/web/app/mobile/views/components/posts.post.vue  | 13 +++++++++----
 src/web/app/mobile/views/components/ui.vue          |  1 +
 3 files changed, 19 insertions(+), 8 deletions(-)

diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index 6fe097909..a05a49811 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -142,8 +142,10 @@ export default Vue.extend({
 		}
 	},
 	created() {
-		this.connection = (this as any).os.stream.getConnection();
-		this.connectionId = (this as any).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();
+		}
 	},
 	mounted() {
 		this.capture(true);
@@ -154,8 +156,11 @@ export default Vue.extend({
 	},
 	beforeDestroy() {
 		this.decapture(true);
-		this.connection.off('_connected_', this.onStreamConnected);
-		(this as any).os.stream.dispose(this.connectionId);
+
+		if ((this as any).os.isSignedIn) {
+			this.connection.off('_connected_', this.onStreamConnected);
+			(this as any).os.stream.dispose(this.connectionId);
+		}
 	},
 	methods: {
 		capture(withHandler = false) {
diff --git a/src/web/app/mobile/views/components/posts.post.vue b/src/web/app/mobile/views/components/posts.post.vue
index 43d8d4a89..43041a61b 100644
--- a/src/web/app/mobile/views/components/posts.post.vue
+++ b/src/web/app/mobile/views/components/posts.post.vue
@@ -115,8 +115,10 @@ export default Vue.extend({
 		}
 	},
 	created() {
-		this.connection = (this as any).os.stream.getConnection();
-		this.connectionId = (this as any).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();
+		}
 	},
 	mounted() {
 		this.capture(true);
@@ -127,8 +129,11 @@ export default Vue.extend({
 	},
 	beforeDestroy() {
 		this.decapture(true);
-		this.connection.off('_connected_', this.onStreamConnected);
-		(this as any).os.stream.dispose(this.connectionId);
+
+		if ((this as any).os.isSignedIn) {
+			this.connection.off('_connected_', this.onStreamConnected);
+			(this as any).os.stream.dispose(this.connectionId);
+		}
 	},
 	methods: {
 		capture(withHandler = false) {
diff --git a/src/web/app/mobile/views/components/ui.vue b/src/web/app/mobile/views/components/ui.vue
index fbe80e8c2..91d7ea29b 100644
--- a/src/web/app/mobile/views/components/ui.vue
+++ b/src/web/app/mobile/views/components/ui.vue
@@ -65,6 +65,7 @@ export default Vue.extend({
 .mk-ui
 	display flex
 	flex 1
+	flex-direction column
 	padding-top 48px
 
 	> .content

From 224d561dfc0fa34a362c6218529a9be7e5c99ca1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 04:26:58 +0900
Subject: [PATCH 0444/1250] v3807

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 37ef587f1..c8f675703 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3493",
+	"version": "0.0.3807",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 8fe4feb336902c77aa6939118d47d9e2e61116eb Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 04:30:42 +0900
Subject: [PATCH 0445/1250] :v:

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

diff --git a/locales/index.ts b/locales/index.ts
index ced3b4cb3..0593af366 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
 };
 

From 9f48e242a56c0ac4bae1ed19b4b3aa3c67ae78ff Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 04:49:21 +0900
Subject: [PATCH 0446/1250] Fix bug

---
 src/web/app/desktop/script.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
index e7c8f8e49..bbd8e9598 100644
--- a/src/web/app/desktop/script.ts
+++ b/src/web/app/desktop/script.ts
@@ -65,7 +65,7 @@ init(async (launch) => {
 		}
 
 		if ((Notification as any).permission == 'granted') {
-			registerNotifications(app.$data.os.stream);
+			registerNotifications(os.stream);
 		}
 	}
 

From 8ba898d61cad6aea3f976d88f90e871ee242a9df Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 04:49:34 +0900
Subject: [PATCH 0447/1250] v3809

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index c8f675703..1a9f8e8d1 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3807",
+	"version": "0.0.3809",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 33479ed54b2ef63db63eb11ecf7436ec20a32d4a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 04:53:33 +0900
Subject: [PATCH 0448/1250] Define NODE_ENV for production build

---
 webpack/plugins/index.ts | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/webpack/plugins/index.ts b/webpack/plugins/index.ts
index 56e73e504..1ae63cb43 100644
--- a/webpack/plugins/index.ts
+++ b/webpack/plugins/index.ts
@@ -1,3 +1,5 @@
+import * as webpack from 'webpack';
+
 import consts from './consts';
 import hoist from './hoist';
 import minify from './minify';
@@ -7,7 +9,12 @@ const isProduction = env === 'production';
 
 export default (version, lang) => {
 	const plugins = [
-		consts(lang)
+		consts(lang),
+		new webpack.DefinePlugin({
+			'process.env': {
+				NODE_ENV: JSON.stringify(process.env.NODE_ENV)
+			}
+		})
 	];
 
 	if (isProduction) {

From 6d1e727cac94a768ed31becb6a605b7f167719b2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 05:06:47 +0900
Subject: [PATCH 0449/1250] :v:

---
 .../desktop/views/components/widgets/channel.channel.vue  | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

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 a28b4aeb9..09154390c 100644
--- a/src/web/app/desktop/views/components/widgets/channel.channel.vue
+++ b/src/web/app/desktop/views/components/widgets/channel.channel.vue
@@ -34,9 +34,7 @@ export default Vue.extend({
 		}
 	},
 	mounted() {
-		this.$nextTick(() => {
-			this.zap();
-		});
+		this.zap();
 	},
 	beforeDestroy() {
 		this.disconnect();
@@ -51,7 +49,9 @@ export default Vue.extend({
 				this.posts = posts;
 				this.fetching = false;
 
-				this.scrollToBottom();
+				this.$nextTick(() => {
+					this.scrollToBottom();
+				});
 
 				this.disconnect();
 				this.connection = new ChannelStream(this.channel.id);

From 28bb02798c8150c6ca1f1894f9d37ff4c5d22d73 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 05:15:20 +0900
Subject: [PATCH 0450/1250] Fix bug

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

diff --git a/src/web/app/common/views/components/post-html.ts b/src/web/app/common/views/components/post-html.ts
index 16d670e85..006e32684 100644
--- a/src/web/app/common/views/components/post-html.ts
+++ b/src/web/app/common/views/components/post-html.ts
@@ -34,7 +34,9 @@ export default Vue.component('mk-post-html', {
 
 					if ((this as any).shouldBreak) {
 						if (text.indexOf('\n') != -1) {
-							return text.split('\n').map(t => [createElement('span', t), createElement('br')]);
+							const x = text.split('\n').map(t => [createElement('span', t), createElement('br')]);
+							x[x.length - 1].pop();
+							return x;
 						} else {
 							return createElement('span', text);
 						}

From 177269aa7f552d1e249cd49dacb6f68c0709e294 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 05:15:51 +0900
Subject: [PATCH 0451/1250] v3814

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 1a9f8e8d1..96760deb3 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3809",
+	"version": "0.0.3814",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From b6aa13ccbccd9b590e84890331f2783b7fac7e7c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 05:27:00 +0900
Subject: [PATCH 0452/1250] Fix bug

---
 .../mobile/views/components/post-detail.vue   | 32 ++++++++++---------
 1 file changed, 17 insertions(+), 15 deletions(-)

diff --git a/src/web/app/mobile/views/components/post-detail.vue b/src/web/app/mobile/views/components/post-detail.vue
index e7c08df7e..05138607f 100644
--- a/src/web/app/mobile/views/components/post-detail.vue
+++ b/src/web/app/mobile/views/components/post-detail.vue
@@ -38,7 +38,7 @@
 			</div>
 		</header>
 		<div class="body">
-			<mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i"/>
+			<mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i" :class="$style.text"/>
 			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 			<div class="media" v-if="p.media">
 				<mk-images :images="p.media"/>
@@ -304,20 +304,8 @@ export default Vue.extend({
 		> .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
+			> .mk-url-preview
+				margin-top 8px
 
 			> .media
 				> img
@@ -360,3 +348,17 @@ export default Vue.extend({
 			border-top 1px solid #eef0f2
 
 </style>
+
+<style lang="stylus" module>
+.text
+	display block
+	margin 0
+	padding 0
+	overflow-wrap break-word
+	font-size 16px
+	color #717171
+
+	@media (min-width 500px)
+		font-size 24px
+
+</style>

From 1c792e4799115626362329ca3f915865a715b0a4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 05:43:19 +0900
Subject: [PATCH 0453/1250] :v:

---
 .../app/common/views/components/messaging-room.message.vue   | 2 +-
 src/web/app/mobile/api/post.ts                               | 1 +
 src/web/app/mobile/views/components/post-form.vue            | 5 ++++-
 3 files changed, 6 insertions(+), 2 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 2464eceb7..ae4075bef 100644
--- a/src/web/app/common/views/components/messaging-room.message.vue
+++ b/src/web/app/common/views/components/messaging-room.message.vue
@@ -224,7 +224,7 @@ export default Vue.extend({
 					> p.is-deleted
 						color rgba(255, 255, 255, 0.5)
 
-					> .text
+					> .text >>>
 						&, *
 							color #fff !important
 
diff --git a/src/web/app/mobile/api/post.ts b/src/web/app/mobile/api/post.ts
index 3b14e0c1d..09bb9dc06 100644
--- a/src/web/app/mobile/api/post.ts
+++ b/src/web/app/mobile/api/post.ts
@@ -37,5 +37,6 @@ export default (os) => (opts) => {
 		vm.$once('cancel', recover);
 		vm.$once('post', recover);
 		document.body.appendChild(vm.$el);
+		(vm as any).focus();
 	}
 };
diff --git a/src/web/app/mobile/views/components/post-form.vue b/src/web/app/mobile/views/components/post-form.vue
index 3e8206c92..7a6eb7741 100644
--- a/src/web/app/mobile/views/components/post-form.vue
+++ b/src/web/app/mobile/views/components/post-form.vue
@@ -49,10 +49,13 @@ export default Vue.extend({
 	},
 	mounted() {
 		this.$nextTick(() => {
-			(this.$refs.text as any).focus();
+			this.focus();
 		});
 	},
 	methods: {
+		focus() {
+			(this.$refs.text as any).focus();
+		},
 		chooseFile() {
 			(this.$refs.file as any).click();
 		},

From cdfc468c5fc515da50fc708e5ade61efae821311 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 05:43:34 +0900
Subject: [PATCH 0454/1250] v3817

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 96760deb3..ff5f57eba 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3814",
+	"version": "0.0.3817",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From ac31f47950e6c3152d12b7023949f2beecbecda5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 05:52:14 +0900
Subject: [PATCH 0455/1250] :v:

---
 src/web/app/desktop/views/components/posts.post.vue | 2 --
 src/web/app/mobile/views/components/posts.post.vue  | 5 +----
 2 files changed, 1 insertion(+), 6 deletions(-)

diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index a05a49811..2c6c8bb8f 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -362,7 +362,6 @@ export default Vue.extend({
 				display flex
 				margin-bottom 4px
 				white-space nowrap
-				line-height 1.4
 
 				> .name
 					display block
@@ -392,7 +391,6 @@ export default Vue.extend({
 
 				> .info
 					margin-left auto
-					text-align right
 					font-size 0.9em
 
 					> .app
diff --git a/src/web/app/mobile/views/components/posts.post.vue b/src/web/app/mobile/views/components/posts.post.vue
index 43041a61b..1a4da5710 100644
--- a/src/web/app/mobile/views/components/posts.post.vue
+++ b/src/web/app/mobile/views/components/posts.post.vue
@@ -311,8 +311,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
 
@@ -320,7 +319,6 @@ export default Vue.extend({
 						text-decoration underline
 
 				> .is-bot
-					text-align left
 					margin 0 0.5em 0 0
 					padding 1px 6px
 					font-size 12px
@@ -329,7 +327,6 @@ export default Vue.extend({
 					border-radius 3px
 
 				> .username
-					text-align left
 					margin 0 0.5em 0 0
 					color #ccc
 

From 4c457c4dc11e38e0b76dc585abf6fb5eaa44cb8a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 05:58:29 +0900
Subject: [PATCH 0456/1250] v3819

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index ff5f57eba..c85a7b56a 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3817",
+	"version": "0.0.3819",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 6d20a55a264f41f7ed683f67c5eaab87759c524a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 06:17:10 +0900
Subject: [PATCH 0457/1250] :v:

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

diff --git a/src/web/app/mobile/views/pages/drive.vue b/src/web/app/mobile/views/pages/drive.vue
index 689be04d8..47aeb52f4 100644
--- a/src/web/app/mobile/views/pages/drive.vue
+++ b/src/web/app/mobile/views/pages/drive.vue
@@ -44,6 +44,7 @@ export default Vue.extend({
 	},
 	mounted() {
 		document.title = 'Misskey Drive';
+		document.documentElement.style.background = '#fff';
 	},
 	beforeDestroy() {
 		window.removeEventListener('popstate', this.onPopState);

From d7279a145356bc713041ae9cdb12e1b71a732346 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 06:30:33 +0900
Subject: [PATCH 0458/1250] Update license.ja.pug

---
 src/web/docs/license.ja.pug | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/web/docs/license.ja.pug b/src/web/docs/license.ja.pug
index 7bd9a6294..6eb9ac308 100644
--- a/src/web/docs/license.ja.pug
+++ b/src/web/docs/license.ja.pug
@@ -3,10 +3,10 @@ h1 ライセンス
 div!= common.license
 
 details
-	summary ライブラリ
+	summary サードパーティ
 
 	section
-		h2 ライブラリ
+		h2 サードパーティ
 
 		each dependency, name in common.dependencies
 			details

From e765d5d963a5b2c4c25b15b8ccce82b6175065d2 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 22 Feb 2018 22:35:47 +0000
Subject: [PATCH 0459/1250] fix(package): update vue-js-modal to version 1.3.12

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index c85a7b56a..b8d0f1122 100644
--- a/package.json
+++ b/package.json
@@ -175,7 +175,7 @@
 		"vhost": "3.0.2",
 		"vue": "2.5.13",
 		"vue-cropperjs": "2.2.0",
-		"vue-js-modal": "1.3.9",
+		"vue-js-modal": "1.3.12",
 		"vue-loader": "14.1.1",
 		"vue-router": "3.0.1",
 		"vue-template-compiler": "2.5.13",

From 7a4972c524c83b199d9678639a2dafb7ca87bc93 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 07:50:42 +0900
Subject: [PATCH 0460/1250] Fix bug

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

diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index ac567c502..aa2ec25c9 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -87,23 +87,6 @@ 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);
 
@@ -117,6 +100,23 @@ 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)
+			});
+
+			os.app = app;
+
 			// マウント
 			app.$mount('#app');
 

From 94ef8ec62fc97d6bef69b9295af87183aebe32c3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 07:56:35 +0900
Subject: [PATCH 0461/1250] :v:

---
 .../{drive-file.vue => drive.file.vue}           |  6 +++---
 .../{drive-folder.vue => drive.folder.vue}       |  6 +++---
 ...drive-nav-folder.vue => drive.nav-folder.vue} |  4 ++--
 src/web/app/desktop/views/components/drive.vue   | 16 ++++++++++++----
 src/web/app/desktop/views/components/index.ts    |  6 ------
 5 files changed, 20 insertions(+), 18 deletions(-)
 rename src/web/app/desktop/views/components/{drive-file.vue => drive.file.vue} (98%)
 rename src/web/app/desktop/views/components/{drive-folder.vue => drive.folder.vue} (99%)
 rename src/web/app/desktop/views/components/{drive-nav-folder.vue => drive.nav-folder.vue} (97%)

diff --git a/src/web/app/desktop/views/components/drive-file.vue b/src/web/app/desktop/views/components/drive.file.vue
similarity index 98%
rename from src/web/app/desktop/views/components/drive-file.vue
rename to src/web/app/desktop/views/components/drive.file.vue
index ffdf7ef57..cc423477e 100644
--- a/src/web/app/desktop/views/components/drive-file.vue
+++ b/src/web/app/desktop/views/components/drive.file.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-drive-file"
+<div class="root file"
 	:data-is-selected="isSelected"
 	:data-is-contextmenu-showing="isContextmenuShowing"
 	@click="onClick"
@@ -51,7 +51,7 @@ export default Vue.extend({
 		},
 		background(): string {
 			return this.file.properties.average_color
-				? `rgb(${this.file.properties.average_color.join(',')})'`
+				? `rgb(${this.file.properties.average_color.join(',')})`
 				: 'transparent';
 		}
 	},
@@ -188,7 +188,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-drive-file
+.root.file
 	padding 8px 0 0 0
 	height 180px
 	border-radius 4px
diff --git a/src/web/app/desktop/views/components/drive-folder.vue b/src/web/app/desktop/views/components/drive.folder.vue
similarity index 99%
rename from src/web/app/desktop/views/components/drive-folder.vue
rename to src/web/app/desktop/views/components/drive.folder.vue
index efb9df30f..4e57d1ca6 100644
--- a/src/web/app/desktop/views/components/drive-folder.vue
+++ b/src/web/app/desktop/views/components/drive.folder.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-drive-folder"
+<div class="root folder"
 	:data-is-contextmenu-showing="isContextmenuShowing"
 	:data-draghover="draghover"
 	@click="onClick"
@@ -124,7 +124,7 @@ export default Vue.extend({
 					this.browser.upload(file, this.folder);
 				});
 				return false;
-			};
+			}
 
 			// データ取得
 			const data = e.dataTransfer.getData('text');
@@ -220,7 +220,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-drive-folder
+.root.folder
 	padding 8px
 	height 64px
 	background lighten($theme-color, 95%)
diff --git a/src/web/app/desktop/views/components/drive-nav-folder.vue b/src/web/app/desktop/views/components/drive.nav-folder.vue
similarity index 97%
rename from src/web/app/desktop/views/components/drive-nav-folder.vue
rename to src/web/app/desktop/views/components/drive.nav-folder.vue
index 44821087a..4c5128588 100644
--- a/src/web/app/desktop/views/components/drive-nav-folder.vue
+++ b/src/web/app/desktop/views/components/drive.nav-folder.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-drive-nav-folder"
+<div class="root nav-folder"
 	:data-draghover="draghover"
 	@click="onClick"
 	@dragover.prevent.stop="onDragover"
@@ -101,7 +101,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-drive-nav-folder
+.root.nav-folder
 	&[data-draghover]
 		background #eee
 
diff --git a/src/web/app/desktop/views/components/drive.vue b/src/web/app/desktop/views/components/drive.vue
index 0dcf07701..ffe0c68de 100644
--- a/src/web/app/desktop/views/components/drive.vue
+++ b/src/web/app/desktop/views/components/drive.vue
@@ -2,10 +2,10 @@
 <div class="mk-drive">
 	<nav>
 		<div class="path" @contextmenu.prevent.stop="() => {}">
-			<mk-drive-nav-folder :class="{ current: folder == null }"/>
+			<x-nav-folder :class="{ current: folder == null }"/>
 			<template v-for="folder in hierarchyFolders">
 				<span class="separator">%fa:angle-right%</span>
-				<mk-drive-nav-folder :folder="folder" :key="folder.id"/>
+				<x-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>
@@ -24,13 +24,13 @@
 		<div class="selection" ref="selection"></div>
 		<div class="contents" ref="contents">
 			<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"/>
+				<x-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"></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"/>
+				<x-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"></div>
 				<button v-if="moreFiles" @click="fetchMoreFiles">%i18n:desktop.tags.mk-drive-browser.load-more%</button>
@@ -57,10 +57,18 @@
 <script lang="ts">
 import Vue from 'vue';
 import MkDriveWindow from './drive-window.vue';
+import XNavFolder from './drive.nav-folder.vue';
+import XFolder from './drive.folder.vue';
+import XFile from './drive.file.vue';
 import contains from '../../../common/scripts/contains';
 import contextmenu from '../../api/contextmenu';
 
 export default Vue.extend({
+	components: {
+		XNavFolder,
+		XFolder,
+		XFile
+	},
 	props: {
 		initFolder: {
 			type: Object,
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index fc30bb729..da59d9219 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -19,9 +19,6 @@ 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';
 import postDetail from './post-detail.vue';
 import settings from './settings.vue';
 import calendar from './calendar.vue';
@@ -71,9 +68,6 @@ 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);
 Vue.component('mk-post-detail', postDetail);
 Vue.component('mk-settings', settings);
 Vue.component('mk-calendar', calendar);

From ac8d0d4ba581cc44a1aecfcb64188b98b3e47cf9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 08:07:30 +0900
Subject: [PATCH 0462/1250] :v:

---
 src/web/app/mobile/views/components/index.ts  |  2 +
 .../app/mobile/views/components/timeline.vue  | 24 ++++++++---
 .../mobile/views/components/user-timeline.vue | 40 ++++++++++++++++---
 src/web/app/mobile/views/pages/user.vue       |  8 +---
 4 files changed, 58 insertions(+), 16 deletions(-)

diff --git a/src/web/app/mobile/views/components/index.ts b/src/web/app/mobile/views/components/index.ts
index 73cc1f9f3..905baaf20 100644
--- a/src/web/app/mobile/views/components/index.ts
+++ b/src/web/app/mobile/views/components/index.ts
@@ -18,6 +18,7 @@ import notifications from './notifications.vue';
 import notificationPreview from './notification-preview.vue';
 import usersList from './users-list.vue';
 import userPreview from './user-preview.vue';
+import userTimeline from './user-timeline.vue';
 
 Vue.component('mk-ui', ui);
 Vue.component('mk-home', home);
@@ -37,3 +38,4 @@ Vue.component('mk-notifications', notifications);
 Vue.component('mk-notification-preview', notificationPreview);
 Vue.component('mk-users-list', usersList);
 Vue.component('mk-user-preview', userPreview);
+Vue.component('mk-user-timeline', userTimeline);
diff --git a/src/web/app/mobile/views/components/timeline.vue b/src/web/app/mobile/views/components/timeline.vue
index e7a9f2df1..c0e766523 100644
--- a/src/web/app/mobile/views/components/timeline.vue
+++ b/src/web/app/mobile/views/components/timeline.vue
@@ -9,9 +9,9 @@
 			%fa:R comments%
 			%i18n:mobile.tags.mk-home-timeline.empty-timeline%
 		</div>
-		<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 v-if="!fetching && existMore" @click="more" :disabled="moreFetching" slot="tail">
+			<span v-if="!moreFetching">%i18n:mobile.tags.mk-timeline.load-more%</span>
+			<span v-if="moreFetching">%i18n:common.loading%<mk-ellipsis/></span>
 		</button>
 	</mk-posts>
 </div>
@@ -19,6 +19,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
+
+const limit = 10;
+
 export default Vue.extend({
 	props: {
 		date: {
@@ -31,6 +34,7 @@ export default Vue.extend({
 			fetching: true,
 			moreFetching: false,
 			posts: [],
+			existMore: false,
 			connection: null,
 			connectionId: null
 		};
@@ -59,10 +63,14 @@ export default Vue.extend({
 	methods: {
 		fetch(cb?) {
 			this.fetching = true;
-
 			(this as any).api('posts/timeline', {
+				limit: limit + 1,
 				until_date: this.date ? (this.date as any).getTime() : undefined
 			}).then(posts => {
+				if (posts.length == limit + 1) {
+					posts.pop();
+					this.existMore = true;
+				}
 				this.posts = posts;
 				this.fetching = false;
 				this.$emit('loaded');
@@ -70,11 +78,17 @@ export default Vue.extend({
 			});
 		},
 		more() {
-			if (this.moreFetching || this.fetching || this.posts.length == 0) return;
 			this.moreFetching = true;
 			(this as any).api('posts/timeline', {
+				limit: limit + 1,
 				until_id: this.posts[this.posts.length - 1].id
 			}).then(posts => {
+				if (posts.length == limit + 1) {
+					posts.pop();
+					this.existMore = true;
+				} else {
+					this.existMore = false;
+				}
 				this.posts = this.posts.concat(posts);
 				this.moreFetching = false;
 			});
diff --git a/src/web/app/mobile/views/components/user-timeline.vue b/src/web/app/mobile/views/components/user-timeline.vue
index ffd628838..39f959187 100644
--- a/src/web/app/mobile/views/components/user-timeline.vue
+++ b/src/web/app/mobile/views/components/user-timeline.vue
@@ -8,9 +8,9 @@
 			%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 v-if="!fetching && existMore" @click="more" :disabled="moreFetching" slot="tail">
+			<span v-if="!moreFetching">%i18n:mobile.tags.mk-user-timeline.load-more%</span>
+			<span v-if="moreFetching">%i18n:common.loading%<mk-ellipsis/></span>
 		</button>
 	</mk-posts>
 </div>
@@ -18,23 +18,53 @@
 
 <script lang="ts">
 import Vue from 'vue';
+
+const limit = 10;
+
 export default Vue.extend({
 	props: ['user', 'withMedia'],
 	data() {
 		return {
 			fetching: true,
-			posts: []
+			posts: [],
+			existMore: false,
+			moreFetching: false
 		};
 	},
 	mounted() {
 		(this as any).api('users/posts', {
 			user_id: this.user.id,
-			with_media: this.withMedia
+			with_media: this.withMedia,
+			limit: limit + 1
 		}).then(posts => {
+			if (posts.length == limit + 1) {
+				posts.pop();
+				this.existMore = true;
+			}
 			this.posts = posts;
 			this.fetching = false;
 			this.$emit('loaded');
 		});
+	},
+	methods: {
+		more() {
+			this.moreFetching = true;
+			(this as any).api('users/posts', {
+				user_id: this.user.id,
+				with_media: this.withMedia,
+				limit: limit + 1,
+				until_id: this.posts[this.posts.length - 1].id
+			}).then(posts => {
+				if (posts.length == limit + 1) {
+					posts.pop();
+					this.existMore = true;
+				} else {
+					this.existMore = false;
+				}
+				this.posts = this.posts.concat(posts);
+				this.moreFetching = false;
+			});
+		}
 	}
 });
 </script>
diff --git a/src/web/app/mobile/views/pages/user.vue b/src/web/app/mobile/views/pages/user.vue
index c9c1c6bfb..27f65e623 100644
--- a/src/web/app/mobile/views/pages/user.vue
+++ b/src/web/app/mobile/views/pages/user.vue
@@ -66,15 +66,11 @@ export default Vue.extend({
 	components: {
 		XHome
 	},
-	props: {
-		page: {
-			default: 'home'
-		}
-	},
 	data() {
 		return {
 			fetching: true,
-			user: null
+			user: null,
+			page: 'home'
 		};
 	},
 	computed: {

From 6159afe681f72c15ca9b963d6bb060dcaaf76b39 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 08:07:48 +0900
Subject: [PATCH 0463/1250] v3825

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index c85a7b56a..9e7524190 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3819",
+	"version": "0.0.3825",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From d1756c3306b587f1a1f74ee14e8cd798dba893ab Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 08:44:16 +0900
Subject: [PATCH 0464/1250] Update home.vue

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

diff --git a/src/web/app/mobile/views/pages/user/home.vue b/src/web/app/mobile/views/pages/user/home.vue
index 040b916ca..39807f542 100644
--- a/src/web/app/mobile/views/pages/user/home.vue
+++ b/src/web/app/mobile/views/pages/user/home.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="root home">
-	<mk-post-detail v-if="user.pinned_post" :post="user.pinned_post" compact/>
+	<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>

From c8f8e9af370ce0c85fa329d79490724899d2ee84 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 08:44:32 +0900
Subject: [PATCH 0465/1250] Update home.vue

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

diff --git a/src/web/app/mobile/views/pages/user/home.vue b/src/web/app/mobile/views/pages/user/home.vue
index 39807f542..4c6831787 100644
--- a/src/web/app/mobile/views/pages/user/home.vue
+++ b/src/web/app/mobile/views/pages/user/home.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="root home">
-	<mk-post-detail v-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>

From eb0daf53e67cd67a76eee2f2cc42d8604ad33161 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, 23 Feb 2018 18:34:14 +0900
Subject: [PATCH 0466/1250] Fix #1124

---
 .../app/common/views/components/post-html.ts  | 31 +++++++------------
 1 file changed, 12 insertions(+), 19 deletions(-)

diff --git a/src/web/app/common/views/components/post-html.ts b/src/web/app/common/views/components/post-html.ts
index 006e32684..37954cd7e 100644
--- a/src/web/app/common/views/components/post-html.ts
+++ b/src/web/app/common/views/components/post-html.ts
@@ -1,15 +1,8 @@
-declare const _URL_: string;
-
 import Vue from 'vue';
 import * as pictograph from 'pictograph';
-
+import { url } from '../../../config';
 import MkUrl from './url.vue';
 
-const escape = text =>
-	text
-		.replace(/>/g, '&gt;')
-		.replace(/</g, '&lt;');
-
 export default Vue.component('mk-post-html', {
 	props: {
 		ast: {
@@ -29,12 +22,12 @@ export default Vue.component('mk-post-html', {
 		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');
+					const text = token.content.replace(/(\r\n|\n|\r)/g, '\n');
 
 					if ((this as any).shouldBreak) {
 						if (text.indexOf('\n') != -1) {
-							const x = text.split('\n').map(t => [createElement('span', t), createElement('br')]);
+							const x = text.split('\n')
+								.map(t => [createElement('span', t), createElement('br')]);
 							x[x.length - 1].pop();
 							return x;
 						} else {
@@ -45,12 +38,12 @@ export default Vue.component('mk-post-html', {
 					}
 
 				case 'bold':
-					return createElement('strong', escape(token.bold));
+					return createElement('strong', token.bold);
 
 				case 'url':
 					return createElement(MkUrl, {
 						props: {
-							url: escape(token.content),
+							url: token.content,
 							target: '_blank'
 						}
 					});
@@ -59,16 +52,16 @@ export default Vue.component('mk-post-html', {
 					return createElement('a', {
 						attrs: {
 							class: 'link',
-							href: escape(token.url),
+							href: token.url,
 							target: '_blank',
-							title: escape(token.url)
+							title: token.url
 						}
-					}, escape(token.title));
+					}, token.title);
 
 				case 'mention':
 					return (createElement as any)('a', {
 						attrs: {
-							href: `${_URL_}/${escape(token.username)}`,
+							href: `${url}/${token.username}`,
 							target: '_blank',
 							dataIsMe: (this as any).i && (this as any).i.username == token.username
 						},
@@ -81,10 +74,10 @@ export default Vue.component('mk-post-html', {
 				case 'hashtag':
 					return createElement('a', {
 						attrs: {
-							href: `${_URL_}/search?q=${escape(token.content)}`,
+							href: `${url}/search?q=${token.content}`,
 							target: '_blank'
 						}
-					}, escape(token.content));
+					}, token.content);
 
 				case 'code':
 					return createElement('pre', [

From bbda46f5fb6337406112e33847f4e464ebb46c2f 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, 23 Feb 2018 18:42:38 +0900
Subject: [PATCH 0467/1250] Clean up

---
 src/web/app/mobile/scripts/open-post-form.ts | 15 ---------------
 src/web/app/mobile/scripts/ui-event.ts       |  5 -----
 2 files changed, 20 deletions(-)
 delete mode 100644 src/web/app/mobile/scripts/open-post-form.ts
 delete mode 100644 src/web/app/mobile/scripts/ui-event.ts

diff --git a/src/web/app/mobile/scripts/open-post-form.ts b/src/web/app/mobile/scripts/open-post-form.ts
deleted file mode 100644
index e0fae4d8c..000000000
--- a/src/web/app/mobile/scripts/open-post-form.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import * as riot from 'riot';
-
-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/scripts/ui-event.ts b/src/web/app/mobile/scripts/ui-event.ts
deleted file mode 100644
index 2e406549a..000000000
--- a/src/web/app/mobile/scripts/ui-event.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import * as riot from 'riot';
-
-const ev = riot.observable();
-
-export default ev;

From 191ebb7bb97d4b09a980fd3c0a0ce5e1bc8b7cb4 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, 23 Feb 2018 18:42:49 +0900
Subject: [PATCH 0468/1250] Fix bug

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

diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index 2c6c8bb8f..4ae980648 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -9,9 +9,9 @@
 				<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('{')) }}
+			<span>{{ '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('{')) }}</span>
 			<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) }}
+			<span>{{ '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1) }}</span>
 		</p>
 		<mk-time :time="post.created_at"/>
 	</div>
diff --git a/src/web/app/mobile/views/components/posts.post.vue b/src/web/app/mobile/views/components/posts.post.vue
index 1a4da5710..87b8032c8 100644
--- a/src/web/app/mobile/views/components/posts.post.vue
+++ b/src/web/app/mobile/views/components/posts.post.vue
@@ -9,9 +9,9 @@
 				<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('{')) }}
+			<span>{{ '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('{')) }}</span>
 			<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) }}
+			<span>{{ '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr('%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1) }}</span>
 		</p>
 		<mk-time :time="post.created_at"/>
 	</div>

From 0ae7ff8feabdabe1276a7da1da0434a87de03309 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 19:03:41 +0900
Subject: [PATCH 0469/1250] Fix bugs

---
 src/web/app/desktop/views/components/ui.header.account.vue | 2 +-
 src/web/app/mobile/views/components/ui.nav.vue             | 6 +++---
 2 files changed, 4 insertions(+), 4 deletions(-)

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 b55333ecc..68cf8477d 100644
--- a/src/web/app/desktop/views/components/ui.header.account.vue
+++ b/src/web/app/desktop/views/components/ui.header.account.vue
@@ -8,7 +8,7 @@
 		<div class="menu" v-if="isOpen">
 			<ul>
 				<li>
-					<a :href="`/${ os.i.username }`">%fa:user%%i18n:desktop.tags.mk-ui-header-account.profile%%fa:angle-right%</a>
+					<router-link :to="`/${ os.i.username }`">%fa:user%%i18n:desktop.tags.mk-ui-header-account.profile%%fa:angle-right%</router-link>
 				</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/mobile/views/components/ui.nav.vue b/src/web/app/mobile/views/components/ui.nav.vue
index 5ca7e2e94..9fe0864aa 100644
--- a/src/web/app/mobile/views/components/ui.nav.vue
+++ b/src/web/app/mobile/views/components/ui.nav.vue
@@ -23,14 +23,14 @@
 				<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="docsUrl"><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>
 
 <script lang="ts">
 import Vue from 'vue';
-import { docsUrl, chUrl } from '../../../config';
+import { docsUrl, chUrl, lang } from '../../../config';
 
 export default Vue.extend({
 	props: ['isOpen'],
@@ -40,7 +40,7 @@ export default Vue.extend({
 			hasUnreadMessagingMessages: false,
 			connection: null,
 			connectionId: null,
-			docsUrl,
+			aboutUrl: `${docsUrl}/${lang}/about`,
 			chUrl
 		};
 	},

From 3f3813b1e65eb7ac2b3e3464413c55c887dfb2a5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 19:04:06 +0900
Subject: [PATCH 0470/1250] v3834

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 69474d670..e4ccc8715 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3825",
+	"version": "0.0.3834",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From fe94ef20b017f1a8e03c66a5ebf765673f1378b8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 21:45:59 +0900
Subject: [PATCH 0471/1250] :v:

---
 .../components/messaging-room.message.vue     |  4 +-
 .../views/components/notifications.vue        | 56 ++++++++---------
 .../views/components/post-detail.sub.vue      | 45 +++++++-------
 .../desktop/views/components/post-preview.vue | 11 ++--
 .../views/components/posts.post.sub.vue       | 10 ++--
 .../mobile/views/components/notification.vue  | 60 +++++++++----------
 .../mobile/views/components/post-preview.vue  | 10 ++--
 7 files changed, 97 insertions(+), 99 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 ae4075bef..9772c7c0d 100644
--- a/src/web/app/common/views/components/messaging-room.message.vue
+++ b/src/web/app/common/views/components/messaging-room.message.vue
@@ -1,8 +1,8 @@
 <template>
 <div class="message" :data-is-me="isMe">
-	<a class="avatar-anchor" :href="`/${message.user.username}`" :title="message.user.username" target="_blank">
+	<router-link class="avatar-anchor" :to="`/${message.user.username}`" :title="message.user.username" target="_blank">
 		<img class="avatar" :src="`${message.user.avatar_url}?thumbnail&size=80`" alt=""/>
-	</a>
+	</router-link>
 	<div class="content-container">
 		<div class="balloon">
 			<p class="read" v-if="isMe && message.is_read">%i18n:common.tags.mk-messaging-message.is-read%</p>
diff --git a/src/web/app/desktop/views/components/notifications.vue b/src/web/app/desktop/views/components/notifications.vue
index bcd7cf35f..d61397d53 100644
--- a/src/web/app/desktop/views/components/notifications.vue
+++ b/src/web/app/desktop/views/components/notifications.vue
@@ -5,84 +5,84 @@
 			<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">
+					<router-link class="avatar-anchor" :to="`/${notification.user.username}`" v-user-preview="notification.user.id">
 						<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
-					</a>
+					</router-link>
 					<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>
+							<router-link :to="`/${notification.user.username}`" v-user-preview="notification.user.id">{{ notification.user.name }}</router-link>
 						</p>
-						<a class="post-ref" :href="`/${notification.post.user.username}/${notification.post.id}`">
+						<router-link class="post-ref" :to="`/${notification.post.user.username}/${notification.post.id}`">
 							%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%
-						</a>
+						</router-link>
 					</div>
 				</template>
 				<template v-if="notification.type == 'repost'">
-					<a class="avatar-anchor" :href="`/${notification.post.user.username}`" v-user-preview="notification.post.user_id">
+					<router-link class="avatar-anchor" :to="`/${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>
+					</router-link>
 					<div class="text">
 						<p>%fa:retweet%
-							<a :href="`/${notification.post.user.username}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</a>
+							<router-link :to="`/${notification.post.user.username}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</router-link>
 						</p>
-						<a class="post-ref" :href="`/${notification.post.user.username}/${notification.post.id}`">
+						<router-link class="post-ref" :to="`/${notification.post.user.username}/${notification.post.id}`">
 							%fa:quote-left%{{ getPostSummary(notification.post.repost) }}%fa:quote-right%
-						</a>
+						</router-link>
 					</div>
 				</template>
 				<template v-if="notification.type == 'quote'">
-					<a class="avatar-anchor" :href="`/${notification.post.user.username}`" v-user-preview="notification.post.user_id">
+					<router-link class="avatar-anchor" :to="`/${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>
+					</router-link>
 					<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>
+							<router-link :to="`/${notification.post.user.username}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</router-link>
 						</p>
-						<a class="post-preview" :href="`/${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</a>
+						<router-link class="post-preview" :to="`/${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link>
 					</div>
 				</template>
 				<template v-if="notification.type == 'follow'">
-					<a class="avatar-anchor" :href="`/${notification.user.username}`" v-user-preview="notification.user.id">
+					<router-link class="avatar-anchor" :to="`/${notification.user.username}`" v-user-preview="notification.user.id">
 						<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
-					</a>
+					</router-link>
 					<div class="text">
 						<p>%fa:user-plus%
-							<a :href="`/${notification.user.username}`" v-user-preview="notification.user.id">{{ notification.user.name }}</a>
+							<router-link :to="`/${notification.user.username}`" v-user-preview="notification.user.id">{{ notification.user.name }}</router-link>
 						</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">
+					<router-link class="avatar-anchor" :to="`/${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>
+					</router-link>
 					<div class="text">
 						<p>%fa:reply%
-							<a :href="`/${notification.post.user.username}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</a>
+							<router-link :to="`/${notification.post.user.username}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</router-link>
 						</p>
-						<a class="post-preview" :href="`/${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</a>
+						<router-link class="post-preview" :to="`/${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link>
 					</div>
 				</template>
 				<template v-if="notification.type == 'mention'">
-					<a class="avatar-anchor" :href="`/${notification.post.user.username}`" v-user-preview="notification.post.user_id">
+					<router-link class="avatar-anchor" :to="`/${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>
+					</router-link>
 					<div class="text">
 						<p>%fa:at%
-							<a :href="`/${notification.post.user.username}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</a>
+							<router-link :to="`/${notification.post.user.username}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</router-link>
 						</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">
+					<router-link class="avatar-anchor" :to="`/${notification.user.username}`" v-user-preview="notification.user.id">
 						<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
-					</a>
+					</router-link>
 					<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}`">
+						<router-link class="post-ref" :to="`/${notification.post.user.username}/${notification.post.id}`">
 							%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%
-						</a>
+						</router-link>
 					</div>
 				</template>
 			</div>
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 69ced0925..bf6e3ac3b 100644
--- a/src/web/app/desktop/views/components/post-detail.sub.vue
+++ b/src/web/app/desktop/views/components/post-detail.sub.vue
@@ -1,24 +1,24 @@
 <template>
 <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>
+	<router-link class="avatar-anchor" :to="`/${post.user.username}`">
+		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar" v-user-preview="post.user_id"/>
+	</router-link>
 	<div class="main">
 		<header>
 			<div class="left">
-				<a class="name" href={ '/' + post.user.username } v-user-preview={ post.user_id }>{ post.user.name }</a>
-				<span class="username">@{ post.user.username }</span>
+				<router-link class="name" :to="`/${post.user.username}`" v-user-preview="post.user_id">{{ post.user.name }}</router-link>
+				<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>
+				<router-link class="time" :to="`/${post.user.username}/${post.id}`">
+					<mk-time :time="post.created_at"/>
+				</router-link>
 			</div>
 		</header>
 		<div class="body">
-			<mk-post-html v-if="post.ast" :ast="post.ast" :i="os.i"/>
+			<mk-post-html v-if="post.ast" :ast="post.ast" :i="os.i" :class="$style.text"/>
 			<div class="media" v-if="post.media">
-				<mk-images images={ post.media }/>
+				<mk-images :images="post.media"/>
 			</div>
 		</div>
 	</div>
@@ -108,18 +108,15 @@ export default Vue.extend({
 					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>
+
+<style lang="stylus" module>
+.text
+	cursor default
+	display block
+	margin 0
+	padding 0
+	overflow-wrap break-word
+	font-size 1em
+	color #717171
 </style>
diff --git a/src/web/app/desktop/views/components/post-preview.vue b/src/web/app/desktop/views/components/post-preview.vue
index 6a0a60e4a..ec3372f90 100644
--- a/src/web/app/desktop/views/components/post-preview.vue
+++ b/src/web/app/desktop/views/components/post-preview.vue
@@ -1,14 +1,15 @@
 <template>
 <div class="mk-post-preview" :title="title">
-	<a class="avatar-anchor" :href="`/${post.user.username}`">
+	<router-link class="avatar-anchor" :to="`/${post.user.username}`">
 		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar" v-user-preview="post.user_id"/>
-	</a>
+	</router-link>
 	<div class="main">
 		<header>
-			<a class="name" :href="`/${post.user.username}`" v-user-preview="post.user_id">{{ post.user.name }}</a>
+			<router-link class="name" :to="`/${post.user.username}`" v-user-preview="post.user_id">{{ post.user.name }}</router-link>
 			<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="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"/>
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 f92077516..69c88fed5 100644
--- a/src/web/app/desktop/views/components/posts.post.sub.vue
+++ b/src/web/app/desktop/views/components/posts.post.sub.vue
@@ -1,15 +1,15 @@
 <template>
 <div class="sub" :title="title">
-	<a class="avatar-anchor" :href="`/${post.user.username}`">
+	<router-link class="avatar-anchor" :to="`/${post.user.username}`">
 		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar" v-user-preview="post.user_id"/>
-	</a>
+	</router-link>
 	<div class="main">
 		<header>
-			<a class="name" :href="`/${post.user.username}`" v-user-preview="post.user_id">{{ post.user.name }}</a>
+			<router-link class="name" :to="`/${post.user.username}`" v-user-preview="post.user_id">{{ post.user.name }}</router-link>
 			<span class="username">@{{ post.user.username }}</span>
-			<a 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"/>
-			</a>
+			</router-link>
 		</header>
 		<div class="body">
 			<mk-sub-post-content class="text" :post="post"/>
diff --git a/src/web/app/mobile/views/components/notification.vue b/src/web/app/mobile/views/components/notification.vue
index dce373b45..4e09f3d83 100644
--- a/src/web/app/mobile/views/components/notification.vue
+++ b/src/web/app/mobile/views/components/notification.vue
@@ -3,99 +3,99 @@
 	<mk-time :time="notification.created_at"/>
 
 	<template v-if="notification.type == 'reaction'">
-		<a class="avatar-anchor" :href="`/${notification.user.username}`">
+		<router-link class="avatar-anchor" :to="`/${notification.user.username}`">
 			<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
-		</a>
+		</router-link>
 		<div class="text">
 			<p>
 				<mk-reaction-icon :reaction="notification.reaction"/>
-				<a :href="`/${notification.user.username}`">{{ notification.user.name }}</a>
+				<router-link :to="`/${notification.user.username}`">{{ notification.user.name }}</router-link>
 			</p>
-			<a class="post-ref" :href="`/${notification.post.user.username}/${notification.post.id}`">
+			<router-link class="post-ref" :to="`/${notification.post.user.username}/${notification.post.id}`">
 				%fa:quote-left%{{ getPostSummary(notification.post) }}
 				%fa:quote-right%
-			</a>
+			</router-link>
 		</div>
 	</template>
 
 	<template v-if="notification.type == 'repost'">
-		<a class="avatar-anchor" :href="`/${notification.post.user.username}`">
+		<router-link class="avatar-anchor" :to="`/${notification.post.user.username}`">
 			<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
-		</a>
+		</router-link>
 		<div class="text">
 			<p>
 				%fa:retweet%
-				<a :href="`/${notification.post.user.username}`">{{ notification.post.user.name }}</a>
+				<router-link :to="`/${notification.post.user.username}`">{{ notification.post.user.name }}</router-link>
 			</p>
-			<a class="post-ref" :href="`/${notification.post.user.username}/${notification.post.id}`">
+			<router-link class="post-ref" :to="`/${notification.post.user.username}/${notification.post.id}`">
 				%fa:quote-left%{{ getPostSummary(notification.post.repost) }}%fa:quote-right%
-			</a>
+			</router-link>
 		</div>
 	</template>
 
 	<template v-if="notification.type == 'quote'">
-		<a class="avatar-anchor" :href="`/${notification.post.user.username}`">
+		<router-link class="avatar-anchor" :to="`/${notification.post.user.username}`">
 			<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
-		</a>
+		</router-link>
 		<div class="text">
 			<p>
 				%fa:quote-left%
-				<a :href="`/${notification.post.user.username}`">{{ notification.post.user.name }}</a>
+				<router-link :to="`/${notification.post.user.username}`">{{ notification.post.user.name }}</router-link>
 			</p>
-			<a class="post-preview" :href="`/${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</a>
+			<router-link class="post-preview" :to="`/${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link>
 		</div>
 	</template>
 
 	<template v-if="notification.type == 'follow'">
-		<a class="avatar-anchor" :href="`/${notification.user.username}`">
+		<router-link class="avatar-anchor" :to="`/${notification.user.username}`">
 			<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
-		</a>
+		</router-link>
 		<div class="text">
 			<p>
 				%fa:user-plus%
-				<a :href="`/${notification.user.username}`">{{ notification.user.name }}</a>
+				<router-link :to="`/${notification.user.username}`">{{ notification.user.name }}</router-link>
 			</p>
 		</div>
 	</template>
 
 	<template v-if="notification.type == 'reply'">
-		<a class="avatar-anchor" :href="`/${notification.post.user.username}`">
+		<router-link class="avatar-anchor" :to="`/${notification.post.user.username}`">
 			<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
-		</a>
+		</router-link>
 		<div class="text">
 			<p>
 				%fa:reply%
-				<a :href="`/${notification.post.user.username}`">{{ notification.post.user.name }}</a>
+				<router-link :to="`/${notification.post.user.username}`">{{ notification.post.user.name }}</router-link>
 			</p>
-			<a class="post-preview" :href="`/${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</a>
+			<router-link class="post-preview" :to="`/${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link>
 		</div>
 	</template>
 
 	<template v-if="notification.type == 'mention'">
-		<a class="avatar-anchor" :href="`/${notification.post.user.username}`">
+		<router-link class="avatar-anchor" :to="`/${notification.post.user.username}`">
 			<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
-		</a>
+		</router-link>
 		<div class="text">
 			<p>
 				%fa:at%
-				<a :href="`/${notification.post.user.username}`">{{ notification.post.user.name }}</a>
+				<router-link :to="`/${notification.post.user.username}`">{{ notification.post.user.name }}</router-link>
 			</p>
-			<a class="post-preview" :href="`/${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</a>
+			<router-link class="post-preview" :to="`/${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link>
 		</div>
 	</template>
 
 	<template v-if="notification.type == 'poll_vote'">
-		<a class="avatar-anchor" :href="`/${notification.user.username}`">
+		<router-link class="avatar-anchor" :to="`/${notification.user.username}`">
 			<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
-		</a>
+		</router-link>
 		<div class="text">
 			<p>
 				%fa:chart-pie%
-				<a :href="`/${notification.user.username}`">{{ notification.user.name }}</a>
+				<router-link :to="`/${notification.user.username}`">{{ notification.user.name }}</router-link>
 			</p>
-			<a class="post-ref" :href="`/${notification.post.user.username}/${notification.post.id}`">
+			<router-link class="post-ref" :to="`/${notification.post.user.username}/${notification.post.id}`">
 				%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%
-			</a>
+			</router-link>
 		</div>
 	</template>
 </div>
diff --git a/src/web/app/mobile/views/components/post-preview.vue b/src/web/app/mobile/views/components/post-preview.vue
index ccb8b5f33..e9a411925 100644
--- a/src/web/app/mobile/views/components/post-preview.vue
+++ b/src/web/app/mobile/views/components/post-preview.vue
@@ -1,15 +1,15 @@
 <template>
 <div class="mk-post-preview">
-	<a class="avatar-anchor" :href="`/${post.user.username}`">
+	<router-link class="avatar-anchor" :to="`/${post.user.username}`">
 		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
-	</a>
+	</router-link>
 	<div class="main">
 		<header>
-			<a class="name" :href="`/${post.user.username}`">{{ post.user.name }}</a>
+			<router-link class="name" :to="`/${post.user.username}`">{{ post.user.name }}</router-link>
 			<span class="username">@{{ post.user.username }}</span>
-			<a class="time" :href="`/${post.user.username}/${post.id}`">
+			<router-link class="time" :to="`/${post.user.username}/${post.id}`">
 				<mk-time :time="post.created_at"/>
-			</a>
+			</router-link>
 		</header>
 		<div class="body">
 			<mk-sub-post-content class="text" :post="post"/>

From c85e7310b1de96e701612f50f71d7132146175a4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 21:46:59 +0900
Subject: [PATCH 0472/1250] :v:

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

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 dbf02bd07..17aa83201 100644
--- a/src/web/app/desktop/views/pages/user/user.home.vue
+++ b/src/web/app/desktop/views/pages/user/user.home.vue
@@ -9,7 +9,7 @@
 		</div>
 	</div>
 	<main>
-		<mk-post-detail v-if="user.pinned_post" :post="user.pinned_post" compact/>
+		<mk-post-detail v-if="user.pinned_post" :post="user.pinned_post" :compact="true"/>
 		<x-timeline class="timeline" ref="tl" :user="user"/>
 	</main>
 	<div>

From 79a1e426c80878710def8c7a7ba2e0fcb2203d42 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 21:47:30 +0900
Subject: [PATCH 0473/1250] v3837

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index e4ccc8715..5f3b8f2e1 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3834",
+	"version": "0.0.3837",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 55e0913e5125b3e4d5d7891af7d988392e086310 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 21:52:21 +0900
Subject: [PATCH 0474/1250] :earth_asia:

---
 locales/ja.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/locales/ja.yml b/locales/ja.yml
index 70ff8739f..b60e9e3f4 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -442,8 +442,8 @@ desktop:
 
     mk-calendar-widget:
       title: "{1}年 {2}月"
-      prev: "先月"
-      next: "来月"
+      prev: "前の月"
+      next: "次の月"
       go: "クリックして時間遡行"
 
     mk-post-form-home-widget:

From 6be725399ae30b92ac473c51bc62335ac7e20650 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 23:30:30 +0900
Subject: [PATCH 0475/1250] :v:

---
 src/web/app/app.styl                          | 10 ---
 .../app/mobile/views/components/ui.nav.vue    | 80 +++++++++++++------
 src/web/style.styl                            |  6 +-
 3 files changed, 59 insertions(+), 37 deletions(-)

diff --git a/src/web/app/app.styl b/src/web/app/app.styl
index c441a445f..431b9daa6 100644
--- a/src/web/app/app.styl
+++ b/src/web/app/app.styl
@@ -124,15 +124,5 @@ pre
 		overflow auto
 		tab-size 2
 
-mk-locker
-	display block
-	position fixed
-	top 0
-	left 0
-	z-index 65536
-	width 100%
-	height 100%
-	cursor wait
-
 [data-fa]
 	display inline-block
diff --git a/src/web/app/mobile/views/components/ui.nav.vue b/src/web/app/mobile/views/components/ui.nav.vue
index 9fe0864aa..a3c0042c3 100644
--- a/src/web/app/mobile/views/components/ui.nav.vue
+++ b/src/web/app/mobile/views/components/ui.nav.vue
@@ -1,30 +1,38 @@
 <template>
-<div class="nav" :style="{ display: isOpen ? 'block' : 'none' }">
-	<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"/>
-			<p class="name">{{ os.i.name }}</p>
-		</router-link>
-		<div class="links">
-			<ul>
-				<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 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 to="/i/settings">%fa:cog%%i18n:mobile.tags.mk-ui-nav.settings%%fa:angle-right%</router-link></li>
-			</ul>
+<div class="nav">
+	<transition name="back">
+		<div class="backdrop"
+			v-if="isOpen"
+			@click="$parent.isDrawerOpening = false"
+			@touchstart="$parent.isDrawerOpening = false"
+		></div>
+	</transition>
+	<transition name="nav">
+		<div class="body" v-if="isOpen">
+			<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><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 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 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>
 		</div>
-		<a :href="aboutUrl"><p class="about">%i18n:mobile.tags.mk-ui-nav.about%</p></a>
-	</div>
+	</transition>
 </div>
 </template>
 
@@ -197,4 +205,26 @@ export default Vue.extend({
 		a
 			color #777
 
+.nav-enter-active,
+.nav-leave-active {
+	opacity: 1;
+	transform: translateX(0);
+	transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+.nav-enter,
+.nav-leave-active {
+	opacity: 0;
+	transform: translateX(-240px);
+}
+
+.back-enter-active,
+.back-leave-active {
+	opacity: 1;
+	transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+.back-enter,
+.back-leave-active {
+	opacity: 0;
+}
+
 </style>
diff --git a/src/web/style.styl b/src/web/style.styl
index c25fc8fb5..6d1e53e5a 100644
--- a/src/web/style.styl
+++ b/src/web/style.styl
@@ -12,8 +12,8 @@
 	position relative
 	box-sizing border-box
 	background-clip padding-box !important
-	tap-highlight-color rgba($theme-color, 0.7)
-	-webkit-tap-highlight-color rgba($theme-color, 0.7)
+	tap-highlight-color transparent
+	-webkit-tap-highlight-color transparent
 
 html, body
 	margin 0
@@ -26,6 +26,8 @@ a
 	text-decoration none
 	color $theme-color
 	cursor pointer
+	tap-highlight-color rgba($theme-color, 0.7) !important
+	-webkit-tap-highlight-color rgba($theme-color, 0.7) !important
 
 	&:hover
 		text-decoration underline

From 83300285f97a37c2ff631ea4c32022a84bdea757 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 23:31:03 +0900
Subject: [PATCH 0476/1250] v3840

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 5f3b8f2e1..3fc42c1a5 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3837",
+	"version": "0.0.3840",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 2b80d30328fae6fbe6b2ef5c3c2f09905ed917fe Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 24 Feb 2018 02:46:09 +0900
Subject: [PATCH 0477/1250] Implement #1098

---
 src/api/endpoints.ts                          |   7 +-
 src/api/endpoints/i/update_home.ts            |  11 +-
 src/api/endpoints/i/update_mobile_home.ts     |  50 +++++
 src/web/app/common/define-widget.ts           |  26 ++-
 .../app/common/scripts/check-for-update.ts    |   4 +-
 src/web/app/common/views/components/index.ts  |  30 +++
 .../views/components/widgets/access-log.vue   |  70 +++----
 .../views/components/widgets/broadcast.vue    |  10 +-
 .../views/components/widgets/calendar.vue     |   7 +
 .../views/components/widgets/donation.vue     |  15 +-
 .../views/components/widgets/nav.vue          |  12 +-
 .../views/components/widgets/photo-stream.vue | 104 +++++++++++
 .../views/components/widgets/profile.vue      |   0
 .../common/views/components/widgets/rss.vue   |  93 ++++++++++
 .../components/widgets/server.cpu-memory.vue  |   0
 .../views/components/widgets/server.cpu.vue   |   0
 .../views/components/widgets/server.disk.vue  |   0
 .../views/components/widgets/server.info.vue  |   0
 .../components/widgets/server.memory.vue      |   0
 .../views/components/widgets/server.pie.vue   |   0
 .../components/widgets/server.uptimes.vue     |   0
 .../views/components/widgets/server.vue       |  93 ++++++++++
 .../views/components/widgets/slideshow.vue    |   0
 .../views/components/widgets/tips.vue         |   0
 .../views/components/widgets/version.vue      |   0
 src/web/app/desktop/views/components/index.ts |  32 +---
 .../views/components/widget-container.vue     |  72 ++++++++
 .../views/components/widgets/photo-stream.vue | 122 ------------
 .../desktop/views/components/widgets/rss.vue  | 111 -----------
 .../views/components/widgets/server.vue       | 131 -------------
 .../activity.vue}                             |   4 +-
 src/web/app/mobile/views/components/home.vue  |  29 ---
 src/web/app/mobile/views/components/index.ts  |  14 +-
 .../app/mobile/views/components/ui.header.vue |   4 +-
 src/web/app/mobile/views/components/ui.vue    |   6 +-
 .../views/components/widget-container.vue     |  65 +++++++
 .../views/components/widgets/activity.vue     |  23 +++
 src/web/app/mobile/views/pages/drive.vue      |   4 +-
 src/web/app/mobile/views/pages/home.vue       | 174 +++++++++++++++++-
 .../app/mobile/views/pages/notifications.vue  |   4 +-
 src/web/app/mobile/views/pages/user.vue       |   1 -
 src/web/app/mobile/views/pages/user/home.vue  |   6 +-
 42 files changed, 823 insertions(+), 511 deletions(-)
 create mode 100644 src/api/endpoints/i/update_mobile_home.ts
 rename src/web/app/{desktop => common}/views/components/widgets/access-log.vue (61%)
 rename src/web/app/{desktop => common}/views/components/widgets/broadcast.vue (96%)
 rename src/web/app/{desktop => common}/views/components/widgets/calendar.vue (96%)
 rename src/web/app/{desktop => common}/views/components/widgets/donation.vue (79%)
 rename src/web/app/{desktop => common}/views/components/widgets/nav.vue (67%)
 create mode 100644 src/web/app/common/views/components/widgets/photo-stream.vue
 rename src/web/app/{desktop => common}/views/components/widgets/profile.vue (100%)
 create mode 100644 src/web/app/common/views/components/widgets/rss.vue
 rename src/web/app/{desktop => common}/views/components/widgets/server.cpu-memory.vue (100%)
 rename src/web/app/{desktop => common}/views/components/widgets/server.cpu.vue (100%)
 rename src/web/app/{desktop => common}/views/components/widgets/server.disk.vue (100%)
 rename src/web/app/{desktop => common}/views/components/widgets/server.info.vue (100%)
 rename src/web/app/{desktop => common}/views/components/widgets/server.memory.vue (100%)
 rename src/web/app/{desktop => common}/views/components/widgets/server.pie.vue (100%)
 rename src/web/app/{desktop => common}/views/components/widgets/server.uptimes.vue (100%)
 create mode 100644 src/web/app/common/views/components/widgets/server.vue
 rename src/web/app/{desktop => common}/views/components/widgets/slideshow.vue (100%)
 rename src/web/app/{desktop => common}/views/components/widgets/tips.vue (100%)
 rename src/web/app/{desktop => common}/views/components/widgets/version.vue (100%)
 create mode 100644 src/web/app/desktop/views/components/widget-container.vue
 delete mode 100644 src/web/app/desktop/views/components/widgets/photo-stream.vue
 delete mode 100644 src/web/app/desktop/views/components/widgets/rss.vue
 delete mode 100644 src/web/app/desktop/views/components/widgets/server.vue
 rename src/web/app/mobile/views/{pages/user/home.activity.vue => components/activity.vue} (96%)
 delete mode 100644 src/web/app/mobile/views/components/home.vue
 create mode 100644 src/web/app/mobile/views/components/widget-container.vue
 create mode 100644 src/web/app/mobile/views/components/widgets/activity.vue

diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts
index ff214c300..cbc016f20 100644
--- a/src/api/endpoints.ts
+++ b/src/api/endpoints.ts
@@ -182,7 +182,12 @@ const endpoints: Endpoint[] = [
 	{
 		name: 'i/update_home',
 		withCredential: true,
-		kind: 'account-write'
+		secure: true
+	},
+	{
+		name: 'i/update_mobile_home',
+		withCredential: true,
+		secure: true
 	},
 	{
 		name: 'i/change_password',
diff --git a/src/api/endpoints/i/update_home.ts b/src/api/endpoints/i/update_home.ts
index 429e88529..5dfb7d791 100644
--- a/src/api/endpoints/i/update_home.ts
+++ b/src/api/endpoints/i/update_home.ts
@@ -4,16 +4,7 @@
 import $ from 'cafy';
 import User from '../../models/user';
 
-/**
- * Update myself
- *
- * @param {any} params
- * @param {any} user
- * @param {any} _
- * @param {boolean} isSecure
- * @return {Promise<any>}
- */
-module.exports = async (params, user, _, isSecure) => new Promise(async (res, rej) => {
+module.exports = async (params, user) => new Promise(async (res, rej) => {
 	// Get 'home' parameter
 	const [home, homeErr] = $(params.home).optional.array().each(
 		$().strict.object()
diff --git a/src/api/endpoints/i/update_mobile_home.ts b/src/api/endpoints/i/update_mobile_home.ts
new file mode 100644
index 000000000..a87d89cad
--- /dev/null
+++ b/src/api/endpoints/i/update_mobile_home.ts
@@ -0,0 +1,50 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import User from '../../models/user';
+
+module.exports = async (params, user) => new Promise(async (res, rej) => {
+	// Get 'home' parameter
+	const [home, homeErr] = $(params.home).optional.array().each(
+		$().strict.object()
+			.have('name', $().string())
+			.have('id', $().string())
+			.have('data', $().object())).$;
+	if (homeErr) return rej('invalid home param');
+
+	// Get 'id' parameter
+	const [id, idErr] = $(params.id).optional.string().$;
+	if (idErr) return rej('invalid id param');
+
+	// Get 'data' parameter
+	const [data, dataErr] = $(params.data).optional.object().$;
+	if (dataErr) return rej('invalid data param');
+
+	if (home) {
+		await User.update(user._id, {
+			$set: {
+				'client_settings.mobile_home': home
+			}
+		});
+
+		res();
+	} else {
+		if (id == null && data == null) return rej('you need to set id and data params if home param unset');
+
+		const _home = user.client_settings.mobile_home || [];
+		const widget = _home.find(w => w.id == id);
+
+		if (widget == null) return rej('widget not found');
+
+		widget.data = data;
+
+		await User.update(user._id, {
+			$set: {
+				'client_settings.mobile_home': _home
+			}
+		});
+
+		res();
+	}
+});
diff --git a/src/web/app/common/define-widget.ts b/src/web/app/common/define-widget.ts
index fd13a3395..60cd1969c 100644
--- a/src/web/app/common/define-widget.ts
+++ b/src/web/app/common/define-widget.ts
@@ -8,6 +8,10 @@ export default function<T extends object>(data: {
 		props: {
 			widget: {
 				type: Object
+			},
+			isMobile: {
+				type: Boolean,
+				default: false
 			}
 		},
 		computed: {
@@ -21,6 +25,7 @@ export default function<T extends object>(data: {
 			};
 		},
 		created() {
+			if (this.widget.data == null) this.widget.data = {};
 			if (this.props) {
 				Object.keys(this.props).forEach(prop => {
 					if (this.widget.data.hasOwnProperty(prop)) {
@@ -30,12 +35,21 @@ export default function<T extends object>(data: {
 			}
 
 			this.$watch('props', 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;
-				});
+				if (this.isMobile) {
+					(this as any).api('i/update_mobile_home', {
+						id: this.id,
+						data: newProps
+					}).then(() => {
+						(this as any).os.i.client_settings.mobile_home.find(w => w.id == this.id).data = newProps;
+					});
+				} else {
+					(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/scripts/check-for-update.ts b/src/web/app/common/scripts/check-for-update.ts
index 0855676a4..fe539407d 100644
--- a/src/web/app/common/scripts/check-for-update.ts
+++ b/src/web/app/common/scripts/check-for-update.ts
@@ -9,7 +9,9 @@ export default async function(mios: MiOS) {
 
 		// Clear cache (serive worker)
 		try {
-			navigator.serviceWorker.controller.postMessage('clear');
+			if (navigator.serviceWorker.controller) {
+				navigator.serviceWorker.controller.postMessage('clear');
+			}
 
 			navigator.serviceWorker.getRegistrations().then(registrations => {
 				registrations.forEach(registration => registration.unregister());
diff --git a/src/web/app/common/views/components/index.ts b/src/web/app/common/views/components/index.ts
index ab0f1767d..e66a32326 100644
--- a/src/web/app/common/views/components/index.ts
+++ b/src/web/app/common/views/components/index.ts
@@ -21,6 +21,21 @@ import urlPreview from './url-preview.vue';
 import twitterSetting from './twitter-setting.vue';
 import fileTypeIcon from './file-type-icon.vue';
 
+//#region widgets
+import wAccessLog from './widgets/access-log.vue';
+import wVersion from './widgets/version.vue';
+import wRss from './widgets/rss.vue';
+import wProfile from './widgets/profile.vue';
+import wServer from './widgets/server.vue';
+import wBroadcast from './widgets/broadcast.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 wNav from './widgets/nav.vue';
+//#endregion
+
 Vue.component('mk-signin', signin);
 Vue.component('mk-signup', signup);
 Vue.component('mk-forkit', forkit);
@@ -41,3 +56,18 @@ 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);
+
+//#region widgets
+Vue.component('mkw-nav', wNav);
+Vue.component('mkw-calendar', wCalendar);
+Vue.component('mkw-photo-stream', wPhotoStream);
+Vue.component('mkw-slideshow', wSlideshow);
+Vue.component('mkw-tips', wTips);
+Vue.component('mkw-donation', wDonation);
+Vue.component('mkw-broadcast', wBroadcast);
+Vue.component('mkw-profile', wProfile);
+Vue.component('mkw-server', wServer);
+Vue.component('mkw-rss', wRss);
+Vue.component('mkw-version', wVersion);
+Vue.component('mkw-access-log', wAccessLog);
+//#endregion
diff --git a/src/web/app/desktop/views/components/widgets/access-log.vue b/src/web/app/common/views/components/widgets/access-log.vue
similarity index 61%
rename from src/web/app/desktop/views/components/widgets/access-log.vue
rename to src/web/app/common/views/components/widgets/access-log.vue
index a04da1daa..c810c2d15 100644
--- a/src/web/app/desktop/views/components/widgets/access-log.vue
+++ b/src/web/app/common/views/components/widgets/access-log.vue
@@ -1,15 +1,16 @@
 <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>
-			<b>{{ req.method }}</b>
-			<span>{{ req.path }}</span>
-		</p>
-	</div>
+	<mk-widget-container :show-header="props.design == 0">
+		<template slot="header">%fa:server%%i18n:desktop.tags.mk-access-log-home-widget.title%</template>
+
+		<div :class="$style.logs" ref="log">
+			<p v-for="req in requests">
+				<span :class="$style.ip" :style="`color:${ req.fg }; background:${ req.bg }`">{{ req.ip }}</span>
+				<b>{{ req.method }}</b>
+				<span>{{ req.path }}</span>
+			</p>
+		</div>
+	</mk-widget-container>
 </div>
 </template>
 
@@ -65,44 +66,25 @@ export default define({
 });
 </script>
 
-<style lang="stylus" scoped>
-.mkw-access-log
-	overflow hidden
-	background #fff
-	border solid 1px rgba(0, 0, 0, 0.075)
-	border-radius 6px
+<style lang="stylus" module>
+.logs
+	max-height 250px
+	overflow auto
 
-	> .title
-		z-index 1
+	> p
 		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)
+		padding 8px
+		font-size 0.8em
+		color #555
 
-		> [data-fa]
+		&:nth-child(odd)
+			background rgba(0, 0, 0, 0.025)
+
+		> b
 			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
-				padding 0 4px
-
-			> b
-				margin-right 4px
+.ip
+	margin-right 4px
+	padding 0 4px
 
 </style>
diff --git a/src/web/app/desktop/views/components/widgets/broadcast.vue b/src/web/app/common/views/components/widgets/broadcast.vue
similarity index 96%
rename from src/web/app/desktop/views/components/widgets/broadcast.vue
rename to src/web/app/common/views/components/widgets/broadcast.vue
index e4b7e2532..0bb59caf4 100644
--- a/src/web/app/desktop/views/components/widgets/broadcast.vue
+++ b/src/web/app/common/views/components/widgets/broadcast.vue
@@ -1,5 +1,9 @@
 <template>
-<div class="mkw-broadcast" :data-found="broadcasts.length != 0" :data-melt="props.design == 1">
+<div class="mkw-broadcast"
+	:data-found="broadcasts.length != 0"
+	:data-melt="props.design == 1"
+	:data-mobile="isMobile"
+>
 	<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>
@@ -150,4 +154,8 @@ export default define({
 		display block
 		font-size 0.7em
 
+	&[data-mobile]
+		> p
+			color #fff
+
 </style>
diff --git a/src/web/app/desktop/views/components/widgets/calendar.vue b/src/web/app/common/views/components/widgets/calendar.vue
similarity index 96%
rename from src/web/app/desktop/views/components/widgets/calendar.vue
rename to src/web/app/common/views/components/widgets/calendar.vue
index c16602db4..bfcbd7f68 100644
--- a/src/web/app/desktop/views/components/widgets/calendar.vue
+++ b/src/web/app/common/views/components/widgets/calendar.vue
@@ -2,6 +2,7 @@
 <div class="mkw-calendar"
 	:data-melt="props.design == 1"
 	:data-special="special"
+	:data-mobile="isMobile"
 >
 	<div class="calendar" :data-is-holiday="isHoliday">
 		<p class="month-and-year">
@@ -66,6 +67,7 @@ export default define({
 	},
 	methods: {
 		func() {
+			if (this.isMobile) return;
 			if (this.props.design == 2) {
 				this.props.design = 0;
 			} else {
@@ -119,6 +121,11 @@ export default define({
 		background transparent
 		border none
 
+	&[data-mobile]
+		border none
+		border-radius 8px
+		box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+
 	&:after
 		content ""
 		display block
diff --git a/src/web/app/desktop/views/components/widgets/donation.vue b/src/web/app/common/views/components/widgets/donation.vue
similarity index 79%
rename from src/web/app/desktop/views/components/widgets/donation.vue
rename to src/web/app/common/views/components/widgets/donation.vue
index fbab0fca6..08aab8ecd 100644
--- a/src/web/app/desktop/views/components/widgets/donation.vue
+++ b/src/web/app/common/views/components/widgets/donation.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mkw-donation">
+<div class="mkw-donation" :data-mobile="isMobile">
 	<article>
 		<h1>%fa:heart%%i18n:desktop.tags.mk-donation-home-widget.title%</h1>
 		<p>
@@ -42,4 +42,17 @@ export default define({
 			font-size 0.8em
 			color #999
 
+	&[data-mobile]
+		border none
+		background #ead8bb
+		border-radius 8px
+		box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+
+		> article
+			> h1
+				color #7b8871
+
+			> p
+				color #777d71
+
 </style>
diff --git a/src/web/app/desktop/views/components/widgets/nav.vue b/src/web/app/common/views/components/widgets/nav.vue
similarity index 67%
rename from src/web/app/desktop/views/components/widgets/nav.vue
rename to src/web/app/common/views/components/widgets/nav.vue
index 5e04c266c..ce88e587a 100644
--- a/src/web/app/desktop/views/components/widgets/nav.vue
+++ b/src/web/app/common/views/components/widgets/nav.vue
@@ -1,6 +1,10 @@
 <template>
 <div class="mkw-nav">
-	<mk-nav/>
+	<mk-widget-container>
+		<div :class="$style.body">
+			<mk-nav/>
+		</div>
+	</mk-widget-container>
 </div>
 </template>
 
@@ -11,14 +15,12 @@ export default define({
 });
 </script>
 
-<style lang="stylus" scoped>
-.mkw-nav
+<style lang="stylus" module>
+.body
 	padding 16px
 	font-size 12px
 	color #aaa
 	background #fff
-	border solid 1px rgba(0, 0, 0, 0.075)
-	border-radius 6px
 
 	a
 		color #999
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..dcaa6624d
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/photo-stream.vue
@@ -0,0 +1,104 @@
+<template>
+<div class="mkw-photo-stream" :class="$style.root" :data-melt="props.design == 2">
+	<mk-widget-container :show-header="props.design == 0" :naked="props.design == 2">
+		<template slot="header">%fa:camera%%i18n:desktop.tags.mk-photo-stream-home-widget.title%</template>
+
+		<p :class="$style.fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+		<div :class="$style.stream" v-if="!fetching && images.length > 0">
+			<div v-for="image in images" :key="image.id" :class="$style.img" :style="`background-image: url(${image.url}?thumbnail&size=256)`"></div>
+		</div>
+		<p :class="$style.empty" v-if="!fetching && images.length == 0">%i18n:desktop.tags.mk-photo-stream-home-widget.no-photos%</p>
+	</mk-widget-container>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../../common/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 as any).os.stream.getConnection();
+		this.connectionId = (this as any).os.stream.use();
+
+		this.connection.on('drive_file_created', this.onDriveFileCreated);
+
+		(this as any).api('drive/stream', {
+			type: 'image/*',
+			limit: 9
+		}).then(images => {
+			this.images = images;
+			this.fetching = false;
+		});
+	},
+	beforeDestroy() {
+		this.connection.off('drive_file_created', this.onDriveFileCreated);
+		(this as any).os.stream.dispose(this.connectionId);
+	},
+	methods: {
+		onDriveFileCreated(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" module>
+.root[data-melt]
+	.stream
+		padding 0
+
+	.img
+		border solid 4px transparent
+		border-radius 8px
+
+.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/desktop/views/components/widgets/profile.vue b/src/web/app/common/views/components/widgets/profile.vue
similarity index 100%
rename from src/web/app/desktop/views/components/widgets/profile.vue
rename to src/web/app/common/views/components/widgets/profile.vue
diff --git a/src/web/app/common/views/components/widgets/rss.vue b/src/web/app/common/views/components/widgets/rss.vue
new file mode 100644
index 000000000..e80896bea
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/rss.vue
@@ -0,0 +1,93 @@
+<template>
+<div class="mkw-rss" :data-mobile="isMobile">
+	<mk-widget-container :show-header="!props.compact">
+		<template slot="header">%fa:rss-square%RSS</template>
+		<button slot="func" title="設定" @click="setting">%fa:cog%</button>
+
+		<p :class="$style.fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+		<div :class="$style.feed" v-else>
+			<a v-for="item in items" :href="item.link" target="_blank">{{ item.title }}</a>
+		</div>
+	</mk-widget-container>
+</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;
+				});
+			});
+		},
+		setting() {
+			alert('not implemented yet');
+		}
+	}
+});
+</script>
+
+<style lang="stylus" module>
+.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
+
+&[data-mobile]
+	.feed
+		padding 0
+		font-size 1em
+
+		> a
+			padding 8px 16px
+
+			&:nth-child(even)
+				background #e2e2e2
+
+</style>
diff --git a/src/web/app/desktop/views/components/widgets/server.cpu-memory.vue b/src/web/app/common/views/components/widgets/server.cpu-memory.vue
similarity index 100%
rename from src/web/app/desktop/views/components/widgets/server.cpu-memory.vue
rename to src/web/app/common/views/components/widgets/server.cpu-memory.vue
diff --git a/src/web/app/desktop/views/components/widgets/server.cpu.vue b/src/web/app/common/views/components/widgets/server.cpu.vue
similarity index 100%
rename from src/web/app/desktop/views/components/widgets/server.cpu.vue
rename to src/web/app/common/views/components/widgets/server.cpu.vue
diff --git a/src/web/app/desktop/views/components/widgets/server.disk.vue b/src/web/app/common/views/components/widgets/server.disk.vue
similarity index 100%
rename from src/web/app/desktop/views/components/widgets/server.disk.vue
rename to src/web/app/common/views/components/widgets/server.disk.vue
diff --git a/src/web/app/desktop/views/components/widgets/server.info.vue b/src/web/app/common/views/components/widgets/server.info.vue
similarity index 100%
rename from src/web/app/desktop/views/components/widgets/server.info.vue
rename to src/web/app/common/views/components/widgets/server.info.vue
diff --git a/src/web/app/desktop/views/components/widgets/server.memory.vue b/src/web/app/common/views/components/widgets/server.memory.vue
similarity index 100%
rename from src/web/app/desktop/views/components/widgets/server.memory.vue
rename to src/web/app/common/views/components/widgets/server.memory.vue
diff --git a/src/web/app/desktop/views/components/widgets/server.pie.vue b/src/web/app/common/views/components/widgets/server.pie.vue
similarity index 100%
rename from src/web/app/desktop/views/components/widgets/server.pie.vue
rename to src/web/app/common/views/components/widgets/server.pie.vue
diff --git a/src/web/app/desktop/views/components/widgets/server.uptimes.vue b/src/web/app/common/views/components/widgets/server.uptimes.vue
similarity index 100%
rename from src/web/app/desktop/views/components/widgets/server.uptimes.vue
rename to src/web/app/common/views/components/widgets/server.uptimes.vue
diff --git a/src/web/app/common/views/components/widgets/server.vue b/src/web/app/common/views/components/widgets/server.vue
new file mode 100644
index 000000000..4ebc5767d
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/server.vue
@@ -0,0 +1,93 @@
+<template>
+<div class="mkw-server">
+	<mk-widget-container :show-header="props.design == 0" :naked="props.design == 2">
+		<template slot="header">%fa:server%%i18n:desktop.tags.mk-server-home-widget.title%</template>
+		<button slot="func" @click="toggle" title="%i18n:desktop.tags.mk-server-home-widget.toggle%">%fa:sort%</button>
+
+		<p :class="$style.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>
+	</mk-widget-container>
+</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: {
+		XCpuMemory,
+		XCpu,
+		XMemory,
+		XDisk,
+		XUptimes,
+		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.view == 5) {
+				this.props.view = 0;
+			} else {
+				this.props.view++;
+			}
+		},
+		func() {
+			if (this.props.design == 2) {
+				this.props.design = 0;
+			} else {
+				this.props.design++;
+			}
+		}
+	}
+});
+</script>
+
+<style lang="stylus" module>
+.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/widgets/slideshow.vue b/src/web/app/common/views/components/widgets/slideshow.vue
similarity index 100%
rename from src/web/app/desktop/views/components/widgets/slideshow.vue
rename to src/web/app/common/views/components/widgets/slideshow.vue
diff --git a/src/web/app/desktop/views/components/widgets/tips.vue b/src/web/app/common/views/components/widgets/tips.vue
similarity index 100%
rename from src/web/app/desktop/views/components/widgets/tips.vue
rename to src/web/app/common/views/components/widgets/tips.vue
diff --git a/src/web/app/desktop/views/components/widgets/version.vue b/src/web/app/common/views/components/widgets/version.vue
similarity index 100%
rename from src/web/app/desktop/views/components/widgets/version.vue
rename to src/web/app/common/views/components/widgets/version.vue
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index da59d9219..7584cb498 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -27,27 +27,19 @@ 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';
-import wSlideshow from './widgets/slideshow.vue';
-import wTips from './widgets/tips.vue';
-import wDonation from './widgets/donation.vue';
+import widgetContainer from './widget-container.vue';
+
+//#region widgets
 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';
 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';
+//#endregion
 
 Vue.component('mk-ui', ui);
 Vue.component('mk-ui-notification', uiNotification);
@@ -76,24 +68,16 @@ 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);
-Vue.component('mkw-slideshow', wSlideshow);
-Vue.component('mkw-tips', wTips);
-Vue.component('mkw-donation', wDonation);
+Vue.component('mk-widget-container', widgetContainer);
+
+//#region widgets
 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);
 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);
+//#endregion
diff --git a/src/web/app/desktop/views/components/widget-container.vue b/src/web/app/desktop/views/components/widget-container.vue
new file mode 100644
index 000000000..7b4e1f55f
--- /dev/null
+++ b/src/web/app/desktop/views/components/widget-container.vue
@@ -0,0 +1,72 @@
+<template>
+<div class="mk-widget-container" :class="{ naked }">
+	<header v-if="showHeader">
+		<div class="title"><slot name="header"></slot></div>
+		<slot name="func"></slot>
+	</header>
+	<slot></slot>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: {
+		showHeader: {
+			type: Boolean,
+			default: true
+		},
+		naked: {
+			type: Boolean,
+			default: false
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-widget-container
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+	overflow hidden
+
+	&.naked
+		background transparent !important
+		border none !important
+
+	> header
+		> .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
+
+			&:empty
+				display none
+
+		> 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
+
+</style>
diff --git a/src/web/app/desktop/views/components/widgets/photo-stream.vue b/src/web/app/desktop/views/components/widgets/photo-stream.vue
deleted file mode 100644
index 04b71975b..000000000
--- a/src/web/app/desktop/views/components/widgets/photo-stream.vue
+++ /dev/null
@@ -1,122 +0,0 @@
-<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 '../../../../common/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 as any).os.stream.getConnection();
-		this.connectionId = (this as any).os.stream.use();
-
-		this.connection.on('drive_file_created', this.onDriveFileCreated);
-
-		(this as any).api('drive/stream', {
-			type: 'image/*',
-			limit: 9
-		}).then(images => {
-			this.images = images;
-			this.fetching = false;
-		});
-	},
-	beforeDestroy() {
-		this.connection.off('drive_file_created', this.onDriveFileCreated);
-		(this as any).os.stream.dispose(this.connectionId);
-	},
-	methods: {
-		onDriveFileCreated(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/desktop/views/components/widgets/rss.vue b/src/web/app/desktop/views/components/widgets/rss.vue
deleted file mode 100644
index 350712971..000000000
--- a/src/web/app/desktop/views/components/widgets/rss.vue
+++ /dev/null
@@ -1,111 +0,0 @@
-<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>
diff --git a/src/web/app/desktop/views/components/widgets/server.vue b/src/web/app/desktop/views/components/widgets/server.vue
deleted file mode 100644
index 1c0da8422..000000000
--- a/src/web/app/desktop/views/components/widgets/server.vue
+++ /dev/null
@@ -1,131 +0,0 @@
-<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: {
-		XCpuMemory,
-		XCpu,
-		XMemory,
-		XDisk,
-		XUptimes,
-		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.view == 5) {
-				this.props.view = 0;
-			} else {
-				this.props.view++;
-			}
-		},
-		func() {
-			if (this.props.design == 2) {
-				this.props.design = 0;
-			} else {
-				this.props.design++;
-			}
-		}
-	}
-});
-</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/mobile/views/pages/user/home.activity.vue b/src/web/app/mobile/views/components/activity.vue
similarity index 96%
rename from src/web/app/mobile/views/pages/user/home.activity.vue
rename to src/web/app/mobile/views/components/activity.vue
index 87970795b..b50044b3d 100644
--- a/src/web/app/mobile/views/pages/user/home.activity.vue
+++ b/src/web/app/mobile/views/components/activity.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="root activity">
+<div class="mk-activity">
 	<svg v-if="data" ref="canvas" viewBox="0 0 30 1" preserveAspectRatio="none">
 		<g v-for="(d, i) in data">
 			<rect width="0.8" :height="d.postsH"
@@ -47,7 +47,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.root.activity
+.mk-activity
 	max-width 600px
 	margin 0 auto
 
diff --git a/src/web/app/mobile/views/components/home.vue b/src/web/app/mobile/views/components/home.vue
deleted file mode 100644
index 3feab581d..000000000
--- a/src/web/app/mobile/views/components/home.vue
+++ /dev/null
@@ -1,29 +0,0 @@
-<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/index.ts b/src/web/app/mobile/views/components/index.ts
index 905baaf20..d372f2233 100644
--- a/src/web/app/mobile/views/components/index.ts
+++ b/src/web/app/mobile/views/components/index.ts
@@ -1,7 +1,6 @@
 import Vue from 'vue';
 
 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';
@@ -19,9 +18,14 @@ import notificationPreview from './notification-preview.vue';
 import usersList from './users-list.vue';
 import userPreview from './user-preview.vue';
 import userTimeline from './user-timeline.vue';
+import activity from './activity.vue';
+import widgetContainer from './widget-container.vue';
+
+//#region widgets
+import wActivity from './widgets/activity.vue';
+//#endregion
 
 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);
@@ -39,3 +43,9 @@ Vue.component('mk-notification-preview', notificationPreview);
 Vue.component('mk-users-list', usersList);
 Vue.component('mk-user-preview', userPreview);
 Vue.component('mk-user-timeline', userTimeline);
+Vue.component('mk-activity', activity);
+Vue.component('mk-widget-container', widgetContainer);
+
+//#region widgets
+Vue.component('mkw-activity', wActivity);
+//#endregion
diff --git a/src/web/app/mobile/views/components/ui.header.vue b/src/web/app/mobile/views/components/ui.header.vue
index 2df5ea162..026e7eb1b 100644
--- a/src/web/app/mobile/views/components/ui.header.vue
+++ b/src/web/app/mobile/views/components/ui.header.vue
@@ -9,9 +9,7 @@
 			<h1>
 				<slot>Misskey</slot>
 			</h1>
-			<button v-if="func" @click="func">
-				<slot name="funcIcon"></slot>
-			</button>
+			<slot name="func"></slot>
 		</div>
 	</div>
 </div>
diff --git a/src/web/app/mobile/views/components/ui.vue b/src/web/app/mobile/views/components/ui.vue
index 91d7ea29b..325ce9d40 100644
--- a/src/web/app/mobile/views/components/ui.vue
+++ b/src/web/app/mobile/views/components/ui.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mk-ui">
-	<x-header :func="func">
-		<template slot="funcIcon"><slot name="funcIcon"></slot></template>
+	<x-header>
+		<template slot="func"><slot name="func"></slot></template>
 		<slot name="header"></slot>
 	</x-header>
 	<x-nav :is-open="isDrawerOpening"/>
@@ -23,7 +23,7 @@ export default Vue.extend({
 		XHeader,
 		XNav
 	},
-	props: ['title', 'func'],
+	props: ['title'],
 	data() {
 		return {
 			isDrawerOpening: false,
diff --git a/src/web/app/mobile/views/components/widget-container.vue b/src/web/app/mobile/views/components/widget-container.vue
new file mode 100644
index 000000000..1775188a9
--- /dev/null
+++ b/src/web/app/mobile/views/components/widget-container.vue
@@ -0,0 +1,65 @@
+<template>
+<div class="mk-widget-container" :class="{ naked }">
+	<header v-if="showHeader">
+		<div class="title"><slot name="header"></slot></div>
+		<slot name="func"></slot>
+	</header>
+	<slot></slot>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: {
+		showHeader: {
+			type: Boolean,
+			default: true
+		},
+		naked: {
+			type: Boolean,
+			default: false
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-widget-container
+	background #eee
+	border-radius 8px
+	box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+	overflow hidden
+
+	&.naked
+		background transparent !important
+		border none !important
+
+	> header
+		> .title
+			margin 0
+			padding 8px 10px
+			font-size 15px
+			font-weight normal
+			color #465258
+			background #fff
+			border-radius 8px 8px 0 0
+
+			> [data-fa]
+				margin-right 6px
+
+			&:empty
+				display none
+
+		> button
+			position absolute
+			z-index 2
+			top 0
+			right 0
+			padding 0
+			width 42px
+			height 100%
+			font-size 15px
+			color #465258
+
+</style>
diff --git a/src/web/app/mobile/views/components/widgets/activity.vue b/src/web/app/mobile/views/components/widgets/activity.vue
new file mode 100644
index 000000000..c3fe63f26
--- /dev/null
+++ b/src/web/app/mobile/views/components/widgets/activity.vue
@@ -0,0 +1,23 @@
+<template>
+<div class="mkw-activity">
+	<mk-widget-container>
+		<template slot="header">%fa:chart-bar%アクティビティ</template>
+		<div :class="$style.body">
+			<mk-activity :user="os.i"/>
+		</div>
+	</mk-widget-container>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../../common/define-widget';
+
+export default define({
+	name: 'activity',
+});
+</script>
+
+<style lang="stylus" module>
+.body
+	padding 8px
+</style>
diff --git a/src/web/app/mobile/views/pages/drive.vue b/src/web/app/mobile/views/pages/drive.vue
index 47aeb52f4..ea61661cf 100644
--- a/src/web/app/mobile/views/pages/drive.vue
+++ b/src/web/app/mobile/views/pages/drive.vue
@@ -1,11 +1,11 @@
 <template>
-<mk-ui :func="fn">
+<mk-ui>
 	<span slot="header">
 		<template v-if="folder">%fa:R folder-open%{{ folder.name }}</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>
+	<template slot="func"><button @click="fn">%fa:ellipsis-h%</button></template>
 	<mk-drive
 		ref="browser"
 		:init-folder="initFolder"
diff --git a/src/web/app/mobile/views/pages/home.vue b/src/web/app/mobile/views/pages/home.vue
index c81cbcadb..0466fbbbf 100644
--- a/src/web/app/mobile/views/pages/home.vue
+++ b/src/web/app/mobile/views/pages/home.vue
@@ -1,24 +1,112 @@
 <template>
-<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>
+	<span slot="header" @click="showTl = !showTl">
+		<template v-if="showTl">%fa:home%タイムライン</template>
+		<template v-else>%fa:home%ウィジェット</template>
+		<span style="margin-left:8px">
+			<template v-if="showTl">%fa:angle-down%</template>
+			<template v-else>%fa:angle-up%</template>
+		</span>
+	</span>
+	<template slot="func">
+		<button @click="fn" v-if="showTl">%fa:pencil-alt%</button>
+		<button @click="customizing = !customizing" v-else>%fa:cog%</button>
+	</template>
+	<main>
+		<div class="tl">
+			<mk-timeline @loaded="onLoaded" v-show="showTl"/>
+		</div>
+		<div class="widgets" v-if="!showTl">
+			<template v-if="customizing">
+				<header>
+					<select v-model="widgetAdderSelected">
+						<option value="profile">プロフィール</option>
+						<option value="calendar">カレンダー</option>
+						<option value="activity">アクティビティ</option>
+						<option value="rss">RSSリーダー</option>
+						<option value="photo-stream">フォトストリーム</option>
+						<option value="version">バージョン</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>
+					<p>移動するには「三」をドラッグします。削除するには「x」をタップします。</p>
+				</header>
+				<x-draggable
+					:list="widgets"
+					:options="{ handle: '.handle', animation: 150 }"
+					@sort="onWidgetSort"
+				>
+					<div v-for="widget in widgets" class="customize-container" :key="widget.id">
+						<header>
+							<span class="handle">%fa:bars%</span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)">%fa:times%</button>
+						</header>
+						<div>
+							<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-mobile="true"/>
+						</div>
+					</div>
+				</x-draggable>
+			</template>
+			<template v-else>
+				<component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :widget="widget" :is-mobile="true" @chosen="warp"/>
+			</template>
+		</div>
+	</main>
 </mk-ui>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import * as XDraggable from 'vuedraggable';
+import * as uuid from 'uuid';
 import Progress from '../../../common/scripts/loading';
 import getPostSummary from '../../../../../common/get-post-summary';
 
 export default Vue.extend({
+	components: {
+		XDraggable
+	},
 	data() {
 		return {
 			connection: null,
 			connectionId: null,
-			unreadCount: 0
+			unreadCount: 0,
+			showTl: true,
+			widgets: [],
+			customizing: false,
+			widgetAdderSelected: null
 		};
 	},
+	created() {
+		if ((this as any).os.i.client_settings.mobile_home == null) {
+			Vue.set((this as any).os.i.client_settings, 'mobile_home',  [{
+				name: 'calendar',
+				id: 'a'
+			}, {
+				name: 'activity',
+				id: 'b'
+			}, {
+				name: 'rss',
+				id: 'c'
+			}, {
+				name: 'photo-stream',
+				id: 'd'
+			}, {
+				name: 'donation',
+				id: 'e'
+			}, {
+				name: 'nav',
+				id: 'f'
+			}, {
+				name: 'version',
+				id: 'g'
+			}]);
+		}
+		this.widgets = (this as any).os.i.client_settings.mobile_home;
+	},
 	mounted() {
 		document.title = 'Misskey';
 		document.documentElement.style.background = '#313a42';
@@ -40,7 +128,7 @@ export default Vue.extend({
 		fn() {
 			(this as any).apis.post();
 		},
-		onHomeLoaded() {
+		onLoaded() {
 			Progress.done();
 		},
 		onStreamPost(post) {
@@ -54,7 +142,81 @@ export default Vue.extend({
 				this.unreadCount = 0;
 				document.title = 'Misskey';
 			}
+		},
+		onWidgetSort() {
+			this.saveHome();
+		},
+		addWidget() {
+			const widget = {
+				name: this.widgetAdderSelected,
+				id: uuid(),
+				data: {}
+			};
+
+			this.widgets.unshift(widget);
+			this.saveHome();
+		},
+		removeWidget(widget) {
+			this.widgets = this.widgets.filter(w => w.id != widget.id);
+			this.saveHome();
+		},
+		saveHome() {
+			(this as any).api('i/update_mobile_home', {
+				home: this.widgets
+			});
+		},
+		warp() {
+
 		}
 	}
 });
 </script>
+
+<style lang="stylus" scoped>
+main
+
+	> .tl
+		> .mk-timeline
+			max-width 600px
+			margin 0 auto
+			padding 8px
+
+			@media (min-width 500px)
+				padding 16px
+
+	> .widgets
+		margin 0 auto
+		max-width 500px
+
+		> header
+			padding 8px
+			background #fff
+
+		.widget
+			margin 8px
+
+		.customize-container
+			margin 8px
+			background #fff
+
+			> header
+				line-height 32px
+				background #eee
+
+				> .handle
+					padding 0 8px
+
+				> .remove
+					position absolute
+					top 0
+					right 0
+					padding 0 8px
+					line-height 32px
+
+			> div
+				padding 8px
+
+				> *
+					pointer-events none
+
+</style>
diff --git a/src/web/app/mobile/views/pages/notifications.vue b/src/web/app/mobile/views/pages/notifications.vue
index b1243dbc7..3dcfb2f38 100644
--- a/src/web/app/mobile/views/pages/notifications.vue
+++ b/src/web/app/mobile/views/pages/notifications.vue
@@ -1,7 +1,7 @@
 <template>
-<mk-ui :func="fn">
+<mk-ui>
 	<span slot="header">%fa:R bell%%i18n:mobile.tags.mk-notifications-page.notifications%</span>
-	<span slot="funcIcon">%fa:check%</span>
+	<template slot="func"><button @click="fn">%fa:check%</button></template>
 	<mk-notifications @fetched="onFetched"/>
 </mk-ui>
 </template>
diff --git a/src/web/app/mobile/views/pages/user.vue b/src/web/app/mobile/views/pages/user.vue
index 27f65e623..378beeaf1 100644
--- a/src/web/app/mobile/views/pages/user.vue
+++ b/src/web/app/mobile/views/pages/user.vue
@@ -1,7 +1,6 @@
 <template>
 <mk-ui>
 	<span slot="header" v-if="!fetching">%fa:user% {{ user.name }}</span>
-	<template slot="funcIcon">%fa:pencil-alt%</template>
 	<main v-if="!fetching">
 		<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/user/home.vue b/src/web/app/mobile/views/pages/user/home.vue
index 4c6831787..fdbfd1bf5 100644
--- a/src/web/app/mobile/views/pages/user/home.vue
+++ b/src/web/app/mobile/views/pages/user/home.vue
@@ -16,7 +16,7 @@
 	<section class="activity">
 		<h2>%fa:chart-bar%%i18n:mobile.tags.mk-user-overview.activity%</h2>
 		<div>
-			<x-activity :user="user"/>
+			<mk-activity :user="user"/>
 		</div>
 	</section>
 	<section class="frequently-replied-users">
@@ -41,15 +41,13 @@ 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
+		XFollowersYouKnow
 	},
 	props: ['user']
 });

From 0cf5cfeaef76477be0264e2769bb13e85b2fb87c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 24 Feb 2018 03:03:26 +0900
Subject: [PATCH 0478/1250] :v:

---
 src/web/app/common/define-widget.ts           |  1 -
 .../views/components/widget-container.vue     |  2 +-
 .../views/components/widgets/activity.vue     | 11 ++++++-
 src/web/app/mobile/views/pages/home.vue       | 33 +++++++++++++------
 4 files changed, 34 insertions(+), 13 deletions(-)

diff --git a/src/web/app/common/define-widget.ts b/src/web/app/common/define-widget.ts
index 60cd1969c..826f9cc63 100644
--- a/src/web/app/common/define-widget.ts
+++ b/src/web/app/common/define-widget.ts
@@ -25,7 +25,6 @@ export default function<T extends object>(data: {
 			};
 		},
 		created() {
-			if (this.widget.data == null) this.widget.data = {};
 			if (this.props) {
 				Object.keys(this.props).forEach(prop => {
 					if (this.widget.data.hasOwnProperty(prop)) {
diff --git a/src/web/app/mobile/views/components/widget-container.vue b/src/web/app/mobile/views/components/widget-container.vue
index 1775188a9..81dde8f7a 100644
--- a/src/web/app/mobile/views/components/widget-container.vue
+++ b/src/web/app/mobile/views/components/widget-container.vue
@@ -33,7 +33,7 @@ export default Vue.extend({
 
 	&.naked
 		background transparent !important
-		border none !important
+		box-shadow none !important
 
 	> header
 		> .title
diff --git a/src/web/app/mobile/views/components/widgets/activity.vue b/src/web/app/mobile/views/components/widgets/activity.vue
index c3fe63f26..c4d30b07a 100644
--- a/src/web/app/mobile/views/components/widgets/activity.vue
+++ b/src/web/app/mobile/views/components/widgets/activity.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="mkw-activity">
-	<mk-widget-container>
+	<mk-widget-container :show-header="!props.compact">
 		<template slot="header">%fa:chart-bar%アクティビティ</template>
 		<div :class="$style.body">
 			<mk-activity :user="os.i"/>
@@ -14,6 +14,15 @@ import define from '../../../../common/define-widget';
 
 export default define({
 	name: 'activity',
+	props: () => ({
+		compact: false
+	})
+}).extend({
+	methods: {
+		func() {
+			this.props.compact = !this.props.compact;
+		}
+	}
 });
 </script>
 
diff --git a/src/web/app/mobile/views/pages/home.vue b/src/web/app/mobile/views/pages/home.vue
index 0466fbbbf..2d09d555a 100644
--- a/src/web/app/mobile/views/pages/home.vue
+++ b/src/web/app/mobile/views/pages/home.vue
@@ -44,7 +44,7 @@
 						<header>
 							<span class="handle">%fa:bars%</span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)">%fa:times%</button>
 						</header>
-						<div>
+						<div @click="widgetFunc(widget.id)">
 							<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-mobile="true"/>
 						</div>
 					</div>
@@ -82,30 +82,39 @@ export default Vue.extend({
 	},
 	created() {
 		if ((this as any).os.i.client_settings.mobile_home == null) {
-			Vue.set((this as any).os.i.client_settings, 'mobile_home',  [{
+			Vue.set((this as any).os.i.client_settings, 'mobile_home', [{
 				name: 'calendar',
-				id: 'a'
+				id: 'a', data: {}
 			}, {
 				name: 'activity',
-				id: 'b'
+				id: 'b', data: {}
 			}, {
 				name: 'rss',
-				id: 'c'
+				id: 'c', data: {}
 			}, {
 				name: 'photo-stream',
-				id: 'd'
+				id: 'd', data: {}
 			}, {
 				name: 'donation',
-				id: 'e'
+				id: 'e', data: {}
 			}, {
 				name: 'nav',
-				id: 'f'
+				id: 'f', data: {}
 			}, {
 				name: 'version',
-				id: 'g'
+				id: 'g', data: {}
 			}]);
+			this.widgets = (this as any).os.i.client_settings.mobile_home;
+			this.saveHome();
+		} else {
+			this.widgets = (this as any).os.i.client_settings.mobile_home;
 		}
-		this.widgets = (this as any).os.i.client_settings.mobile_home;
+
+		this.$watch('os.i', i => {
+			this.widgets = (this as any).os.i.client_settings.mobile_home;
+		}, {
+			deep: true
+		});
 	},
 	mounted() {
 		document.title = 'Misskey';
@@ -143,6 +152,10 @@ export default Vue.extend({
 				document.title = 'Misskey';
 			}
 		},
+		widgetFunc(id) {
+			const w = this.$refs[id][0];
+			if (w.func) w.func();
+		},
 		onWidgetSort() {
 			this.saveHome();
 		},

From c6113bc89af8eb9ac4016d794099ce0e4812f2cf Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 24 Feb 2018 03:05:50 +0900
Subject: [PATCH 0479/1250] :v:

---
 src/web/app/mobile/views/pages/home.vue | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/web/app/mobile/views/pages/home.vue b/src/web/app/mobile/views/pages/home.vue
index 2d09d555a..70650ce49 100644
--- a/src/web/app/mobile/views/pages/home.vue
+++ b/src/web/app/mobile/views/pages/home.vue
@@ -33,7 +33,7 @@
 						<option value="tips">ヒント</option>
 					</select>
 					<button @click="addWidget">追加</button>
-					<p>移動するには「三」をドラッグします。削除するには「x」をタップします。</p>
+					<p><a @click="hint">カスタマイズのヒント</a></p>
 				</header>
 				<x-draggable
 					:list="widgets"
@@ -152,6 +152,9 @@ export default Vue.extend({
 				document.title = 'Misskey';
 			}
 		},
+		hint() {
+			alert('ウィジェットを追加/削除したり並べ替えたりできます。ウィジェットを移動するには「三」をドラッグします。ウィジェットを削除するには「x」をタップします。いくつかのウィジェットはタップすることで表示を変更できます。');
+		},
 		widgetFunc(id) {
 			const w = this.$refs[id][0];
 			if (w.func) w.func();

From 7e0ac079316ecf80cf5540a091a46a9bf00f317b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 24 Feb 2018 03:06:26 +0900
Subject: [PATCH 0480/1250] v3845

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 3fc42c1a5..4e20f7e19 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3840",
+	"version": "0.0.3845",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 184515ecb593aa940aa275b8412670c82aed2a67 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 24 Feb 2018 03:30:13 +0900
Subject: [PATCH 0481/1250] :v:

---
 src/web/app/mobile/views/pages/home.vue | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/web/app/mobile/views/pages/home.vue b/src/web/app/mobile/views/pages/home.vue
index 70650ce49..e4ba2e719 100644
--- a/src/web/app/mobile/views/pages/home.vue
+++ b/src/web/app/mobile/views/pages/home.vue
@@ -16,7 +16,7 @@
 		<div class="tl">
 			<mk-timeline @loaded="onLoaded" v-show="showTl"/>
 		</div>
-		<div class="widgets" v-if="!showTl">
+		<div class="widgets" v-show="!showTl">
 			<template v-if="customizing">
 				<header>
 					<select v-model="widgetAdderSelected">
@@ -204,6 +204,9 @@ main
 		margin 0 auto
 		max-width 500px
 
+		@media (min-width 500px)
+			padding 8px
+
 		> header
 			padding 8px
 			background #fff

From 68f8d3c0ea64e7e43170921dd730c44509452bb0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 24 Feb 2018 03:30:24 +0900
Subject: [PATCH 0482/1250] v3846

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 4e20f7e19..0921b7072 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3845",
+	"version": "0.0.3846",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From c74aea6e46c5e88e1ee16ab5c0909a7b2734e295 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 24 Feb 2018 03:33:27 +0900
Subject: [PATCH 0483/1250] :v:

---
 src/web/app/common/views/components/widgets/rss.vue      | 2 +-
 src/web/app/mobile/views/components/widget-container.vue | 5 ++++-
 2 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/src/web/app/common/views/components/widgets/rss.vue b/src/web/app/common/views/components/widgets/rss.vue
index e80896bea..186d495d0 100644
--- a/src/web/app/common/views/components/widgets/rss.vue
+++ b/src/web/app/common/views/components/widgets/rss.vue
@@ -88,6 +88,6 @@ export default define({
 			padding 8px 16px
 
 			&:nth-child(even)
-				background #e2e2e2
+				background rgba(0, 0, 0, 0.05)
 
 </style>
diff --git a/src/web/app/mobile/views/components/widget-container.vue b/src/web/app/mobile/views/components/widget-container.vue
index 81dde8f7a..7319c9084 100644
--- a/src/web/app/mobile/views/components/widget-container.vue
+++ b/src/web/app/mobile/views/components/widget-container.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-widget-container" :class="{ naked }">
+<div class="mk-widget-container" :class="{ naked, hideHeader: !showHeader }">
 	<header v-if="showHeader">
 		<div class="title"><slot name="header"></slot></div>
 		<slot name="func"></slot>
@@ -31,6 +31,9 @@ export default Vue.extend({
 	box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
 	overflow hidden
 
+	&.hideHeader
+		background #fff
+
 	&.naked
 		background transparent !important
 		box-shadow none !important

From ff89db2f4d2bdabed22f2e74815ce1f02ed61a42 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 24 Feb 2018 03:33:35 +0900
Subject: [PATCH 0484/1250] v3848

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 0921b7072..673499a55 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3846",
+	"version": "0.0.3848",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From e7daaf10e87a6fd3b4ffe0e145c3202d6ee280bf Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Fri, 23 Feb 2018 20:22:30 +0000
Subject: [PATCH 0485/1250] fix(package): update mongodb to version 3.0.3

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 673499a55..21d7eca3b 100644
--- a/package.json
+++ b/package.json
@@ -130,7 +130,7 @@
 		"mkdirp": "0.5.1",
 		"mocha": "5.0.1",
 		"moji": "0.5.1",
-		"mongodb": "3.0.2",
+		"mongodb": "3.0.3",
 		"monk": "6.0.5",
 		"morgan": "1.9.0",
 		"ms": "2.1.1",

From b8d68f88a188b24726ee08357633d784ab26dc53 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Fri, 23 Feb 2018 21:02:26 +0000
Subject: [PATCH 0486/1250] fix(package): update web-push to version 3.3.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 21d7eca3b..5ee2417be 100644
--- a/package.json
+++ b/package.json
@@ -180,7 +180,7 @@
 		"vue-router": "3.0.1",
 		"vue-template-compiler": "2.5.13",
 		"vuedraggable": "2.16.0",
-		"web-push": "3.2.5",
+		"web-push": "3.3.0",
 		"webpack": "3.11.0",
 		"webpack-replace-loader": "1.3.0",
 		"websocket": "1.0.25",

From b2d31bdf8888fe2d7bcfecd06faf4d7b245d9a7c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 24 Feb 2018 06:29:05 +0900
Subject: [PATCH 0487/1250] Fix #1131

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

diff --git a/src/web/app/mobile/views/pages/home.vue b/src/web/app/mobile/views/pages/home.vue
index e4ba2e719..5e80bbceb 100644
--- a/src/web/app/mobile/views/pages/home.vue
+++ b/src/web/app/mobile/views/pages/home.vue
@@ -177,6 +177,7 @@ export default Vue.extend({
 			this.saveHome();
 		},
 		saveHome() {
+			(this as any).os.i.client_settings.mobile_home = this.widgets;
 			(this as any).api('i/update_mobile_home', {
 				home: this.widgets
 			});

From 91722826c2f3ac27dabe0d74a46569caed2d64b4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 24 Feb 2018 06:29:21 +0900
Subject: [PATCH 0488/1250] :v:

---
 src/web/app/common/define-widget.ts           |  5 +-
 src/web/app/common/views/components/index.ts  |  2 -
 .../views/components/widgets/profile.vue      |  0
 src/web/app/mobile/views/components/index.ts  |  2 +
 .../views/components/widgets/profile.vue      | 62 +++++++++++++++++++
 5 files changed, 67 insertions(+), 4 deletions(-)
 rename src/web/app/{common => desktop}/views/components/widgets/profile.vue (100%)
 create mode 100644 src/web/app/mobile/views/components/widgets/profile.vue

diff --git a/src/web/app/common/define-widget.ts b/src/web/app/common/define-widget.ts
index 826f9cc63..21821629a 100644
--- a/src/web/app/common/define-widget.ts
+++ b/src/web/app/common/define-widget.ts
@@ -34,19 +34,20 @@ export default function<T extends object>(data: {
 			}
 
 			this.$watch('props', newProps => {
+				const w = (this as any).os.i.client_settings.mobile_home.find(w => w.id == this.id);
 				if (this.isMobile) {
 					(this as any).api('i/update_mobile_home', {
 						id: this.id,
 						data: newProps
 					}).then(() => {
-						(this as any).os.i.client_settings.mobile_home.find(w => w.id == this.id).data = newProps;
+						w.data = newProps;
 					});
 				} else {
 					(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;
+						w.data = newProps;
 					});
 				}
 			}, {
diff --git a/src/web/app/common/views/components/index.ts b/src/web/app/common/views/components/index.ts
index e66a32326..5460d7577 100644
--- a/src/web/app/common/views/components/index.ts
+++ b/src/web/app/common/views/components/index.ts
@@ -25,7 +25,6 @@ import fileTypeIcon from './file-type-icon.vue';
 import wAccessLog from './widgets/access-log.vue';
 import wVersion from './widgets/version.vue';
 import wRss from './widgets/rss.vue';
-import wProfile from './widgets/profile.vue';
 import wServer from './widgets/server.vue';
 import wBroadcast from './widgets/broadcast.vue';
 import wCalendar from './widgets/calendar.vue';
@@ -65,7 +64,6 @@ Vue.component('mkw-slideshow', wSlideshow);
 Vue.component('mkw-tips', wTips);
 Vue.component('mkw-donation', wDonation);
 Vue.component('mkw-broadcast', wBroadcast);
-Vue.component('mkw-profile', wProfile);
 Vue.component('mkw-server', wServer);
 Vue.component('mkw-rss', wRss);
 Vue.component('mkw-version', wVersion);
diff --git a/src/web/app/common/views/components/widgets/profile.vue b/src/web/app/desktop/views/components/widgets/profile.vue
similarity index 100%
rename from src/web/app/common/views/components/widgets/profile.vue
rename to src/web/app/desktop/views/components/widgets/profile.vue
diff --git a/src/web/app/mobile/views/components/index.ts b/src/web/app/mobile/views/components/index.ts
index d372f2233..ea2349802 100644
--- a/src/web/app/mobile/views/components/index.ts
+++ b/src/web/app/mobile/views/components/index.ts
@@ -23,6 +23,7 @@ import widgetContainer from './widget-container.vue';
 
 //#region widgets
 import wActivity from './widgets/activity.vue';
+import wProfile from './widgets/profile.vue';
 //#endregion
 
 Vue.component('mk-ui', ui);
@@ -48,4 +49,5 @@ Vue.component('mk-widget-container', widgetContainer);
 
 //#region widgets
 Vue.component('mkw-activity', wActivity);
+Vue.component('mkw-profile', wProfile);
 //#endregion
diff --git a/src/web/app/mobile/views/components/widgets/profile.vue b/src/web/app/mobile/views/components/widgets/profile.vue
new file mode 100644
index 000000000..9336068e5
--- /dev/null
+++ b/src/web/app/mobile/views/components/widgets/profile.vue
@@ -0,0 +1,62 @@
+<template>
+<div class="mkw-profile">
+	<mk-widget-container>
+		<div :class="$style.banner"
+			:style="os.i.banner_url ? `background-image: url(${os.i.banner_url}?thumbnail&size=256)` : ''"
+		></div>
+		<img :class="$style.avatar"
+			:src="`${os.i.avatar_url}?thumbnail&size=96`"
+			alt="avatar"
+		/>
+		<router-link :class="$style.name" :to="`/${os.i.username}`">{{ os.i.name }}</router-link>
+	</mk-widget-container>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../../common/define-widget';
+export default define({
+	name: 'profile'
+});
+</script>
+
+<style lang="stylus" module>
+.banner
+	height 100px
+	background-color #f5f5f5
+	background-size cover
+	background-position center
+	cursor pointer
+
+.banner:before
+	content ""
+	display block
+	width 100%
+	height 100%
+	background rgba(0, 0, 0, 0.5)
+
+.avatar
+	display block
+	position absolute
+	width 58px
+	height 58px
+	margin 0
+	vertical-align bottom
+	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
+	display block
+	position absolute
+	top 0
+	left 92px
+	margin 0
+	line-height 100px
+	color #fff
+	font-weight bold
+	text-shadow 0 0 8px rgba(0, 0, 0, 0.5)
+
+</style>

From 5bf38fb3eb361b790192df86a01d5ecd4666a7da Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 24 Feb 2018 07:49:03 +0900
Subject: [PATCH 0489/1250] :v:

---
 src/api/endpoints/i/update_home.ts            |  9 ++++++
 src/api/endpoints/i/update_mobile_home.ts     |  9 ++++++
 src/web/app/common/define-widget.ts           | 24 ++++++++++++---
 src/web/app/desktop/views/components/home.vue | 30 ++++++++++++++++++-
 src/web/app/desktop/views/components/index.ts |  2 ++
 src/web/app/mobile/views/pages/home.vue       | 18 ++++++++++-
 6 files changed, 86 insertions(+), 6 deletions(-)

diff --git a/src/api/endpoints/i/update_home.ts b/src/api/endpoints/i/update_home.ts
index 5dfb7d791..394686cbd 100644
--- a/src/api/endpoints/i/update_home.ts
+++ b/src/api/endpoints/i/update_home.ts
@@ -3,6 +3,7 @@
  */
 import $ from 'cafy';
 import User from '../../models/user';
+import event from '../../event';
 
 module.exports = async (params, user) => new Promise(async (res, rej) => {
 	// Get 'home' parameter
@@ -30,6 +31,10 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 		});
 
 		res();
+
+		event(user._id, 'home_updated', {
+			home
+		});
 	} else {
 		if (id == null && data == null) return rej('you need to set id and data params if home param unset');
 
@@ -47,5 +52,9 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 		});
 
 		res();
+
+		event(user._id, 'home_updated', {
+			id, data
+		});
 	}
 });
diff --git a/src/api/endpoints/i/update_mobile_home.ts b/src/api/endpoints/i/update_mobile_home.ts
index a87d89cad..70181431a 100644
--- a/src/api/endpoints/i/update_mobile_home.ts
+++ b/src/api/endpoints/i/update_mobile_home.ts
@@ -3,6 +3,7 @@
  */
 import $ from 'cafy';
 import User from '../../models/user';
+import event from '../../event';
 
 module.exports = async (params, user) => new Promise(async (res, rej) => {
 	// Get 'home' parameter
@@ -29,6 +30,10 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 		});
 
 		res();
+
+		event(user._id, 'mobile_home_updated', {
+			home
+		});
 	} else {
 		if (id == null && data == null) return rej('you need to set id and data params if home param unset');
 
@@ -46,5 +51,9 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 		});
 
 		res();
+
+		event(user._id, 'mobile_home_updated', {
+			id, data
+		});
 	}
 });
diff --git a/src/web/app/common/define-widget.ts b/src/web/app/common/define-widget.ts
index 21821629a..844603daa 100644
--- a/src/web/app/common/define-widget.ts
+++ b/src/web/app/common/define-widget.ts
@@ -21,7 +21,9 @@ export default function<T extends object>(data: {
 		},
 		data() {
 			return {
-				props: data.props ? data.props() : {} as T
+				props: data.props ? data.props() : {} as T,
+				bakedOldProps: null,
+				preventSave: false
 			};
 		},
 		created() {
@@ -33,26 +35,40 @@ export default function<T extends object>(data: {
 				});
 			}
 
+			this.bakeProps();
+
 			this.$watch('props', newProps => {
-				const w = (this as any).os.i.client_settings.mobile_home.find(w => w.id == this.id);
+				if (this.preventSave) {
+					this.preventSave = false;
+					return;
+				}
+				if (this.bakedOldProps == JSON.stringify(newProps)) return;
+
+				this.bakeProps();
+
 				if (this.isMobile) {
 					(this as any).api('i/update_mobile_home', {
 						id: this.id,
 						data: newProps
 					}).then(() => {
-						w.data = newProps;
+						(this as any).os.i.client_settings.mobile_home.find(w => w.id == this.id).data = newProps;
 					});
 				} else {
 					(this as any).api('i/update_home', {
 						id: this.id,
 						data: newProps
 					}).then(() => {
-						w.data = newProps;
+						(this as any).os.i.client_settings.home.find(w => w.id == this.id).data = newProps;
 					});
 				}
 			}, {
 				deep: true
 			});
+		},
+		methods: {
+			bakeProps() {
+				this.bakedOldProps = JSON.stringify(this.props);
+			}
 		}
 	});
 }
diff --git a/src/web/app/desktop/views/components/home.vue b/src/web/app/desktop/views/components/home.vue
index 8a61c378e..d64d83698 100644
--- a/src/web/app/desktop/views/components/home.vue
+++ b/src/web/app/desktop/views/components/home.vue
@@ -60,7 +60,7 @@
 		</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"/>
+				<component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" @chosen="warp"/>
 			</div>
 			<div class="main">
 				<mk-post-form v-if="os.i.client_settings.showPostFormOnTopOfTl"/>
@@ -90,6 +90,8 @@ export default Vue.extend({
 	},
 	data() {
 		return {
+			connection: null,
+			connectionId: null,
 			widgetAdderSelected: null,
 			trash: [],
 			widgets: {
@@ -131,6 +133,16 @@ export default Vue.extend({
 			deep: true
 		});
 	},
+	mounted() {
+		this.connection = (this as any).os.stream.getConnection();
+		this.connectionId = (this as any).os.stream.use();
+
+		this.connection.on('home_updated', this.onHomeUpdated);
+	},
+	beforeDestroy() {
+		this.connection.off('home_updated', this.onHomeUpdated);
+		(this as any).os.stream.dispose(this.connectionId);
+	},
 	methods: {
 		hint() {
 			(this as any).apis.dialog({
@@ -147,6 +159,22 @@ export default Vue.extend({
 		onTlLoaded() {
 			this.$emit('loaded');
 		},
+		onHomeUpdated(data) {
+			if (data.home) {
+				(this as any).os.i.client_settings.home = data.home;
+				this.widgets.left = data.home.filter(w => w.place == 'left');
+				this.widgets.right = data.home.filter(w => w.place == 'right');
+			} else {
+				const w = (this as any).os.i.client_settings.home.find(w => w.id == data.id);
+				if (w != null) {
+					w.data = data.data;
+					this.$refs[w.id][0].preventSave = true;
+					this.$refs[w.id][0].props = w.data;
+					this.widgets.left = (this as any).os.i.client_settings.home.filter(w => w.place == 'left');
+					this.widgets.right = (this as any).os.i.client_settings.home.filter(w => w.place == 'right');
+				}
+			}
+		},
 		onWidgetContextmenu(widgetId) {
 			const w = (this.$refs[widgetId] as any)[0];
 			if (w.func) w.func();
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index 7584cb498..5cb09e031 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -39,6 +39,7 @@ 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 wProfile from './widgets/profile.vue';
 //#endregion
 
 Vue.component('mk-ui', ui);
@@ -80,4 +81,5 @@ Vue.component('mkw-polls', wPolls);
 Vue.component('mkw-post-form', wPostForm);
 Vue.component('mkw-messaging', wMessaging);
 Vue.component('mkw-channel', wChannel);
+Vue.component('mkw-profile', wProfile);
 //#endregion
diff --git a/src/web/app/mobile/views/pages/home.vue b/src/web/app/mobile/views/pages/home.vue
index 5e80bbceb..f4f458068 100644
--- a/src/web/app/mobile/views/pages/home.vue
+++ b/src/web/app/mobile/views/pages/home.vue
@@ -51,7 +51,7 @@
 				</x-draggable>
 			</template>
 			<template v-else>
-				<component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :widget="widget" :is-mobile="true" @chosen="warp"/>
+				<component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" :is-mobile="true" @chosen="warp"/>
 			</template>
 		</div>
 	</main>
@@ -124,12 +124,14 @@ export default Vue.extend({
 		this.connectionId = (this as any).os.stream.use();
 
 		this.connection.on('post', this.onStreamPost);
+		this.connection.on('mobile_home_updated', this.onHomeUpdated);
 		document.addEventListener('visibilitychange', this.onVisibilitychange, false);
 
 		Progress.start();
 	},
 	beforeDestroy() {
 		this.connection.off('post', this.onStreamPost);
+		this.connection.off('mobile_home_updated', this.onHomeUpdated);
 		(this as any).os.stream.dispose(this.connectionId);
 		document.removeEventListener('visibilitychange', this.onVisibilitychange);
 	},
@@ -152,6 +154,20 @@ export default Vue.extend({
 				document.title = 'Misskey';
 			}
 		},
+		onHomeUpdated(data) {
+			if (data.home) {
+				(this as any).os.i.client_settings.mobile_home = data.home;
+				this.widgets = data.home;
+			} else {
+				const w = (this as any).os.i.client_settings.mobile_home.find(w => w.id == data.id);
+				if (w != null) {
+					w.data = data.data;
+					this.$refs[w.id][0].preventSave = true;
+					this.$refs[w.id][0].props = w.data;
+					this.widgets = (this as any).os.i.client_settings.mobile_home;
+				}
+			}
+		},
 		hint() {
 			alert('ウィジェットを追加/削除したり並べ替えたりできます。ウィジェットを移動するには「三」をドラッグします。ウィジェットを削除するには「x」をタップします。いくつかのウィジェットはタップすることで表示を変更できます。');
 		},

From 4e80bb20bee793f24bd7735aaf34628f49871cd2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 24 Feb 2018 07:54:37 +0900
Subject: [PATCH 0490/1250] Fix #1130

---
 .../app/common/views/components/post-html.ts  |  6 ++-
 .../desktop/views/components/posts.post.vue   | 49 +++++++++----------
 .../mobile/views/components/posts.post.vue    | 28 ++++++-----
 3 files changed, 44 insertions(+), 39 deletions(-)

diff --git a/src/web/app/common/views/components/post-html.ts b/src/web/app/common/views/components/post-html.ts
index 37954cd7e..adb025589 100644
--- a/src/web/app/common/views/components/post-html.ts
+++ b/src/web/app/common/views/components/post-html.ts
@@ -81,7 +81,11 @@ export default Vue.component('mk-post-html', {
 
 				case 'code':
 					return createElement('pre', [
-						createElement('code', token.html)
+						createElement('code', {
+							domProps: {
+								innerHTML: token.html
+							}
+						})
 					]);
 
 				case 'inline-code':
diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index 4ae980648..4898de0b6 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -37,7 +37,7 @@
 						<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"/>
+					<mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i" :class="$style.text"/>
 					<a class="quote" v-if="p.repost">RP:</a>
 					<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 				</div>
@@ -413,9 +413,6 @@ export default Vue.extend({
 					font-size 1.1em
 					color #717171
 
-					> .dummy
-						display none
-
 					.mk-url-preview
 						margin-top 8px
 
@@ -431,27 +428,6 @@ export default Vue.extend({
 						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%
 
@@ -505,3 +481,26 @@ export default Vue.extend({
 
 </style>
 
+<style lang="stylus" module>
+.text
+	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
+</style>
diff --git a/src/web/app/mobile/views/components/posts.post.vue b/src/web/app/mobile/views/components/posts.post.vue
index 87b8032c8..ae1dfc59a 100644
--- a/src/web/app/mobile/views/components/posts.post.vue
+++ b/src/web/app/mobile/views/components/posts.post.vue
@@ -34,7 +34,7 @@
 					<a class="reply" v-if="p.reply">
 						%fa:reply%
 					</a>
-					<mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i"/>
+					<mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i" :class="$style.text"/>
 					<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 					<a class="quote" v-if="p.repost != null">RP:</a>
 				</div>
@@ -364,18 +364,6 @@ export default Vue.extend({
 						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
@@ -445,3 +433,17 @@ export default Vue.extend({
 
 </style>
 
+<style lang="stylus" module>
+.text
+	code
+		padding 4px 8px
+		margin 0 0.5em
+		font-size 80%
+		color #525252
+		background #f8f8f8
+		border-radius 2px
+
+	pre > code
+		padding 16px
+		margin 0
+</style>

From 8d5f43d18ac33609cba087dc8cd1a15e2eea6f3b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 24 Feb 2018 07:55:05 +0900
Subject: [PATCH 0491/1250] v3855

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 21d7eca3b..afb95fd8c 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3848",
+	"version": "0.0.3855",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 5139f8c252d5d7d282874fa9dbf12980ea1f98b2 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 24 Feb 2018 10:34:36 +0900
Subject: [PATCH 0492/1250] Update init.ts

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

diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index aa2ec25c9..6011871e4 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -41,6 +41,9 @@ import MiOS, { API } from './common/mios';
  */
 
 console.info(`Misskey v${_VERSION_} (葵 aoi)`);
+console.info(
+	'%cここにコードを入力したり張り付けたりしないでください。アカウントが不正利用される可能性があります。',
+	'color: red; background: yellow; font-size: 16px;');
 
 // BootTimer解除
 window.clearTimeout((window as any).mkBootTimer);

From 74d4a5638bf1e395e5da43ad2675f39bfb1e215b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 24 Feb 2018 12:32:49 +0900
Subject: [PATCH 0493/1250] Fix #1137

---
 src/web/app/common/define-widget.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/web/app/common/define-widget.ts b/src/web/app/common/define-widget.ts
index 844603daa..97925cf44 100644
--- a/src/web/app/common/define-widget.ts
+++ b/src/web/app/common/define-widget.ts
@@ -40,6 +40,7 @@ export default function<T extends object>(data: {
 			this.$watch('props', newProps => {
 				if (this.preventSave) {
 					this.preventSave = false;
+					this.bakeProps();
 					return;
 				}
 				if (this.bakedOldProps == JSON.stringify(newProps)) return;

From 727a3f2bdbc4316236cc52144cfed0d3105ab6cd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 24 Feb 2018 12:33:44 +0900
Subject: [PATCH 0494/1250] v3859

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index afb95fd8c..e09796039 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3855",
+	"version": "0.0.3859",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 1f4d21587537f9983a71450e96715d9476292ec2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 24 Feb 2018 23:56:57 +0900
Subject: [PATCH 0495/1250] Fix #1129

---
 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 d64d83698..59fd2aa36 100644
--- a/src/web/app/desktop/views/components/home.vue
+++ b/src/web/app/desktop/views/components/home.vue
@@ -207,7 +207,7 @@ export default Vue.extend({
 			});
 		},
 		warp(date) {
-			(this.$refs.tl as any)[0].warp(date);
+			(this.$refs.tl as any).warp(date);
 		}
 	}
 });

From 7db79e26420144415b1edb536d0fc104140645bb Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 25 Feb 2018 00:18:09 +0900
Subject: [PATCH 0496/1250] Refactor

---
 package.json                                  |  3 --
 src/web/app/app.vue                           |  2 +-
 src/web/app/auth/script.ts                    | 23 -------------
 src/web/app/common/mios.ts                    | 15 +++------
 src/web/app/common/views/components/index.ts  | 28 ----------------
 .../{components => }/widgets/access-log.vue   |  2 +-
 .../{components => }/widgets/broadcast.vue    |  4 +--
 .../{components => }/widgets/calendar.vue     |  2 +-
 .../{components => }/widgets/donation.vue     |  2 +-
 src/web/app/common/views/widgets/index.ts     | 25 +++++++++++++++
 .../views/{components => }/widgets/nav.vue    |  2 +-
 .../{components => }/widgets/photo-stream.vue |  2 +-
 .../views/{components => }/widgets/rss.vue    |  2 +-
 .../widgets/server.cpu-memory.vue             |  0
 .../{components => }/widgets/server.cpu.vue   |  0
 .../{components => }/widgets/server.disk.vue  |  0
 .../{components => }/widgets/server.info.vue  |  0
 .../widgets/server.memory.vue                 |  0
 .../{components => }/widgets/server.pie.vue   |  0
 .../widgets/server.uptimes.vue                |  0
 .../views/{components => }/widgets/server.vue |  2 +-
 .../{components => }/widgets/slideshow.vue    |  2 +-
 .../views/{components => }/widgets/tips.vue   |  2 +-
 .../{components => }/widgets/version.vue      |  4 +--
 src/web/app/desktop/script.ts                 |  1 +
 src/web/app/desktop/views/components/index.ts | 26 ---------------
 .../{components => }/widgets/activity.vue     |  2 +-
 .../widgets/channel.channel.form.vue          |  0
 .../widgets/channel.channel.post.vue          |  0
 .../widgets/channel.channel.vue               |  2 +-
 .../{components => }/widgets/channel.vue      |  2 +-
 src/web/app/desktop/views/widgets/index.ts    | 23 +++++++++++++
 .../{components => }/widgets/messaging.vue    |  4 +--
 .../widgets/notifications.vue                 |  2 +-
 .../views/{components => }/widgets/polls.vue  |  2 +-
 .../{components => }/widgets/post-form.vue    |  2 +-
 .../{components => }/widgets/profile.vue      |  2 +-
 .../{components => }/widgets/timemachine.vue  |  2 +-
 .../views/{components => }/widgets/trends.vue |  2 +-
 .../views/{components => }/widgets/users.vue  |  2 +-
 src/web/app/init.ts                           | 32 +++++++++----------
 src/web/app/mobile/script.ts                  |  1 +
 src/web/app/mobile/views/components/index.ts  | 10 ------
 .../{components => }/widgets/activity.vue     |  2 +-
 src/web/app/mobile/views/widgets/index.ts     |  7 ++++
 .../{components => }/widgets/profile.vue      |  2 +-
 src/web/app/stats/script.ts                   | 23 -------------
 src/web/app/status/script.ts                  | 23 -------------
 48 files changed, 105 insertions(+), 191 deletions(-)
 delete mode 100644 src/web/app/auth/script.ts
 rename src/web/app/common/views/{components => }/widgets/access-log.vue (97%)
 rename src/web/app/common/views/{components => }/widgets/broadcast.vue (97%)
 rename src/web/app/common/views/{components => }/widgets/calendar.vue (98%)
 rename src/web/app/common/views/{components => }/widgets/donation.vue (95%)
 create mode 100644 src/web/app/common/views/widgets/index.ts
 rename src/web/app/common/views/{components => }/widgets/nav.vue (86%)
 rename src/web/app/common/views/{components => }/widgets/photo-stream.vue (97%)
 rename src/web/app/common/views/{components => }/widgets/rss.vue (96%)
 rename src/web/app/common/views/{components => }/widgets/server.cpu-memory.vue (100%)
 rename src/web/app/common/views/{components => }/widgets/server.cpu.vue (100%)
 rename src/web/app/common/views/{components => }/widgets/server.disk.vue (100%)
 rename src/web/app/common/views/{components => }/widgets/server.info.vue (100%)
 rename src/web/app/common/views/{components => }/widgets/server.memory.vue (100%)
 rename src/web/app/common/views/{components => }/widgets/server.pie.vue (100%)
 rename src/web/app/common/views/{components => }/widgets/server.uptimes.vue (100%)
 rename src/web/app/common/views/{components => }/widgets/server.vue (97%)
 rename src/web/app/common/views/{components => }/widgets/slideshow.vue (98%)
 rename src/web/app/common/views/{components => }/widgets/tips.vue (98%)
 rename src/web/app/common/views/{components => }/widgets/version.vue (75%)
 rename src/web/app/desktop/views/{components => }/widgets/activity.vue (89%)
 rename src/web/app/desktop/views/{components => }/widgets/channel.channel.form.vue (100%)
 rename src/web/app/desktop/views/{components => }/widgets/channel.channel.post.vue (100%)
 rename src/web/app/desktop/views/{components => }/widgets/channel.channel.vue (95%)
 rename src/web/app/desktop/views/{components => }/widgets/channel.vue (97%)
 create mode 100644 src/web/app/desktop/views/widgets/index.ts
 rename src/web/app/desktop/views/{components => }/widgets/messaging.vue (88%)
 rename src/web/app/desktop/views/{components => }/widgets/notifications.vue (95%)
 rename src/web/app/desktop/views/{components => }/widgets/polls.vue (97%)
 rename src/web/app/desktop/views/{components => }/widgets/post-form.vue (97%)
 rename src/web/app/desktop/views/{components => }/widgets/profile.vue (97%)
 rename src/web/app/desktop/views/{components => }/widgets/timemachine.vue (88%)
 rename src/web/app/desktop/views/{components => }/widgets/trends.vue (97%)
 rename src/web/app/desktop/views/{components => }/widgets/users.vue (98%)
 rename src/web/app/mobile/views/{components => }/widgets/activity.vue (90%)
 create mode 100644 src/web/app/mobile/views/widgets/index.ts
 rename src/web/app/mobile/views/{components => }/widgets/profile.vue (95%)
 delete mode 100644 src/web/app/stats/script.ts
 delete mode 100644 src/web/app/status/script.ts

diff --git a/package.json b/package.json
index e09796039..274962c36 100644
--- a/package.json
+++ b/package.json
@@ -68,7 +68,6 @@
 		"@types/redis": "2.8.5",
 		"@types/request": "2.47.0",
 		"@types/rimraf": "2.0.2",
-		"@types/riot": "3.6.1",
 		"@types/seedrandom": "2.4.27",
 		"@types/serve-favicon": "2.2.30",
 		"@types/speakeasy": "2.0.2",
@@ -148,8 +147,6 @@
 		"redis": "2.8.0",
 		"request": "2.83.0",
 		"rimraf": "2.6.2",
-		"riot": "3.8.1",
-		"riot-tag-loader": "2.0.2",
 		"rndstr": "1.0.0",
 		"s-age": "1.1.2",
 		"seedrandom": "2.4.3",
diff --git a/src/web/app/app.vue b/src/web/app/app.vue
index 321e00393..7a46e7dea 100644
--- a/src/web/app/app.vue
+++ b/src/web/app/app.vue
@@ -1,3 +1,3 @@
 <template>
-	<router-view id="app"></router-view>
+<router-view id="app"></router-view>
 </template>
diff --git a/src/web/app/auth/script.ts b/src/web/app/auth/script.ts
deleted file mode 100644
index dd598d1ed..000000000
--- a/src/web/app/auth/script.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Authorize Form
- */
-
-// Style
-import './style.styl';
-
-import * as riot from 'riot';
-require('./tags');
-import init from '../init';
-
-document.title = 'Misskey | アプリの連携';
-
-/**
- * init
- */
-init(() => {
-	mount(document.createElement('mk-index'));
-});
-
-function mount(content) {
-	riot.mount(document.getElementById('app').appendChild(content));
-}
diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
index e20f4bfe4..6c95e5b9b 100644
--- a/src/web/app/common/mios.ts
+++ b/src/web/app/common/mios.ts
@@ -1,5 +1,7 @@
 import Vue from 'vue';
 import { EventEmitter } from 'eventemitter3';
+
+import { apiUrl, swPublickey, version, lang } from '../config';
 import api from './scripts/api';
 import signout from './scripts/signout';
 import Progress from './scripts/loading';
@@ -11,13 +13,6 @@ import MessagingIndexStreamManager from './scripts/streaming/messaging-index-str
 
 import Err from '../common/views/components/connect-failed.vue';
 
-//#region environment variables
-declare const _VERSION_: string;
-declare const _LANG_: string;
-declare const _API_URL_: string;
-declare const _SW_PUBLICKEY_: string;
-//#endregion
-
 export type API = {
 	chooseDriveFile: (opts: {
 		title?: string;
@@ -204,7 +199,7 @@ export default class MiOS extends EventEmitter {
 			}
 
 			// Fetch user
-			fetch(`${_API_URL_}/i`, {
+			fetch(`${apiUrl}/i`, {
 				method: 'POST',
 				body: JSON.stringify({
 					i: token
@@ -311,7 +306,7 @@ export default class MiOS extends EventEmitter {
 
 				// A public key your push server will use to send
 				// messages to client apps via a push server.
-				applicationServerKey: urlBase64ToUint8Array(_SW_PUBLICKEY_)
+				applicationServerKey: urlBase64ToUint8Array(swPublickey)
 			};
 
 			// Subscribe push notification
@@ -348,7 +343,7 @@ export default class MiOS extends EventEmitter {
 		});
 
 		// The path of service worker script
-		const sw = `/sw.${_VERSION_}.${_LANG_}.js`;
+		const sw = `/sw.${version}.${lang}.js`;
 
 		// Register service worker
 		navigator.serviceWorker.register(sw).then(registration => {
diff --git a/src/web/app/common/views/components/index.ts b/src/web/app/common/views/components/index.ts
index 5460d7577..ab0f1767d 100644
--- a/src/web/app/common/views/components/index.ts
+++ b/src/web/app/common/views/components/index.ts
@@ -21,20 +21,6 @@ import urlPreview from './url-preview.vue';
 import twitterSetting from './twitter-setting.vue';
 import fileTypeIcon from './file-type-icon.vue';
 
-//#region widgets
-import wAccessLog from './widgets/access-log.vue';
-import wVersion from './widgets/version.vue';
-import wRss from './widgets/rss.vue';
-import wServer from './widgets/server.vue';
-import wBroadcast from './widgets/broadcast.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 wNav from './widgets/nav.vue';
-//#endregion
-
 Vue.component('mk-signin', signin);
 Vue.component('mk-signup', signup);
 Vue.component('mk-forkit', forkit);
@@ -55,17 +41,3 @@ 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);
-
-//#region widgets
-Vue.component('mkw-nav', wNav);
-Vue.component('mkw-calendar', wCalendar);
-Vue.component('mkw-photo-stream', wPhotoStream);
-Vue.component('mkw-slideshow', wSlideshow);
-Vue.component('mkw-tips', wTips);
-Vue.component('mkw-donation', wDonation);
-Vue.component('mkw-broadcast', wBroadcast);
-Vue.component('mkw-server', wServer);
-Vue.component('mkw-rss', wRss);
-Vue.component('mkw-version', wVersion);
-Vue.component('mkw-access-log', wAccessLog);
-//#endregion
diff --git a/src/web/app/common/views/components/widgets/access-log.vue b/src/web/app/common/views/widgets/access-log.vue
similarity index 97%
rename from src/web/app/common/views/components/widgets/access-log.vue
rename to src/web/app/common/views/widgets/access-log.vue
index c810c2d15..f7bb17d83 100644
--- a/src/web/app/common/views/components/widgets/access-log.vue
+++ b/src/web/app/common/views/widgets/access-log.vue
@@ -15,7 +15,7 @@
 </template>
 
 <script lang="ts">
-import define from '../../../../common/define-widget';
+import define from '../../../common/define-widget';
 import * as seedrandom from 'seedrandom';
 
 export default define({
diff --git a/src/web/app/common/views/components/widgets/broadcast.vue b/src/web/app/common/views/widgets/broadcast.vue
similarity index 97%
rename from src/web/app/common/views/components/widgets/broadcast.vue
rename to src/web/app/common/views/widgets/broadcast.vue
index 0bb59caf4..bf41a5fc6 100644
--- a/src/web/app/common/views/components/widgets/broadcast.vue
+++ b/src/web/app/common/views/widgets/broadcast.vue
@@ -24,8 +24,8 @@
 </template>
 
 <script lang="ts">
-import define from '../../../../common/define-widget';
-import { lang } from '../../../../config';
+import define from '../../../common/define-widget';
+import { lang } from '../../../config';
 
 export default define({
 	name: 'broadcast',
diff --git a/src/web/app/common/views/components/widgets/calendar.vue b/src/web/app/common/views/widgets/calendar.vue
similarity index 98%
rename from src/web/app/common/views/components/widgets/calendar.vue
rename to src/web/app/common/views/widgets/calendar.vue
index bfcbd7f68..2bcdb07f9 100644
--- a/src/web/app/common/views/components/widgets/calendar.vue
+++ b/src/web/app/common/views/widgets/calendar.vue
@@ -36,7 +36,7 @@
 </template>
 
 <script lang="ts">
-import define from '../../../../common/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/common/views/widgets/donation.vue
similarity index 95%
rename from src/web/app/common/views/components/widgets/donation.vue
rename to src/web/app/common/views/widgets/donation.vue
index 08aab8ecd..e218df06e 100644
--- a/src/web/app/common/views/components/widgets/donation.vue
+++ b/src/web/app/common/views/widgets/donation.vue
@@ -12,7 +12,7 @@
 </template>
 
 <script lang="ts">
-import define from '../../../../common/define-widget';
+import define from '../../../common/define-widget';
 export default define({
 	name: 'donation'
 });
diff --git a/src/web/app/common/views/widgets/index.ts b/src/web/app/common/views/widgets/index.ts
new file mode 100644
index 000000000..e41030e85
--- /dev/null
+++ b/src/web/app/common/views/widgets/index.ts
@@ -0,0 +1,25 @@
+import Vue from 'vue';
+
+import wAccessLog from './access-log.vue';
+import wVersion from './version.vue';
+import wRss from './rss.vue';
+import wServer from './server.vue';
+import wBroadcast from './broadcast.vue';
+import wCalendar from './calendar.vue';
+import wPhotoStream from './photo-stream.vue';
+import wSlideshow from './slideshow.vue';
+import wTips from './tips.vue';
+import wDonation from './donation.vue';
+import wNav from './nav.vue';
+
+Vue.component('mkw-nav', wNav);
+Vue.component('mkw-calendar', wCalendar);
+Vue.component('mkw-photo-stream', wPhotoStream);
+Vue.component('mkw-slideshow', wSlideshow);
+Vue.component('mkw-tips', wTips);
+Vue.component('mkw-donation', wDonation);
+Vue.component('mkw-broadcast', wBroadcast);
+Vue.component('mkw-server', wServer);
+Vue.component('mkw-rss', wRss);
+Vue.component('mkw-version', wVersion);
+Vue.component('mkw-access-log', wAccessLog);
diff --git a/src/web/app/common/views/components/widgets/nav.vue b/src/web/app/common/views/widgets/nav.vue
similarity index 86%
rename from src/web/app/common/views/components/widgets/nav.vue
rename to src/web/app/common/views/widgets/nav.vue
index ce88e587a..7bd5a7832 100644
--- a/src/web/app/common/views/components/widgets/nav.vue
+++ b/src/web/app/common/views/widgets/nav.vue
@@ -9,7 +9,7 @@
 </template>
 
 <script lang="ts">
-import define from '../../../../common/define-widget';
+import define from '../../../common/define-widget';
 export default define({
 	name: 'nav'
 });
diff --git a/src/web/app/common/views/components/widgets/photo-stream.vue b/src/web/app/common/views/widgets/photo-stream.vue
similarity index 97%
rename from src/web/app/common/views/components/widgets/photo-stream.vue
rename to src/web/app/common/views/widgets/photo-stream.vue
index dcaa6624d..78864cc8b 100644
--- a/src/web/app/common/views/components/widgets/photo-stream.vue
+++ b/src/web/app/common/views/widgets/photo-stream.vue
@@ -13,7 +13,7 @@
 </template>
 
 <script lang="ts">
-import define from '../../../../common/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/rss.vue b/src/web/app/common/views/widgets/rss.vue
similarity index 96%
rename from src/web/app/common/views/components/widgets/rss.vue
rename to src/web/app/common/views/widgets/rss.vue
index 186d495d0..4d74b2f7a 100644
--- a/src/web/app/common/views/components/widgets/rss.vue
+++ b/src/web/app/common/views/widgets/rss.vue
@@ -13,7 +13,7 @@
 </template>
 
 <script lang="ts">
-import define from '../../../../common/define-widget';
+import define from '../../../common/define-widget';
 export default define({
 	name: 'rss',
 	props: () => ({
diff --git a/src/web/app/common/views/components/widgets/server.cpu-memory.vue b/src/web/app/common/views/widgets/server.cpu-memory.vue
similarity index 100%
rename from src/web/app/common/views/components/widgets/server.cpu-memory.vue
rename to src/web/app/common/views/widgets/server.cpu-memory.vue
diff --git a/src/web/app/common/views/components/widgets/server.cpu.vue b/src/web/app/common/views/widgets/server.cpu.vue
similarity index 100%
rename from src/web/app/common/views/components/widgets/server.cpu.vue
rename to src/web/app/common/views/widgets/server.cpu.vue
diff --git a/src/web/app/common/views/components/widgets/server.disk.vue b/src/web/app/common/views/widgets/server.disk.vue
similarity index 100%
rename from src/web/app/common/views/components/widgets/server.disk.vue
rename to src/web/app/common/views/widgets/server.disk.vue
diff --git a/src/web/app/common/views/components/widgets/server.info.vue b/src/web/app/common/views/widgets/server.info.vue
similarity index 100%
rename from src/web/app/common/views/components/widgets/server.info.vue
rename to src/web/app/common/views/widgets/server.info.vue
diff --git a/src/web/app/common/views/components/widgets/server.memory.vue b/src/web/app/common/views/widgets/server.memory.vue
similarity index 100%
rename from src/web/app/common/views/components/widgets/server.memory.vue
rename to src/web/app/common/views/widgets/server.memory.vue
diff --git a/src/web/app/common/views/components/widgets/server.pie.vue b/src/web/app/common/views/widgets/server.pie.vue
similarity index 100%
rename from src/web/app/common/views/components/widgets/server.pie.vue
rename to src/web/app/common/views/widgets/server.pie.vue
diff --git a/src/web/app/common/views/components/widgets/server.uptimes.vue b/src/web/app/common/views/widgets/server.uptimes.vue
similarity index 100%
rename from src/web/app/common/views/components/widgets/server.uptimes.vue
rename to src/web/app/common/views/widgets/server.uptimes.vue
diff --git a/src/web/app/common/views/components/widgets/server.vue b/src/web/app/common/views/widgets/server.vue
similarity index 97%
rename from src/web/app/common/views/components/widgets/server.vue
rename to src/web/app/common/views/widgets/server.vue
index 4ebc5767d..3d5248998 100644
--- a/src/web/app/common/views/components/widgets/server.vue
+++ b/src/web/app/common/views/widgets/server.vue
@@ -18,7 +18,7 @@
 </template>
 
 <script lang="ts">
-import define from '../../../../common/define-widget';
+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';
diff --git a/src/web/app/common/views/components/widgets/slideshow.vue b/src/web/app/common/views/widgets/slideshow.vue
similarity index 98%
rename from src/web/app/common/views/components/widgets/slideshow.vue
rename to src/web/app/common/views/widgets/slideshow.vue
index c2f4eb70d..56eb654c2 100644
--- a/src/web/app/common/views/components/widgets/slideshow.vue
+++ b/src/web/app/common/views/widgets/slideshow.vue
@@ -12,7 +12,7 @@
 
 <script lang="ts">
 import * as anime from 'animejs';
-import define from '../../../../common/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/common/views/widgets/tips.vue
similarity index 98%
rename from src/web/app/common/views/components/widgets/tips.vue
rename to src/web/app/common/views/widgets/tips.vue
index 2991fbc3b..bdecc068e 100644
--- a/src/web/app/common/views/components/widgets/tips.vue
+++ b/src/web/app/common/views/widgets/tips.vue
@@ -6,7 +6,7 @@
 
 <script lang="ts">
 import * as anime from 'animejs';
-import define from '../../../../common/define-widget';
+import define from '../../../common/define-widget';
 
 const tips = [
 	'<kbd>t</kbd>でタイムラインにフォーカスできます',
diff --git a/src/web/app/common/views/components/widgets/version.vue b/src/web/app/common/views/widgets/version.vue
similarity index 75%
rename from src/web/app/common/views/components/widgets/version.vue
rename to src/web/app/common/views/widgets/version.vue
index ad2b27bc4..5072d9b74 100644
--- a/src/web/app/common/views/components/widgets/version.vue
+++ b/src/web/app/common/views/widgets/version.vue
@@ -3,8 +3,8 @@
 </template>
 
 <script lang="ts">
-import { version } from '../../../../config';
-import define from '../../../../common/define-widget';
+import { version } from '../../../config';
+import define from '../../../common/define-widget';
 export default define({
 	name: 'version'
 }).extend({
diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
index bbd8e9598..f0412805e 100644
--- a/src/web/app/desktop/script.ts
+++ b/src/web/app/desktop/script.ts
@@ -37,6 +37,7 @@ init(async (launch) => {
 
 	// Register components
 	require('./views/components');
+	require('./views/widgets');
 
 	// Launch the app
 	const [app, os] = launch(os => ({
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index 5cb09e031..52b9680ba 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -29,19 +29,6 @@ import following from './following.vue';
 import usersList from './users-list.vue';
 import widgetContainer from './widget-container.vue';
 
-//#region widgets
-import wNotifications from './widgets/notifications.vue';
-import wTimemachine from './widgets/timemachine.vue';
-import wActivity from './widgets/activity.vue';
-import wTrends from './widgets/trends.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 wProfile from './widgets/profile.vue';
-//#endregion
-
 Vue.component('mk-ui', ui);
 Vue.component('mk-ui-notification', uiNotification);
 Vue.component('mk-home', home);
@@ -70,16 +57,3 @@ Vue.component('mk-followers', followers);
 Vue.component('mk-following', following);
 Vue.component('mk-users-list', usersList);
 Vue.component('mk-widget-container', widgetContainer);
-
-//#region widgets
-Vue.component('mkw-notifications', wNotifications);
-Vue.component('mkw-timemachine', wTimemachine);
-Vue.component('mkw-activity', wActivity);
-Vue.component('mkw-trends', wTrends);
-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-profile', wProfile);
-//#endregion
diff --git a/src/web/app/desktop/views/components/widgets/activity.vue b/src/web/app/desktop/views/widgets/activity.vue
similarity index 89%
rename from src/web/app/desktop/views/components/widgets/activity.vue
rename to src/web/app/desktop/views/widgets/activity.vue
index 2ff5fe4f0..0bdf4622a 100644
--- a/src/web/app/desktop/views/components/widgets/activity.vue
+++ b/src/web/app/desktop/views/widgets/activity.vue
@@ -7,7 +7,7 @@
 </template>
 
 <script lang="ts">
-import define from '../../../../common/define-widget';
+import define from '../../../common/define-widget';
 export default define({
 	name: 'activity',
 	props: () => ({
diff --git a/src/web/app/desktop/views/components/widgets/channel.channel.form.vue b/src/web/app/desktop/views/widgets/channel.channel.form.vue
similarity index 100%
rename from src/web/app/desktop/views/components/widgets/channel.channel.form.vue
rename to src/web/app/desktop/views/widgets/channel.channel.form.vue
diff --git a/src/web/app/desktop/views/components/widgets/channel.channel.post.vue b/src/web/app/desktop/views/widgets/channel.channel.post.vue
similarity index 100%
rename from src/web/app/desktop/views/components/widgets/channel.channel.post.vue
rename to src/web/app/desktop/views/widgets/channel.channel.post.vue
diff --git a/src/web/app/desktop/views/components/widgets/channel.channel.vue b/src/web/app/desktop/views/widgets/channel.channel.vue
similarity index 95%
rename from src/web/app/desktop/views/components/widgets/channel.channel.vue
rename to src/web/app/desktop/views/widgets/channel.channel.vue
index 09154390c..70dac316c 100644
--- a/src/web/app/desktop/views/components/widgets/channel.channel.vue
+++ b/src/web/app/desktop/views/widgets/channel.channel.vue
@@ -11,7 +11,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import ChannelStream from '../../../../common/scripts/streaming/channel-stream';
+import ChannelStream from '../../../common/scripts/streaming/channel-stream';
 import XForm from './channel.channel.form.vue';
 import XPost from './channel.channel.post.vue';
 
diff --git a/src/web/app/desktop/views/components/widgets/channel.vue b/src/web/app/desktop/views/widgets/channel.vue
similarity index 97%
rename from src/web/app/desktop/views/components/widgets/channel.vue
rename to src/web/app/desktop/views/widgets/channel.vue
index 5c3afd9ec..fc143bb1d 100644
--- a/src/web/app/desktop/views/components/widgets/channel.vue
+++ b/src/web/app/desktop/views/widgets/channel.vue
@@ -10,7 +10,7 @@
 </template>
 
 <script lang="ts">
-import define from '../../../../common/define-widget';
+import define from '../../../common/define-widget';
 import XChannel from './channel.channel.vue';
 
 export default define({
diff --git a/src/web/app/desktop/views/widgets/index.ts b/src/web/app/desktop/views/widgets/index.ts
new file mode 100644
index 000000000..77d771d6b
--- /dev/null
+++ b/src/web/app/desktop/views/widgets/index.ts
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+
+import wNotifications from './notifications.vue';
+import wTimemachine from './timemachine.vue';
+import wActivity from './activity.vue';
+import wTrends from './trends.vue';
+import wUsers from './users.vue';
+import wPolls from './polls.vue';
+import wPostForm from './post-form.vue';
+import wMessaging from './messaging.vue';
+import wChannel from './channel.vue';
+import wProfile from './profile.vue';
+
+Vue.component('mkw-notifications', wNotifications);
+Vue.component('mkw-timemachine', wTimemachine);
+Vue.component('mkw-activity', wActivity);
+Vue.component('mkw-trends', wTrends);
+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-profile', wProfile);
diff --git a/src/web/app/desktop/views/components/widgets/messaging.vue b/src/web/app/desktop/views/widgets/messaging.vue
similarity index 88%
rename from src/web/app/desktop/views/components/widgets/messaging.vue
rename to src/web/app/desktop/views/widgets/messaging.vue
index ae7d6934a..2c9f473bd 100644
--- a/src/web/app/desktop/views/components/widgets/messaging.vue
+++ b/src/web/app/desktop/views/widgets/messaging.vue
@@ -6,8 +6,8 @@
 </template>
 
 <script lang="ts">
-import define from '../../../../common/define-widget';
-import MkMessagingRoomWindow from '../messaging-room-window.vue';
+import define from '../../../common/define-widget';
+import MkMessagingRoomWindow from '../components/messaging-room-window.vue';
 
 export default define({
 	name: 'messaging',
diff --git a/src/web/app/desktop/views/components/widgets/notifications.vue b/src/web/app/desktop/views/widgets/notifications.vue
similarity index 95%
rename from src/web/app/desktop/views/components/widgets/notifications.vue
rename to src/web/app/desktop/views/widgets/notifications.vue
index 978cf5218..1a2b3d3f8 100644
--- a/src/web/app/desktop/views/components/widgets/notifications.vue
+++ b/src/web/app/desktop/views/widgets/notifications.vue
@@ -9,7 +9,7 @@
 </template>
 
 <script lang="ts">
-import define from '../../../../common/define-widget';
+import define from '../../../common/define-widget';
 export default define({
 	name: 'notifications',
 	props: () => ({
diff --git a/src/web/app/desktop/views/components/widgets/polls.vue b/src/web/app/desktop/views/widgets/polls.vue
similarity index 97%
rename from src/web/app/desktop/views/components/widgets/polls.vue
rename to src/web/app/desktop/views/widgets/polls.vue
index f1b34ceed..fda4e17d8 100644
--- a/src/web/app/desktop/views/components/widgets/polls.vue
+++ b/src/web/app/desktop/views/widgets/polls.vue
@@ -15,7 +15,7 @@
 </template>
 
 <script lang="ts">
-import define from '../../../../common/define-widget';
+import define from '../../../common/define-widget';
 export default define({
 	name: 'polls',
 	props: () => ({
diff --git a/src/web/app/desktop/views/components/widgets/post-form.vue b/src/web/app/desktop/views/widgets/post-form.vue
similarity index 97%
rename from src/web/app/desktop/views/components/widgets/post-form.vue
rename to src/web/app/desktop/views/widgets/post-form.vue
index ab87ba721..e51b4f357 100644
--- a/src/web/app/desktop/views/components/widgets/post-form.vue
+++ b/src/web/app/desktop/views/widgets/post-form.vue
@@ -9,7 +9,7 @@
 </template>
 
 <script lang="ts">
-import define from '../../../../common/define-widget';
+import define from '../../../common/define-widget';
 export default define({
 	name: 'post-form',
 	props: () => ({
diff --git a/src/web/app/desktop/views/components/widgets/profile.vue b/src/web/app/desktop/views/widgets/profile.vue
similarity index 97%
rename from src/web/app/desktop/views/components/widgets/profile.vue
rename to src/web/app/desktop/views/widgets/profile.vue
index 68cf46978..e067a0eb2 100644
--- a/src/web/app/desktop/views/components/widgets/profile.vue
+++ b/src/web/app/desktop/views/widgets/profile.vue
@@ -21,7 +21,7 @@
 </template>
 
 <script lang="ts">
-import define from '../../../../common/define-widget';
+import define from '../../../common/define-widget';
 export default define({
 	name: 'profile',
 	props: () => ({
diff --git a/src/web/app/desktop/views/components/widgets/timemachine.vue b/src/web/app/desktop/views/widgets/timemachine.vue
similarity index 88%
rename from src/web/app/desktop/views/components/widgets/timemachine.vue
rename to src/web/app/desktop/views/widgets/timemachine.vue
index 742048216..6db3b14c6 100644
--- a/src/web/app/desktop/views/components/widgets/timemachine.vue
+++ b/src/web/app/desktop/views/widgets/timemachine.vue
@@ -5,7 +5,7 @@
 </template>
 
 <script lang="ts">
-import define from '../../../../common/define-widget';
+import define from '../../../common/define-widget';
 export default define({
 	name: 'timemachine',
 	props: () => ({
diff --git a/src/web/app/desktop/views/components/widgets/trends.vue b/src/web/app/desktop/views/widgets/trends.vue
similarity index 97%
rename from src/web/app/desktop/views/components/widgets/trends.vue
rename to src/web/app/desktop/views/widgets/trends.vue
index 934351b8a..09cad9ba4 100644
--- a/src/web/app/desktop/views/components/widgets/trends.vue
+++ b/src/web/app/desktop/views/widgets/trends.vue
@@ -14,7 +14,7 @@
 </template>
 
 <script lang="ts">
-import define from '../../../../common/define-widget';
+import define from '../../../common/define-widget';
 export default define({
 	name: 'trends',
 	props: () => ({
diff --git a/src/web/app/desktop/views/components/widgets/users.vue b/src/web/app/desktop/views/widgets/users.vue
similarity index 98%
rename from src/web/app/desktop/views/components/widgets/users.vue
rename to src/web/app/desktop/views/widgets/users.vue
index f3a1509cf..f7af8205e 100644
--- a/src/web/app/desktop/views/components/widgets/users.vue
+++ b/src/web/app/desktop/views/widgets/users.vue
@@ -22,7 +22,7 @@
 </template>
 
 <script lang="ts">
-import define from '../../../../common/define-widget';
+import define from '../../../common/define-widget';
 
 const limit = 3;
 
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index 6011871e4..4a8f34f8d 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -2,11 +2,6 @@
  * App initializer
  */
 
-declare const _VERSION_: string;
-declare const _LANG_: string;
-declare const _HOST_: string;
-//declare const __CONSTS__: any;
-
 import Vue from 'vue';
 import VueRouter from 'vue-router';
 import VModal from 'vue-js-modal';
@@ -19,6 +14,7 @@ require('./common/views/directives');
 
 // Register global components
 require('./common/views/components');
+require('./common/views/widgets');
 
 // Register global filters
 require('./common/filters');
@@ -35,12 +31,13 @@ import App from './app.vue';
 
 import checkForUpdate from './common/scripts/check-for-update';
 import MiOS, { API } from './common/mios';
+import { version, host, lang } from './config';
 
 /**
  * APP ENTRY POINT!
  */
 
-console.info(`Misskey v${_VERSION_} (葵 aoi)`);
+console.info(`Misskey v${version} (葵 aoi)`);
 console.info(
 	'%cここにコードを入力したり張り付けたりしないでください。アカウントが不正利用される可能性があります。',
 	'color: red; background: yellow; font-size: 16px;');
@@ -49,13 +46,13 @@ console.info(
 window.clearTimeout((window as any).mkBootTimer);
 delete (window as any).mkBootTimer;
 
-if (_HOST_ != 'localhost') {
-	document.domain = _HOST_;
+if (host != 'localhost') {
+	document.domain = host;
 }
 
 //#region Set lang attr
 const html = document.documentElement;
-html.setAttribute('lang', _LANG_);
+html.setAttribute('lang', lang);
 //#endregion
 
 //#region Set description meta tag
@@ -66,9 +63,6 @@ meta.setAttribute('content', '%i18n:common.misskey%');
 head.appendChild(meta);
 //#endregion
 
-// Set global configuration
-//(riot as any).mixin(__CONSTS__);
-
 // iOSでプライベートモードだとlocalStorageが使えないので既存のメソッドを上書きする
 try {
 	localStorage.setItem('kyoppie', 'yuppie');
@@ -132,10 +126,14 @@ export default (callback: (launch: (api: (os: MiOS) => API) => [Vue, MiOS]) => v
 			panic(e);
 		}
 
-		// 更新チェック
-		setTimeout(() => {
-			checkForUpdate(os);
-		}, 3000);
+		//#region 更新チェック
+		const preventUpdate = localStorage.getItem('preventUpdate') == 'true';
+		if (!preventUpdate) {
+			setTimeout(() => {
+				checkForUpdate(os);
+			}, 3000);
+		}
+		//#endregion
 	});
 };
 
@@ -152,7 +150,7 @@ function panic(e) {
 			+ '<hr>'
 			+ `<p>エラーコード: ${e.toString()}</p>`
 			+ `<p>ブラウザ バージョン: ${navigator.userAgent}</p>`
-			+ `<p>クライアント バージョン: ${_VERSION_}</p>`
+			+ `<p>クライアント バージョン: ${version}</p>`
 			+ '<hr>'
 			+ '<p>問題が解決しない場合は、上記の情報をお書き添えの上 syuilotan@yahoo.co.jp までご連絡ください。</p>'
 			+ '<p>Thank you for using Misskey.</p>'
diff --git a/src/web/app/mobile/script.ts b/src/web/app/mobile/script.ts
index fe73155c7..eeadfd92b 100644
--- a/src/web/app/mobile/script.ts
+++ b/src/web/app/mobile/script.ts
@@ -38,6 +38,7 @@ init((launch) => {
 
 	// Register components
 	require('./views/components');
+	require('./views/widgets');
 
 	// http://qiita.com/junya/items/3ff380878f26ca447f85
 	document.body.setAttribute('ontouchstart', '');
diff --git a/src/web/app/mobile/views/components/index.ts b/src/web/app/mobile/views/components/index.ts
index ea2349802..fe65aab20 100644
--- a/src/web/app/mobile/views/components/index.ts
+++ b/src/web/app/mobile/views/components/index.ts
@@ -21,11 +21,6 @@ import userTimeline from './user-timeline.vue';
 import activity from './activity.vue';
 import widgetContainer from './widget-container.vue';
 
-//#region widgets
-import wActivity from './widgets/activity.vue';
-import wProfile from './widgets/profile.vue';
-//#endregion
-
 Vue.component('mk-ui', ui);
 Vue.component('mk-timeline', timeline);
 Vue.component('mk-posts', posts);
@@ -46,8 +41,3 @@ Vue.component('mk-user-preview', userPreview);
 Vue.component('mk-user-timeline', userTimeline);
 Vue.component('mk-activity', activity);
 Vue.component('mk-widget-container', widgetContainer);
-
-//#region widgets
-Vue.component('mkw-activity', wActivity);
-Vue.component('mkw-profile', wProfile);
-//#endregion
diff --git a/src/web/app/mobile/views/components/widgets/activity.vue b/src/web/app/mobile/views/widgets/activity.vue
similarity index 90%
rename from src/web/app/mobile/views/components/widgets/activity.vue
rename to src/web/app/mobile/views/widgets/activity.vue
index c4d30b07a..48dcafb3e 100644
--- a/src/web/app/mobile/views/components/widgets/activity.vue
+++ b/src/web/app/mobile/views/widgets/activity.vue
@@ -10,7 +10,7 @@
 </template>
 
 <script lang="ts">
-import define from '../../../../common/define-widget';
+import define from '../../../common/define-widget';
 
 export default define({
 	name: 'activity',
diff --git a/src/web/app/mobile/views/widgets/index.ts b/src/web/app/mobile/views/widgets/index.ts
new file mode 100644
index 000000000..4de912b64
--- /dev/null
+++ b/src/web/app/mobile/views/widgets/index.ts
@@ -0,0 +1,7 @@
+import Vue from 'vue';
+
+import wActivity from './activity.vue';
+import wProfile from './profile.vue';
+
+Vue.component('mkw-activity', wActivity);
+Vue.component('mkw-profile', wProfile);
diff --git a/src/web/app/mobile/views/components/widgets/profile.vue b/src/web/app/mobile/views/widgets/profile.vue
similarity index 95%
rename from src/web/app/mobile/views/components/widgets/profile.vue
rename to src/web/app/mobile/views/widgets/profile.vue
index 9336068e5..6bc7bfffc 100644
--- a/src/web/app/mobile/views/components/widgets/profile.vue
+++ b/src/web/app/mobile/views/widgets/profile.vue
@@ -14,7 +14,7 @@
 </template>
 
 <script lang="ts">
-import define from '../../../../common/define-widget';
+import define from '../../../common/define-widget';
 export default define({
 	name: 'profile'
 });
diff --git a/src/web/app/stats/script.ts b/src/web/app/stats/script.ts
deleted file mode 100644
index 3bbd80c33..000000000
--- a/src/web/app/stats/script.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Stats
- */
-
-// Style
-import './style.styl';
-
-import * as riot from 'riot';
-require('./tags');
-import init from '../init';
-
-document.title = 'Misskey Statistics';
-
-/**
- * init
- */
-init(() => {
-	mount(document.createElement('mk-index'));
-});
-
-function mount(content) {
-	riot.mount(document.getElementById('app').appendChild(content));
-}
diff --git a/src/web/app/status/script.ts b/src/web/app/status/script.ts
deleted file mode 100644
index 84483acab..000000000
--- a/src/web/app/status/script.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Status
- */
-
-// Style
-import './style.styl';
-
-import * as riot from 'riot';
-require('./tags');
-import init from '../init';
-
-document.title = 'Misskey System Status';
-
-/**
- * init
- */
-init(() => {
-	mount(document.createElement('mk-index'));
-});
-
-function mount(content) {
-	riot.mount(document.getElementById('app').appendChild(content));
-}

From 14602599901da8acd9b4b7ee4df8bbf908ffa82a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 25 Feb 2018 01:55:49 +0900
Subject: [PATCH 0497/1250] #1126

---
 .../views/components/autocomplete.vue         | 119 +++++++++++-------
 .../views/components/messaging-room.form.vue  |   2 +-
 .../views/directives/autocomplete.ts          |  74 +++++++----
 src/web/app/common/views/directives/focus.ts  |   5 -
 src/web/app/common/views/directives/index.ts  |   4 +-
 src/web/app/desktop/views/directives/index.ts |   2 -
 6 files changed, 121 insertions(+), 85 deletions(-)
 rename src/web/app/{desktop => common}/views/components/autocomplete.vue (53%)
 rename src/web/app/{desktop => common}/views/directives/autocomplete.ts (57%)
 delete mode 100644 src/web/app/common/views/directives/focus.ts

diff --git a/src/web/app/desktop/views/components/autocomplete.vue b/src/web/app/common/views/components/autocomplete.vue
similarity index 53%
rename from src/web/app/desktop/views/components/autocomplete.vue
rename to src/web/app/common/views/components/autocomplete.vue
index a99d405e8..198080492 100644
--- a/src/web/app/desktop/views/components/autocomplete.vue
+++ b/src/web/app/common/views/components/autocomplete.vue
@@ -1,26 +1,40 @@
 <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">
+<div class="mk-autocomplete" @contextmenu.prevent="() => {}">
+	<ol class="users" ref="suggests" v-if="users.length > 0">
+		<li v-for="user in users" @click="complete(type, 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>
+	<ol class="emojis" ref="suggests" v-if="emojis.length > 0">
+		<li v-for="emoji in emojis" @click="complete(type, pictograph.dic[emoji])" @keydown="onKeydown" tabindex="-1">
+			<span class="emoji">{{ pictograph.dic[emoji] }}</span>
+			<span class="name" v-html="emoji.replace(q, `<b>${q}</b>`)"></span>
+		</li>
+	</ol>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import * as pictograph from 'pictograph';
 import contains from '../../../common/scripts/contains';
 
 export default Vue.extend({
-	props: ['q', 'textarea', 'complete', 'close'],
+	props: ['type', 'q', 'textarea', 'complete', 'close'],
 	data() {
 		return {
 			fetching: true,
 			users: [],
-			select: -1
+			emojis: [],
+			select: -1,
+			pictograph
+		}
+	},
+	computed: {
+		items(): HTMLCollection {
+			return (this.$refs.suggests as Element).children;
 		}
 	},
 	mounted() {
@@ -30,13 +44,19 @@ export default Vue.extend({
 			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;
-		});
+		if (this.type == 'user') {
+			(this as any).api('users/search_by_username', {
+				query: this.q,
+				limit: 30
+			}).then(users => {
+				this.users = users;
+				this.fetching = false;
+			});
+		} else if (this.type == 'emoji') {
+			const emojis = Object.keys(pictograph.dic).sort((a, b) => a.length - b.length);
+			const matched = emojis.filter(e => e.indexOf(this.q) > -1);
+			this.emojis = matched.filter((x, i) => i <= 30);
+		}
 	},
 	beforeDestroy() {
 		this.textarea.removeEventListener('keydown', this.onKeydown);
@@ -61,7 +81,7 @@ export default Vue.extend({
 				case 13: // [ENTER]
 					if (this.select !== -1) {
 						cancel();
-						this.complete(this.users[this.select]);
+						(this.items[this.select] as any).click();
 					} else {
 						this.close();
 					}
@@ -93,24 +113,22 @@ export default Vue.extend({
 		},
 
 		selectNext() {
-			if (++this.select >= this.users.length) this.select = 0;
+			if (++this.select >= this.items.length) this.select = 0;
 			this.applySelect();
 		},
 
 		selectPrev() {
-			if (--this.select < 0) this.select = this.users.length - 1;
+			if (--this.select < 0) this.select = this.items.length - 1;
 			this.applySelect();
 		},
 
 		applySelect() {
-			const els = (this.$refs.users as Element).children;
-
-			Array.from(els).forEach(el => {
+			Array.from(this.items).forEach(el => {
 				el.removeAttribute('data-selected');
 			});
 
-			els[this.select].setAttribute('data-selected', 'true');
-			(els[this.select] as any).focus();
+			this.items[this.select].setAttribute('data-selected', 'true');
+			(this.items[this.select] as any).focus();
 		}
 	}
 });
@@ -126,7 +144,7 @@ export default Vue.extend({
 	border solid 1px rgba(0, 0, 0, 0.1)
 	border-radius 4px
 
-	> .users
+	> ol
 		display block
 		margin 0
 		padding 4px 0
@@ -149,42 +167,47 @@ export default Vue.extend({
 
 			&:hover
 			&[data-selected='true']
-				color #fff
 				background $theme-color
 
-				.name
-					color #fff
-
-				.username
-					color #fff
+				&, *
+					color #fff !important
 
 			&:active
-				color #fff
 				background darken($theme-color, 10%)
 
-				.name
-					color #fff
+				&, *
+					color #fff !important
 
-				.username
-					color #fff
+	> .users > li
 
-			.avatar
-				vertical-align middle
-				min-width 28px
-				min-height 28px
-				max-width 28px
-				max-height 28px
-				margin 0 8px 0 0
-				border-radius 100%
+		.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)
+		.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)
+		.username
+			font-weight normal
+			color rgba(0, 0, 0, 0.3)
+
+	> .emojis > li
+
+		.emoji
+			display inline-block
+			margin 0 4px 0 0
+			width 24px
+
+		.name
+			font-weight normal
+			color rgba(0, 0, 0, 0.8)
 
 </style>
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 b89365a5d..aa07217b3 100644
--- a/src/web/app/common/views/components/messaging-room.form.vue
+++ b/src/web/app/common/views/components/messaging-room.form.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="mk-messaging-form">
-	<textarea v-model="text" @keypress="onKeypress" @paste="onPaste" placeholder="%i18n:common.input-message-here%"></textarea>
+	<textarea v-model="text" @keypress="onKeypress" @paste="onPaste" placeholder="%i18n:common.input-message-here%" v-autocomplete></textarea>
 	<div class="file" v-if="file">{{ file.name }}</div>
 	<mk-uploader ref="uploader"/>
 	<button class="send" @click="send" :disabled="sending" title="%i18n:common.send%">
diff --git a/src/web/app/desktop/views/directives/autocomplete.ts b/src/web/app/common/views/directives/autocomplete.ts
similarity index 57%
rename from src/web/app/desktop/views/directives/autocomplete.ts
rename to src/web/app/common/views/directives/autocomplete.ts
index 53fa5a4df..bd9c9cb61 100644
--- a/src/web/app/desktop/views/directives/autocomplete.ts
+++ b/src/web/app/common/views/directives/autocomplete.ts
@@ -25,11 +25,11 @@ class Autocomplete {
 	 * 対象のテキストエリアを与えてインスタンスを初期化します。
 	 */
 	constructor(textarea) {
-		// BIND ---------------------------------
-		this.onInput =  this.onInput.bind(this);
+		//#region BIND
+		this.onInput = this.onInput.bind(this);
 		this.complete = this.complete.bind(this);
-		this.close =    this.close.bind(this);
-		// --------------------------------------
+		this.close = this.close.bind(this);
+		//#endregion
 
 		this.suggestion = null;
 		this.textarea = textarea;
@@ -60,14 +60,19 @@ class Autocomplete {
 		const text = this.textarea.value.substr(0, caret);
 
 		const mentionIndex = text.lastIndexOf('@');
+		const emojiIndex = text.lastIndexOf(':');
 
-		if (mentionIndex == -1) return;
+		if (mentionIndex != -1 && mentionIndex > emojiIndex) {
+			const username = text.substr(mentionIndex + 1);
+			if (!username.match(/^[a-zA-Z0-9-]+$/)) return;
+			this.open('user', username);
+		}
 
-		const username = text.substr(mentionIndex + 1);
-
-		if (!username.match(/^[a-zA-Z0-9-]+$/)) return;
-
-		this.open('user', username);
+		if (emojiIndex != -1 && emojiIndex > mentionIndex) {
+			const emoji = text.substr(emojiIndex + 1);
+			if (!emoji.match(/^[\+\-a-z_]+$/)) return;
+			this.open('emoji', emoji);
+		}
 	}
 
 	/**
@@ -88,14 +93,14 @@ class Autocomplete {
 			}
 		}).$mount();
 
-		// ~ サジェストを表示すべき位置を計算 ~
-
+		//#region サジェストを表示すべき位置を計算
 		const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart);
 
 		const rect = this.textarea.getBoundingClientRect();
 
-		const x = rect.left + window.pageXOffset + caretPosition.left;
-		const y = rect.top + window.pageYOffset + caretPosition.top;
+		const x = rect.left + window.pageXOffset + caretPosition.left - this.textarea.scrollLeft;
+		const y = rect.top + window.pageYOffset + caretPosition.top - this.textarea.scrollTop;
+		//#endregion
 
 		this.suggestion.$el.style.left = x + 'px';
 		this.suggestion.$el.style.top = y + 'px';
@@ -119,24 +124,39 @@ class Autocomplete {
 	/**
 	 * オートコンプリートする
 	 */
-	private complete(user) {
+	private complete(type, value) {
 		this.close();
 
-		const value = user.username;
-
 		const caret = this.textarea.selectionStart;
-		const source = this.textarea.value;
 
-		const before = source.substr(0, caret);
-		const trimmedBefore = before.substring(0, before.lastIndexOf('@'));
-		const after = source.substr(caret);
+		if (type == 'user') {
+			const source = this.textarea.value;
 
-		// 結果を挿入する
-		this.textarea.value = trimmedBefore + '@' + value + ' ' + after;
+			const before = source.substr(0, caret);
+			const trimmedBefore = before.substring(0, before.lastIndexOf('@'));
+			const after = source.substr(caret);
 
-		// キャレットを戻す
-		this.textarea.focus();
-		const pos = caret + value.length;
-		this.textarea.setSelectionRange(pos, pos);
+			// 挿入
+			this.textarea.value = trimmedBefore + '@' + value.username + ' ' + after;
+
+			// キャレットを戻す
+			this.textarea.focus();
+			const pos = caret + value.username.length;
+			this.textarea.setSelectionRange(pos, pos);
+		} else if (type == 'emoji') {
+			const source = this.textarea.value;
+
+			const before = source.substr(0, caret);
+			const trimmedBefore = before.substring(0, before.lastIndexOf(':'));
+			const after = source.substr(caret);
+
+			// 挿入
+			this.textarea.value = trimmedBefore + value + after;
+
+			// キャレットを戻す
+			this.textarea.focus();
+			const pos = caret + value.length;
+			this.textarea.setSelectionRange(pos, pos);
+		}
 	}
 }
diff --git a/src/web/app/common/views/directives/focus.ts b/src/web/app/common/views/directives/focus.ts
deleted file mode 100644
index b4fbcb6a8..000000000
--- a/src/web/app/common/views/directives/focus.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-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
index 358866f50..268f07a95 100644
--- a/src/web/app/common/views/directives/index.ts
+++ b/src/web/app/common/views/directives/index.ts
@@ -1,5 +1,5 @@
 import Vue from 'vue';
 
-import focus from './focus';
+import autocomplete from './autocomplete';
 
-Vue.directive('focus', focus);
+Vue.directive('autocomplete', autocomplete);
diff --git a/src/web/app/desktop/views/directives/index.ts b/src/web/app/desktop/views/directives/index.ts
index 3d0c73b6b..324e07596 100644
--- a/src/web/app/desktop/views/directives/index.ts
+++ b/src/web/app/desktop/views/directives/index.ts
@@ -1,8 +1,6 @@
 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 668a249e42bd2a9e3f6d35a2692ca3c8d1463208 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 25 Feb 2018 02:57:19 +0900
Subject: [PATCH 0498/1250] :v:

---
 package.json                                  |  1 +
 .../common/views/components/autocomplete.vue  | 25 +++++++++++++++++--
 .../views/components/messaging-room.form.vue  |  8 +++++-
 .../views/components/messaging-room.vue       | 13 ----------
 .../common/views/directives/autocomplete.ts   | 25 +++++++++----------
 5 files changed, 43 insertions(+), 29 deletions(-)

diff --git a/package.json b/package.json
index 274962c36..2ced82cd1 100644
--- a/package.json
+++ b/package.json
@@ -78,6 +78,7 @@
 		"@types/websocket": "0.0.37",
 		"accesses": "2.5.0",
 		"animejs": "2.2.0",
+		"autosize": "4.0.0",
 		"autwh": "0.0.1",
 		"bcryptjs": "2.4.3",
 		"body-parser": "1.18.2",
diff --git a/src/web/app/common/views/components/autocomplete.vue b/src/web/app/common/views/components/autocomplete.vue
index 198080492..b068ecc4f 100644
--- a/src/web/app/common/views/components/autocomplete.vue
+++ b/src/web/app/common/views/components/autocomplete.vue
@@ -22,7 +22,7 @@ import * as pictograph from 'pictograph';
 import contains from '../../../common/scripts/contains';
 
 export default Vue.extend({
-	props: ['type', 'q', 'textarea', 'complete', 'close'],
+	props: ['type', 'q', 'textarea', 'complete', 'close', 'x', 'y'],
 	data() {
 		return {
 			fetching: true,
@@ -37,6 +37,27 @@ export default Vue.extend({
 			return (this.$refs.suggests as Element).children;
 		}
 	},
+	updated() {
+		//#region 位置調整
+		const margin = 32;
+
+		if (this.x + this.$el.offsetWidth > window.innerWidth - margin) {
+			this.$el.style.left = (this.x - this.$el.offsetWidth) + 'px';
+			this.$el.style.marginLeft = '-16px';
+		} else {
+			this.$el.style.left = this.x + 'px';
+			this.$el.style.marginLeft = '0';
+		}
+
+		if (this.y + this.$el.offsetHeight > window.innerHeight - margin) {
+			this.$el.style.top = (this.y - this.$el.offsetHeight) + 'px';
+			this.$el.style.marginTop = '0';
+		} else {
+			this.$el.style.top = this.y + 'px';
+			this.$el.style.marginTop = 'calc(1em + 8px)';
+		}
+		//#endregion
+	},
 	mounted() {
 		this.textarea.addEventListener('keydown', this.onKeydown);
 
@@ -136,7 +157,7 @@ export default Vue.extend({
 
 <style lang="stylus" scoped>
 .mk-autocomplete
-	position absolute
+	position fixed
 	z-index 65535
 	margin-top calc(1em + 8px)
 	overflow hidden
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 aa07217b3..adc3372be 100644
--- a/src/web/app/common/views/components/messaging-room.form.vue
+++ b/src/web/app/common/views/components/messaging-room.form.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="mk-messaging-form">
-	<textarea v-model="text" @keypress="onKeypress" @paste="onPaste" placeholder="%i18n:common.input-message-here%" v-autocomplete></textarea>
+	<textarea v-model="text" ref="textarea" @keypress="onKeypress" @paste="onPaste" placeholder="%i18n:common.input-message-here%" v-autocomplete></textarea>
 	<div class="file" v-if="file">{{ file.name }}</div>
 	<mk-uploader ref="uploader"/>
 	<button class="send" @click="send" :disabled="sending" title="%i18n:common.send%">
@@ -18,6 +18,8 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import * as autosize from 'autosize';
+
 export default Vue.extend({
 	props: ['user'],
 	data() {
@@ -27,6 +29,9 @@ export default Vue.extend({
 			sending: false
 		};
 	},
+	mounted() {
+		autosize(this.$refs.textarea);
+	},
 	methods: {
 		onPaste(e) {
 			const data = e.clipboardData;
@@ -93,6 +98,7 @@ export default Vue.extend({
 		height 64px
 		margin 0
 		padding 8px
+		resize none
 		font-size 1em
 		color #000
 		outline none
diff --git a/src/web/app/common/views/components/messaging-room.vue b/src/web/app/common/views/components/messaging-room.vue
index 7af6b3fae..310b56f6f 100644
--- a/src/web/app/common/views/components/messaging-room.vue
+++ b/src/web/app/common/views/components/messaging-room.vue
@@ -16,7 +16,6 @@
 	</div>
 	<footer>
 		<div ref="notifications" class="notifications"></div>
-		<div class="grippie" title="%i18n:common.tags.mk-messaging-room.resize-form%"></div>
 		<x-form :user="user"/>
 	</footer>
 </div>
@@ -316,16 +315,4 @@ export default Vue.extend({
 					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/common/views/directives/autocomplete.ts b/src/web/app/common/views/directives/autocomplete.ts
index bd9c9cb61..4d6ba41df 100644
--- a/src/web/app/common/views/directives/autocomplete.ts
+++ b/src/web/app/common/views/directives/autocomplete.ts
@@ -82,6 +82,15 @@ class Autocomplete {
 		// 既に開いているサジェストは閉じる
 		this.close();
 
+		//#region サジェストを表示すべき位置を計算
+		const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart);
+
+		const rect = this.textarea.getBoundingClientRect();
+
+		const x = rect.left + caretPosition.left - this.textarea.scrollLeft;
+		const y = rect.top + caretPosition.top - this.textarea.scrollTop;
+		//#endregion
+
 		// サジェスト要素作成
 		this.suggestion = new MkAutocomplete({
 			propsData: {
@@ -89,22 +98,12 @@ class Autocomplete {
 				complete: this.complete,
 				close: this.close,
 				type: type,
-				q: q
+				q: q,
+				x,
+				y
 			}
 		}).$mount();
 
-		//#region サジェストを表示すべき位置を計算
-		const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart);
-
-		const rect = this.textarea.getBoundingClientRect();
-
-		const x = rect.left + window.pageXOffset + caretPosition.left - this.textarea.scrollLeft;
-		const y = rect.top + window.pageYOffset + caretPosition.top - this.textarea.scrollTop;
-		//#endregion
-
-		this.suggestion.$el.style.left = x + 'px';
-		this.suggestion.$el.style.top = y + 'px';
-
 		// 要素追加
 		document.body.appendChild(this.suggestion.$el);
 	}

From 894688bbbfaa88dabbdaad2b4b47d6ad72ba4fe8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 25 Feb 2018 02:59:03 +0900
Subject: [PATCH 0499/1250] v3865

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 1a7c7610b..0b9a67117 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3859",
+	"version": "0.0.3865",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From c9002fe392ba9745359e406ce2c9e0847075c5c9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 25 Feb 2018 03:05:29 +0900
Subject: [PATCH 0500/1250] :art:

---
 src/web/app/common/views/components/poll-editor.vue | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/web/app/common/views/components/poll-editor.vue b/src/web/app/common/views/components/poll-editor.vue
index 065e91966..20a334d3c 100644
--- a/src/web/app/common/views/components/poll-editor.vue
+++ b/src/web/app/common/views/components/poll-editor.vue
@@ -97,7 +97,9 @@ export default Vue.extend({
 				margin-bottom 0
 
 			> input
-				padding 6px
+				padding 6px 8px
+				width 300px
+				font-size 14px
 				border solid 1px rgba($theme-color, 0.1)
 				border-radius 4px
 

From 65ba9aeaab41fcdb008c1c1ca5b1fade3b549a7d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 25 Feb 2018 03:12:57 +0900
Subject: [PATCH 0501/1250] :art:

---
 src/web/app/reset.styl | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/web/app/reset.styl b/src/web/app/reset.styl
index 3d4b06dbd..db4d87486 100644
--- a/src/web/app/reset.styl
+++ b/src/web/app/reset.styl
@@ -11,6 +11,9 @@ progress
 	appearance none
 	box-shadow none
 
+textarea
+	font-family sans-serif
+
 button
 	margin 0
 	padding 0

From c2a902986ded8297d0a35436bd3fdfe1d19e7772 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 25 Feb 2018 03:17:13 +0900
Subject: [PATCH 0502/1250] :art:

---
 src/web/app/common/views/components/messaging.vue | 6 +++++-
 src/web/app/mobile/views/pages/messaging.vue      | 2 +-
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/web/app/common/views/components/messaging.vue b/src/web/app/common/views/components/messaging.vue
index 6dc19b874..ea75c9081 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="!compact">
+	<div class="search" v-if="!compact" :style="{ top: headerTop + 'px' }">
 		<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%"/>
@@ -57,6 +57,10 @@ export default Vue.extend({
 		compact: {
 			type: Boolean,
 			default: false
+		},
+		headerTop: {
+			type: Number,
+			default: 0
 		}
 	},
 	data() {
diff --git a/src/web/app/mobile/views/pages/messaging.vue b/src/web/app/mobile/views/pages/messaging.vue
index f36ad4a4f..e0fdb4944 100644
--- a/src/web/app/mobile/views/pages/messaging.vue
+++ b/src/web/app/mobile/views/pages/messaging.vue
@@ -1,7 +1,7 @@
 <template>
 <mk-ui>
 	<span slot="header">%fa:R comments%%i18n:mobile.tags.mk-messaging-page.message%</span>
-	<mk-messaging @navigate="navigate"/>
+	<mk-messaging @navigate="navigate" :header-top="48"/>
 </mk-ui>
 </template>
 

From 43dcf4e2407f9b7bd2bf844f8821ce071daaf304 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 25 Feb 2018 03:17:41 +0900
Subject: [PATCH 0503/1250] v3869

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 0b9a67117..ef6258afb 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3865",
+	"version": "0.0.3869",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 98339b3527995af275cc75fe57e8b258fd706a90 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 25 Feb 2018 13:05:55 +0900
Subject: [PATCH 0504/1250] Fix #1141

---
 .../views/components/messaging-room.form.vue  |  9 ++++++-
 .../common/views/directives/autocomplete.ts   | 27 ++++++++++++++-----
 .../desktop/views/components/post-form.vue    |  2 +-
 3 files changed, 29 insertions(+), 9 deletions(-)

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 adc3372be..670040a57 100644
--- a/src/web/app/common/views/components/messaging-room.form.vue
+++ b/src/web/app/common/views/components/messaging-room.form.vue
@@ -1,6 +1,13 @@
 <template>
 <div class="mk-messaging-form">
-	<textarea v-model="text" ref="textarea" @keypress="onKeypress" @paste="onPaste" placeholder="%i18n:common.input-message-here%" v-autocomplete></textarea>
+	<textarea
+		v-model="text"
+		ref="textarea"
+		@keypress="onKeypress"
+		@paste="onPaste"
+		placeholder="%i18n:common.input-message-here%"
+		v-autocomplete="'text'"
+	></textarea>
 	<div class="file" v-if="file">{{ file.name }}</div>
 	<mk-uploader ref="uploader"/>
 	<button class="send" @click="send" :disabled="sending" title="%i18n:common.send%">
diff --git a/src/web/app/common/views/directives/autocomplete.ts b/src/web/app/common/views/directives/autocomplete.ts
index 4d6ba41df..7d04026f8 100644
--- a/src/web/app/common/views/directives/autocomplete.ts
+++ b/src/web/app/common/views/directives/autocomplete.ts
@@ -4,8 +4,9 @@ import MkAutocomplete from '../components/autocomplete.vue';
 export default {
 	bind(el, binding, vn) {
 		const self = el._autoCompleteDirective_ = {} as any;
-		self.x = new Autocomplete(el);
+		self.x = new Autocomplete(el, vn.context, binding.value);
 		self.x.attach();
+		console.log(vn.context);
 	},
 
 	unbind(el, binding, vn) {
@@ -20,11 +21,21 @@ export default {
 class Autocomplete {
 	private suggestion: any;
 	private textarea: any;
+	private vm: any;
+	private model: any;
+
+	private get text(): string {
+		return this.vm[this.model];
+	}
+
+	private set text(text: string) {
+		this.vm[this.model] = text;
+	}
 
 	/**
 	 * 対象のテキストエリアを与えてインスタンスを初期化します。
 	 */
-	constructor(textarea) {
+	constructor(textarea, vm, model) {
 		//#region BIND
 		this.onInput = this.onInput.bind(this);
 		this.complete = this.complete.bind(this);
@@ -33,6 +44,8 @@ class Autocomplete {
 
 		this.suggestion = null;
 		this.textarea = textarea;
+		this.vm = vm;
+		this.model = model;
 	}
 
 	/**
@@ -57,7 +70,7 @@ class Autocomplete {
 		this.close();
 
 		const caret = this.textarea.selectionStart;
-		const text = this.textarea.value.substr(0, caret);
+		const text = this.text.substr(0, caret);
 
 		const mentionIndex = text.lastIndexOf('@');
 		const emojiIndex = text.lastIndexOf(':');
@@ -129,28 +142,28 @@ class Autocomplete {
 		const caret = this.textarea.selectionStart;
 
 		if (type == 'user') {
-			const source = this.textarea.value;
+			const source = this.text;
 
 			const before = source.substr(0, caret);
 			const trimmedBefore = before.substring(0, before.lastIndexOf('@'));
 			const after = source.substr(caret);
 
 			// 挿入
-			this.textarea.value = trimmedBefore + '@' + value.username + ' ' + after;
+			this.text = trimmedBefore + '@' + value.username + ' ' + after;
 
 			// キャレットを戻す
 			this.textarea.focus();
 			const pos = caret + value.username.length;
 			this.textarea.setSelectionRange(pos, pos);
 		} else if (type == 'emoji') {
-			const source = this.textarea.value;
+			const source = this.text;
 
 			const before = source.substr(0, caret);
 			const trimmedBefore = before.substring(0, before.lastIndexOf(':'));
 			const after = source.substr(caret);
 
 			// 挿入
-			this.textarea.value = trimmedBefore + value + after;
+			this.text = trimmedBefore + value + after;
 
 			// キャレットを戻す
 			this.textarea.focus();
diff --git a/src/web/app/desktop/views/components/post-form.vue b/src/web/app/desktop/views/components/post-form.vue
index d38ed9a04..ecb686a10 100644
--- a/src/web/app/desktop/views/components/post-form.vue
+++ b/src/web/app/desktop/views/components/post-form.vue
@@ -9,7 +9,7 @@
 		<textarea :class="{ with: (files.length != 0 || poll) }"
 			ref="text" v-model="text" :disabled="posting"
 			@keydown="onKeydown" @paste="onPaste" :placeholder="placeholder"
-			v-autocomplete
+			v-autocomplete="'text'"
 		></textarea>
 		<div class="medias" :class="{ with: poll }" v-show="files.length != 0">
 			<x-draggable :list="files" :options="{ animation: 150 }">

From d94935c32dfd0fcf964137b8ed7537f28bf0acbb Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 25 Feb 2018 13:31:02 +0900
Subject: [PATCH 0505/1250] Fix #81

---
 .../app/common/views/directives/autocomplete.ts  | 16 ++++++++++------
 1 file changed, 10 insertions(+), 6 deletions(-)

diff --git a/src/web/app/common/views/directives/autocomplete.ts b/src/web/app/common/views/directives/autocomplete.ts
index 7d04026f8..8a33b4e59 100644
--- a/src/web/app/common/views/directives/autocomplete.ts
+++ b/src/web/app/common/views/directives/autocomplete.ts
@@ -152,9 +152,11 @@ class Autocomplete {
 			this.text = trimmedBefore + '@' + value.username + ' ' + after;
 
 			// キャレットを戻す
-			this.textarea.focus();
-			const pos = caret + value.username.length;
-			this.textarea.setSelectionRange(pos, pos);
+			this.vm.$nextTick(() => {
+				this.textarea.focus();
+				const pos = trimmedBefore.length + (value.username.length + 2);
+				this.textarea.setSelectionRange(pos, pos);
+			});
 		} else if (type == 'emoji') {
 			const source = this.text;
 
@@ -166,9 +168,11 @@ class Autocomplete {
 			this.text = trimmedBefore + value + after;
 
 			// キャレットを戻す
-			this.textarea.focus();
-			const pos = caret + value.length;
-			this.textarea.setSelectionRange(pos, pos);
+			this.vm.$nextTick(() => {
+				this.textarea.focus();
+				const pos = trimmedBefore.length + 1;
+				this.textarea.setSelectionRange(pos, pos);
+			});
 		}
 	}
 }

From efb915cb44b4b66b259f762803e9ce7904301cdb Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 25 Feb 2018 13:31:51 +0900
Subject: [PATCH 0506/1250] v3872

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index ef6258afb..d97b450ab 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3869",
+	"version": "0.0.3872",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 038cf2a24257777c43d41462e44adcf937707061 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sun, 25 Feb 2018 04:31:58 +0000
Subject: [PATCH 0507/1250] fix(package): update webpack to version 4.0.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index ef6258afb..6de6bbbb5 100644
--- a/package.json
+++ b/package.json
@@ -179,7 +179,7 @@
 		"vue-template-compiler": "2.5.13",
 		"vuedraggable": "2.16.0",
 		"web-push": "3.3.0",
-		"webpack": "3.11.0",
+		"webpack": "4.0.0",
 		"webpack-replace-loader": "1.3.0",
 		"websocket": "1.0.25",
 		"xev": "2.0.0"

From 1afa9d581267ca16460c7fc34c2d9077dd4f38f7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 25 Feb 2018 14:32:37 +0900
Subject: [PATCH 0508/1250] Show progress bar

---
 package.json             | 1 +
 webpack/plugins/index.ts | 2 ++
 2 files changed, 3 insertions(+)

diff --git a/package.json b/package.json
index e65bcb451..828435e22 100644
--- a/package.json
+++ b/package.json
@@ -138,6 +138,7 @@
 		"nprogress": "0.2.0",
 		"os-utils": "0.0.14",
 		"pictograph": "2.2.0",
+		"progress-bar-webpack-plugin": "^1.11.0",
 		"prominence": "0.2.0",
 		"proxy-addr": "2.0.3",
 		"pug": "2.0.0-rc.4",
diff --git a/webpack/plugins/index.ts b/webpack/plugins/index.ts
index 1ae63cb43..b2ecb5c60 100644
--- a/webpack/plugins/index.ts
+++ b/webpack/plugins/index.ts
@@ -1,4 +1,5 @@
 import * as webpack from 'webpack';
+const ProgressBarPlugin = require('progress-bar-webpack-plugin');
 
 import consts from './consts';
 import hoist from './hoist';
@@ -9,6 +10,7 @@ const isProduction = env === 'production';
 
 export default (version, lang) => {
 	const plugins = [
+		new ProgressBarPlugin(),
 		consts(lang),
 		new webpack.DefinePlugin({
 			'process.env': {

From 986c53a8d00a5e192f86010e06f9a16713f57e4e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 25 Feb 2018 15:50:25 +0900
Subject: [PATCH 0509/1250] :v:

---
 webpack/plugins/index.ts | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/webpack/plugins/index.ts b/webpack/plugins/index.ts
index b2ecb5c60..b97cde231 100644
--- a/webpack/plugins/index.ts
+++ b/webpack/plugins/index.ts
@@ -1,5 +1,6 @@
 import * as webpack from 'webpack';
 const ProgressBarPlugin = require('progress-bar-webpack-plugin');
+import chalk from 'chalk';
 
 import consts from './consts';
 import hoist from './hoist';
@@ -10,7 +11,10 @@ const isProduction = env === 'production';
 
 export default (version, lang) => {
 	const plugins = [
-		new ProgressBarPlugin(),
+		new ProgressBarPlugin({
+			format: chalk`  {cyan.bold yes we can} {bold [}:bar{bold ]} {green.bold :percent} {gray (:current/:total)} :elapseds`,
+			clear: false
+		}),
 		consts(lang),
 		new webpack.DefinePlugin({
 			'process.env': {

From e2c80be48f3b4a8f9fa30cf84d206dcf4d3090da Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 25 Feb 2018 17:03:39 +0900
Subject: [PATCH 0510/1250] Improve emojis

---
 package.json                                  |  2 +-
 .../common/views/components/autocomplete.vue  | 49 ++++++++++++++-----
 .../app/common/views/components/post-html.ts  |  4 +-
 .../common/views/directives/autocomplete.ts   |  2 +-
 webpack/webpack.config.ts                     |  2 +-
 5 files changed, 42 insertions(+), 17 deletions(-)

diff --git a/package.json b/package.json
index 828435e22..d20a46c4d 100644
--- a/package.json
+++ b/package.json
@@ -96,6 +96,7 @@
 		"deepcopy": "0.6.3",
 		"diskusage": "0.2.4",
 		"elasticsearch": "14.1.0",
+		"emojilib": "^2.2.12",
 		"escape-regexp": "0.0.1",
 		"eslint": "4.18.0",
 		"eslint-plugin-vue": "4.2.2",
@@ -137,7 +138,6 @@
 		"multer": "1.3.0",
 		"nprogress": "0.2.0",
 		"os-utils": "0.0.14",
-		"pictograph": "2.2.0",
 		"progress-bar-webpack-plugin": "^1.11.0",
 		"prominence": "0.2.0",
 		"proxy-addr": "2.0.3",
diff --git a/src/web/app/common/views/components/autocomplete.vue b/src/web/app/common/views/components/autocomplete.vue
index b068ecc4f..f31a62474 100644
--- a/src/web/app/common/views/components/autocomplete.vue
+++ b/src/web/app/common/views/components/autocomplete.vue
@@ -8,9 +8,10 @@
 		</li>
 	</ol>
 	<ol class="emojis" ref="suggests" v-if="emojis.length > 0">
-		<li v-for="emoji in emojis" @click="complete(type, pictograph.dic[emoji])" @keydown="onKeydown" tabindex="-1">
-			<span class="emoji">{{ pictograph.dic[emoji] }}</span>
-			<span class="name" v-html="emoji.replace(q, `<b>${q}</b>`)"></span>
+		<li v-for="emoji in emojis" @click="complete(type, emoji.emoji)" @keydown="onKeydown" tabindex="-1">
+			<span class="emoji">{{ emoji.emoji }}</span>
+			<span class="name" v-html="emoji.name.replace(q, `<b>${q}</b>`)"></span>
+			<span class="alias" v-if="emoji.alias">({{ emoji.alias }})</span>
 		</li>
 	</ol>
 </div>
@@ -18,9 +19,30 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import * as pictograph from 'pictograph';
+import * as emojilib from 'emojilib';
 import contains from '../../../common/scripts/contains';
 
+const lib = Object.entries(emojilib.lib).filter((x: any) => {
+	return x[1].category != 'flags';
+});
+const emjdb = lib.map((x: any) => ({
+	emoji: x[1].char,
+	name: x[0],
+	alias: null
+}));
+lib.forEach((x: any) => {
+	if (x[1].keywords) {
+		x[1].keywords.forEach(k => {
+			emjdb.push({
+				emoji: x[1].char,
+				name: k,
+				alias: x[0]
+			});
+		});
+	}
+});
+emjdb.sort((a, b) => a.name.length - b.name.length);
+
 export default Vue.extend({
 	props: ['type', 'q', 'textarea', 'complete', 'close', 'x', 'y'],
 	data() {
@@ -29,7 +51,7 @@ export default Vue.extend({
 			users: [],
 			emojis: [],
 			select: -1,
-			pictograph
+			emojilib
 		}
 	},
 	computed: {
@@ -74,9 +96,12 @@ export default Vue.extend({
 				this.fetching = false;
 			});
 		} else if (this.type == 'emoji') {
-			const emojis = Object.keys(pictograph.dic).sort((a, b) => a.length - b.length);
-			const matched = emojis.filter(e => e.indexOf(this.q) > -1);
-			this.emojis = matched.filter((x, i) => i <= 30);
+			const matched = [];
+			emjdb.some(x => {
+				if (x.name.indexOf(this.q) > -1 && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
+				return matched.length == 30;
+			});
+			this.emojis = matched;
 		}
 	},
 	beforeDestroy() {
@@ -212,12 +237,9 @@ export default Vue.extend({
 
 		.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)
 
 	> .emojis > li
@@ -228,7 +250,10 @@ export default Vue.extend({
 			width 24px
 
 		.name
-			font-weight normal
 			color rgba(0, 0, 0, 0.8)
 
+		.alias
+			margin 0 0 0 8px
+			color rgba(0, 0, 0, 0.3)
+
 </style>
diff --git a/src/web/app/common/views/components/post-html.ts b/src/web/app/common/views/components/post-html.ts
index adb025589..37f60bd08 100644
--- a/src/web/app/common/views/components/post-html.ts
+++ b/src/web/app/common/views/components/post-html.ts
@@ -1,5 +1,5 @@
 import Vue from 'vue';
-import * as pictograph from 'pictograph';
+import * as emojilib from 'emojilib';
 import { url } from '../../../config';
 import MkUrl from './url.vue';
 
@@ -92,7 +92,7 @@ export default Vue.component('mk-post-html', {
 					return createElement('code', token.html);
 
 				case 'emoji':
-					return createElement('span', pictograph.dic[token.emoji] || token.content);
+					return createElement('span', emojilib.lib[token.emoji] || token.content);
 			}
 		}));
 
diff --git a/src/web/app/common/views/directives/autocomplete.ts b/src/web/app/common/views/directives/autocomplete.ts
index 8a33b4e59..e221cce71 100644
--- a/src/web/app/common/views/directives/autocomplete.ts
+++ b/src/web/app/common/views/directives/autocomplete.ts
@@ -83,7 +83,7 @@ class Autocomplete {
 
 		if (emojiIndex != -1 && emojiIndex > mentionIndex) {
 			const emoji = text.substr(emojiIndex + 1);
-			if (!emoji.match(/^[\+\-a-z_]+$/)) return;
+			if (!emoji.match(/^[\+\-a-z0-9_]+$/)) return;
 			this.open('emoji', emoji);
 		}
 	}
diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts
index 76d298078..69e05dc8c 100644
--- a/webpack/webpack.config.ts
+++ b/webpack/webpack.config.ts
@@ -145,7 +145,7 @@ module.exports = Object.keys(langs).map(lang => {
 		output,
 		resolve: {
 			extensions: [
-				'.js', '.ts'
+				'.js', '.ts', '.json'
 			]
 		},
 		resolveLoader: {

From 4aa5a51935b5a7f8660fe38c1a798433aceb225d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 25 Feb 2018 17:03:44 +0900
Subject: [PATCH 0511/1250] :v:

---
 package.json | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/package.json b/package.json
index d20a46c4d..47f463205 100644
--- a/package.json
+++ b/package.json
@@ -180,7 +180,8 @@
 		"vue-template-compiler": "2.5.13",
 		"vuedraggable": "2.16.0",
 		"web-push": "3.3.0",
-		"webpack": "4.0.0",
+		"webpack": "^3.11.0",
+		"webpack-cli": "^2.0.8",
 		"webpack-replace-loader": "1.3.0",
 		"websocket": "1.0.25",
 		"xev": "2.0.0"

From 9668aec8c9bcd70b4cbace51e0cbd5a7a71bd4bd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 25 Feb 2018 17:06:41 +0900
Subject: [PATCH 0512/1250] v3878

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 47f463205..5f35c5e01 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3872",
+	"version": "0.0.3878",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From c39397a8ae473c6fc6ef4a91a1e0d8b1fb1931f1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 25 Feb 2018 17:38:16 +0900
Subject: [PATCH 0513/1250] v3880

---
 package.json                                  |  2 +-
 .../common/views/components/autocomplete.vue  | 52 ++++++++++------
 .../common/views/directives/autocomplete.ts   | 62 ++++++++++++-------
 3 files changed, 73 insertions(+), 43 deletions(-)

diff --git a/package.json b/package.json
index 5f35c5e01..aa2ce5634 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3878",
+	"version": "0.0.3880",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",
diff --git a/src/web/app/common/views/components/autocomplete.vue b/src/web/app/common/views/components/autocomplete.vue
index f31a62474..401eb043d 100644
--- a/src/web/app/common/views/components/autocomplete.vue
+++ b/src/web/app/common/views/components/autocomplete.vue
@@ -87,22 +87,9 @@ export default Vue.extend({
 			el.addEventListener('mousedown', this.onMousedown);
 		});
 
-		if (this.type == 'user') {
-			(this as any).api('users/search_by_username', {
-				query: this.q,
-				limit: 30
-			}).then(users => {
-				this.users = users;
-				this.fetching = false;
-			});
-		} else if (this.type == 'emoji') {
-			const matched = [];
-			emjdb.some(x => {
-				if (x.name.indexOf(this.q) > -1 && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
-				return matched.length == 30;
-			});
-			this.emojis = matched;
-		}
+		this.exec();
+
+		this.$watch('q', this.exec);
 	},
 	beforeDestroy() {
 		this.textarea.removeEventListener('keydown', this.onKeydown);
@@ -112,6 +99,35 @@ export default Vue.extend({
 		});
 	},
 	methods: {
+		exec() {
+			if (this.type == 'user') {
+				const cache = sessionStorage.getItem(this.q);
+				if (cache) {
+					const users = JSON.parse(cache);
+					this.users = users;
+					this.fetching = false;
+				} else {
+					(this as any).api('users/search_by_username', {
+						query: this.q,
+						limit: 30
+					}).then(users => {
+						this.users = users;
+						this.fetching = false;
+
+						// キャッシュ
+						sessionStorage.setItem(this.q, JSON.stringify(users));
+					});
+				}
+			} else if (this.type == 'emoji') {
+				const matched = [];
+				emjdb.some(x => {
+					if (x.name.indexOf(this.q) > -1 && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
+					return matched.length == 30;
+				});
+				this.emojis = matched;
+			}
+		},
+
 		onMousedown(e) {
 			if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close();
 		},
@@ -152,9 +168,6 @@ export default Vue.extend({
 					cancel();
 					this.selectNext();
 					break;
-
-				default:
-					this.close();
 			}
 		},
 
@@ -189,6 +202,7 @@ export default Vue.extend({
 	background #fff
 	border solid 1px rgba(0, 0, 0, 0.1)
 	border-radius 4px
+	transition top 0.1s ease, left 0.1s ease
 
 	> ol
 		display block
diff --git a/src/web/app/common/views/directives/autocomplete.ts b/src/web/app/common/views/directives/autocomplete.ts
index e221cce71..3440c4212 100644
--- a/src/web/app/common/views/directives/autocomplete.ts
+++ b/src/web/app/common/views/directives/autocomplete.ts
@@ -6,7 +6,6 @@ export default {
 		const self = el._autoCompleteDirective_ = {} as any;
 		self.x = new Autocomplete(el, vn.context, binding.value);
 		self.x.attach();
-		console.log(vn.context);
 	},
 
 	unbind(el, binding, vn) {
@@ -23,6 +22,7 @@ class Autocomplete {
 	private textarea: any;
 	private vm: any;
 	private model: any;
+	private currentType: string;
 
 	private get text(): string {
 		return this.vm[this.model];
@@ -67,24 +67,32 @@ class Autocomplete {
 	 * テキスト入力時
 	 */
 	private onInput() {
-		this.close();
-
 		const caret = this.textarea.selectionStart;
 		const text = this.text.substr(0, caret);
 
 		const mentionIndex = text.lastIndexOf('@');
 		const emojiIndex = text.lastIndexOf(':');
 
+		let opened = false;
+
 		if (mentionIndex != -1 && mentionIndex > emojiIndex) {
 			const username = text.substr(mentionIndex + 1);
-			if (!username.match(/^[a-zA-Z0-9-]+$/)) return;
-			this.open('user', username);
+			if (username != '' && username.match(/^[a-zA-Z0-9-]+$/)) {
+				this.open('user', username);
+				opened = true;
+			}
 		}
 
 		if (emojiIndex != -1 && emojiIndex > mentionIndex) {
 			const emoji = text.substr(emojiIndex + 1);
-			if (!emoji.match(/^[\+\-a-z0-9_]+$/)) return;
-			this.open('emoji', emoji);
+			if (emoji != '' && emoji.match(/^[\+\-a-z0-9_]+$/)) {
+				this.open('emoji', emoji);
+				opened = true;
+			}
+		}
+
+		if (!opened) {
+			this.close();
 		}
 	}
 
@@ -92,8 +100,10 @@ class Autocomplete {
 	 * サジェストを提示します。
 	 */
 	private open(type, q) {
-		// 既に開いているサジェストは閉じる
-		this.close();
+		if (type != this.currentType) {
+			this.close();
+		}
+		this.currentType = type;
 
 		//#region サジェストを表示すべき位置を計算
 		const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart);
@@ -104,21 +114,27 @@ class Autocomplete {
 		const y = rect.top + caretPosition.top - this.textarea.scrollTop;
 		//#endregion
 
-		// サジェスト要素作成
-		this.suggestion = new MkAutocomplete({
-			propsData: {
-				textarea: this.textarea,
-				complete: this.complete,
-				close: this.close,
-				type: type,
-				q: q,
-				x,
-				y
-			}
-		}).$mount();
+		if (this.suggestion) {
+			this.suggestion.x = x;
+			this.suggestion.y = y;
+			this.suggestion.q = q;
+		} else {
+			// サジェスト要素作成
+			this.suggestion = new MkAutocomplete({
+				propsData: {
+					textarea: this.textarea,
+					complete: this.complete,
+					close: this.close,
+					type: type,
+					q: q,
+					x,
+					y
+				}
+			}).$mount();
 
-		// 要素追加
-		document.body.appendChild(this.suggestion.$el);
+			// 要素追加
+			document.body.appendChild(this.suggestion.$el);
+		}
 	}
 
 	/**

From 6779e80b0961efbdb7f078e32f8e2627aeea505c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 25 Feb 2018 18:40:39 +0900
Subject: [PATCH 0514/1250] Fix #1144

---
 .../common/views/components/autocomplete.vue  | 21 +++++++++++++++++--
 1 file changed, 19 insertions(+), 2 deletions(-)

diff --git a/src/web/app/common/views/components/autocomplete.vue b/src/web/app/common/views/components/autocomplete.vue
index 401eb043d..04a74e4e2 100644
--- a/src/web/app/common/views/components/autocomplete.vue
+++ b/src/web/app/common/views/components/autocomplete.vue
@@ -87,9 +87,15 @@ export default Vue.extend({
 			el.addEventListener('mousedown', this.onMousedown);
 		});
 
-		this.exec();
+		this.$nextTick(() => {
+			this.exec();
 
-		this.$watch('q', this.exec);
+			this.$watch('q', () => {
+				this.$nextTick(() => {
+					this.exec();
+				});
+			});
+		});
 	},
 	beforeDestroy() {
 		this.textarea.removeEventListener('keydown', this.onKeydown);
@@ -100,6 +106,13 @@ export default Vue.extend({
 	},
 	methods: {
 		exec() {
+			this.select = -1;
+			if (this.$refs.suggests) {
+				Array.from(this.items).forEach(el => {
+					el.removeAttribute('data-selected');
+				});
+			}
+
 			if (this.type == 'user') {
 				const cache = sessionStorage.getItem(this.q);
 				if (cache) {
@@ -168,6 +181,10 @@ export default Vue.extend({
 					cancel();
 					this.selectNext();
 					break;
+
+				default:
+					e.stopPropagation();
+					this.textarea.focus();
 			}
 		},
 

From 384de5599d74dcdbfd94b3eee225b92d337dcbfc Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 25 Feb 2018 18:41:54 +0900
Subject: [PATCH 0515/1250] v3882

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index aa2ce5634..7cc451573 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3880",
+	"version": "0.0.3882",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From fb8ee8f829f1bea81319222414accfdbf312ddfe Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 25 Feb 2018 22:50:26 +0900
Subject: [PATCH 0516/1250] :v:

---
 src/web/app/common/define-widget.ts           |  4 +++
 .../app/common/views/widgets/slideshow.vue    | 28 +++++++++----------
 src/web/app/desktop/views/components/home.vue |  2 +-
 src/web/app/mobile/views/pages/home.vue       |  3 +-
 4 files changed, 20 insertions(+), 17 deletions(-)

diff --git a/src/web/app/common/define-widget.ts b/src/web/app/common/define-widget.ts
index 97925cf44..efce7e813 100644
--- a/src/web/app/common/define-widget.ts
+++ b/src/web/app/common/define-widget.ts
@@ -12,6 +12,10 @@ export default function<T extends object>(data: {
 			isMobile: {
 				type: Boolean,
 				default: false
+			},
+			isCustomizeMode: {
+				type: Boolean,
+				default: false
 			}
 		},
 		computed: {
diff --git a/src/web/app/common/views/widgets/slideshow.vue b/src/web/app/common/views/widgets/slideshow.vue
index 56eb654c2..eb83c6570 100644
--- a/src/web/app/common/views/widgets/slideshow.vue
+++ b/src/web/app/common/views/widgets/slideshow.vue
@@ -1,12 +1,14 @@
 <template>
 <div class="mkw-slideshow">
 	<div @click="choose">
-		<p v-if="props.folder === undefined">クリックしてフォルダを指定してください</p>
+		<p v-if="props.folder === undefined">
+			<template v-if="isCustomizeMode">フォルダを指定するには、カスタマイズモードを終了してください</template>
+			<template v-else>クリックしてフォルダを指定してください</template>
+		</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>
-	<button @click="resize">%fa:expand%</button>
 </div>
 </template>
 
@@ -42,6 +44,9 @@ export default define({
 		clearInterval(this.clock);
 	},
 	methods: {
+		func() {
+			this.resize();
+		},
 		applySize() {
 			let h;
 
@@ -117,24 +122,17 @@ export default define({
 	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
 
+		> p
+			display block
+			margin 1em
+			text-align center
+			color #888
+
 		> *
 			pointer-events none
 
diff --git a/src/web/app/desktop/views/components/home.vue b/src/web/app/desktop/views/components/home.vue
index 59fd2aa36..69f06fbf3 100644
--- a/src/web/app/desktop/views/components/home.vue
+++ b/src/web/app/desktop/views/components/home.vue
@@ -47,7 +47,7 @@
 				: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"/>
+					<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true"/>
 				</div>
 			</x-draggable>
 			<div class="main">
diff --git a/src/web/app/mobile/views/pages/home.vue b/src/web/app/mobile/views/pages/home.vue
index f4f458068..2aa650ec5 100644
--- a/src/web/app/mobile/views/pages/home.vue
+++ b/src/web/app/mobile/views/pages/home.vue
@@ -25,6 +25,7 @@
 						<option value="activity">アクティビティ</option>
 						<option value="rss">RSSリーダー</option>
 						<option value="photo-stream">フォトストリーム</option>
+						<option value="slideshow">スライドショー</option>
 						<option value="version">バージョン</option>
 						<option value="access-log">アクセスログ</option>
 						<option value="server">サーバー情報</option>
@@ -45,7 +46,7 @@
 							<span class="handle">%fa:bars%</span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)">%fa:times%</button>
 						</header>
 						<div @click="widgetFunc(widget.id)">
-							<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-mobile="true"/>
+							<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" :is-mobile="true"/>
 						</div>
 					</div>
 				</x-draggable>

From f4062fee54570fae3977e363afaa605d5b0844ae Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 25 Feb 2018 22:56:23 +0900
Subject: [PATCH 0517/1250] #1145

---
 .../views/components/messaging-room.form.vue  | 45 ++++++++++++++++++-
 1 file changed, 44 insertions(+), 1 deletion(-)

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 670040a57..9b5f1522f 100644
--- a/src/web/app/common/views/components/messaging-room.form.vue
+++ b/src/web/app/common/views/components/messaging-room.form.vue
@@ -36,8 +36,28 @@ export default Vue.extend({
 			sending: false
 		};
 	},
+	computed: {
+		draftId(): string {
+			return this.user.id;
+		}
+	},
+	watch: {
+		text() {
+			this.saveDraft();
+		},
+		file() {
+			this.saveDraft();
+		}
+	},
 	mounted() {
 		autosize(this.$refs.textarea);
+
+		// 書きかけの投稿を復元
+		const draft = JSON.parse(localStorage.getItem('message_drafts') || '{}')[this.draftId];
+		if (draft) {
+			this.text = draft.data.text;
+			this.file = draft.data.file;
+		}
 	},
 	methods: {
 		onPaste(e) {
@@ -89,7 +109,30 @@ export default Vue.extend({
 		clear() {
 			this.text = '';
 			this.file = null;
-		}
+			this.deleteDraft();
+		},
+
+		saveDraft() {
+			const data = JSON.parse(localStorage.getItem('message_drafts') || '{}');
+
+			data[this.draftId] = {
+				updated_at: new Date(),
+				data: {
+					text: this.text,
+					file: this.file
+				}
+			}
+
+			localStorage.setItem('message_drafts', JSON.stringify(data));
+		},
+
+		deleteDraft() {
+			const data = JSON.parse(localStorage.getItem('message_drafts') || '{}');
+
+			delete data[this.draftId];
+
+			localStorage.setItem('message_drafts', JSON.stringify(data));
+		},
 	}
 });
 </script>

From cb2125d89f4f3aa287d34b865ca2e83979c66694 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 25 Feb 2018 23:31:41 +0900
Subject: [PATCH 0518/1250] :v:

---
 .../desktop/views/components/ui.header.vue    | 90 ++++++++++++++++---
 .../app/mobile/views/components/ui.header.vue | 66 +++++++++++++-
 2 files changed, 139 insertions(+), 17 deletions(-)

diff --git a/src/web/app/desktop/views/components/ui.header.vue b/src/web/app/desktop/views/components/ui.header.vue
index 99de05fac..22239ecd7 100644
--- a/src/web/app/desktop/views/components/ui.header.vue
+++ b/src/web/app/desktop/views/components/ui.header.vue
@@ -1,10 +1,11 @@
 <template>
 <div class="header">
 	<mk-special-message/>
-	<div class="main">
+	<div class="main" ref="main">
 		<div class="backdrop"></div>
 		<div class="main">
-			<div class="container">
+			<p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ os.i.name }}</b>さん</p>
+			<div class="container" ref="mainContainer">
 				<div class="left">
 					<x-nav/>
 				</div>
@@ -23,6 +24,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import * as anime from 'animejs';
 
 import XNav from './ui.header.nav.vue';
 import XSearch from './ui.header.search.vue';
@@ -39,6 +41,53 @@ export default Vue.extend({
 		XNotifications,
 		XPost,
 		XClock,
+	},
+	mounted() {
+		if ((this as any).os.isSignedIn) {
+			const ago = (new Date().getTime() - new Date((this as any).os.i.last_used_at).getTime()) / 1000
+			const isHisasiburi = ago >= 3600;
+			if (isHisasiburi) {
+				(this.$refs.main as any).style.overflow = 'hidden';
+
+				anime({
+					targets: this.$refs.welcomeback,
+					top: '0',
+					opacity: 1,
+					delay: 1000,
+					duration: 500,
+					easing: 'easeOutQuad'
+				});
+
+				anime({
+					targets: this.$refs.mainContainer,
+					opacity: 0,
+					delay: 1000,
+					duration: 500,
+					easing: 'easeOutQuad'
+				});
+
+				setTimeout(() => {
+					anime({
+						targets: this.$refs.welcomeback,
+						top: '-48px',
+						opacity: 0,
+						duration: 500,
+						complete: () => {
+							(this.$refs.welcomeback as any).style.display = 'none';
+							(this.$refs.main as any).style.overflow = 'initial';
+						},
+						easing: 'easeInQuad'
+					});
+
+					anime({
+						targets: this.$refs.mainContainer,
+						opacity: 1,
+						duration: 500,
+						easing: 'easeInQuad'
+					});
+				}, 2000);
+			}
+		}
 	}
 });
 </script>
@@ -53,6 +102,7 @@ export default Vue.extend({
 	box-shadow 0 1px 1px rgba(0, 0, 0, 0.075)
 
 	> .main
+		height 48px
 
 		> .backdrop
 			position absolute
@@ -63,17 +113,6 @@ export default Vue.extend({
 			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
@@ -82,12 +121,37 @@ export default Vue.extend({
 			font-size 0.9rem
 			user-select none
 
+			> p
+				display block
+				position absolute
+				top 48px
+				width 100%
+				line-height 48px
+				margin 0
+				text-align center
+				color #888
+				opacity 0
+
 			> .container
 				display flex
 				width 100%
 				max-width 1300px
 				margin 0 auto
 
+				&:before
+					content ""
+					position absolute
+					top 0
+					left 0
+					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
+
 				> .left
 					margin 0 auto 0 0
 					height 48px
diff --git a/src/web/app/mobile/views/components/ui.header.vue b/src/web/app/mobile/views/components/ui.header.vue
index 026e7eb1b..ad7e9ace6 100644
--- a/src/web/app/mobile/views/components/ui.header.vue
+++ b/src/web/app/mobile/views/components/ui.header.vue
@@ -1,9 +1,10 @@
 <template>
 <div class="header">
 	<mk-special-message/>
-	<div class="main">
+	<div class="main" ref="main">
 		<div class="backdrop"></div>
-		<div class="content">
+		<p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ os.i.name }}</b>さん</p>
+		<div class="content" ref="mainContainer">
 			<button class="nav" @click="$parent.isDrawerOpening = true">%fa:bars%</button>
 			<template v-if="hasUnreadNotifications || hasUnreadMessagingMessages">%fa:circle%</template>
 			<h1>
@@ -17,6 +18,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import * as anime from 'animejs';
 
 export default Vue.extend({
 	props: ['func'],
@@ -51,6 +53,50 @@ export default Vue.extend({
 					this.hasUnreadMessagingMessages = true;
 				}
 			});
+
+			const ago = (new Date().getTime() - new Date((this as any).os.i.last_used_at).getTime()) / 1000
+			const isHisasiburi = ago >= 3600;
+			if (isHisasiburi) {
+				(this.$refs.main as any).style.overflow = 'hidden';
+
+				anime({
+					targets: this.$refs.welcomeback,
+					top: '0',
+					opacity: 1,
+					delay: 1000,
+					duration: 500,
+					easing: 'easeOutQuad'
+				});
+
+				anime({
+					targets: this.$refs.mainContainer,
+					opacity: 0,
+					delay: 1000,
+					duration: 500,
+					easing: 'easeOutQuad'
+				});
+
+				setTimeout(() => {
+					anime({
+						targets: this.$refs.welcomeback,
+						top: '-48px',
+						opacity: 0,
+						duration: 500,
+						complete: () => {
+							(this.$refs.welcomeback as any).style.display = 'none';
+							(this.$refs.main as any).style.overflow = 'initial';
+						},
+						easing: 'easeInQuad'
+					});
+
+					anime({
+						targets: this.$refs.mainContainer,
+						opacity: 1,
+						duration: 500,
+						easing: 'easeInQuad'
+					});
+				}, 2000);
+			}
 		}
 	},
 	beforeDestroy() {
@@ -95,15 +141,27 @@ export default Vue.extend({
 		> .backdrop
 			position absolute
 			top 0
-			z-index 1023
+			z-index 1000
 			width 100%
 			height $height
 			-webkit-backdrop-filter blur(12px)
 			backdrop-filter blur(12px)
 			background-color rgba(#1b2023, 0.75)
 
+		> p
+			display block
+			position absolute
+			z-index 1002
+			top $height
+			width 100%
+			line-height $height
+			margin 0
+			text-align center
+			color #fff
+			opacity 0
+
 		> .content
-			z-index 1024
+			z-index 1001
 
 			> h1
 				display block

From e34b64e984cd5bac1357d141d35a54efd75871ca Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 25 Feb 2018 23:31:53 +0900
Subject: [PATCH 0519/1250] v3886

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 7cc451573..50aa1e26b 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3882",
+	"version": "0.0.3886",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 7a94d563673d380ef72a230e71970e64faffdbda Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 26 Feb 2018 00:39:05 +0900
Subject: [PATCH 0520/1250] :v:

---
 src/api/endpoints/posts/create.ts             | 24 +++++++-
 .../desktop/views/components/post-detail.vue  | 29 +++++++++
 .../desktop/views/components/posts.post.vue   | 35 ++++++++++-
 .../app/desktop/views/components/timeline.vue | 14 ++++-
 src/web/app/desktop/views/pages/search.vue    | 55 ++++++++++++-----
 .../mobile/views/components/post-detail.vue   | 25 ++++++++
 .../mobile/views/components/posts.post.vue    | 30 ++++++++--
 src/web/app/mobile/views/pages/search.vue     | 59 +++++++++++++------
 8 files changed, 227 insertions(+), 44 deletions(-)

diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index 0fa52221f..075e1ac9f 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -31,6 +31,10 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 	const [text, textErr] = $(params.text).optional.string().pipe(isValidText).$;
 	if (textErr) return rej('invalid text');
 
+	// Get 'tags' parameter
+	const [tags = [], tagsErr] = $(params.tags).optional.array('string').unique().eachQ(t => t.range(1, 32)).$;
+	if (tagsErr) return rej('invalid tags');
+
 	// Get 'media_ids' parameter
 	const [mediaIds, mediaIdsErr] = $(params.media_ids).optional.array('id').unique().range(1, 4).$;
 	if (mediaIdsErr) return rej('invalid media_ids');
@@ -205,6 +209,23 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		}
 	}
 
+	let tokens = null;
+	if (text) {
+		// Analyze
+		tokens = parse(text);
+
+		// Extract hashtags
+		const hashtags = tokens
+			.filter(t => t.type == 'hashtag')
+			.map(t => t.hashtag);
+
+		hashtags.forEach(tag => {
+			if (tags.indexOf(tag) == -1) {
+				tags.push(tag);
+			}
+		});
+	}
+
 	// 投稿を作成
 	const post = await Post.insert({
 		created_at: new Date(),
@@ -215,6 +236,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		repost_id: repost ? repost._id : undefined,
 		poll: poll,
 		text: text,
+		tags: tags,
 		user_id: user._id,
 		app_id: app ? app._id : null,
 
@@ -423,8 +445,6 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 
 	// If has text content
 	if (text) {
-		// Analyze
-		const tokens = parse(text);
 		/*
 				// Extract a hashtags
 				const hashtags = tokens
diff --git a/src/web/app/desktop/views/components/post-detail.vue b/src/web/app/desktop/views/components/post-detail.vue
index c453867df..1e31752fe 100644
--- a/src/web/app/desktop/views/components/post-detail.vue
+++ b/src/web/app/desktop/views/components/post-detail.vue
@@ -44,6 +44,9 @@
 				<mk-images :images="p.media"/>
 			</div>
 			<mk-poll v-if="p.poll" :post="p"/>
+			<div class="tags" v-if="p.tags && p.tags.length > 0">
+				<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=${tag}`">{{ tag }}</router-link>
+			</div>
 		</div>
 		<footer>
 			<mk-reactions-viewer :post="p"/>
@@ -306,6 +309,32 @@ export default Vue.extend({
 			> .mk-url-preview
 				margin-top 8px
 
+			> .tags
+				> *
+					margin 0 8px 0 0
+					padding 0 8px 0 16px
+					font-size 90%
+					color #8d969e
+					background #edf0f3
+					border-radius 4px
+
+					&:before
+						content ""
+						display block
+						position absolute
+						top 0
+						bottom 0
+						left 4px
+						width 8px
+						height 8px
+						margin auto 0
+						background #fff
+						border-radius 100%
+
+					&:hover
+						text-decoration none
+						background #e2e7ec
+
 		> footer
 			font-size 1.2em
 
diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index 4898de0b6..382a8de97 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -38,6 +38,9 @@
 					</p>
 					<a class="reply" v-if="p.reply">%fa:reply%</a>
 					<mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i" :class="$style.text"/>
+					<div class="tags" v-if="p.tags && p.tags.length > 0">
+						<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=${tag}`">{{ tag }}</router-link>
+					</div>
 					<a class="quote" v-if="p.repost">RP:</a>
 					<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 				</div>
@@ -342,9 +345,9 @@ export default Vue.extend({
 			display block
 			float left
 			margin 0 16px 10px 0
-			position -webkit-sticky
-			position sticky
-			top 74px
+			//position -webkit-sticky
+			//position sticky
+			//top 74px
 
 			> .avatar
 				display block
@@ -428,6 +431,32 @@ export default Vue.extend({
 						font-style oblique
 						color #a0bf46
 
+					> .tags
+						> *
+							margin 0 8px 0 0
+							padding 0 8px 0 16px
+							font-size 90%
+							color #8d969e
+							background #edf0f3
+							border-radius 4px
+
+							&:before
+								content ""
+								display block
+								position absolute
+								top 0
+								bottom 0
+								left 4px
+								width 8px
+								height 8px
+								margin auto 0
+								background #fff
+								border-radius 100%
+
+							&:hover
+								text-decoration none
+								background #e2e7ec
+
 				> .mk-poll
 					font-size 80%
 
diff --git a/src/web/app/desktop/views/components/timeline.vue b/src/web/app/desktop/views/components/timeline.vue
index eef62104e..0d16d60df 100644
--- a/src/web/app/desktop/views/components/timeline.vue
+++ b/src/web/app/desktop/views/components/timeline.vue
@@ -24,6 +24,7 @@ export default Vue.extend({
 		return {
 			fetching: true,
 			moreFetching: false,
+			existMore: false,
 			posts: [],
 			connection: null,
 			connectionId: null,
@@ -62,8 +63,13 @@ export default Vue.extend({
 			this.fetching = true;
 
 			(this as any).api('posts/timeline', {
+				limit: 11,
 				until_date: this.date ? this.date.getTime() : undefined
 			}).then(posts => {
+				if (posts.length == 11) {
+					posts.pop();
+					this.existMore = true;
+				}
 				this.posts = posts;
 				this.fetching = false;
 				this.$emit('loaded');
@@ -71,11 +77,17 @@ export default Vue.extend({
 			});
 		},
 		more() {
-			if (this.moreFetching || this.fetching || this.posts.length == 0) return;
+			if (this.moreFetching || this.fetching || this.posts.length == 0 || !this.existMore) return;
 			this.moreFetching = true;
 			(this as any).api('posts/timeline', {
+				limit: 11,
 				until_id: this.posts[this.posts.length - 1].id
 			}).then(posts => {
+				if (posts.length == 11) {
+					posts.pop();
+				} else {
+					this.existMore = false;
+				}
 				this.posts = this.posts.concat(posts);
 				this.moreFetching = false;
 			});
diff --git a/src/web/app/desktop/views/pages/search.vue b/src/web/app/desktop/views/pages/search.vue
index b8e8db2e7..afd37c8ce 100644
--- a/src/web/app/desktop/views/pages/search.vue
+++ b/src/web/app/desktop/views/pages/search.vue
@@ -1,13 +1,13 @@
 <template>
 <mk-ui>
 	<header :class="$style.header">
-		<h1>{{ query }}</h1>
+		<h1>{{ q }}</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">
+	<p :class="$style.empty" v-if="!fetching && empty">%fa:search%「{{ q }}」に関する投稿は見つかりませんでした。</p>
+	<mk-posts ref="timeline" :class="$style.posts" :posts="posts">
 		<div slot="footer">
 			<template v-if="!moreFetching">%fa:search%</template>
 			<template v-if="moreFetching">%fa:spinner .pulse .fw%</template>
@@ -21,33 +21,34 @@ import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
 import parse from '../../../common/scripts/parse-search-query';
 
-const limit = 30;
+const limit = 20;
 
 export default Vue.extend({
-	props: ['query'],
 	data() {
 		return {
 			fetching: true,
 			moreFetching: false,
+			existMore: false,
 			offset: 0,
 			posts: []
 		};
 	},
+	watch: {
+		$route: 'fetch'
+	},
 	computed: {
 		empty(): boolean {
 			return this.posts.length == 0;
+		},
+		q(): string {
+			return this.$route.query.q;
 		}
 	},
 	mounted() {
-		Progress.start();
-
 		document.addEventListener('keydown', this.onDocumentKeydown);
 		window.addEventListener('scroll', this.onScroll);
 
-		(this as any).api('posts/search', parse(this.query)).then(posts => {
-			this.posts = posts;
-			this.fetching = false;
-		});
+		this.fetch();
 	},
 	beforeDestroy() {
 		document.removeEventListener('keydown', this.onDocumentKeydown);
@@ -61,16 +62,38 @@ export default Vue.extend({
 				}
 			}
 		},
+		fetch() {
+			this.fetching = true;
+			Progress.start();
+
+			(this as any).api('posts/search', Object.assign({
+				limit: limit + 1,
+				offset: this.offset
+			}, parse(this.q))).then(posts => {
+				if (posts.length == limit + 1) {
+					posts.pop();
+					this.existMore = true;
+				}
+				this.posts = posts;
+				this.fetching = false;
+				Progress.done();
+			});
+		},
 		more() {
-			if (this.moreFetching || this.fetching || this.posts.length == 0) return;
+			if (this.moreFetching || this.fetching || this.posts.length == 0 || !this.existMore) return;
 			this.offset += limit;
 			this.moreFetching = true;
-			return (this as any).api('posts/search', Object.assign({}, parse(this.query), {
-				limit: limit,
+			return (this as any).api('posts/search', Object.assign({
+				limit: limit + 1,
 				offset: this.offset
-			})).then(posts => {
-				this.moreFetching = false;
+			}, parse(this.q))).then(posts => {
+				if (posts.length == limit + 1) {
+					posts.pop();
+				} else {
+					this.existMore = false;
+				}
 				this.posts = this.posts.concat(posts);
+				this.moreFetching = false;
 			});
 		},
 		onScroll() {
diff --git a/src/web/app/mobile/views/components/post-detail.vue b/src/web/app/mobile/views/components/post-detail.vue
index 05138607f..a83744ec4 100644
--- a/src/web/app/mobile/views/components/post-detail.vue
+++ b/src/web/app/mobile/views/components/post-detail.vue
@@ -39,6 +39,9 @@
 		</header>
 		<div class="body">
 			<mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i" :class="$style.text"/>
+			<div class="tags" v-if="p.tags && p.tags.length > 0">
+				<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=${tag}`">{{ tag }}</router-link>
+			</div>
 			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 			<div class="media" v-if="p.media">
 				<mk-images :images="p.media"/>
@@ -312,6 +315,28 @@ export default Vue.extend({
 					display block
 					max-width 100%
 
+			> .tags
+				> *
+					margin 0 8px 0 0
+					padding 0 8px 0 16px
+					font-size 90%
+					color #8d969e
+					background #edf0f3
+					border-radius 4px
+
+					&:before
+						content ""
+						display block
+						position absolute
+						top 0
+						bottom 0
+						left 4px
+						width 8px
+						height 8px
+						margin auto 0
+						background #fff
+						border-radius 100%
+
 		> .time
 			font-size 16px
 			color #c0c0c0
diff --git a/src/web/app/mobile/views/components/posts.post.vue b/src/web/app/mobile/views/components/posts.post.vue
index ae1dfc59a..e26c337f5 100644
--- a/src/web/app/mobile/views/components/posts.post.vue
+++ b/src/web/app/mobile/views/components/posts.post.vue
@@ -35,6 +35,9 @@
 						%fa:reply%
 					</a>
 					<mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i" :class="$style.text"/>
+					<div class="tags" v-if="p.tags && p.tags.length > 0">
+						<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=${tag}`">{{ tag }}</router-link>
+					</div>
 					<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 					<a class="quote" v-if="p.repost != null">RP:</a>
 				</div>
@@ -346,10 +349,7 @@ export default Vue.extend({
 					font-size 1.1em
 					color #717171
 
-					> .dummy
-						display none
-
-					mk-url-preview
+					.mk-url-preview
 						margin-top 8px
 
 					> .channel
@@ -364,6 +364,28 @@ export default Vue.extend({
 						font-style oblique
 						color #a0bf46
 
+					> .tags
+						> *
+							margin 0 8px 0 0
+							padding 0 8px 0 16px
+							font-size 90%
+							color #8d969e
+							background #edf0f3
+							border-radius 4px
+
+							&:before
+								content ""
+								display block
+								position absolute
+								top 0
+								bottom 0
+								left 4px
+								width 8px
+								height 8px
+								margin auto 0
+								background #fff
+								border-radius 100%
+
 					[data-is-me]:after
 						content "you"
 						padding 0 4px
diff --git a/src/web/app/mobile/views/pages/search.vue b/src/web/app/mobile/views/pages/search.vue
index b6e114a82..cbab504e3 100644
--- a/src/web/app/mobile/views/pages/search.vue
+++ b/src/web/app/mobile/views/pages/search.vue
@@ -1,10 +1,10 @@
 <template>
 <mk-ui>
-	<span slot="header">%fa:search% {{ query }}</span>
+	<span slot="header">%fa:search% {{ q }}</span>
 	<main v-if="!fetching">
 		<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="posts.length == 0">{{ '%i18n:mobile.tags.mk-search-posts.empty%'.replace('{}', q) }}</span>
+			<button v-if="existMore" @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>
@@ -18,38 +18,61 @@ import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
 import parse from '../../../common/scripts/parse-search-query';
 
-const limit = 30;
+const limit = 20;
 
 export default Vue.extend({
-	props: ['query'],
 	data() {
 		return {
 			fetching: true,
+			existMore: false,
 			posts: [],
 			offset: 0
 		};
 	},
+	watch: {
+		$route: 'fetch'
+	},
+	computed: {
+		q(): string {
+			return this.$route.query.q;
+		}
+	},
 	mounted() {
-		document.title = `%i18n:mobile.tags.mk-search-page.search%: ${this.query} | Misskey`;
+		document.title = `%i18n:mobile.tags.mk-search-page.search%: ${this.q} | Misskey`;
 		document.documentElement.style.background = '#313a42';
 
-		Progress.start();
-
-		(this as any).api('posts/search', Object.assign({}, parse(this.query), {
-			limit: limit
-		})).then(posts => {
-			this.posts = posts;
-			this.fetching = false;
-			Progress.done();
-		});
+		this.fetch();
 	},
 	methods: {
+		fetch() {
+			this.fetching = true;
+			Progress.start();
+
+			(this as any).api('posts/search', Object.assign({
+				limit: limit + 1
+			}, parse(this.q))).then(posts => {
+				if (posts.length == limit + 1) {
+					posts.pop();
+					this.existMore = true;
+				}
+				this.posts = posts;
+				this.fetching = false;
+				Progress.done();
+			});
+		},
 		more() {
 			this.offset += limit;
-			return (this as any).api('posts/search', Object.assign({}, parse(this.query), {
-				limit: limit,
+			(this as any).api('posts/search', Object.assign({
+				limit: limit + 1,
 				offset: this.offset
-			}));
+			}, parse(this.q))).then(posts => {
+				if (posts.length == limit + 1) {
+					posts.pop();
+				} else {
+					this.existMore = false;
+				}
+				this.posts = this.posts.concat(posts);
+			});
 		}
 	}
 });

From b275773806f18a57d39fb3bb5e3f89d5e863b8a8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 26 Feb 2018 00:39:24 +0900
Subject: [PATCH 0521/1250] v3888

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 50aa1e26b..7bb70cff7 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3886",
+	"version": "0.0.3888",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 04e177c65963cc66e74d6065a8614b14aec3bf15 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 26 Feb 2018 01:06:21 +0900
Subject: [PATCH 0522/1250] :v:

---
 src/api/endpoints/posts/search.ts                    | 9 +++++++++
 src/web/app/desktop/views/components/post-detail.vue | 2 +-
 src/web/app/desktop/views/components/posts.post.vue  | 2 +-
 src/web/app/mobile/views/components/post-detail.vue  | 2 +-
 src/web/app/mobile/views/components/posts.post.vue   | 2 +-
 src/web/docs/search.ja.pug                           | 4 ++++
 6 files changed, 17 insertions(+), 4 deletions(-)

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index 6e26f5539..a36d1178a 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -121,6 +121,15 @@ async function search(
 				text: x
 			});
 		} else {
+			const tags = text.split(' ').filter(x => x[0] == '#');
+			if (tags) {
+				push({
+					$and: tags.map(x => ({
+						tags: x
+					}))
+				});
+			}
+
 			push({
 				$and: text.split(' ').map(x => ({
 					// キーワードが-で始まる場合そのキーワードを除外する
diff --git a/src/web/app/desktop/views/components/post-detail.vue b/src/web/app/desktop/views/components/post-detail.vue
index 1e31752fe..e6e0ffa02 100644
--- a/src/web/app/desktop/views/components/post-detail.vue
+++ b/src/web/app/desktop/views/components/post-detail.vue
@@ -45,7 +45,7 @@
 			</div>
 			<mk-poll v-if="p.poll" :post="p"/>
 			<div class="tags" v-if="p.tags && p.tags.length > 0">
-				<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=${tag}`">{{ tag }}</router-link>
+				<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
 			</div>
 		</div>
 		<footer>
diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index 382a8de97..647590e25 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -39,7 +39,7 @@
 					<a class="reply" v-if="p.reply">%fa:reply%</a>
 					<mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i" :class="$style.text"/>
 					<div class="tags" v-if="p.tags && p.tags.length > 0">
-						<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=${tag}`">{{ tag }}</router-link>
+						<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
 					</div>
 					<a class="quote" v-if="p.repost">RP:</a>
 					<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
diff --git a/src/web/app/mobile/views/components/post-detail.vue b/src/web/app/mobile/views/components/post-detail.vue
index a83744ec4..ae1c3fdc9 100644
--- a/src/web/app/mobile/views/components/post-detail.vue
+++ b/src/web/app/mobile/views/components/post-detail.vue
@@ -40,7 +40,7 @@
 		<div class="body">
 			<mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i" :class="$style.text"/>
 			<div class="tags" v-if="p.tags && p.tags.length > 0">
-				<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=${tag}`">{{ tag }}</router-link>
+				<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
 			</div>
 			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 			<div class="media" v-if="p.media">
diff --git a/src/web/app/mobile/views/components/posts.post.vue b/src/web/app/mobile/views/components/posts.post.vue
index e26c337f5..b94d9d16b 100644
--- a/src/web/app/mobile/views/components/posts.post.vue
+++ b/src/web/app/mobile/views/components/posts.post.vue
@@ -36,7 +36,7 @@
 					</a>
 					<mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i" :class="$style.text"/>
 					<div class="tags" v-if="p.tags && p.tags.length > 0">
-						<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=${tag}`">{{ tag }}</router-link>
+						<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
 					</div>
 					<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 					<a class="quote" v-if="p.repost != null">RP:</a>
diff --git a/src/web/docs/search.ja.pug b/src/web/docs/search.ja.pug
index f33091ee6..e14e8c867 100644
--- a/src/web/docs/search.ja.pug
+++ b/src/web/docs/search.ja.pug
@@ -16,6 +16,10 @@ section
 	p テキストを「"""」で囲むと、そのテキストと完全に一致する投稿を検索します。
 	p 例えば、「"""にゃーん"""」と検索すると、「にゃーん」という投稿のみがヒットし、「にゃーん…」という投稿はヒットしません。
 
+section
+	h2 タグ
+	p キーワードの前に「#」(シャープ)をプリフィクスすると、そのキーワードと一致するタグを持つ投稿に限定します。
+
 section
 	h2 オプション
 	p

From 52889875ac83aff27a5384ab29589b08302ab814 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 26 Feb 2018 01:06:33 +0900
Subject: [PATCH 0523/1250] v3890

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 7bb70cff7..a1c9073f9 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3888",
+	"version": "0.0.3890",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 631b2a4aa3364fc31d87aeb1921e5eb3104eb074 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 26 Feb 2018 01:36:36 +0900
Subject: [PATCH 0524/1250] Fix bug

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

diff --git a/src/web/app/desktop/views/components/ui.header.vue b/src/web/app/desktop/views/components/ui.header.vue
index 22239ecd7..c83eb925b 100644
--- a/src/web/app/desktop/views/components/ui.header.vue
+++ b/src/web/app/desktop/views/components/ui.header.vue
@@ -47,6 +47,7 @@ export default Vue.extend({
 			const ago = (new Date().getTime() - new Date((this as any).os.i.last_used_at).getTime()) / 1000
 			const isHisasiburi = ago >= 3600;
 			if (isHisasiburi) {
+				(this.$refs.welcomeback as any).style.display = 'block';
 				(this.$refs.main as any).style.overflow = 'hidden';
 
 				anime({
@@ -122,7 +123,7 @@ export default Vue.extend({
 			user-select none
 
 			> p
-				display block
+				display none
 				position absolute
 				top 48px
 				width 100%
diff --git a/src/web/app/mobile/views/components/ui.header.vue b/src/web/app/mobile/views/components/ui.header.vue
index ad7e9ace6..4962c5b0b 100644
--- a/src/web/app/mobile/views/components/ui.header.vue
+++ b/src/web/app/mobile/views/components/ui.header.vue
@@ -57,6 +57,7 @@ export default Vue.extend({
 			const ago = (new Date().getTime() - new Date((this as any).os.i.last_used_at).getTime()) / 1000
 			const isHisasiburi = ago >= 3600;
 			if (isHisasiburi) {
+				(this.$refs.welcomeback as any).style.display = 'block';
 				(this.$refs.main as any).style.overflow = 'hidden';
 
 				anime({
@@ -149,7 +150,7 @@ export default Vue.extend({
 			background-color rgba(#1b2023, 0.75)
 
 		> p
-			display block
+			display none
 			position absolute
 			z-index 1002
 			top $height

From f1501e018b7ea6ebd881b97ebbd9b02fb5f943ee Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 26 Feb 2018 01:36:44 +0900
Subject: [PATCH 0525/1250] v3892

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index a1c9073f9..4bb288e95 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3890",
+	"version": "0.0.3892",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 9c9323d4d0b46d059549c6e9f86e4fc08c5afcb5 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, 26 Feb 2018 18:31:55 +0900
Subject: [PATCH 0526/1250] Fix bug

---
 src/web/app/common/scripts/streaming/home-stream.ts     | 1 +
 src/web/app/common/views/components/twitter-setting.vue | 8 +++++---
 src/web/app/desktop/views/components/home.vue           | 2 +-
 src/web/app/mobile/views/components/ui.header.vue       | 1 +
 src/web/app/mobile/views/pages/home.vue                 | 2 +-
 5 files changed, 9 insertions(+), 5 deletions(-)

diff --git a/src/web/app/common/scripts/streaming/home-stream.ts b/src/web/app/common/scripts/streaming/home-stream.ts
index a92b61cae..b8e417b6d 100644
--- a/src/web/app/common/scripts/streaming/home-stream.ts
+++ b/src/web/app/common/scripts/streaming/home-stream.ts
@@ -13,6 +13,7 @@ export default class Connection extends Stream {
 		// 最終利用日時を更新するため定期的にaliveメッセージを送信
 		setInterval(() => {
 			this.send({ type: 'alive' });
+			me.last_used_at = new Date();
 		}, 1000 * 60);
 
 		// 自分の情報が更新されたとき
diff --git a/src/web/app/common/views/components/twitter-setting.vue b/src/web/app/common/views/components/twitter-setting.vue
index aaca6ccdd..a0de27085 100644
--- a/src/web/app/common/views/components/twitter-setting.vue
+++ b/src/web/app/common/views/components/twitter-setting.vue
@@ -23,12 +23,14 @@ export default Vue.extend({
 			docsUrl
 		};
 	},
-	watch: {
-		'os.i'() {
+	mounted() {
+		this.$watch('os.i', () => {
 			if ((this as any).os.i.twitter) {
 				if (this.form) this.form.close();
 			}
-		}
+		}, {
+			deep: true
+		});
 	},
 	methods: {
 		connect() {
diff --git a/src/web/app/desktop/views/components/home.vue b/src/web/app/desktop/views/components/home.vue
index 69f06fbf3..a7c61f4c5 100644
--- a/src/web/app/desktop/views/components/home.vue
+++ b/src/web/app/desktop/views/components/home.vue
@@ -126,7 +126,7 @@ export default Vue.extend({
 	created() {
 		this.widgets.left = this.left;
 		this.widgets.right = this.right;
-		this.$watch('os.i', i => {
+		this.$watch('os.i.client_settings', i => {
 			this.widgets.left = this.left;
 			this.widgets.right = this.right;
 		}, {
diff --git a/src/web/app/mobile/views/components/ui.header.vue b/src/web/app/mobile/views/components/ui.header.vue
index 4962c5b0b..f4b98d36f 100644
--- a/src/web/app/mobile/views/components/ui.header.vue
+++ b/src/web/app/mobile/views/components/ui.header.vue
@@ -56,6 +56,7 @@ export default Vue.extend({
 
 			const ago = (new Date().getTime() - new Date((this as any).os.i.last_used_at).getTime()) / 1000
 			const isHisasiburi = ago >= 3600;
+			(this as any).os.i.last_used_at = new Date();
 			if (isHisasiburi) {
 				(this.$refs.welcomeback as any).style.display = 'block';
 				(this.$refs.main as any).style.overflow = 'hidden';
diff --git a/src/web/app/mobile/views/pages/home.vue b/src/web/app/mobile/views/pages/home.vue
index 2aa650ec5..44b072491 100644
--- a/src/web/app/mobile/views/pages/home.vue
+++ b/src/web/app/mobile/views/pages/home.vue
@@ -111,7 +111,7 @@ export default Vue.extend({
 			this.widgets = (this as any).os.i.client_settings.mobile_home;
 		}
 
-		this.$watch('os.i', i => {
+		this.$watch('os.i.client_settings', i => {
 			this.widgets = (this as any).os.i.client_settings.mobile_home;
 		}, {
 			deep: true

From f85ba0b76d223fd6aba48ed557f22057ff5bfe6a 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, 26 Feb 2018 18:32:24 +0900
Subject: [PATCH 0527/1250] oops

---
 src/web/app/desktop/views/components/ui.header.vue | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/web/app/desktop/views/components/ui.header.vue b/src/web/app/desktop/views/components/ui.header.vue
index c83eb925b..0ce8459c7 100644
--- a/src/web/app/desktop/views/components/ui.header.vue
+++ b/src/web/app/desktop/views/components/ui.header.vue
@@ -46,6 +46,7 @@ export default Vue.extend({
 		if ((this as any).os.isSignedIn) {
 			const ago = (new Date().getTime() - new Date((this as any).os.i.last_used_at).getTime()) / 1000
 			const isHisasiburi = ago >= 3600;
+			(this as any).os.i.last_used_at = new Date();
 			if (isHisasiburi) {
 				(this.$refs.welcomeback as any).style.display = 'block';
 				(this.$refs.main as any).style.overflow = 'hidden';

From 28843ef941f14649b2e267e4ee291eda56b16faa Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 26 Feb 2018 18:57:52 +0900
Subject: [PATCH 0528/1250] :v:

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

diff --git a/src/web/app/desktop/views/components/ui.header.vue b/src/web/app/desktop/views/components/ui.header.vue
index 0ce8459c7..2cbc33211 100644
--- a/src/web/app/desktop/views/components/ui.header.vue
+++ b/src/web/app/desktop/views/components/ui.header.vue
@@ -87,7 +87,7 @@ export default Vue.extend({
 						duration: 500,
 						easing: 'easeInQuad'
 					});
-				}, 2000);
+				}, 2500);
 			}
 		}
 	}
diff --git a/src/web/app/mobile/views/components/ui.header.vue b/src/web/app/mobile/views/components/ui.header.vue
index f4b98d36f..1aff555f8 100644
--- a/src/web/app/mobile/views/components/ui.header.vue
+++ b/src/web/app/mobile/views/components/ui.header.vue
@@ -97,7 +97,7 @@ export default Vue.extend({
 						duration: 500,
 						easing: 'easeInQuad'
 					});
-				}, 2000);
+				}, 2500);
 			}
 		}
 	},

From 197e93707bb511ea9af1c4897161969ef6643c56 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 26 Feb 2018 18:59:26 +0900
Subject: [PATCH 0529/1250] :blowfish:

---
 src/web/app/common/views/widgets/slideshow.vue | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/web/app/common/views/widgets/slideshow.vue b/src/web/app/common/views/widgets/slideshow.vue
index eb83c6570..df6cb900d 100644
--- a/src/web/app/common/views/widgets/slideshow.vue
+++ b/src/web/app/common/views/widgets/slideshow.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mkw-slideshow">
+<div class="mkw-slideshow" :data-mobile="isMobile">
 	<div @click="choose">
 		<p v-if="props.folder === undefined">
 			<template v-if="isCustomizeMode">フォルダを指定するには、カスタマイズモードを終了してください</template>
@@ -122,6 +122,11 @@ export default define({
 	border solid 1px rgba(0, 0, 0, 0.075)
 	border-radius 6px
 
+	&[data-mobile]
+		border none
+		border-radius 8px
+		box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+
 	> div
 		width 100%
 		height 100%

From 1e0aaa4720f98e8455ba4b6284eb329c7b6f690d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 26 Feb 2018 19:00:46 +0900
Subject: [PATCH 0530/1250] Fix bug

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

diff --git a/src/web/app/mobile/views/components/notification.vue b/src/web/app/mobile/views/components/notification.vue
index 4e09f3d83..2a0e5c117 100644
--- a/src/web/app/mobile/views/components/notification.vue
+++ b/src/web/app/mobile/views/components/notification.vue
@@ -154,7 +154,7 @@ export default Vue.extend({
 		p
 			margin 0
 
-			i, mk-reaction-icon
+			i, .mk-reaction-icon
 				margin-right 4px
 
 	.post-preview

From 459e6cffd1bd4fb76ef241e2983180f995ff8711 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 26 Feb 2018 19:23:53 +0900
Subject: [PATCH 0531/1250] Fix bugs

---
 src/web/app/common/mios.ts                    | 13 ++++++----
 src/web/app/common/scripts/signout.ts         |  7 ------
 .../scripts/streaming/home-stream-manager.ts  |  7 ++++--
 .../common/scripts/streaming/home-stream.ts   |  6 ++---
 .../scripts/streaming/stream-manager.ts       | 13 ++++++++++
 .../app/common/scripts/streaming/stream.ts    |  9 ++++---
 .../views/components/stream-indicator.vue     | 24 +++++++------------
 .../views/components/ui.header.account.vue    |  7 +++---
 8 files changed, 47 insertions(+), 39 deletions(-)
 delete mode 100644 src/web/app/common/scripts/signout.ts

diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
index 6c95e5b9b..6cc441cd1 100644
--- a/src/web/app/common/mios.ts
+++ b/src/web/app/common/mios.ts
@@ -1,9 +1,8 @@
 import Vue from 'vue';
 import { EventEmitter } from 'eventemitter3';
 
-import { apiUrl, swPublickey, version, lang } from '../config';
+import { host, apiUrl, swPublickey, version, lang } from '../config';
 import api from './scripts/api';
-import signout from './scripts/signout';
 import Progress from './scripts/loading';
 import HomeStreamManager from './scripts/streaming/home-stream-manager';
 import DriveStreamManager from './scripts/streaming/drive-stream-manager';
@@ -153,7 +152,7 @@ export default class MiOS extends EventEmitter {
 
 		this.once('signedin', () => {
 			// Init home stream manager
-			this.stream = new HomeStreamManager(this.i);
+			this.stream = new HomeStreamManager(this, this.i);
 
 			// Init other stream manager
 			this.streams.driveStream = new DriveStreamManager(this.i);
@@ -184,6 +183,12 @@ export default class MiOS extends EventEmitter {
 		console.error.apply(null, args);
 	}
 
+	public signout() {
+		localStorage.removeItem('me');
+		document.cookie = `i=; domain=.${host}; expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
+		location.href = '/';
+	}
+
 	/**
 	 * Initialize MiOS (boot)
 	 * @param callback A function that call when initialized
@@ -209,7 +214,7 @@ export default class MiOS extends EventEmitter {
 			.then(res => {
 				// When failed to authenticate user
 				if (res.status !== 200) {
-					return signout();
+					return this.signout();
 				}
 
 				// Parse response
diff --git a/src/web/app/common/scripts/signout.ts b/src/web/app/common/scripts/signout.ts
deleted file mode 100644
index 292319654..000000000
--- a/src/web/app/common/scripts/signout.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-declare const _HOST_: string;
-
-export default () => {
-	localStorage.removeItem('me');
-	document.cookie = `i=; domain=.${_HOST_}; expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
-	location.href = '/';
-};
diff --git a/src/web/app/common/scripts/streaming/home-stream-manager.ts b/src/web/app/common/scripts/streaming/home-stream-manager.ts
index ad1dc870e..ab56d5a73 100644
--- a/src/web/app/common/scripts/streaming/home-stream-manager.ts
+++ b/src/web/app/common/scripts/streaming/home-stream-manager.ts
@@ -1,18 +1,21 @@
 import StreamManager from './stream-manager';
 import Connection from './home-stream';
+import MiOS from '../../mios';
 
 export default class HomeStreamManager extends StreamManager<Connection> {
 	private me;
+	private os: MiOS;
 
-	constructor(me) {
+	constructor(os: MiOS, me) {
 		super();
 
 		this.me = me;
+		this.os = os;
 	}
 
 	public getConnection() {
 		if (this.connection == null) {
-			this.connection = new Connection(this.me);
+			this.connection = new Connection(this.os, this.me);
 		}
 
 		return this.connection;
diff --git a/src/web/app/common/scripts/streaming/home-stream.ts b/src/web/app/common/scripts/streaming/home-stream.ts
index b8e417b6d..57bf0ec2a 100644
--- a/src/web/app/common/scripts/streaming/home-stream.ts
+++ b/src/web/app/common/scripts/streaming/home-stream.ts
@@ -1,11 +1,11 @@
 import Stream from './stream';
-import signout from '../signout';
+import MiOS from '../../mios';
 
 /**
  * Home stream connection
  */
 export default class Connection extends Stream {
-	constructor(me) {
+	constructor(os: MiOS, me) {
 		super('', {
 			i: me.token
 		});
@@ -25,7 +25,7 @@ export default class Connection extends Stream {
 		// このままではAPIが利用できないので強制的にサインアウトさせる
 		this.on('my_token_regenerated', () => {
 			alert('%i18n:common.my-token-regenerated%');
-			signout();
+			os.signout();
 		});
 	}
 }
diff --git a/src/web/app/common/scripts/streaming/stream-manager.ts b/src/web/app/common/scripts/streaming/stream-manager.ts
index 5bb0dc701..a4a73c561 100644
--- a/src/web/app/common/scripts/streaming/stream-manager.ts
+++ b/src/web/app/common/scripts/streaming/stream-manager.ts
@@ -23,6 +23,14 @@ export default abstract class StreamManager<T extends Connection> extends EventE
 			this.emit('disconnected');
 		} else {
 			this.emit('connected', this._connection);
+
+			this._connection.on('_connected_', () => {
+				this.emit('_connected_');
+			});
+
+			this._connection.on('_disconnected_', () => {
+				this.emit('_disconnected_');
+			});
 		}
 	}
 
@@ -37,6 +45,11 @@ export default abstract class StreamManager<T extends Connection> extends EventE
 		return this._connection != null;
 	}
 
+	public get state(): string {
+		if (!this.hasConnection) return 'no-connection';
+		return this._connection.state;
+	}
+
 	/**
 	 * コネクションを要求します
 	 */
diff --git a/src/web/app/common/scripts/streaming/stream.ts b/src/web/app/common/scripts/streaming/stream.ts
index 770d77510..8799f6fe6 100644
--- a/src/web/app/common/scripts/streaming/stream.ts
+++ b/src/web/app/common/scripts/streaming/stream.ts
@@ -1,13 +1,12 @@
-declare const _API_URL_: string;
-
 import { EventEmitter } from 'eventemitter3';
 import * as ReconnectingWebsocket from 'reconnecting-websocket';
+import { apiUrl } from '../../../config';
 
 /**
  * Misskey stream connection
  */
 export default class Connection extends EventEmitter {
-	private state: string;
+	public state: string;
 	private buffer: any[];
 	private socket: ReconnectingWebsocket;
 
@@ -25,7 +24,7 @@ export default class Connection extends EventEmitter {
 		this.state = 'initializing';
 		this.buffer = [];
 
-		const host = _API_URL_.replace('http', 'ws');
+		const host = apiUrl.replace('http', 'ws');
 		const query = params
 			? Object.keys(params)
 				.map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
@@ -58,7 +57,7 @@ export default class Connection extends EventEmitter {
 	 */
 	private onClose() {
 		this.state = 'reconnecting';
-		this.emit('_closed_');
+		this.emit('_disconnected_');
 	}
 
 	/**
diff --git a/src/web/app/common/views/components/stream-indicator.vue b/src/web/app/common/views/components/stream-indicator.vue
index c1c0672e4..1f18fa76e 100644
--- a/src/web/app/common/views/components/stream-indicator.vue
+++ b/src/web/app/common/views/components/stream-indicator.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-stream-indicator" v-if="stream">
+<div class="mk-stream-indicator">
 	<p v-if=" stream.state == 'initializing' ">
 		%fa:spinner .pulse%
 		<span>%i18n:common.tags.mk-stream-indicator.connecting%<mk-ellipsis/></span>
@@ -20,16 +20,14 @@ import Vue from 'vue';
 import * as anime from 'animejs';
 
 export default Vue.extend({
-	data() {
-		return {
-			stream: null
-		};
+	computed: {
+		stream() {
+			return (this as any).os.stream;
+		}
 	},
 	created() {
-		this.stream = (this as any).os.stream.borrow();
-
-		(this as any).os.stream.on('connected', this.onConnected);
-		(this as any).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,13 +36,11 @@ export default Vue.extend({
 		});
 	},
 	beforeDestroy() {
-		(this as any).os.stream.off('connected', this.onConnected);
-		(this as any).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 as any).os.stream.borrow();
-
 			setTimeout(() => {
 				anime({
 					targets: this.$el,
@@ -55,8 +51,6 @@ export default Vue.extend({
 			}, 1000);
 		},
 		onDisconnected() {
-			this.stream = null;
-
 			anime({
 				targets: this.$el,
 				opacity: 1,
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 68cf8477d..ad6533339 100644
--- a/src/web/app/desktop/views/components/ui.header.account.vue
+++ b/src/web/app/desktop/views/components/ui.header.account.vue
@@ -37,13 +37,11 @@ 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';
 
 export default Vue.extend({
 	data() {
 		return {
-			isOpen: false,
-			signout
+			isOpen: false
 		};
 	},
 	beforeDestroy() {
@@ -77,6 +75,9 @@ export default Vue.extend({
 		settings() {
 			this.close();
 			(this as any).os.new(MkSettingsWindow);
+		},
+		signout() {
+			(this as any).os.signout();
 		}
 	}
 });

From f11299316b484ed84f07cf6a183627683ed9a9c5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 26 Feb 2018 19:24:26 +0900
Subject: [PATCH 0532/1250] v3899

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 4bb288e95..0ad230f90 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3892",
+	"version": "0.0.3899",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 98eb2ded3e669ab2c983cc9317a7e4699396f3d8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 27 Feb 2018 02:10:52 +0900
Subject: [PATCH 0533/1250] #372

---
 src/api/models/messaging-message.ts           |   2 +-
 .../views/components/messaging-room.form.vue  |  36 ++++--
 .../components/messaging-room.message.vue     | 107 +++++++++++-------
 3 files changed, 91 insertions(+), 54 deletions(-)

diff --git a/src/api/models/messaging-message.ts b/src/api/models/messaging-message.ts
index 90cf1cd71..fcb356c5c 100644
--- a/src/api/models/messaging-message.ts
+++ b/src/api/models/messaging-message.ts
@@ -67,7 +67,7 @@ export const pack = (
 	// Populate user
 	_message.user = await packUser(_message.user_id, me);
 
-	if (_message.file) {
+	if (_message.file_id) {
 		// Populate file
 		_message.file = await packFile(_message.file_id);
 	}
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 9b5f1522f..48ccaa4d2 100644
--- a/src/web/app/common/views/components/messaging-room.form.vue
+++ b/src/web/app/common/views/components/messaging-room.form.vue
@@ -8,18 +8,18 @@
 		placeholder="%i18n:common.input-message-here%"
 		v-autocomplete="'text'"
 	></textarea>
-	<div class="file" v-if="file">{{ file.name }}</div>
-	<mk-uploader ref="uploader"/>
-	<button class="send" @click="send" :disabled="sending" title="%i18n:common.send%">
+	<div class="file" @click="file = null" v-if="file">{{ file.name }}</div>
+	<mk-uploader ref="uploader" @uploaded="onUploaded"/>
+	<button class="send" @click="send" :disabled="!canSend || 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" title="%i18n:common.tags.mk-messaging-form.attach-from-local%">
+	<button class="attach-from-local" @click="chooseFile" title="%i18n:common.tags.mk-messaging-form.attach-from-local%">
 		%fa:upload%
 	</button>
 	<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/*"/>
+	<input ref="file" type="file" @change="onChangeFile"/>
 </div>
 </template>
 
@@ -39,6 +39,9 @@ export default Vue.extend({
 	computed: {
 		draftId(): string {
 			return this.user.id;
+		},
+		canSend(): boolean {
+			return (this.text != null && this.text != '') || this.file != null;
 		}
 	},
 	watch: {
@@ -88,15 +91,24 @@ export default Vue.extend({
 			});
 		},
 
-		upload() {
-			// TODO
+		onChangeFile() {
+			this.upload((this.$refs.file as any).files[0]);
+		},
+
+		upload(file) {
+			(this.$refs.uploader as any).upload(file);
+		},
+
+		onUploaded(file) {
+			this.file = file;
 		},
 
 		send() {
 			this.sending = true;
 			(this as any).api('messaging/messages/create', {
 				user_id: this.user.id,
-				text: this.text
+				text: this.text ? this.text : undefined,
+				file_id: this.file ? this.file.id : undefined
 			}).then(message => {
 				this.clear();
 			}).catch(err => {
@@ -158,13 +170,18 @@ export default Vue.extend({
 		box-shadow none
 		background transparent
 
+	> .file
+		padding 8px
+		color #444
+		background #eee
+		cursor pointer
+
 	> .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
@@ -222,7 +239,6 @@ export default Vue.extend({
 	.attach-from-drive
 		margin 0
 		padding 10px 14px
-		line-height 1em
 		font-size 1em
 		font-weight normal
 		text-decoration none
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 9772c7c0d..56854ca2f 100644
--- a/src/web/app/common/views/components/messaging-room.message.vue
+++ b/src/web/app/common/views/components/messaging-room.message.vue
@@ -3,23 +3,27 @@
 	<router-link class="avatar-anchor" :to="`/${message.user.username}`" :title="message.user.username" target="_blank">
 		<img class="avatar" :src="`${message.user.avatar_url}?thumbnail&size=80`" alt=""/>
 	</router-link>
-	<div class="content-container">
-		<div class="balloon">
+	<div class="content">
+		<div class="balloon" :data-no-text="message.text == null">
 			<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">
 				<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 class="file" v-if="message.file">
+					<a :href="message.file.url" target="_blank" :title="message.file.name">
+						<img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/>
+						<p v-else>{{ message.file.name }}</p>
+					</a>
 				</div>
 			</div>
 			<div class="content" v-if="message.is_deleted">
 				<p class="is-deleted">%i18n:common.tags.mk-messaging-message.deleted%</p>
 			</div>
 		</div>
+		<div></div>
+		<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 		<footer>
 			<mk-time :time="message.created_at"/>
 			<template v-if="message.is_edited">%fa:pencil-alt%</template>
@@ -57,13 +61,10 @@ export default Vue.extend({
 	padding 10px 12px 10px 12px
 	background-color transparent
 
-	&:after
-		content ""
-		display block
-		clear both
-
 	> .avatar-anchor
 		display block
+		position absolute
+		top 10px
 
 		> .avatar
 			display block
@@ -75,18 +76,12 @@ export default Vue.extend({
 			border-radius 8px
 			transition all 0.1s ease
 
-	> .content-container
-		display block
-		margin 0 12px
-		padding 0
-		max-width calc(100% - 78px)
+	> .content
 
 		> .balloon
 			display block
-			float inherit
-			margin 0
 			padding 0
-			max-width 100%
+			max-width calc(100% - 16px)
 			min-height 38px
 			border-radius 16px
 
@@ -97,6 +92,9 @@ export default Vue.extend({
 				position absolute
 				top 12px
 
+			& + *
+				clear both
+
 			&:hover
 				> .delete-button
 					display block
@@ -153,28 +151,43 @@ export default Vue.extend({
 					font-size 1em
 					color rgba(0, 0, 0, 0.8)
 
-					&, *
-						user-select text
-						cursor auto
-
 					& + .file
-						&.image
-							> img
-								border-radius 0 0 16px 16px
+						> a
+							border-radius 0 0 16px 16px
 
 				> .file
-					&.image
-						> img
+					> a
+						display block
+						max-width 100%
+						max-height 512px
+						border-radius 16px
+						overflow hidden
+						text-decoration none
+
+						&:hover
+							text-decoration none
+
+							> p
+								background #ccc
+
+						> *
 							display block
-							max-width 100%
-							max-height 512px
-							border-radius 16px
+							margin 0
+							width 100%
+							height 100%
+
+						> p
+							padding 30px
+							text-align center
+							color #555
+							background #ddd
+
+		> .mk-url-preview
+			margin 8px 0
 
 		> footer
 			display block
-			clear both
-			margin 0
-			padding 2px
+			margin 2px 0 0 0
 			font-size 10px
 			color rgba(0, 0, 0, 0.4)
 
@@ -183,15 +196,19 @@ export default Vue.extend({
 
 	&:not([data-is-me])
 		> .avatar-anchor
-			float left
+			left 12px
 
-		> .content-container
-			float left
+		> .content
+			padding-left 66px
 
 			> .balloon
+				float left
 				background #eee
 
-				&:before
+				&[data-no-text]
+					background transparent
+
+				&:not([data-no-text]):before
 					left -14px
 					border-top solid 8px transparent
 					border-right solid 8px #eee
@@ -203,15 +220,19 @@ export default Vue.extend({
 
 	&[data-is-me]
 		> .avatar-anchor
-			float right
+			right 12px
 
-		> .content-container
-			float right
+		> .content
+			padding-right 66px
 
 			> .balloon
+				float right
 				background $me-balloon-color
 
-				&:before
+				&[data-no-text]
+					background transparent
+
+				&:not([data-no-text]):before
 					right -14px
 					left auto
 					border-top solid 8px transparent
@@ -232,7 +253,7 @@ export default Vue.extend({
 				text-align right
 
 	&[data-is-deleted]
-			> .content-container
-				opacity 0.5
+		> .baloon
+			opacity 0.5
 
 </style>

From cc7110b4c3c774674e7e3e538078fc39c31e05a4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 27 Feb 2018 02:17:10 +0900
Subject: [PATCH 0534/1250] :v:

---
 src/web/app/common/filters/index.ts             | 1 -
 src/web/app/common/{ => views}/filters/bytes.ts | 0
 src/web/app/common/views/filters/index.ts       | 2 ++
 src/web/app/common/views/filters/number.ts      | 5 +++++
 src/web/app/mobile/views/pages/user.vue         | 6 +++---
 5 files changed, 10 insertions(+), 4 deletions(-)
 delete mode 100644 src/web/app/common/filters/index.ts
 rename src/web/app/common/{ => views}/filters/bytes.ts (100%)
 create mode 100644 src/web/app/common/views/filters/index.ts
 create mode 100644 src/web/app/common/views/filters/number.ts

diff --git a/src/web/app/common/filters/index.ts b/src/web/app/common/filters/index.ts
deleted file mode 100644
index 16ff8c87a..000000000
--- a/src/web/app/common/filters/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-require('./bytes');
diff --git a/src/web/app/common/filters/bytes.ts b/src/web/app/common/views/filters/bytes.ts
similarity index 100%
rename from src/web/app/common/filters/bytes.ts
rename to src/web/app/common/views/filters/bytes.ts
diff --git a/src/web/app/common/views/filters/index.ts b/src/web/app/common/views/filters/index.ts
new file mode 100644
index 000000000..3a1d1ac23
--- /dev/null
+++ b/src/web/app/common/views/filters/index.ts
@@ -0,0 +1,2 @@
+require('./bytes');
+require('./number');
diff --git a/src/web/app/common/views/filters/number.ts b/src/web/app/common/views/filters/number.ts
new file mode 100644
index 000000000..d9f48229d
--- /dev/null
+++ b/src/web/app/common/views/filters/number.ts
@@ -0,0 +1,5 @@
+import Vue from 'vue';
+
+Vue.filter('number', (n) => {
+	return n.toLocaleString();
+});
diff --git a/src/web/app/mobile/views/pages/user.vue b/src/web/app/mobile/views/pages/user.vue
index 378beeaf1..ae2fbe85d 100644
--- a/src/web/app/mobile/views/pages/user.vue
+++ b/src/web/app/mobile/views/pages/user.vue
@@ -27,15 +27,15 @@
 				</div>
 				<div class="status">
 					<a>
-						<b>{{ user.posts_count }}</b>
+						<b>{{ user.posts_count | number }}</b>
 						<i>%i18n:mobile.tags.mk-user.posts%</i>
 					</a>
 					<a :href="`${user.username}/following`">
-						<b>{{ user.following_count }}</b>
+						<b>{{ user.following_count | number }}</b>
 						<i>%i18n:mobile.tags.mk-user.following%</i>
 					</a>
 					<a :href="`${user.username}/followers`">
-						<b>{{ user.followers_count }}</b>
+						<b>{{ user.followers_count | number }}</b>
 						<i>%i18n:mobile.tags.mk-user.followers%</i>
 					</a>
 				</div>

From 73a0e0dee433b9abe151db901399f33bfc2cbe53 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 27 Feb 2018 02:17:25 +0900
Subject: [PATCH 0535/1250] v3902

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 0ad230f90..ba35426af 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3899",
+	"version": "0.0.3902",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 4cfe93da30b4aafe642db5404dc26d98f3f9e239 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 27 Feb 2018 02:36:19 +0900
Subject: [PATCH 0536/1250] oops

---
 src/web/app/init.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index 4a8f34f8d..5c0b4c2d3 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -17,7 +17,7 @@ require('./common/views/components');
 require('./common/views/widgets');
 
 // Register global filters
-require('./common/filters');
+require('./common/views/filters');
 
 Vue.mixin({
 	destroyed(this: any) {

From 9357184e6fda75db246a76f1ff8ad7bfd9b6e8f5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 27 Feb 2018 02:36:57 +0900
Subject: [PATCH 0537/1250] v3904

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index ba35426af..e58d2748a 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3902",
+	"version": "0.0.3904",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From ecc017d7ed94b68a4463a7f7d811979dc4d1a639 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 27 Feb 2018 03:48:14 +0900
Subject: [PATCH 0538/1250] Fix bug

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

diff --git a/src/web/app/common/views/components/post-html.ts b/src/web/app/common/views/components/post-html.ts
index 37f60bd08..3676e9e6a 100644
--- a/src/web/app/common/views/components/post-html.ts
+++ b/src/web/app/common/views/components/post-html.ts
@@ -92,7 +92,8 @@ export default Vue.component('mk-post-html', {
 					return createElement('code', token.html);
 
 				case 'emoji':
-					return createElement('span', emojilib.lib[token.emoji] || token.content);
+					const emoji = emojilib.lib[token.emoji];
+					return createElement('span', emoji ? emoji.char : token.content);
 			}
 		}));
 

From 519c8749aceb2da57eaec47b9b36c251fec6af8b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 27 Feb 2018 03:48:24 +0900
Subject: [PATCH 0539/1250] v3906

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index e58d2748a..cd1c8bea6 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3904",
+	"version": "0.0.3906",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From e4ac2a4f506f16ae71a7a3342314802e015961e4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 27 Feb 2018 04:36:16 +0900
Subject: [PATCH 0540/1250] Improve usability

---
 .../views/components/messaging-room.form.vue  | 54 +++++++++++++++++--
 .../views/components/messaging-room.vue       | 54 ++++++++++++++++++-
 .../desktop/views/components/post-form.vue    |  4 +-
 3 files changed, 105 insertions(+), 7 deletions(-)

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 48ccaa4d2..3698ccd5b 100644
--- a/src/web/app/common/views/components/messaging-room.form.vue
+++ b/src/web/app/common/views/components/messaging-room.form.vue
@@ -1,5 +1,8 @@
 <template>
-<div class="mk-messaging-form">
+<div class="mk-messaging-form"
+	@dragover.prevent.stop="onDragover"
+	@drop.prevent.stop="onDrop"
+>
 	<textarea
 		v-model="text"
 		ref="textarea"
@@ -42,6 +45,9 @@ export default Vue.extend({
 		},
 		canSend(): boolean {
 			return (this.text != null && this.text != '') || this.file != null;
+		},
+		room(): any {
+			return this.$parent;
 		}
 	},
 	watch: {
@@ -50,6 +56,10 @@ export default Vue.extend({
 		},
 		file() {
 			this.saveDraft();
+
+			if (this.room.isBottom()) {
+				this.room.scrollToBottom();
+			}
 		}
 	},
 	mounted() {
@@ -66,10 +76,46 @@ export default Vue.extend({
 		onPaste(e) {
 			const data = e.clipboardData;
 			const items = data.items;
-			for (const item of items) {
-				if (item.kind == 'file') {
-					//this.upload(item.getAsFile());
+
+			if (items.length == 1) {
+				if (items[0].kind == 'file') {
+					this.upload(items[0].getAsFile());
 				}
+			} else {
+				if (items[0].kind == 'file') {
+					alert('メッセージに添付できるのはひとつのファイルのみです');
+				}
+			}
+		},
+
+		onDragover(e) {
+			e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+		},
+
+		onDrop(e): void {
+			// ファイルだったら
+			if (e.dataTransfer.files.length == 1) {
+				this.upload(e.dataTransfer.files[0]);
+				return;
+			} else if (e.dataTransfer.files.length > 1) {
+				alert('メッセージに添付できるのはひとつのファイルのみです');
+				return;
+			}
+
+			// データ取得
+			const data = e.dataTransfer.getData('text');
+			if (data == null) return;
+
+			try {
+				// パース
+				const obj = JSON.parse(data);
+
+				// (ドライブの)ファイルだったら
+				if (obj.type == 'file') {
+					this.file = obj.file;
+				}
+			} catch (e) {
+				// not a json, so noop
 			}
 		},
 
diff --git a/src/web/app/common/views/components/messaging-room.vue b/src/web/app/common/views/components/messaging-room.vue
index 310b56f6f..fb67d126c 100644
--- a/src/web/app/common/views/components/messaging-room.vue
+++ b/src/web/app/common/views/components/messaging-room.vue
@@ -1,5 +1,8 @@
 <template>
-<div class="mk-messaging-room">
+<div class="mk-messaging-room"
+	@dragover.prevent.stop="onDragover"
+	@drop.prevent.stop="onDrop"
+>
 	<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>
@@ -16,7 +19,7 @@
 	</div>
 	<footer>
 		<div ref="notifications" class="notifications"></div>
-		<x-form :user="user"/>
+		<x-form :user="user" ref="form"/>
 	</footer>
 </div>
 </template>
@@ -32,7 +35,9 @@ export default Vue.extend({
 		XMessage,
 		XForm
 	},
+
 	props: ['user', 'isNaked'],
+
 	data() {
 		return {
 			init: true,
@@ -42,6 +47,7 @@ export default Vue.extend({
 			connection: null
 		};
 	},
+
 	computed: {
 		_messages(): any[] {
 			return (this.messages as any).map(message => {
@@ -51,6 +57,10 @@ export default Vue.extend({
 				message._datetext = `${month}月 ${date}日`;
 				return message;
 			});
+		},
+
+		form(): any {
+			return this.$refs.form;
 		}
 	},
 
@@ -67,6 +77,7 @@ export default Vue.extend({
 			this.scrollToBottom();
 		});
 	},
+
 	beforeDestroy() {
 		this.connection.off('message', this.onMessage);
 		this.connection.off('read', this.onRead);
@@ -74,7 +85,39 @@ export default Vue.extend({
 
 		document.removeEventListener('visibilitychange', this.onVisibilitychange);
 	},
+
 	methods: {
+		onDragover(e) {
+			e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+		},
+
+		onDrop(e): void {
+			// ファイルだったら
+			if (e.dataTransfer.files.length == 1) {
+				this.form.upload(e.dataTransfer.files[0]);
+				return;
+			} else if (e.dataTransfer.files.length > 1) {
+				alert('メッセージに添付できるのはひとつのファイルのみです');
+				return;
+			}
+
+			// データ取得
+			const data = e.dataTransfer.getData('text');
+			if (data == null) return;
+
+			try {
+				// パース
+				const obj = JSON.parse(data);
+
+				// (ドライブの)ファイルだったら
+				if (obj.type == 'file') {
+					this.form.file = obj.file;
+				}
+			} catch (e) {
+				// not a json, so noop
+			}
+		},
+
 		fetchMessages() {
 			return new Promise((resolve, reject) => {
 				const max = this.existMoreMessages ? 20 : 10;
@@ -96,12 +139,14 @@ export default Vue.extend({
 				});
 			});
 		},
+
 		fetchMoreMessages() {
 			this.fetchingMoreMessages = true;
 			this.fetchMessages().then(() => {
 				this.fetchingMoreMessages = false;
 			});
 		},
+
 		onMessage(message) {
 			const isBottom = this.isBottom();
 
@@ -123,6 +168,7 @@ export default Vue.extend({
 				this.notify('%i18n:common.tags.mk-messaging-room.new-message%');
 			}
 		},
+
 		onRead(ids) {
 			if (!Array.isArray(ids)) ids = [ids];
 			ids.forEach(id => {
@@ -132,6 +178,7 @@ export default Vue.extend({
 				}
 			});
 		},
+
 		isBottom() {
 			const asobi = 64;
 			const current = this.isNaked
@@ -142,6 +189,7 @@ export default Vue.extend({
 				: this.$el.scrollHeight;
 			return current > (max - asobi);
 		},
+
 		scrollToBottom() {
 			if (this.isNaked) {
 				window.scroll(0, document.body.offsetHeight);
@@ -149,6 +197,7 @@ export default Vue.extend({
 				this.$el.scrollTop = this.$el.scrollHeight;
 			}
 		},
+
 		notify(message) {
 			const n = document.createElement('p') as any;
 			n.innerHTML = '%fa:arrow-circle-down%' + message;
@@ -163,6 +212,7 @@ export default Vue.extend({
 				setTimeout(() => n.parentNode.removeChild(n), 1000);
 			}, 4000);
 		},
+
 		onVisibilitychange() {
 			if (document.hidden) return;
 			this.messages.forEach(message => {
diff --git a/src/web/app/desktop/views/components/post-form.vue b/src/web/app/desktop/views/components/post-form.vue
index ecb686a10..4d43d98d7 100644
--- a/src/web/app/desktop/views/components/post-form.vue
+++ b/src/web/app/desktop/views/components/post-form.vue
@@ -190,7 +190,9 @@ export default Vue.extend({
 					this.files.push(obj.file);
 					this.$emit('change-attached-media', this.files);
 				}
-			} catch (e) { }
+			} catch (e) {
+				// not a json, so noop
+			}
 		},
 		post() {
 			this.posting = true;

From a0b9c1f11c6f871ec12813207e63fffa2a2f48fb Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 27 Feb 2018 04:36:25 +0900
Subject: [PATCH 0541/1250] v3908

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index cd1c8bea6..bf3239abe 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3906",
+	"version": "0.0.3908",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From d9bfab76ad7f39b312f618745505f00c6d8cd44c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 27 Feb 2018 06:25:17 +0900
Subject: [PATCH 0542/1250] Improve drag and drop

---
 .../views/components/messaging-room.form.vue  | 33 +++++----
 .../views/components/messaging-room.vue       | 29 ++++----
 .../desktop/views/components/drive.file.vue   |  6 +-
 .../desktop/views/components/drive.folder.vue | 72 +++++++++----------
 .../views/components/drive.nav-folder.vue     | 58 +++++++--------
 .../app/desktop/views/components/drive.vue    | 67 ++++++++---------
 .../desktop/views/components/post-form.vue    | 37 +++++-----
 7 files changed, 148 insertions(+), 154 deletions(-)

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 3698ccd5b..edcda069a 100644
--- a/src/web/app/common/views/components/messaging-room.form.vue
+++ b/src/web/app/common/views/components/messaging-room.form.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mk-messaging-form"
-	@dragover.prevent.stop="onDragover"
-	@drop.prevent.stop="onDrop"
+	@dragover.stop="onDragover"
+	@drop.stop="onDrop"
 >
 	<textarea
 		v-model="text"
@@ -89,34 +89,33 @@ export default Vue.extend({
 		},
 
 		onDragover(e) {
-			e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+			const isFile = e.dataTransfer.items[0].kind == 'file';
+			const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file';
+			if (isFile || isDriveFile) {
+				e.preventDefault();
+				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+			}
 		},
 
 		onDrop(e): void {
 			// ファイルだったら
 			if (e.dataTransfer.files.length == 1) {
+				e.preventDefault();
 				this.upload(e.dataTransfer.files[0]);
 				return;
 			} else if (e.dataTransfer.files.length > 1) {
+				e.preventDefault();
 				alert('メッセージに添付できるのはひとつのファイルのみです');
 				return;
 			}
 
-			// データ取得
-			const data = e.dataTransfer.getData('text');
-			if (data == null) return;
-
-			try {
-				// パース
-				const obj = JSON.parse(data);
-
-				// (ドライブの)ファイルだったら
-				if (obj.type == 'file') {
-					this.file = obj.file;
-				}
-			} catch (e) {
-				// not a json, so noop
+			//#region ドライブのファイル
+			const driveFile = e.dataTransfer.getData('mk_drive_file');
+			if (driveFile != null && driveFile != '') {
+				this.file = JSON.parse(driveFile);
+				e.preventDefault();
 			}
+			//#endregion
 		},
 
 		onKeypress(e) {
diff --git a/src/web/app/common/views/components/messaging-room.vue b/src/web/app/common/views/components/messaging-room.vue
index fb67d126c..f7f31c557 100644
--- a/src/web/app/common/views/components/messaging-room.vue
+++ b/src/web/app/common/views/components/messaging-room.vue
@@ -88,7 +88,14 @@ export default Vue.extend({
 
 	methods: {
 		onDragover(e) {
-			e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+			const isFile = e.dataTransfer.items[0].kind == 'file';
+			const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file';
+
+			if (isFile || isDriveFile) {
+				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+			} else {
+				e.dataTransfer.dropEffect = 'none';
+			}
 		},
 
 		onDrop(e): void {
@@ -101,21 +108,13 @@ export default Vue.extend({
 				return;
 			}
 
-			// データ取得
-			const data = e.dataTransfer.getData('text');
-			if (data == null) return;
-
-			try {
-				// パース
-				const obj = JSON.parse(data);
-
-				// (ドライブの)ファイルだったら
-				if (obj.type == 'file') {
-					this.form.file = obj.file;
-				}
-			} catch (e) {
-				// not a json, so noop
+			//#region ドライブのファイル
+			const driveFile = e.dataTransfer.getData('mk_drive_file');
+			if (driveFile != null && driveFile != '') {
+				const file = JSON.parse(driveFile);
+				this.form.file = file;
 			}
+			//#endregion
 		},
 
 		fetchMessages() {
diff --git a/src/web/app/desktop/views/components/drive.file.vue b/src/web/app/desktop/views/components/drive.file.vue
index cc423477e..6390ed351 100644
--- a/src/web/app/desktop/views/components/drive.file.vue
+++ b/src/web/app/desktop/views/components/drive.file.vue
@@ -115,11 +115,7 @@ export default Vue.extend({
 
 		onDragstart(e) {
 			e.dataTransfer.effectAllowed = 'move';
-			e.dataTransfer.setData('text', JSON.stringify({
-				type: 'file',
-				id: this.file.id,
-				file: this.file
-			}));
+			e.dataTransfer.setData('mk_drive_file', JSON.stringify(this.file));
 			this.isDragging = true;
 
 			// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
diff --git a/src/web/app/desktop/views/components/drive.folder.vue b/src/web/app/desktop/views/components/drive.folder.vue
index 4e57d1ca6..d5f4b11ee 100644
--- a/src/web/app/desktop/views/components/drive.folder.vue
+++ b/src/web/app/desktop/views/components/drive.folder.vue
@@ -92,19 +92,22 @@ export default Vue.extend({
 		},
 
 		onDragover(e) {
-			// 自分自身がドラッグされていない場合
-			if (!this.isDragging) {
-				// ドラッグされてきたものがファイルだったら
-				if (e.dataTransfer.effectAllowed === 'all') {
-					e.dataTransfer.dropEffect = 'copy';
-				} else {
-					e.dataTransfer.dropEffect = 'move';
-				}
-			} else {
+			// 自分自身がドラッグされている場合
+			if (this.isDragging) {
 				// 自分自身にはドロップさせない
 				e.dataTransfer.dropEffect = 'none';
+				return;
+			}
+
+			const isFile = e.dataTransfer.items[0].kind == 'file';
+			const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file';
+			const isDriveFolder = e.dataTransfer.types[0] == 'mk_drive_folder';
+
+			if (isFile || isDriveFile || isDriveFolder) {
+				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+			} else {
+				e.dataTransfer.dropEffect = 'none';
 			}
-			return false;
 		},
 
 		onDragenter() {
@@ -123,36 +126,35 @@ export default Vue.extend({
 				Array.from(e.dataTransfer.files).forEach(file => {
 					this.browser.upload(file, this.folder);
 				});
-				return false;
+				return;
 			}
 
-			// データ取得
-			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);
+			//#region ドライブのファイル
+			const driveFile = e.dataTransfer.getData('mk_drive_file');
+			if (driveFile != null && driveFile != '') {
+				const file = JSON.parse(driveFile);
+				this.browser.removeFile(file.id);
 				(this as any).api('drive/files/update', {
-					file_id: file,
+					file_id: file.id,
 					folder_id: this.folder.id
 				});
-			// (ドライブの)フォルダーだったら
-			} else if (obj.type == 'folder') {
-				const folder = obj.id;
+			}
+			//#endregion
+
+			//#region ドライブのフォルダ
+			const driveFolder = e.dataTransfer.getData('mk_drive_folder');
+			if (driveFolder != null && driveFolder != '') {
+				const folder = JSON.parse(driveFolder);
+
 				// 移動先が自分自身ならreject
-				if (folder == this.folder.id) return false;
-				this.browser.removeFolder(folder);
+				if (folder.id == this.folder.id) return;
+
+				this.browser.removeFolder(folder.id);
 				(this as any).api('drive/folders/update', {
-					folder_id: folder,
+					folder_id: folder.id,
 					parent_id: this.folder.id
 				}).then(() => {
-					// something
+					// noop
 				}).catch(err => {
 					switch (err) {
 						case 'detected-circular-definition':
@@ -169,16 +171,12 @@ export default Vue.extend({
 					}
 				});
 			}
-
-			return false;
+			//#endregion
 		},
 
 		onDragstart(e) {
 			e.dataTransfer.effectAllowed = 'move';
-			e.dataTransfer.setData('text', JSON.stringify({
-				type: 'folder',
-				id: this.folder.id
-			}));
+			e.dataTransfer.setData('mk_drive_folder', JSON.stringify(this.folder));
 			this.isDragging = true;
 
 			// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
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 4c5128588..a5677dcb4 100644
--- a/src/web/app/desktop/views/components/drive.nav-folder.vue
+++ b/src/web/app/desktop/views/components/drive.nav-folder.vue
@@ -41,13 +41,17 @@ export default Vue.extend({
 			// このフォルダがルートかつカレントディレクトリならドロップ禁止
 			if (this.folder == null && this.browser.folder == null) {
 				e.dataTransfer.dropEffect = 'none';
-			// ドラッグされてきたものがファイルだったら
-			} else if (e.dataTransfer.effectAllowed == 'all') {
-				e.dataTransfer.dropEffect = 'copy';
-			} else {
-				e.dataTransfer.dropEffect = 'move';
 			}
-			return false;
+
+			const isFile = e.dataTransfer.items[0].kind == 'file';
+			const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file';
+			const isDriveFolder = e.dataTransfer.types[0] == 'mk_drive_folder';
+
+			if (isFile || isDriveFile || isDriveFolder) {
+				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+			} else {
+				e.dataTransfer.dropEffect = 'none';
+			}
 		},
 		onDragenter() {
 			if (this.folder || this.browser.folder) this.draghover = true;
@@ -63,38 +67,34 @@ export default Vue.extend({
 				Array.from(e.dataTransfer.files).forEach(file => {
 					this.browser.upload(file, this.folder);
 				});
-				return false;
-			};
+				return;
+			}
 
-			// データ取得
-			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);
+			//#region ドライブのファイル
+			const driveFile = e.dataTransfer.getData('mk_drive_file');
+			if (driveFile != null && driveFile != '') {
+				const file = JSON.parse(driveFile);
+				this.browser.removeFile(file.id);
 				(this as any).api('drive/files/update', {
-					file_id: file,
+					file_id: file.id,
 					folder_id: this.folder ? this.folder.id : null
 				});
-			// (ドライブの)フォルダーだったら
-			} else if (obj.type == 'folder') {
-				const folder = obj.id;
+			}
+			//#endregion
+
+			//#region ドライブのフォルダ
+			const driveFolder = e.dataTransfer.getData('mk_drive_folder');
+			if (driveFolder != null && driveFolder != '') {
+				const folder = JSON.parse(driveFolder);
 				// 移動先が自分自身ならreject
-				if (this.folder && folder == this.folder.id) return false;
-				this.browser.removeFolder(folder);
+				if (this.folder && folder.id == this.folder.id) return;
+				this.browser.removeFolder(folder.id);
 				(this as any).api('drive/folders/update', {
-					folder_id: folder,
+					folder_id: folder.id,
 					parent_id: this.folder ? this.folder.id : null
 				});
 			}
-
-			return false;
+			//#endregion
 		}
 	}
 });
diff --git a/src/web/app/desktop/views/components/drive.vue b/src/web/app/desktop/views/components/drive.vue
index ffe0c68de..7c4b08d3f 100644
--- a/src/web/app/desktop/views/components/drive.vue
+++ b/src/web/app/desktop/views/components/drive.vue
@@ -235,15 +235,21 @@ export default Vue.extend({
 		},
 
 		onDragover(e): any {
-			// ドラッグ元が自分自身の所有するアイテムかどうか
-			if (!this.isDragSource) {
-				// ドラッグされてきたものがファイルだったら
-				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
-				this.draghover = true;
-			} else {
+			// ドラッグ元が自分自身の所有するアイテムだったら
+			if (this.isDragSource) {
 				// 自分自身にはドロップさせない
 				e.dataTransfer.dropEffect = 'none';
-				return false;
+				return;
+			}
+
+			const isFile = e.dataTransfer.items[0].kind == 'file';
+			const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file';
+			const isDriveFolder = e.dataTransfer.types[0] == 'mk_drive_folder';
+
+			if (isFile || isDriveFile || isDriveFolder) {
+				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+			} else {
+				e.dataTransfer.dropEffect = 'none';
 			}
 		},
 
@@ -263,38 +269,36 @@ export default Vue.extend({
 				Array.from(e.dataTransfer.files).forEach(file => {
 					this.upload(file, this.folder);
 				});
-				return false;
+				return;
 			}
 
-			// データ取得
-			const data = e.dataTransfer.getData('text');
-			if (data == null) return false;
-
-			// パース
-			// TODO: JSONじゃなかったら中断
-			const obj = JSON.parse(data);
-
-			// (ドライブの)ファイルだったら
-			if (obj.type == 'file') {
-				const file = obj.id;
-				if (this.files.some(f => f.id == file)) return false;
-				this.removeFile(file);
+			//#region ドライブのファイル
+			const driveFile = e.dataTransfer.getData('mk_drive_file');
+			if (driveFile != null && driveFile != '') {
+				const file = JSON.parse(driveFile);
+				if (this.files.some(f => f.id == file.id)) return;
+				this.removeFile(file.id);
 				(this as any).api('drive/files/update', {
-					file_id: file,
+					file_id: file.id,
 					folder_id: this.folder ? this.folder.id : null
 				});
-			// (ドライブの)フォルダーだったら
-			} else if (obj.type == 'folder') {
-				const folder = obj.id;
+			}
+			//#endregion
+
+			//#region ドライブのフォルダ
+			const driveFolder = e.dataTransfer.getData('mk_drive_folder');
+			if (driveFolder != null && driveFolder != '') {
+				const folder = JSON.parse(driveFolder);
+
 				// 移動先が自分自身ならreject
-				if (this.folder && folder == this.folder.id) return false;
-				if (this.folders.some(f => f.id == folder)) return false;
-				this.removeFolder(folder);
+				if (this.folder && folder.id == this.folder.id) return false;
+				if (this.folders.some(f => f.id == folder.id)) return false;
+				this.removeFolder(folder.id);
 				(this as any).api('drive/folders/update', {
-					folder_id: folder,
+					folder_id: folder.id,
 					parent_id: this.folder ? this.folder.id : null
 				}).then(() => {
-					// something
+					// noop
 				}).catch(err => {
 					switch (err) {
 						case 'detected-circular-definition':
@@ -311,8 +315,7 @@ export default Vue.extend({
 					}
 				});
 			}
-
-			return false;
+			//#endregion
 		},
 
 		selectLocalFile() {
diff --git a/src/web/app/desktop/views/components/post-form.vue b/src/web/app/desktop/views/components/post-form.vue
index 4d43d98d7..b94b0f853 100644
--- a/src/web/app/desktop/views/components/post-form.vue
+++ b/src/web/app/desktop/views/components/post-form.vue
@@ -1,9 +1,9 @@
 <template>
 <div class="mk-post-form"
-	@dragover.prevent.stop="onDragover"
+	@dragover.stop="onDragover"
 	@dragenter="onDragenter"
 	@dragleave="onDragleave"
-	@drop.prevent.stop="onDrop"
+	@drop.stop="onDrop"
 >
 	<div class="content">
 		<textarea :class="{ with: (files.length != 0 || poll) }"
@@ -159,8 +159,13 @@ export default Vue.extend({
 			});
 		},
 		onDragover(e) {
-			this.draghover = true;
-			e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+			const isFile = e.dataTransfer.items[0].kind == 'file';
+			const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file';
+			if (isFile || isDriveFile) {
+				e.preventDefault();
+				this.draghover = true;
+				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+			}
 		},
 		onDragenter(e) {
 			this.draghover = true;
@@ -173,26 +178,20 @@ export default Vue.extend({
 
 			// ファイルだったら
 			if (e.dataTransfer.files.length > 0) {
+				e.preventDefault();
 				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', this.files);
-				}
-			} catch (e) {
-				// not a json, so noop
+			//#region ドライブのファイル
+			const driveFile = e.dataTransfer.getData('mk_drive_file');
+			if (driveFile != null && driveFile != '') {
+				const file = JSON.parse(driveFile);
+				this.files.push(file);
+				this.$emit('change-attached-media', this.files);
+				e.preventDefault();
 			}
+			//#endregion
 		},
 		post() {
 			this.posting = true;

From 2b69e16b7111d4ba08bcdfe238e1b5ded2ef539c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 27 Feb 2018 06:25:30 +0900
Subject: [PATCH 0543/1250] v3910

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index bf3239abe..39f9f4fc8 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3908",
+	"version": "0.0.3910",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 40ca02c538a4fd950ddce5584d9db060e14d997e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 27 Feb 2018 06:35:16 +0900
Subject: [PATCH 0544/1250] :v:

---
 src/web/app/desktop/views/components/drive.nav-folder.vue | 5 +++++
 src/web/app/desktop/views/components/drive.vue            | 4 +++-
 2 files changed, 8 insertions(+), 1 deletion(-)

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 a5677dcb4..dfbf116bf 100644
--- a/src/web/app/desktop/views/components/drive.nav-folder.vue
+++ b/src/web/app/desktop/views/components/drive.nav-folder.vue
@@ -52,6 +52,8 @@ export default Vue.extend({
 			} else {
 				e.dataTransfer.dropEffect = 'none';
 			}
+
+			return false;
 		},
 		onDragenter() {
 			if (this.folder || this.browser.folder) this.draghover = true;
@@ -102,6 +104,9 @@ export default Vue.extend({
 
 <style lang="stylus" scoped>
 .root.nav-folder
+	> *
+		pointer-events none
+
 	&[data-draghover]
 		background #eee
 
diff --git a/src/web/app/desktop/views/components/drive.vue b/src/web/app/desktop/views/components/drive.vue
index 7c4b08d3f..1d84c2409 100644
--- a/src/web/app/desktop/views/components/drive.vue
+++ b/src/web/app/desktop/views/components/drive.vue
@@ -16,7 +16,7 @@
 		ref="main"
 		@mousedown="onMousedown"
 		@dragover.prevent.stop="onDragover"
-		@dragenter.prevent="onDragenter"
+		@dragenter="onDragenter"
 		@dragleave="onDragleave"
 		@drop.prevent.stop="onDrop"
 		@contextmenu.prevent.stop="onContextmenu"
@@ -251,6 +251,8 @@ export default Vue.extend({
 			} else {
 				e.dataTransfer.dropEffect = 'none';
 			}
+
+			return false;
 		},
 
 		onDragenter(e) {

From f2e62377bdcf5e18ce6d7da346202261d365571a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 27 Feb 2018 06:37:20 +0900
Subject: [PATCH 0545/1250] Fix bug

---
 src/web/app/common/views/widgets/photo-stream.vue   | 2 +-
 src/web/app/mobile/views/pages/user/home.photos.vue | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/web/app/common/views/widgets/photo-stream.vue b/src/web/app/common/views/widgets/photo-stream.vue
index 78864cc8b..baafd4066 100644
--- a/src/web/app/common/views/widgets/photo-stream.vue
+++ b/src/web/app/common/views/widgets/photo-stream.vue
@@ -5,7 +5,7 @@
 
 		<p :class="$style.fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 		<div :class="$style.stream" v-if="!fetching && images.length > 0">
-			<div v-for="image in images" :key="image.id" :class="$style.img" :style="`background-image: url(${image.url}?thumbnail&size=256)`"></div>
+			<div v-for="image in images" :class="$style.img" :style="`background-image: url(${image.url}?thumbnail&size=256)`"></div>
 		</div>
 		<p :class="$style.empty" v-if="!fetching && images.length == 0">%i18n:desktop.tags.mk-photo-stream-home-widget.no-photos%</p>
 	</mk-widget-container>
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 2a6343189..ddbced608 100644
--- a/src/web/app/mobile/views/pages/user/home.photos.vue
+++ b/src/web/app/mobile/views/pages/user/home.photos.vue
@@ -2,7 +2,7 @@
 <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"
+		<a v-for="image in images"
 			class="img"
 			:style="`background-image: url(${image.media.url}?thumbnail&size=256)`"
 			:href="`/${image.post.user.username}/${image.post.id}`"

From 72d20418c90830d698b8b595eb035ba831a42fa1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 27 Feb 2018 12:32:01 +0900
Subject: [PATCH 0546/1250] #1153

---
 src/api/common/text/elements/quote.ts         | 14 +++++
 src/api/common/text/index.ts                  | 11 ++--
 .../app/common/views/components/post-html.ts  | 54 +++++++++++++++----
 .../desktop/views/components/posts.post.vue   |  7 +++
 .../mobile/views/components/posts.post.vue    |  6 +++
 5 files changed, 77 insertions(+), 15 deletions(-)
 create mode 100644 src/api/common/text/elements/quote.ts

diff --git a/src/api/common/text/elements/quote.ts b/src/api/common/text/elements/quote.ts
new file mode 100644
index 000000000..cc8cfffdc
--- /dev/null
+++ b/src/api/common/text/elements/quote.ts
@@ -0,0 +1,14 @@
+/**
+ * Quoted text
+ */
+
+module.exports = text => {
+	const match = text.match(/^"([\s\S]+?)\n"/);
+	if (!match) return null;
+	const quote = match[0];
+	return {
+		type: 'quote',
+		content: quote,
+		quote: quote.substr(1, quote.length - 2).trim(),
+	};
+};
diff --git a/src/api/common/text/index.ts b/src/api/common/text/index.ts
index 47127e864..1e2398dc3 100644
--- a/src/api/common/text/index.ts
+++ b/src/api/common/text/index.ts
@@ -10,6 +10,7 @@ const elements = [
 	require('./elements/hashtag'),
 	require('./elements/code'),
 	require('./elements/inline-code'),
+	require('./elements/quote'),
 	require('./elements/emoji')
 ];
 
@@ -33,12 +34,12 @@ export default (source: string) => {
 	// パース
 	while (source != '') {
 		const parsed = elements.some(el => {
-			let tokens = el(source, i);
-			if (tokens) {
-				if (!Array.isArray(tokens)) {
-					tokens = [tokens];
+			let _tokens = el(source, i);
+			if (_tokens) {
+				if (!Array.isArray(_tokens)) {
+					_tokens = [_tokens];
 				}
-				tokens.forEach(push);
+				_tokens.forEach(push);
 				return true;
 			} else {
 				return false;
diff --git a/src/web/app/common/views/components/post-html.ts b/src/web/app/common/views/components/post-html.ts
index 3676e9e6a..dae118e82 100644
--- a/src/web/app/common/views/components/post-html.ts
+++ b/src/web/app/common/views/components/post-html.ts
@@ -3,6 +3,10 @@ import * as emojilib from 'emojilib';
 import { url } from '../../../config';
 import MkUrl from './url.vue';
 
+const flatten = list => list.reduce(
+	(a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []
+);
+
 export default Vue.component('mk-post-html', {
 	props: {
 		ast: {
@@ -19,20 +23,16 @@ export default Vue.component('mk-post-html', {
 		}
 	},
 	render(createElement) {
-		const els = [].concat.apply([], (this as any).ast.map(token => {
+		const els = flatten((this as any).ast.map(token => {
 			switch (token.type) {
 				case 'text':
 					const text = token.content.replace(/(\r\n|\n|\r)/g, '\n');
 
 					if ((this as any).shouldBreak) {
-						if (text.indexOf('\n') != -1) {
-							const x = text.split('\n')
-								.map(t => [createElement('span', t), createElement('br')]);
-							x[x.length - 1].pop();
-							return x;
-						} else {
-							return createElement('span', text);
-						}
+						const x = text.split('\n')
+							.map(t => t == '' ? [createElement('br')] : [createElement('span', t), createElement('br')]);
+						x[x.length - 1].pop();
+						return x;
 					} else {
 						return createElement('span', text.replace(/\n/g, ' '));
 					}
@@ -91,12 +91,46 @@ export default Vue.component('mk-post-html', {
 				case 'inline-code':
 					return createElement('code', token.html);
 
+				case 'quote':
+					const text2 = token.quote.replace(/(\r\n|\n|\r)/g, '\n');
+
+					if ((this as any).shouldBreak) {
+						const x = text2.split('\n')
+							.map(t => [createElement('span', t), createElement('br')]);
+						x[x.length - 1].pop();
+						return createElement('div', {
+							attrs: {
+								class: 'quote'
+							}
+						}, x);
+					} else {
+						return createElement('span', {
+							attrs: {
+								class: 'quote'
+							}
+						}, text2.replace(/\n/g, ' '));
+					}
+
 				case 'emoji':
 					const emoji = emojilib.lib[token.emoji];
 					return createElement('span', emoji ? emoji.char : token.content);
+
+				default:
+					console.log('unknown ast type:', token.type);
 			}
 		}));
 
-		return createElement('span', els);
+		const _els = [];
+		els.forEach((el, i) => {
+			if (el.tag == 'br') {
+				if (els[i - 1].tag != 'div') {
+					_els.push(el);
+				}
+			} else {
+				_els.push(el);
+			}
+		});
+
+		return createElement('span', _els);
 	}
 });
diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index 647590e25..118884fcd 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -416,6 +416,12 @@ export default Vue.extend({
 					font-size 1.1em
 					color #717171
 
+					>>> .quote
+						margin 8px
+						padding 6px 12px
+						color #aaa
+						border-left solid 3px #eee
+
 					.mk-url-preview
 						margin-top 8px
 
@@ -512,6 +518,7 @@ export default Vue.extend({
 
 <style lang="stylus" module>
 .text
+
 	code
 		padding 4px 8px
 		margin 0 0.5em
diff --git a/src/web/app/mobile/views/components/posts.post.vue b/src/web/app/mobile/views/components/posts.post.vue
index b94d9d16b..b98fadb43 100644
--- a/src/web/app/mobile/views/components/posts.post.vue
+++ b/src/web/app/mobile/views/components/posts.post.vue
@@ -349,6 +349,12 @@ export default Vue.extend({
 					font-size 1.1em
 					color #717171
 
+					>>> .quote
+						margin 8px
+						padding 6px 12px
+						color #aaa
+						border-left solid 3px #eee
+
 					.mk-url-preview
 						margin-top 8px
 

From 8642fb3f29cc4df3595c288533a971d91b5e0719 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 27 Feb 2018 12:32:06 +0900
Subject: [PATCH 0547/1250] :art:

---
 .../common/views/components/connect-failed.troubleshooter.vue   | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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 bede504b5..cadbd36ba 100644
--- a/src/web/app/common/views/components/connect-failed.troubleshooter.vue
+++ b/src/web/app/common/views/components/connect-failed.troubleshooter.vue
@@ -117,7 +117,7 @@ export default Vue.extend({
 
 	> p
 		margin 0
-		padding 0.6em 1.2em
+		padding 0.7em 1.2em
 		font-size 1em
 		color #444
 		border-top solid 1px #eee

From cbb412afcb68198ef9bbac1efbe5c9e827b90e5b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 27 Feb 2018 14:05:01 +0900
Subject: [PATCH 0548/1250] :art:

---
 src/web/app/desktop/views/components/post-detail.vue | 5 ++++-
 src/web/app/desktop/views/components/posts.post.vue  | 5 ++++-
 src/web/app/mobile/views/components/post-detail.vue  | 5 ++++-
 src/web/app/mobile/views/components/posts.post.vue   | 5 ++++-
 4 files changed, 16 insertions(+), 4 deletions(-)

diff --git a/src/web/app/desktop/views/components/post-detail.vue b/src/web/app/desktop/views/components/post-detail.vue
index e6e0ffa02..b41053133 100644
--- a/src/web/app/desktop/views/components/post-detail.vue
+++ b/src/web/app/desktop/views/components/post-detail.vue
@@ -310,9 +310,12 @@ export default Vue.extend({
 				margin-top 8px
 
 			> .tags
+				margin 4px 0 0 0
+
 				> *
+					display inline-block
 					margin 0 8px 0 0
-					padding 0 8px 0 16px
+					padding 2px 8px 2px 16px
 					font-size 90%
 					color #8d969e
 					background #edf0f3
diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index 118884fcd..198110233 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -438,9 +438,12 @@ export default Vue.extend({
 						color #a0bf46
 
 					> .tags
+						margin 4px 0 0 0
+
 						> *
+							display inline-block
 							margin 0 8px 0 0
-							padding 0 8px 0 16px
+							padding 2px 8px 2px 16px
 							font-size 90%
 							color #8d969e
 							background #edf0f3
diff --git a/src/web/app/mobile/views/components/post-detail.vue b/src/web/app/mobile/views/components/post-detail.vue
index ae1c3fdc9..05ee07123 100644
--- a/src/web/app/mobile/views/components/post-detail.vue
+++ b/src/web/app/mobile/views/components/post-detail.vue
@@ -316,9 +316,12 @@ export default Vue.extend({
 					max-width 100%
 
 			> .tags
+				margin 4px 0 0 0
+
 				> *
+					display inline-block
 					margin 0 8px 0 0
-					padding 0 8px 0 16px
+					padding 2px 8px 2px 16px
 					font-size 90%
 					color #8d969e
 					background #edf0f3
diff --git a/src/web/app/mobile/views/components/posts.post.vue b/src/web/app/mobile/views/components/posts.post.vue
index b98fadb43..2862191b3 100644
--- a/src/web/app/mobile/views/components/posts.post.vue
+++ b/src/web/app/mobile/views/components/posts.post.vue
@@ -371,9 +371,12 @@ export default Vue.extend({
 						color #a0bf46
 
 					> .tags
+						margin 4px 0 0 0
+
 						> *
+							display inline-block
 							margin 0 8px 0 0
-							padding 0 8px 0 16px
+							padding 2px 8px 2px 16px
 							font-size 90%
 							color #8d969e
 							background #edf0f3

From 2960d88ea52477a999cd79403522c33030f96e19 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 27 Feb 2018 14:11:18 +0900
Subject: [PATCH 0549/1250] #1152

---
 .../app/common/views/components/autocomplete.vue   | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/src/web/app/common/views/components/autocomplete.vue b/src/web/app/common/views/components/autocomplete.vue
index 04a74e4e2..2ad951b1f 100644
--- a/src/web/app/common/views/components/autocomplete.vue
+++ b/src/web/app/common/views/components/autocomplete.vue
@@ -134,9 +134,21 @@ export default Vue.extend({
 			} else if (this.type == 'emoji') {
 				const matched = [];
 				emjdb.some(x => {
-					if (x.name.indexOf(this.q) > -1 && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
+					if (x.name.indexOf(this.q) == 0 && !x.alias && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
 					return matched.length == 30;
 				});
+				if (matched.length < 30) {
+					emjdb.some(x => {
+						if (x.name.indexOf(this.q) == 0 && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
+						return matched.length == 30;
+					});
+				}
+				if (matched.length < 30) {
+					emjdb.some(x => {
+						if (x.name.indexOf(this.q) > -1 && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
+						return matched.length == 30;
+					});
+				}
 				this.emojis = matched;
 			}
 		},

From cee5ff23c8e95a453425e935e2db020d54686c8f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 27 Feb 2018 14:11:43 +0900
Subject: [PATCH 0550/1250] v3917

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 39f9f4fc8..755e5c8eb 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3910",
+	"version": "0.0.3917",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 934bf176bc8255f8692cc1e0686d787a2d820fa1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Feb 2018 00:11:28 +0900
Subject: [PATCH 0551/1250] :v:

---
 src/web/app/auth/script.ts             |  25 +++
 src/web/app/auth/views/index.vue       |   8 +-
 src/web/app/dev/script.ts              |  18 +-
 src/web/app/dev/tags/index.ts          |   5 -
 src/web/app/dev/tags/new-app-form.tag  | 252 -------------------------
 src/web/app/dev/tags/pages/app.tag     |  32 ----
 src/web/app/dev/tags/pages/apps.tag    |  33 ----
 src/web/app/dev/tags/pages/index.tag   |   6 -
 src/web/app/dev/tags/pages/new-app.tag |  42 -----
 src/web/app/dev/views/app.vue          |  43 +++++
 src/web/app/dev/views/apps.vue         |  24 +++
 src/web/app/dev/views/index.vue        |  10 +
 src/web/app/dev/views/new-app.vue      | 145 ++++++++++++++
 src/web/app/init.ts                    |   2 +-
 webpack/webpack.config.ts              |   4 +-
 15 files changed, 272 insertions(+), 377 deletions(-)
 create mode 100644 src/web/app/auth/script.ts
 delete mode 100644 src/web/app/dev/tags/index.ts
 delete mode 100644 src/web/app/dev/tags/new-app-form.tag
 delete mode 100644 src/web/app/dev/tags/pages/app.tag
 delete mode 100644 src/web/app/dev/tags/pages/apps.tag
 delete mode 100644 src/web/app/dev/tags/pages/index.tag
 delete mode 100644 src/web/app/dev/tags/pages/new-app.tag
 create mode 100644 src/web/app/dev/views/app.vue
 create mode 100644 src/web/app/dev/views/apps.vue
 create mode 100644 src/web/app/dev/views/index.vue
 create mode 100644 src/web/app/dev/views/new-app.vue

diff --git a/src/web/app/auth/script.ts b/src/web/app/auth/script.ts
new file mode 100644
index 000000000..31c758ebc
--- /dev/null
+++ b/src/web/app/auth/script.ts
@@ -0,0 +1,25 @@
+/**
+ * Authorize Form
+ */
+
+// Style
+import './style.styl';
+
+import init from '../init';
+
+import Index from './views/index.vue';
+
+/**
+ * init
+ */
+init(async (launch) => {
+	document.title = 'Misskey | アプリの連携';
+
+	// Launch the app
+	const [app] = launch();
+
+	// Routing
+	app.$router.addRoutes([
+		{ path: '/:token', component: Index },
+	]);
+});
diff --git a/src/web/app/auth/views/index.vue b/src/web/app/auth/views/index.vue
index 1e372c0bd..17e5cc610 100644
--- a/src/web/app/auth/views/index.vue
+++ b/src/web/app/auth/views/index.vue
@@ -42,10 +42,14 @@ export default Vue.extend({
 		return {
 			state: null,
 			session: null,
-			fetching: true,
-			token: window.location.href.split('/').pop()
+			fetching: true
 		};
 	},
+	computed: {
+		token(): string {
+			return this.$route.params.token;
+		}
+	},
 	mounted() {
 		if (!this.$root.$data.os.isSignedIn) return;
 
diff --git a/src/web/app/dev/script.ts b/src/web/app/dev/script.ts
index bb4341119..757bfca49 100644
--- a/src/web/app/dev/script.ts
+++ b/src/web/app/dev/script.ts
@@ -5,11 +5,25 @@
 // Style
 import './style.styl';
 
-require('./tags');
 import init from '../init';
 
+import Index from './views/index.vue';
+import Apps from './views/apps.vue';
+import AppNew from './views/new-app.vue';
+import App from './views/app.vue';
+
 /**
  * init
  */
-init(() => {
+init(launch => {
+	// Launch the app
+	const [app] = launch();
+
+	// Routing
+	app.$router.addRoutes([
+		{ path: '/', component: Index },
+		{ path: '/app', component: Apps },
+		{ path: '/app/new', component: AppNew },
+		{ path: '/app/:id', component: App },
+	]);
 });
diff --git a/src/web/app/dev/tags/index.ts b/src/web/app/dev/tags/index.ts
deleted file mode 100644
index 1e0c73697..000000000
--- a/src/web/app/dev/tags/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-require('./pages/index.tag');
-require('./pages/apps.tag');
-require('./pages/app.tag');
-require('./pages/new-app.tag');
-require('./new-app-form.tag');
diff --git a/src/web/app/dev/tags/new-app-form.tag b/src/web/app/dev/tags/new-app-form.tag
deleted file mode 100644
index cf3c44007..000000000
--- a/src/web/app/dev/tags/new-app-form.tag
+++ /dev/null
@@ -1,252 +0,0 @@
-<mk-new-app-form>
-	<form onsubmit={ onsubmit } autocomplete="off">
-		<section class="name">
-			<label>
-				<p class="caption">アプリケーション名</p>
-				<input ref="name" type="text" placeholder="ex) Misskey for iOS" autocomplete="off" required="required"/>
-			</label>
-		</section>
-		<section class="nid">
-			<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" 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">
-			<label>
-				<p class="caption">アプリの概要</p>
-				<textarea ref="description" placeholder="ex) Misskey iOSクライアント。" autocomplete="off" required="required"></textarea>
-			</label>
-		</section>
-		<section class="callback">
-			<label>
-				<p class="caption">コールバックURL (オプション)</p>
-				<input ref="cb" type="url" placeholder="ex) https://your.app.example.com/callback.php" autocomplete="off"/>
-			</label>
-		</section>
-		<section class="permission">
-			<p class="caption">権限</p>
-			<div ref="permission">
-				<label>
-					<input type="checkbox" value="account-read"/>
-					<p>アカウントの情報を見る。</p>
-				</label>
-				<label>
-					<input type="checkbox" value="account-write"/>
-					<p>アカウントの情報を操作する。</p>
-				</label>
-				<label>
-					<input type="checkbox" value="post-write"/>
-					<p>投稿する。</p>
-				</label>
-				<label>
-					<input type="checkbox" value="reaction-write"/>
-					<p>リアクションしたりリアクションをキャンセルする。</p>
-				</label>
-				<label>
-					<input type="checkbox" value="following-write"/>
-					<p>フォローしたりフォロー解除する。</p>
-				</label>
-				<label>
-					<input type="checkbox" value="drive-read"/>
-					<p>ドライブを見る。</p>
-				</label>
-				<label>
-					<input type="checkbox" value="drive-write"/>
-					<p>ドライブを操作する。</p>
-				</label>
-				<label>
-					<input type="checkbox" value="notification-read"/>
-					<p>通知を見る。</p>
-				</label>
-				<label>
-					<input type="checkbox" value="notification-write"/>
-					<p>通知を操作する。</p>
-				</label>
-			</div>
-			<p>%fa:exclamation-triangle%アプリ作成後も変更できますが、新たな権限を付与する場合、その時点で関連付けられているユーザーキーはすべて無効になります。</p>
-		</section>
-		<button @click="onsubmit">アプリ作成</button>
-	</form>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			overflow hidden
-
-			> form
-
-				section
-					display block
-					margin 16px 0
-
-					.caption
-						margin 0 0 4px 0
-						color #616161
-						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
-
-				section.permission
-					div
-						padding 8px 0
-						max-height 160px
-						overflow auto
-						background #fff
-						border solid 1px #cecece
-						border-radius 4px
-
-					label
-						display block
-						padding 0 12px
-						line-height 32px
-						cursor pointer
-
-						&:hover
-							> p
-								color #999
-
-							[type='checkbox']:checked + p
-								color #000
-
-						[type='checkbox']
-							margin-right 4px
-
-						[type='checkbox']:checked + p
-							color #111
-
-						> p
-							display inline
-							color #aaa
-							user-select none
-
-					> p:last-child
-						margin 6px
-						font-size 0.8em
-						color #999
-
-						> [data-fa]
-							margin-right 4px
-
-				[type=text]
-				[type=url]
-				textarea
-					user-select text
-					display inline-block
-					cursor auto
-					padding 8px 12px
-					margin 0
-					width 100%
-					font-size 1em
-					color #333
-					background #fff
-					outline none
-					border solid 1px #cecece
-					border-radius 4px
-
-					&:hover
-						border-color #bbb
-
-					&:focus
-						border-color $theme-color
-
-					&:disabled
-						opacity 0.5
-
-				> button
-					margin 20px 0 32px 0
-					width 100%
-					font-size 1em
-					color #111
-					border-radius 3px
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.nidState = null;
-
-		this.onChangeNid = () => {
-			const nid = this.$refs.nid.value;
-
-			if (nid == '') {
-				this.update({
-					nidState: null
-				});
-				return;
-			}
-
-			const err =
-				!nid.match(/^[a-zA-Z0-9\-]+$/) ? 'invalid-format' :
-				nid.length < 3                 ? 'min-range' :
-				nid.length > 30                ? 'max-range' :
-				null;
-
-			if (err) {
-				this.update({
-					nidState: err
-				});
-				return;
-			}
-
-			this.update({
-				nidState: 'wait'
-			});
-
-			this.$root.$data.os.api('app/name_id/available', {
-				name_id: nid
-			}).then(result => {
-				this.update({
-					nidState: result.available ? 'ok' : 'unavailable'
-				});
-			}).catch(err => {
-				this.update({
-					nidState: 'error'
-				});
-			});
-		};
-
-		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 permission = [];
-
-			this.$refs.permission.querySelectorAll('input').forEach(el => {
-				if (el.checked) permission.push(el.value);
-			});
-
-			const locker = document.body.appendChild(document.createElement('mk-locker'));
-
-			this.$root.$data.os.api('app/create', {
-				name: name,
-				name_id: nid,
-				description: description,
-				callback_url: cb,
-				permission: permission
-			}).then(() => {
-				location.href = '/apps';
-			}).catch(() => {
-				alert('アプリの作成に失敗しました。再度お試しください。');
-				locker.parentNode.removeChild(locker);
-			});
-		};
-	</script>
-</mk-new-app-form>
diff --git a/src/web/app/dev/tags/pages/app.tag b/src/web/app/dev/tags/pages/app.tag
deleted file mode 100644
index 982549ed2..000000000
--- a/src/web/app/dev/tags/pages/app.tag
+++ /dev/null
@@ -1,32 +0,0 @@
-<mk-app-page>
-	<p v-if="fetching">読み込み中</p>
-	<main v-if="!fetching">
-		<header>
-			<h1>{ app.name }</h1>
-		</header>
-		<div class="body">
-			<p>App Secret</p>
-			<input value={ app.secret } readonly="readonly"/>
-		</div>
-	</main>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.fetching = true;
-
-		this.on('mount', () => {
-			this.$root.$data.os.api('app/show', {
-				app_id: this.opts.app
-			}).then(app => {
-				this.update({
-					fetching: false,
-					app: app
-				});
-			});
-		});
-	</script>
-</mk-app-page>
diff --git a/src/web/app/dev/tags/pages/apps.tag b/src/web/app/dev/tags/pages/apps.tag
deleted file mode 100644
index 6ae6031e6..000000000
--- a/src/web/app/dev/tags/pages/apps.tag
+++ /dev/null
@@ -1,33 +0,0 @@
-<mk-apps-page>
-	<h1>アプリを管理</h1><a href="/app/new">アプリ作成</a>
-	<div class="apps">
-		<p v-if="fetching">読み込み中</p>
-		<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>
-		</template>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.fetching = true;
-
-		this.on('mount', () => {
-			this.$root.$data.os.api('my/apps').then(apps => {
-				this.fetching = false
-				this.apps = apps
-				this.update({
-					fetching: false,
-					apps: apps
-				});
-			});
-		});
-	</script>
-</mk-apps-page>
diff --git a/src/web/app/dev/tags/pages/index.tag b/src/web/app/dev/tags/pages/index.tag
deleted file mode 100644
index ca270b377..000000000
--- a/src/web/app/dev/tags/pages/index.tag
+++ /dev/null
@@ -1,6 +0,0 @@
-<mk-index><a href="/apps">アプリ</a>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-	</style>
-</mk-index>
diff --git a/src/web/app/dev/tags/pages/new-app.tag b/src/web/app/dev/tags/pages/new-app.tag
deleted file mode 100644
index 26185f278..000000000
--- a/src/web/app/dev/tags/pages/new-app.tag
+++ /dev/null
@@ -1,42 +0,0 @@
-<mk-new-app-page>
-	<main>
-		<header>
-			<h1>新しいアプリを作成</h1>
-			<p>MisskeyのAPIを利用したアプリケーションを作成できます。</p>
-		</header>
-		<mk-new-app-form/>
-	</main>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			padding 64px 0
-
-			> main
-				width 100%
-				max-width 700px
-				margin 0 auto
-
-				> header
-					margin 0 0 16px 0
-					padding 0 0 16px 0
-					border-bottom solid 1px #282827
-
-					> h1
-						margin 0 0 12px 0
-						padding 0
-						line-height 32px
-						font-size 32px
-						font-weight normal
-						color #000
-
-					> p
-						margin 0
-						line-height 16px
-						color #9a9894
-
-
-
-
-
-	</style>
-</mk-new-app-page>
diff --git a/src/web/app/dev/views/app.vue b/src/web/app/dev/views/app.vue
new file mode 100644
index 000000000..9eddabbec
--- /dev/null
+++ b/src/web/app/dev/views/app.vue
@@ -0,0 +1,43 @@
+<template>
+<div>
+	<p v-if="fetching">読み込み中</p>
+	<main v-if="!fetching">
+		<header>
+			<h1>{{ app.name }}</h1>
+		</header>
+		<div class="body">
+			<p>App Secret</p>
+			<input :value="app.secret" readonly/>
+		</div>
+	</main>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	data() {
+		return {
+			fetching: true,
+			app: null
+		};
+	},
+	watch: {
+		$route: 'fetch'
+	},
+	mounted() {
+		this.fetch();
+	},
+	methods: {
+		fetch() {
+			this.fetching = true;
+			(this as any).api('app/show', {
+				app_id: this.$route.params.id
+			}).then(app => {
+				this.app = app;
+				this.fetching = false;
+			});
+		}
+	}
+});
+</script>
diff --git a/src/web/app/dev/views/apps.vue b/src/web/app/dev/views/apps.vue
new file mode 100644
index 000000000..e8adbea2d
--- /dev/null
+++ b/src/web/app/dev/views/apps.vue
@@ -0,0 +1,24 @@
+<template>
+<div>
+	<h1>アプリを管理</h1>
+	<router-link to="/app/new">アプリ作成</router-link>
+	<div class="apps">
+		<p v-if="fetching">読み込み中</p>
+		<template v-if="!fetching">
+			<p v-if="apps.length == 0">アプリなし</p>
+			<ul v-else>
+				<li v-for="app in apps" :key="app.id">
+					<router-link :to="`/app/${app.id}`">
+						<p class="name">{{ app.name }}</p>
+					</router-link>
+				</li>
+			</ul>
+		</template>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend();
+</script>
diff --git a/src/web/app/dev/views/index.vue b/src/web/app/dev/views/index.vue
new file mode 100644
index 000000000..a8429a56d
--- /dev/null
+++ b/src/web/app/dev/views/index.vue
@@ -0,0 +1,10 @@
+<template>
+<div>
+	<router-link to="/app">アプリ</router-link>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend();
+</script>
diff --git a/src/web/app/dev/views/new-app.vue b/src/web/app/dev/views/new-app.vue
new file mode 100644
index 000000000..f2e5659a4
--- /dev/null
+++ b/src/web/app/dev/views/new-app.vue
@@ -0,0 +1,145 @@
+<template>
+<div>
+	<form @submit="onSubmit" autocomplete="off">
+		<section class="name">
+			<label>
+				<p class="caption">アプリケーション名</p>
+				<input v-model="name" type="text" placeholder="ex) Misskey for iOS" autocomplete="off" required/>
+			</label>
+		</section>
+		<section class="nid">
+			<label>
+				<p class="caption">Named ID</p>
+				<input v-model="nid" type="text" pattern="^[a-zA-Z0-9-]{3,30}$" placeholder="ex) misskey-for-ios" autocomplete="off" required/>
+				<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">
+			<label>
+				<p class="caption">アプリの概要</p>
+				<textarea v-model="description" placeholder="ex) Misskey iOSクライアント。" autocomplete="off" required></textarea>
+			</label>
+		</section>
+		<section class="callback">
+			<label>
+				<p class="caption">コールバックURL (オプション)</p>
+				<input v-model="cb" type="url" placeholder="ex) https://your.app.example.com/callback.php" autocomplete="off"/>
+			</label>
+		</section>
+		<section class="permission">
+			<p class="caption">権限</p>
+			<div ref="permission">
+				<label>
+					<input type="checkbox" value="account-read"/>
+					<p>アカウントの情報を見る。</p>
+				</label>
+				<label>
+					<input type="checkbox" value="account-write"/>
+					<p>アカウントの情報を操作する。</p>
+				</label>
+				<label>
+					<input type="checkbox" value="post-write"/>
+					<p>投稿する。</p>
+				</label>
+				<label>
+					<input type="checkbox" value="reaction-write"/>
+					<p>リアクションしたりリアクションをキャンセルする。</p>
+				</label>
+				<label>
+					<input type="checkbox" value="following-write"/>
+					<p>フォローしたりフォロー解除する。</p>
+				</label>
+				<label>
+					<input type="checkbox" value="drive-read"/>
+					<p>ドライブを見る。</p>
+				</label>
+				<label>
+					<input type="checkbox" value="drive-write"/>
+					<p>ドライブを操作する。</p>
+				</label>
+				<label>
+					<input type="checkbox" value="notification-read"/>
+					<p>通知を見る。</p>
+				</label>
+				<label>
+					<input type="checkbox" value="notification-write"/>
+					<p>通知を操作する。</p>
+				</label>
+			</div>
+			<p>%fa:exclamation-triangle%アプリ作成後も変更できますが、新たな権限を付与する場合、その時点で関連付けられているユーザーキーはすべて無効になります。</p>
+		</section>
+		<button type="submit">アプリ作成</button>
+	</form>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	data() {
+		return {
+			name: '',
+			nid: '',
+			description: '',
+			cb: '',
+			nidState: null
+		};
+	},
+	watch: {
+		nid() {
+			if (this.nid == null || this.nid == '') {
+				this.nidState = null;
+				return;
+			}
+
+			const err =
+				!this.nid.match(/^[a-zA-Z0-9\-]+$/) ? 'invalid-format' :
+				this.nid.length < 3                 ? 'min-range' :
+				this.nid.length > 30                ? 'max-range' :
+				null;
+
+			if (err) {
+				this.nidState = err;
+				return;
+			}
+
+			this.nidState = 'wait';
+
+			(this as any).api('app/name_id/available', {
+				name_id: this.nid
+			}).then(result => {
+				this.nidState = result.available ? 'ok' : 'unavailable';
+			}).catch(err => {
+				this.nidState = 'error';
+			});
+		}
+	},
+	methods: {
+		onSubmit() {
+			const permission = [];
+
+			(this.$refs.permission as any).querySelectorAll('input').forEach(el => {
+				if (el.checked) permission.push(el.value);
+			});
+
+			(this as any).api('app/create', {
+				name: this.name,
+				name_id: this.nid,
+				description: this.description,
+				callback_url: this.cb,
+				permission: permission
+			}).then(() => {
+				location.href = '/apps';
+			}).catch(() => {
+				alert('アプリの作成に失敗しました。再度お試しください。');
+			});
+		}
+	}
+});
+</script>
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index 5c0b4c2d3..2e90d62d7 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -77,7 +77,7 @@ if (localStorage.getItem('should-refresh') == 'true') {
 }
 
 // MiOSを初期化してコールバックする
-export default (callback: (launch: (api: (os: MiOS) => API) => [Vue, MiOS]) => void, sw = false) => {
+export default (callback: (launch: (api?: (os: MiOS) => API) => [Vue, MiOS]) => void, sw = false) => {
 	const os = new MiOS(sw);
 
 	os.init(() => {
diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts
index 69e05dc8c..775ecbd34 100644
--- a/webpack/webpack.config.ts
+++ b/webpack/webpack.config.ts
@@ -38,8 +38,8 @@ module.exports = Object.keys(langs).map(lang => {
 		//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',
+		dev: './src/web/app/dev/script.ts',
+		auth: './src/web/app/auth/script.ts',
 		sw: './src/web/app/sw.js'
 	};
 

From cfe240aaa6d8f2e5706df08a8e3333d5eeeec63a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Feb 2018 00:30:36 +0900
Subject: [PATCH 0552/1250] oops

---
 src/web/app/dev/views/apps.vue | 15 ++++++++++++++-
 src/web/app/init.ts            |  4 ++--
 2 files changed, 16 insertions(+), 3 deletions(-)

diff --git a/src/web/app/dev/views/apps.vue b/src/web/app/dev/views/apps.vue
index e8adbea2d..42b4abc9b 100644
--- a/src/web/app/dev/views/apps.vue
+++ b/src/web/app/dev/views/apps.vue
@@ -20,5 +20,18 @@
 
 <script lang="ts">
 import Vue from 'vue';
-export default Vue.extend();
+export default Vue.extend({
+	data() {
+		return {
+			fetching: true,
+			apps: []
+		};
+	},
+	mounted() {
+		(this as any).api('my/apps').then(apps => {
+			this.apps = apps;
+			this.fetching = false;
+		});
+	}
+});
 </script>
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index 2e90d62d7..f9855fd5c 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -84,8 +84,8 @@ export default (callback: (launch: (api?: (os: MiOS) => API) => [Vue, MiOS]) =>
 		// アプリ基底要素マウント
 		document.body.innerHTML = '<div id="app"></div>';
 
-		const launch = (api: (os: MiOS) => API) => {
-			os.apis = api(os);
+		const launch = (api?: (os: MiOS) => API) => {
+			os.apis = api ? api(os) : null;
 
 			Vue.mixin({
 				data() {

From 674ebd3af571c685b307225a5236d1ae5a90209c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Feb 2018 00:34:57 +0900
Subject: [PATCH 0553/1250] Fix #1156

---
 src/web/app/mobile/views/components/drive.file-detail.vue | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

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 9a47eeb12..e41ebbb45 100644
--- a/src/web/app/mobile/views/components/drive.file-detail.vue
+++ b/src/web/app/mobile/views/components/drive.file-detail.vue
@@ -6,7 +6,7 @@
 			:alt="file.name"
 			:title="file.name"
 			@load="onImageLoaded"
-			:style="`background-color:rgb(${ file.properties.average_color.join(',') })`">
+			:style="style">
 		<template v-if="kind != 'image'">%fa:file%</template>
 		<footer v-if="kind == 'image' && file.properties && file.properties.width && file.properties.height">
 			<span class="size">
@@ -84,6 +84,11 @@ export default Vue.extend({
 		},
 		kind(): string {
 			return this.file.type.split('/')[0];
+		},
+		style(): any {
+			return this.file.properties.average_color ? {
+				'background-color': `rgb(${ this.file.properties.average_color.join(',') })`
+			} : {};
 		}
 	},
 	methods: {

From d739620428effd306165f78ef0abacc940def7aa Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Feb 2018 00:35:18 +0900
Subject: [PATCH 0554/1250] v3921

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 755e5c8eb..689fd2cc6 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3917",
+	"version": "0.0.3921",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 0f0b61160d24572ac9c7784bc7b033c05e1ab08e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Feb 2018 00:38:00 +0900
Subject: [PATCH 0555/1250] Fix bug

---
 src/web/app/desktop/views/components/posts.post.vue       | 4 ++--
 src/web/app/desktop/views/components/sub-post-content.vue | 4 ++--
 src/web/app/mobile/views/components/posts.post.vue        | 4 ++--
 src/web/app/mobile/views/components/sub-post-content.vue  | 4 ++--
 4 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index 198110233..458e244b3 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -41,7 +41,7 @@
 					<div class="tags" v-if="p.tags && p.tags.length > 0">
 						<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
 					</div>
-					<a class="quote" v-if="p.repost">RP:</a>
+					<a class="rp" 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">
@@ -432,7 +432,7 @@ export default Vue.extend({
 						margin-right 8px
 						color #717171
 
-					> .quote
+					> .rp
 						margin-left 4px
 						font-style oblique
 						color #a0bf46
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 f048eb4f0..7f4c3b4f6 100644
--- a/src/web/app/desktop/views/components/sub-post-content.vue
+++ b/src/web/app/desktop/views/components/sub-post-content.vue
@@ -3,7 +3,7 @@
 	<div class="body">
 		<a class="reply" v-if="post.reply_id">%fa:reply%</a>
 		<mk-post-html :ast="post.ast" :i="os.i"/>
-		<a class="quote" v-if="post.repost_id" :href="`/post:${post.repost_id}`">RP: ...</a>
+		<a class="rp" 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">
@@ -45,7 +45,7 @@ export default Vue.extend({
 			margin-right 6px
 			color #717171
 
-		> .quote
+		> .rp
 			margin-left 4px
 			font-style oblique
 			color #a0bf46
diff --git a/src/web/app/mobile/views/components/posts.post.vue b/src/web/app/mobile/views/components/posts.post.vue
index 2862191b3..3038cdb0e 100644
--- a/src/web/app/mobile/views/components/posts.post.vue
+++ b/src/web/app/mobile/views/components/posts.post.vue
@@ -39,7 +39,7 @@
 						<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
 					</div>
 					<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
-					<a class="quote" v-if="p.repost != null">RP:</a>
+					<a class="rp" v-if="p.repost != null">RP:</a>
 				</div>
 				<div class="media" v-if="p.media">
 					<mk-images :images="p.media"/>
@@ -365,7 +365,7 @@ export default Vue.extend({
 						margin-right 8px
 						color #717171
 
-					> .quote
+					> .rp
 						margin-left 4px
 						font-style oblique
 						color #a0bf46
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 429e76005..b97f08255 100644
--- a/src/web/app/mobile/views/components/sub-post-content.vue
+++ b/src/web/app/mobile/views/components/sub-post-content.vue
@@ -3,7 +3,7 @@
 	<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="os.i"/>
-		<a class="quote" v-if="post.repost_id">RP: ...</a>
+		<a class="rp" v-if="post.repost_id">RP: ...</a>
 	</div>
 	<details v-if="post.media">
 		<summary>({{ post.media.length }}個のメディア)</summary>
@@ -32,7 +32,7 @@ export default Vue.extend({
 			margin-right 6px
 			color #717171
 
-		> .quote
+		> .rp
 			margin-left 4px
 			font-style oblique
 			color #a0bf46

From 174802f4c92a6e8f0fab191cd36400126ce9c2ea Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Feb 2018 01:13:42 +0900
Subject: [PATCH 0556/1250] Fix bug

---
 src/web/app/mobile/api/post.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/web/app/mobile/api/post.ts b/src/web/app/mobile/api/post.ts
index 09bb9dc06..9b78ce10c 100644
--- a/src/web/app/mobile/api/post.ts
+++ b/src/web/app/mobile/api/post.ts
@@ -30,6 +30,7 @@ export default (os) => (opts) => {
 		}
 
 		const vm = new PostForm({
+			parent: os.app,
 			propsData: {
 				reply: o.reply
 			}

From 0423f7619aed3638c5ea428e25f2195185de7680 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Feb 2018 05:43:14 +0900
Subject: [PATCH 0557/1250] :v:

---
 package.json                      |   1 +
 src/web/app/dev/script.ts         |  12 ++-
 src/web/app/dev/style.styl        |   5 ++
 src/web/app/dev/views/app.vue     |  18 ++---
 src/web/app/dev/views/apps.vue    |  34 ++++-----
 src/web/app/dev/views/index.vue   |   6 +-
 src/web/app/dev/views/new-app.vue | 118 ++++++++++--------------------
 src/web/app/dev/views/ui.vue      |  20 +++++
 8 files changed, 103 insertions(+), 111 deletions(-)
 create mode 100644 src/web/app/dev/views/ui.vue

diff --git a/package.json b/package.json
index 689fd2cc6..0f651147b 100644
--- a/package.json
+++ b/package.json
@@ -82,6 +82,7 @@
 		"autwh": "0.0.1",
 		"bcryptjs": "2.4.3",
 		"body-parser": "1.18.2",
+		"bootstrap-vue": "^2.0.0-rc.1",
 		"cache-loader": "1.2.0",
 		"cafy": "3.2.1",
 		"chai": "4.1.2",
diff --git a/src/web/app/dev/script.ts b/src/web/app/dev/script.ts
index 757bfca49..2f4a16fab 100644
--- a/src/web/app/dev/script.ts
+++ b/src/web/app/dev/script.ts
@@ -2,6 +2,11 @@
  * Developer Center
  */
 
+import Vue from 'vue';
+import BootstrapVue from 'bootstrap-vue';
+import 'bootstrap/dist/css/bootstrap.css';
+import 'bootstrap-vue/dist/bootstrap-vue.css';
+
 // Style
 import './style.styl';
 
@@ -11,6 +16,11 @@ import Index from './views/index.vue';
 import Apps from './views/apps.vue';
 import AppNew from './views/new-app.vue';
 import App from './views/app.vue';
+import ui from './views/ui.vue';
+
+Vue.use(BootstrapVue);
+
+Vue.component('mk-ui', ui);
 
 /**
  * init
@@ -22,7 +32,7 @@ init(launch => {
 	// Routing
 	app.$router.addRoutes([
 		{ path: '/', component: Index },
-		{ path: '/app', component: Apps },
+		{ path: '/apps', component: Apps },
 		{ path: '/app/new', component: AppNew },
 		{ path: '/app/:id', component: App },
 	]);
diff --git a/src/web/app/dev/style.styl b/src/web/app/dev/style.styl
index cdbcb0e26..e635897b1 100644
--- a/src/web/app/dev/style.styl
+++ b/src/web/app/dev/style.styl
@@ -1,5 +1,10 @@
 @import "../app"
 @import "../reset"
 
+// Bootstrapのデザインを崩すので:
+*
+	position initial
+	background-clip initial !important
+
 html
 	background-color #fff
diff --git a/src/web/app/dev/views/app.vue b/src/web/app/dev/views/app.vue
index 9eddabbec..2c2a3c83c 100644
--- a/src/web/app/dev/views/app.vue
+++ b/src/web/app/dev/views/app.vue
@@ -1,16 +1,12 @@
 <template>
-<div>
+<mk-ui>
 	<p v-if="fetching">読み込み中</p>
-	<main v-if="!fetching">
-		<header>
-			<h1>{{ app.name }}</h1>
-		</header>
-		<div class="body">
-			<p>App Secret</p>
-			<input :value="app.secret" readonly/>
-		</div>
-	</main>
-</div>
+	<b-card v-if="!fetching" :header="app.name">
+		<b-form-group label="App Secret">
+			<b-input :value="app.secret" readonly/>
+		</b-form-group>
+	</b-card>
+</mk-ui>
 </template>
 
 <script lang="ts">
diff --git a/src/web/app/dev/views/apps.vue b/src/web/app/dev/views/apps.vue
index 42b4abc9b..7e0b107a3 100644
--- a/src/web/app/dev/views/apps.vue
+++ b/src/web/app/dev/views/apps.vue
@@ -1,21 +1,21 @@
 <template>
-<div>
-	<h1>アプリを管理</h1>
-	<router-link to="/app/new">アプリ作成</router-link>
-	<div class="apps">
-		<p v-if="fetching">読み込み中</p>
-		<template v-if="!fetching">
-			<p v-if="apps.length == 0">アプリなし</p>
-			<ul v-else>
-				<li v-for="app in apps" :key="app.id">
-					<router-link :to="`/app/${app.id}`">
-						<p class="name">{{ app.name }}</p>
-					</router-link>
-				</li>
-			</ul>
-		</template>
-	</div>
-</div>
+<mk-ui>
+	<b-card header="アプリを管理">
+		<b-button to="/app/new" variant="primary">アプリ作成</b-button>
+		<hr>
+		<div class="apps">
+			<p v-if="fetching">読み込み中</p>
+			<template v-if="!fetching">
+				<b-alert v-if="apps.length == 0">アプリなし</b-alert>
+				<b-list-group v-else>
+					<b-list-group-item v-for="app in apps" :key="app.id" :to="`/app/${app.id}`">
+						{{ app.name }}
+					</b-list-group-item>
+				</b-list-group>
+			</template>
+		</div>
+	</b-card>
+</mk-ui>
 </template>
 
 <script lang="ts">
diff --git a/src/web/app/dev/views/index.vue b/src/web/app/dev/views/index.vue
index a8429a56d..3f572b390 100644
--- a/src/web/app/dev/views/index.vue
+++ b/src/web/app/dev/views/index.vue
@@ -1,7 +1,7 @@
 <template>
-<div>
-	<router-link to="/app">アプリ</router-link>
-</div>
+<mk-ui>
+	<b-button to="/apps" variant="primary">アプリの管理</b-button>
+</mk-ui>
 </template>
 
 <script lang="ts">
diff --git a/src/web/app/dev/views/new-app.vue b/src/web/app/dev/views/new-app.vue
index f2e5659a4..344e8468f 100644
--- a/src/web/app/dev/views/new-app.vue
+++ b/src/web/app/dev/views/new-app.vue
@@ -1,16 +1,12 @@
 <template>
-<div>
-	<form @submit="onSubmit" autocomplete="off">
-		<section class="name">
-			<label>
-				<p class="caption">アプリケーション名</p>
-				<input v-model="name" type="text" placeholder="ex) Misskey for iOS" autocomplete="off" required/>
-			</label>
-		</section>
-		<section class="nid">
-			<label>
-				<p class="caption">Named ID</p>
-				<input v-model="nid" type="text" pattern="^[a-zA-Z0-9-]{3,30}$" placeholder="ex) misskey-for-ios" autocomplete="off" required/>
+<mk-ui>
+	<b-card header="アプリケーションの作成">
+		<b-form @submit.prevent="onSubmit" autocomplete="off">
+			<b-form-group label="アプリケーション名" description="あなたのアプリの名称。">
+				<b-form-input v-model="name" type="text" placeholder="ex) Misskey for iOS" autocomplete="off" required/>
+			</b-form-group>
+			<b-form-group label="ID" description="あなたのアプリのID。">
+				<b-input v-model="nid" type="text" pattern="^[a-zA-Z0-9-]{3,30}$" placeholder="ex) misskey-for-ios" autocomplete="off" required/>
 				<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>
@@ -18,65 +14,34 @@
 				<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">
-			<label>
-				<p class="caption">アプリの概要</p>
-				<textarea v-model="description" placeholder="ex) Misskey iOSクライアント。" autocomplete="off" required></textarea>
-			</label>
-		</section>
-		<section class="callback">
-			<label>
-				<p class="caption">コールバックURL (オプション)</p>
-				<input v-model="cb" type="url" placeholder="ex) https://your.app.example.com/callback.php" autocomplete="off"/>
-			</label>
-		</section>
-		<section class="permission">
-			<p class="caption">権限</p>
-			<div ref="permission">
-				<label>
-					<input type="checkbox" value="account-read"/>
-					<p>アカウントの情報を見る。</p>
-				</label>
-				<label>
-					<input type="checkbox" value="account-write"/>
-					<p>アカウントの情報を操作する。</p>
-				</label>
-				<label>
-					<input type="checkbox" value="post-write"/>
-					<p>投稿する。</p>
-				</label>
-				<label>
-					<input type="checkbox" value="reaction-write"/>
-					<p>リアクションしたりリアクションをキャンセルする。</p>
-				</label>
-				<label>
-					<input type="checkbox" value="following-write"/>
-					<p>フォローしたりフォロー解除する。</p>
-				</label>
-				<label>
-					<input type="checkbox" value="drive-read"/>
-					<p>ドライブを見る。</p>
-				</label>
-				<label>
-					<input type="checkbox" value="drive-write"/>
-					<p>ドライブを操作する。</p>
-				</label>
-				<label>
-					<input type="checkbox" value="notification-read"/>
-					<p>通知を見る。</p>
-				</label>
-				<label>
-					<input type="checkbox" value="notification-write"/>
-					<p>通知を操作する。</p>
-				</label>
-			</div>
-			<p>%fa:exclamation-triangle%アプリ作成後も変更できますが、新たな権限を付与する場合、その時点で関連付けられているユーザーキーはすべて無効になります。</p>
-		</section>
-		<button type="submit">アプリ作成</button>
-	</form>
-</div>
+			</b-form-group>
+			<b-form-group label="アプリの概要" description="あなたのアプリの簡単な説明や紹介。">
+				<b-textarea v-model="description" placeholder="ex) Misskey iOSクライアント。" autocomplete="off" required></b-textarea>
+			</b-form-group>
+			<b-form-group label="コールバックURL (オプション)" description="ユーザーが認証フォームで認証した際にリダイレクトするURLを設定できます。">
+				<b-input v-model="cb" type="url" placeholder="ex) https://your.app.example.com/callback.php" autocomplete="off"/>
+			</b-form-group>
+			<b-card header="権限">
+				<b-form-group description="ここで要求した機能だけがAPIからアクセスできます。">
+					<b-alert show variant="warning">%fa:exclamation-triangle%アプリ作成後も変更できますが、新たな権限を付与する場合、その時点で関連付けられているユーザーキーはすべて無効になります。</b-alert>
+					<b-form-checkbox-group v-model="permission" stacked>
+						<b-form-checkbox value="account-read">アカウントの情報を見る。</b-form-checkbox>
+						<b-form-checkbox value="account-write">アカウントの情報を操作する。</b-form-checkbox>
+						<b-form-checkbox value="post-write">投稿する。</b-form-checkbox>
+						<b-form-checkbox value="reaction-write">リアクションしたりリアクションをキャンセルする。</b-form-checkbox>
+						<b-form-checkbox value="following-write">フォローしたりフォロー解除する。</b-form-checkbox>
+						<b-form-checkbox value="drive-read">ドライブを見る。</b-form-checkbox>
+						<b-form-checkbox value="drive-write">ドライブを操作する。</b-form-checkbox>
+						<b-form-checkbox value="notification-read">通知を見る。</b-form-checkbox>
+						<b-form-checkbox value="notification-write">通知を操作する。</b-form-checkbox>
+					</b-form-checkbox-group>
+				</b-form-group>
+			</b-card>
+			<hr>
+			<b-button type="submit" variant="primary">アプリ作成</b-button>
+		</b-form>
+	</b-card>
+</mk-ui>
 </template>
 
 <script lang="ts">
@@ -88,7 +53,8 @@ export default Vue.extend({
 			nid: '',
 			description: '',
 			cb: '',
-			nidState: null
+			nidState: null,
+			permission: []
 		};
 	},
 	watch: {
@@ -122,18 +88,12 @@ export default Vue.extend({
 	},
 	methods: {
 		onSubmit() {
-			const permission = [];
-
-			(this.$refs.permission as any).querySelectorAll('input').forEach(el => {
-				if (el.checked) permission.push(el.value);
-			});
-
 			(this as any).api('app/create', {
 				name: this.name,
 				name_id: this.nid,
 				description: this.description,
 				callback_url: this.cb,
-				permission: permission
+				permission: this.permission
 			}).then(() => {
 				location.href = '/apps';
 			}).catch(() => {
diff --git a/src/web/app/dev/views/ui.vue b/src/web/app/dev/views/ui.vue
new file mode 100644
index 000000000..4a0fcee63
--- /dev/null
+++ b/src/web/app/dev/views/ui.vue
@@ -0,0 +1,20 @@
+<template>
+<div>
+	<b-navbar toggleable="md" type="dark" variant="info">
+		<b-navbar-brand>Misskey Developers</b-navbar-brand>
+		<b-navbar-nav>
+			<b-nav-item to="/">Home</b-nav-item>
+			<b-nav-item to="/apps">Apps</b-nav-item>
+		</b-navbar-nav>
+	</b-navbar>
+	<main>
+		<slot></slot>
+	</main>
+</div>
+</template>
+
+<style lang="stylus" scoped>
+main
+	padding 32px
+	max-width 700px
+</style>

From 88362602d53f6a695098514d932ff5010a893e44 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Feb 2018 05:43:55 +0900
Subject: [PATCH 0558/1250] v3925

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 0f651147b..29067b4ea 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3921",
+	"version": "0.0.3925",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 187b80ec4f80e89951b971b3c30c396d0016582c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 2 Mar 2018 06:26:31 +0900
Subject: [PATCH 0559/1250] nanka iroiro

---
 package.json                                  |  7 ++++-
 src/api/endpoints/i/update.ts                 |  8 +++++-
 src/api/models/user.ts                        |  1 +
 src/web/app/common/views/components/poll.vue  | 12 +++++---
 src/web/app/config.ts                         |  2 ++
 src/web/app/desktop/script.ts                 |  1 +
 src/web/app/desktop/style.styl                |  1 -
 .../views/components/settings.profile.vue     |  9 ++++++
 .../app/desktop/views/components/settings.vue | 28 ++++++++++++++-----
 src/web/app/init.ts                           |  2 ++
 src/web/docs/api/entities/user.yaml           |  6 ++++
 src/web/element.scss                          | 12 ++++++++
 webpack/webpack.config.ts                     | 17 +++++++++++
 13 files changed, 92 insertions(+), 14 deletions(-)
 create mode 100644 src/web/element.scss

diff --git a/package.json b/package.json
index 29067b4ea..f7001725e 100644
--- a/package.json
+++ b/package.json
@@ -97,6 +97,7 @@
 		"deepcopy": "0.6.3",
 		"diskusage": "0.2.4",
 		"elasticsearch": "14.1.0",
+		"element-ui": "^2.2.0",
 		"emojilib": "^2.2.12",
 		"escape-regexp": "0.0.1",
 		"eslint": "4.18.0",
@@ -104,6 +105,7 @@
 		"eventemitter3": "3.0.1",
 		"exif-js": "2.3.0",
 		"express": "4.16.2",
+		"file-loader": "^1.1.10",
 		"file-type": "7.6.0",
 		"fuckadblock": "3.2.1",
 		"gm": "1.23.1",
@@ -137,6 +139,8 @@
 		"morgan": "1.9.0",
 		"ms": "2.1.1",
 		"multer": "1.3.0",
+		"node-sass": "^4.7.2",
+		"node-sass-json-importer": "^3.1.3",
 		"nprogress": "0.2.0",
 		"os-utils": "0.0.14",
 		"progress-bar-webpack-plugin": "^1.11.0",
@@ -152,6 +156,7 @@
 		"rimraf": "2.6.2",
 		"rndstr": "1.0.0",
 		"s-age": "1.1.2",
+		"sass-loader": "^6.0.6",
 		"seedrandom": "2.4.3",
 		"serve-favicon": "2.4.5",
 		"speakeasy": "2.0.0",
@@ -181,7 +186,7 @@
 		"vue-template-compiler": "2.5.13",
 		"vuedraggable": "2.16.0",
 		"web-push": "3.3.0",
-		"webpack": "^3.11.0",
+		"webpack": "3.11.0",
 		"webpack-cli": "^2.0.8",
 		"webpack-replace-loader": "1.3.0",
 		"websocket": "1.0.25",
diff --git a/src/api/endpoints/i/update.ts b/src/api/endpoints/i/update.ts
index 43c524504..fc12665ad 100644
--- a/src/api/endpoints/i/update.ts
+++ b/src/api/endpoints/i/update.ts
@@ -46,13 +46,19 @@ 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 'is_bot' parameter
+	const [isBot, isBotErr] = $(params.is_bot).optional.boolean().$;
+	if (isBotErr) return rej('invalid is_bot param');
+	if (isBot) user.is_bot = isBot;
+
 	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
+			profile: user.profile,
+			is_bot: user.is_bot
 		}
 	});
 
diff --git a/src/api/models/user.ts b/src/api/models/user.ts
index e92f244dd..278b949db 100644
--- a/src/api/models/user.ts
+++ b/src/api/models/user.ts
@@ -75,6 +75,7 @@ export type IUser = {
 	last_used_at: Date;
 	latest_post: IPost;
 	pinned_post_id: mongo.ObjectID;
+	is_bot: boolean;
 	is_pro: boolean;
 	is_suspended: boolean;
 	keywords: string[];
diff --git a/src/web/app/common/views/components/poll.vue b/src/web/app/common/views/components/poll.vue
index 7ed5bc6b1..556d8ebf6 100644
--- a/src/web/app/common/views/components/poll.vue
+++ b/src/web/app/common/views/components/poll.vue
@@ -5,14 +5,14 @@
 			<div class="backdrop" :style="{ 'width': (showResult ? (choice.votes / total * 100) : 0) + '%' }"></div>
 			<span>
 				<template v-if="choice.is_voted">%fa:check%</template>
-				{{ choice.text }}
+				<span>{{ choice.text }}</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>
-		・
+		<span>・</span>
 		<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>
@@ -98,8 +98,12 @@ export default Vue.extend({
 				background $theme-color
 				transition width 1s ease
 
-			> .votes
-				margin-left 4px
+			> span
+				> [data-fa]
+					margin-right 4px
+
+				> .votes
+					margin-left 4px
 
 	> p
 		a
diff --git a/src/web/app/config.ts b/src/web/app/config.ts
index 2461b2215..b51279192 100644
--- a/src/web/app/config.ts
+++ b/src/web/app/config.ts
@@ -12,6 +12,7 @@ declare const _SW_PUBLICKEY_: string;
 declare const _THEME_COLOR_: string;
 declare const _COPYRIGHT_: string;
 declare const _VERSION_: string;
+declare const _LICENSE_: string;
 
 export const host = _HOST_;
 export const url = _URL_;
@@ -27,3 +28,4 @@ export const swPublickey = _SW_PUBLICKEY_;
 export const themeColor = _THEME_COLOR_;
 export const copyright = _COPYRIGHT_;
 export const version = _VERSION_;
+export const license = _LICENSE_;
diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
index f0412805e..78549741b 100644
--- a/src/web/app/desktop/script.ts
+++ b/src/web/app/desktop/script.ts
@@ -4,6 +4,7 @@
 
 // Style
 import './style.styl';
+import '../../element.scss';
 
 import init from '../init';
 import fuckAdBlock from '../common/scripts/fuck-ad-block';
diff --git a/src/web/app/desktop/style.styl b/src/web/app/desktop/style.styl
index 4d295035f..49f71fbde 100644
--- a/src/web/app/desktop/style.styl
+++ b/src/web/app/desktop/style.styl
@@ -1,6 +1,5 @@
 @import "../app"
 @import "../reset"
-@import "../../../../node_modules/cropperjs/dist/cropper.css"
 
 @import "./ui"
 
diff --git a/src/web/app/desktop/views/components/settings.profile.vue b/src/web/app/desktop/views/components/settings.profile.vue
index 97a382d79..218da67e8 100644
--- a/src/web/app/desktop/views/components/settings.profile.vue
+++ b/src/web/app/desktop/views/components/settings.profile.vue
@@ -22,6 +22,10 @@
 		<input v-model="birthday" type="date" class="ui"/>
 	</label>
 	<button class="ui primary" @click="save">%i18n:desktop.tags.mk-profile-setting.save%</button>
+	<section>
+		<h2>その他</h2>
+		<el-switch v-model="os.i.is_bot" @change="onChangeIsBot" active-text="このアカウントはbotです"/>
+	</section>
 </div>
 </template>
 
@@ -56,6 +60,11 @@ export default Vue.extend({
 			}).then(() => {
 				(this as any).apis.notify('プロフィールを更新しました');
 			});
+		},
+		onChangeIsBot() {
+			(this as any).api('i/update', {
+				is_bot: (this as any).os.i.is_bot
+			});
 		}
 	}
 });
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index c210997c3..b65623e33 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -23,10 +23,7 @@
 			<div>
 				<button class="ui button" @click="customizeHome">ホームをカスタマイズ</button>
 			</div>
-			<label>
-				<input type="checkbox" v-model="showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl">
-				<span>タイムライン上部に投稿フォームを表示する</span>
-			</label>
+			<el-switch v-model="showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" active-text="タイムライン上部に投稿フォームを表示する"/>
 		</section>
 
 		<section class="drive" v-show="page == 'drive'">
@@ -71,7 +68,8 @@
 
 		<section class="other" v-show="page == 'other'">
 			<h1>%i18n:desktop.tags.mk-settings.license%</h1>
-			%license%
+			<div v-html="license"></div>
+			<a :href="licenseUrl" target="_blank">サードパーティ</a>
 		</section>
 	</div>
 </div>
@@ -84,6 +82,7 @@ import XMute from './settings.mute.vue';
 import XPassword from './settings.password.vue';
 import X2fa from './settings.2fa.vue';
 import XApi from './settings.api.vue';
+import { docsUrl, license, lang } from '../../../config';
 
 export default Vue.extend({
 	components: {
@@ -96,10 +95,15 @@ export default Vue.extend({
 	data() {
 		return {
 			page: 'profile',
-
+			license,
 			showPostFormOnTopOfTl: false
 		};
 	},
+	computed: {
+		licenseUrl(): string {
+			return `${docsUrl}/${lang}/license`;
+		}
+	},
 	created() {
 		this.showPostFormOnTopOfTl = (this as any).os.i.client_settings.showPostFormOnTopOfTl;
 	},
@@ -162,13 +166,23 @@ export default Vue.extend({
 			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
 
+			&, >>> *
+				> section
+					margin 32px 0
+
+					> h2
+						margin 0 0 1em 0
+						padding 0 0 8px 0
+						font-size 1em
+						color #555
+						border-bottom solid 1px #eee
+
 		> .web
 			> div
 				border-bottom solid 1px #eee
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index f9855fd5c..dc3057935 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -5,9 +5,11 @@
 import Vue from 'vue';
 import VueRouter from 'vue-router';
 import VModal from 'vue-js-modal';
+import Element from 'element-ui';
 
 Vue.use(VueRouter);
 Vue.use(VModal);
+Vue.use(Element);
 
 // Register global directives
 require('./common/views/directives');
diff --git a/src/web/docs/api/entities/user.yaml b/src/web/docs/api/entities/user.yaml
index e62ad84db..528b9b0e1 100644
--- a/src/web/docs/api/entities/user.yaml
+++ b/src/web/docs/api/entities/user.yaml
@@ -111,6 +111,12 @@ props:
     desc:
       ja: "ドライブの容量(bytes)"
       en: "The capacity of drive of this user (bytes)"
+  - name: "is_bot"
+    type: "boolean"
+    optional: true
+    desc:
+      ja: "botか否か(自己申告であることに留意)"
+      en: "Whether is bot or not"
   - name: "twitter"
     type: "object"
     optional: true
diff --git a/src/web/element.scss b/src/web/element.scss
new file mode 100644
index 000000000..917198e02
--- /dev/null
+++ b/src/web/element.scss
@@ -0,0 +1,12 @@
+/* Element variable definitons */
+/* SEE: http://element.eleme.io/#/en-US/component/custom-theme */
+
+@import '../const.json';
+
+/* theme color */
+$--color-primary: $themeColor;
+
+/* icon font path, required */
+$--font-path: '~element-ui/lib/theme-chalk/fonts';
+
+@import "~element-ui/packages/theme-chalk/src/index";
diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts
index 775ecbd34..a87341945 100644
--- a/webpack/webpack.config.ts
+++ b/webpack/webpack.config.ts
@@ -3,6 +3,7 @@
  */
 
 import * as fs from 'fs';
+import jsonImporter from 'node-sass-json-importer';
 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';
@@ -111,12 +112,28 @@ module.exports = Object.keys(langs).map(lang => {
 					{ loader: 'css-loader' },
 					{ loader: 'stylus-loader' }
 				]
+			}, {
+				test: /\.scss$/,
+				exclude: /node_modules/,
+				use: [{
+					loader: 'style-loader'
+				}, {
+					loader: 'css-loader'
+				}, {
+					loader: 'sass-loader',
+					options: {
+						importer: jsonImporter,
+					}
+				}]
 			}, {
 				test: /\.css$/,
 				use: [
 					{ loader: 'style-loader' },
 					{ loader: 'css-loader' }
 				]
+			}, {
+				test: /\.(eot|woff|woff2|svg|ttf)([\?]?.*)$/,
+				loader: 'file-loader'
 			}, {
 				test: /\.ts$/,
 				exclude: /node_modules/,

From 8298a744ee2b7a3da837d6ebf3ef9a87ff2d19b7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 2 Mar 2018 06:27:03 +0900
Subject: [PATCH 0560/1250] v3927

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index f7001725e..d7d409a73 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3925",
+	"version": "0.0.3927",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 49e298d2e81cbf8437a7a490c9ef4183ff71469b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 2 Mar 2018 07:01:47 +0900
Subject: [PATCH 0561/1250] oops

---
 src/api/endpoints/i/update.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/api/endpoints/i/update.ts b/src/api/endpoints/i/update.ts
index fc12665ad..2a5dce64a 100644
--- a/src/api/endpoints/i/update.ts
+++ b/src/api/endpoints/i/update.ts
@@ -49,7 +49,7 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re
 	// Get 'is_bot' parameter
 	const [isBot, isBotErr] = $(params.is_bot).optional.boolean().$;
 	if (isBotErr) return rej('invalid is_bot param');
-	if (isBot) user.is_bot = isBot;
+	if (isBot != null) user.is_bot = isBot;
 
 	await User.update(user._id, {
 		$set: {

From 5ef88710c094deb421888f7a3b933e00a179d4c8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 2 Mar 2018 18:46:08 +0900
Subject: [PATCH 0562/1250] #1161, #1163

---
 src/web/app/common/mios.ts                    |   2 +-
 .../app/common/scripts/check-for-update.ts    |  12 +-
 src/web/app/common/views/components/index.ts  |   2 +
 .../app/common/views/components/switch.vue    | 170 ++++++++++++++++++
 .../app/desktop/views/components/dialog.vue   |  17 +-
 .../views/components/settings.profile.vue     |   2 +-
 .../app/desktop/views/components/settings.vue |  75 +++++++-
 7 files changed, 265 insertions(+), 15 deletions(-)
 create mode 100644 src/web/app/common/views/components/switch.vue

diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
index 6cc441cd1..3701a24c3 100644
--- a/src/web/app/common/mios.ts
+++ b/src/web/app/common/mios.ts
@@ -27,7 +27,7 @@ export type API = {
 	dialog: (opts: {
 		title: string;
 		text: string;
-		actions: Array<{
+		actions?: Array<{
 			text: string;
 			id?: string;
 		}>;
diff --git a/src/web/app/common/scripts/check-for-update.ts b/src/web/app/common/scripts/check-for-update.ts
index fe539407d..3e7eb79d8 100644
--- a/src/web/app/common/scripts/check-for-update.ts
+++ b/src/web/app/common/scripts/check-for-update.ts
@@ -1,8 +1,8 @@
 import MiOS from '../mios';
 import { version } from '../../config';
 
-export default async function(mios: MiOS) {
-	const meta = await mios.getMeta();
+export default async function(mios: MiOS, force = false, silent = false) {
+	const meta = await mios.getMeta(force);
 
 	if (meta.version != version) {
 		localStorage.setItem('should-refresh', 'true');
@@ -20,6 +20,12 @@ export default async function(mios: MiOS) {
 			console.error(e);
 		}
 
-		alert('%i18n:common.update-available%'.replace('{newer}', meta.version).replace('{current}', version));
+		if (!silent) {
+			alert('%i18n:common.update-available%'.replace('{newer}', meta.version).replace('{current}', version));
+		}
+
+		return meta.version;
+	} else {
+		return null;
 	}
 }
diff --git a/src/web/app/common/views/components/index.ts b/src/web/app/common/views/components/index.ts
index ab0f1767d..527492022 100644
--- a/src/web/app/common/views/components/index.ts
+++ b/src/web/app/common/views/components/index.ts
@@ -20,6 +20,7 @@ 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';
+import Switch from './switch.vue';
 
 Vue.component('mk-signin', signin);
 Vue.component('mk-signup', signup);
@@ -41,3 +42,4 @@ 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);
+Vue.component('mk-switch', Switch);
diff --git a/src/web/app/common/views/components/switch.vue b/src/web/app/common/views/components/switch.vue
new file mode 100644
index 000000000..fc12e0054
--- /dev/null
+++ b/src/web/app/common/views/components/switch.vue
@@ -0,0 +1,170 @@
+<template>
+<div
+	class="mk-switch"
+	:class="{ disabled, checked }"
+	role="switch"
+	:aria-checked="checked"
+	:aria-disabled="disabled"
+	@click="switchValue"
+	@mouseover="mouseenter"
+>
+	<input
+		type="checkbox"
+		@change="handleChange"
+		ref="input"
+		:disabled="disabled"
+		@keydown.enter="switchValue"
+	>
+	<span class="button">
+		<span :style="{ transform }"></span>
+	</span>
+	<span class="label">
+		<span :aria-hidden="!checked">{{ text }}</span>
+		<p :aria-hidden="!checked">
+			<slot></slot>
+		</p>
+	</span>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: {
+		value: {
+			type: Boolean,
+			default: false
+		},
+		disabled: {
+			type: Boolean,
+			default: false
+		},
+		text: String
+	},/*
+	created() {
+		if (!~[true, false].indexOf(this.value)) {
+			this.$emit('input', false);
+		}
+	},*/
+	computed: {
+		checked(): boolean {
+			return this.value;
+		},
+		transform(): string {
+			return this.checked ? 'translate3d(20px, 0, 0)' : '';
+		}
+	},
+	watch: {
+		value() {
+			(this.$refs.input as any).checked = this.checked;
+		}
+	},
+	mounted() {
+		(this.$refs.input as any).checked = this.checked;
+	},
+	methods: {
+		mouseenter() {
+			(this.$el).style.transition = 'all 0s';
+		},
+		handleChange() {
+			(this.$el).style.transition = 'all 0.3s';
+			this.$emit('input', !this.checked);
+			this.$emit('change', !this.checked);
+			this.$nextTick(() => {
+				// set input's checked property
+				// in case parent refuses to change component's value
+				(this.$refs.input as any).checked = this.checked;
+			});
+		},
+		switchValue() {
+			!this.disabled && this.handleChange();
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-switch
+	display flex
+	cursor pointer
+	transition all 0.3s
+
+	> *
+		user-select none
+
+	&.disabled
+		opacity 0.6
+		cursor not-allowed
+
+	&.checked
+		> .button
+			background-color $theme-color
+			border-color $theme-color
+
+		> .label
+			> span
+				color $theme-color
+
+		&:hover
+			> .label
+				> span
+					color darken($theme-color, 10%)
+
+			> .button
+				background darken($theme-color, 10%)
+				border-color darken($theme-color, 10%)
+
+	&:hover
+		> .label
+			> span
+				color #2e3338
+
+		> .button
+			background #ced2da
+			border-color #ced2da
+
+	> input
+		position absolute
+		width 0
+		height 0
+		opacity 0
+		margin 0
+
+	> .button
+		display inline-block
+		margin 0
+		width 40px
+		height 20px
+		background #dcdfe6
+		border 1px solid #dcdfe6
+		outline none
+		border-radius 10px
+		transition inherit
+
+		> *
+			position absolute
+			top 1px
+			left 1px
+			border-radius 100%
+			transition transform 0.3s
+			width 16px
+			height 16px
+			background-color #fff
+
+	> .label
+		margin-left 8px
+		display block
+		font-size 14px
+		cursor pointer
+		transition inherit
+
+		> span
+			line-height 20px
+			color #4a535a
+			transition inherit
+
+		> p
+			margin 0
+			color #9daab3
+
+</style>
diff --git a/src/web/app/desktop/views/components/dialog.vue b/src/web/app/desktop/views/components/dialog.vue
index 28f22f7b6..e89e8654e 100644
--- a/src/web/app/desktop/views/components/dialog.vue
+++ b/src/web/app/desktop/views/components/dialog.vue
@@ -16,21 +16,28 @@ import Vue from 'vue';
 import * as anime from 'animejs';
 
 export default Vue.extend({
-	props: ['title', 'text', 'buttons', 'modal']/*{
+	props: {
 		title: {
-			type: String
+			type: String,
+			required: false
 		},
 		text: {
-			type: String
+			type: String,
+			required: true
 		},
 		buttons: {
-			type: Array
+			type: Array,
+			default: () => {
+				return [{
+					text: 'OK'
+				}];
+			}
 		},
 		modal: {
 			type: Boolean,
 			default: false
 		}
-	}*/,
+	},
 	mounted() {
 		this.$nextTick(() => {
 			(this.$refs.bg as any).style.pointerEvents = 'auto';
diff --git a/src/web/app/desktop/views/components/settings.profile.vue b/src/web/app/desktop/views/components/settings.profile.vue
index 218da67e8..b57ac1028 100644
--- a/src/web/app/desktop/views/components/settings.profile.vue
+++ b/src/web/app/desktop/views/components/settings.profile.vue
@@ -24,7 +24,7 @@
 	<button class="ui primary" @click="save">%i18n:desktop.tags.mk-profile-setting.save%</button>
 	<section>
 		<h2>その他</h2>
-		<el-switch v-model="os.i.is_bot" @change="onChangeIsBot" active-text="このアカウントはbotです"/>
+		<mk-switch v-model="os.i.is_bot" @change="onChangeIsBot" text="このアカウントはbotです"/>
 	</section>
 </div>
 </template>
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index b65623e33..4a9db5f48 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -20,10 +20,18 @@
 
 		<section class="web" v-show="page == 'web'">
 			<h1>デザイン</h1>
-			<div>
+			<div class="div">
 				<button class="ui button" @click="customizeHome">ホームをカスタマイズ</button>
 			</div>
-			<el-switch v-model="showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" active-text="タイムライン上部に投稿フォームを表示する"/>
+			<mk-switch v-model="showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="タイムライン上部に投稿フォームを表示する"/>
+		</section>
+
+		<section class="web" v-show="page == 'web'">
+			<h1>キャッシュ</h1>
+			<button class="ui button" @click="clean">クリーンアップ</button>
+			<div class="none ui info">
+				<p>%fa:info-circle%クリーンアップを行うと、ブラウザに記憶されたアカウント情報のキャッシュ、書きかけの投稿・返信・メッセージ、およびその他のデータ(設定情報含む)が削除されます。クリーンアップを行った後はページを再度読み込みする必要があります。</p>
+			</div>
 		</section>
 
 		<section class="drive" v-show="page == 'drive'">
@@ -66,6 +74,28 @@
 			<x-api/>
 		</section>
 
+		<section class="other" v-show="page == 'other'">
+			<h1>Misskey Update</h1>
+			<p>
+				<span>バージョン: <i>{{ version }}</i></span>
+				<template v-if="latestVersion !== undefined">
+					<br>
+					<span>最新のバージョン: <i>{{ latestVersion ? latestVersion : version }}</i></span>
+				</template>
+			</p>
+			<button class="ui button" @click="checkForUpdate" :disabled="checkingForUpdate">
+				<template v-if="checkingForUpdate">アップデートを確認中<mk-ellipsis/></template>
+				<template v-else>アップデートを確認</template>
+			</button>
+		</section>
+
+		<section class="other" v-show="page == 'other'">
+			<h1>高度な設定</h1>
+			<mk-switch v-model="debug" text="デバッグモードを有効にする">
+				<span>この設定はアカウントに保存されません。</span>
+			</mk-switch>
+		</section>
+
 		<section class="other" v-show="page == 'other'">
 			<h1>%i18n:desktop.tags.mk-settings.license%</h1>
 			<div v-html="license"></div>
@@ -82,7 +112,8 @@ import XMute from './settings.mute.vue';
 import XPassword from './settings.password.vue';
 import X2fa from './settings.2fa.vue';
 import XApi from './settings.api.vue';
-import { docsUrl, license, lang } from '../../../config';
+import { docsUrl, license, lang, version } from '../../../config';
+import checkForUpdate from '../../../common/scripts/check-for-update';
 
 export default Vue.extend({
 	components: {
@@ -96,9 +127,18 @@ export default Vue.extend({
 		return {
 			page: 'profile',
 			license,
-			showPostFormOnTopOfTl: false
+			version,
+			latestVersion: undefined,
+			checkingForUpdate: false,
+			showPostFormOnTopOfTl: false,
+			debug: localStorage.getItem('debug') == 'true'
 		};
 	},
+	watch: {
+		debug() {
+			localStorage.setItem('debug', this.debug ? 'true' : 'false');
+		}
+	},
 	computed: {
 		licenseUrl(): string {
 			return `${docsUrl}/${lang}/license`;
@@ -117,6 +157,31 @@ export default Vue.extend({
 				name: 'showPostFormOnTopOfTl',
 				value: this.showPostFormOnTopOfTl
 			});
+		},
+		checkForUpdate() {
+			this.checkingForUpdate = true;
+			checkForUpdate((this as any).os, true, true).then(newer => {
+				this.checkingForUpdate = false;
+				this.latestVersion = newer;
+				if (newer == null) {
+					(this as any).apis.dialog({
+						title: '利用可能な更新はありません',
+						text: 'お使いのMisskeyは最新です。'
+					});
+				} else {
+					(this as any).apis.dialog({
+						title: '新しいバージョンが利用可能です',
+						text: 'ページを再度読み込みすると更新が適用されます。'
+					});
+				}
+			});
+		},
+		clean() {
+			localStorage.clear();
+			(this as any).apis.dialog({
+				title: 'キャッシュを削除しました',
+				text: 'ページを再度読み込みしてください。'
+			});
 		}
 	}
 });
@@ -184,7 +249,7 @@ export default Vue.extend({
 						border-bottom solid 1px #eee
 
 		> .web
-			> div
+			> .div
 				border-bottom solid 1px #eee
 				padding 0 0 16px 0
 				margin 0 0 16px 0

From cafc666ed76db01e0d5cc110ab2ff7dba0ddb47b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 2 Mar 2018 18:46:32 +0900
Subject: [PATCH 0563/1250] v3930

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index d7d409a73..8aafb6340 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3927",
+	"version": "0.0.3930",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 52b4e7b5746bbb6ae44008cab0960c5607539b99 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 2 Mar 2018 19:20:52 +0900
Subject: [PATCH 0564/1250] :v:

---
 gulpfile.ts  | 15 +--------------
 package.json |  4 +++-
 2 files changed, 4 insertions(+), 15 deletions(-)

diff --git a/gulpfile.ts b/gulpfile.ts
index 736507baf..aa6712914 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -111,23 +111,11 @@ gulp.task('default', ['build']);
 gulp.task('build:client', [
 	'build:ts',
 	'build:js',
-	'webpack',
 	'build:client:script',
 	'build:client:pug',
 	'copy:client'
 ]);
 
-gulp.task('webpack', done => {
-	const webpack = childProcess.spawn(
-		Path.join('.', 'node_modules', '.bin', 'webpack'),
-		['--config', './webpack/webpack.config.ts'], {
-			shell: true,
-			stdio: 'inherit'
-		});
-
-	webpack.on('exit', done);
-});
-
 gulp.task('build:client:script', () =>
 	gulp.src(['./src/web/app/boot.js', './src/web/app/safe.js'])
 		.pipe(replace('VERSION', JSON.stringify(version)))
@@ -147,8 +135,7 @@ gulp.task('build:client:styles', () =>
 );
 
 gulp.task('copy:client', [
-	'build:client:script',
-	'webpack'
+	'build:client:script'
 ], () =>
 		gulp.src([
 			'./assets/**/*',
diff --git a/package.json b/package.json
index 8aafb6340..03db892ec 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,9 @@
 		"start": "node ./built",
 		"debug": "DEBUG=misskey:* node ./built",
 		"swagger": "node ./swagger.js",
-		"build": "gulp build",
+		"build": "./node_modules/.bin/webpack --config ./webpack/webpack.config.ts && gulp build",
+		"webpack": "./node_modules/.bin/webpack --config ./webpack/webpack.config.ts",
+		"gulp": "gulp build",
 		"rebuild": "gulp rebuild",
 		"clean": "gulp clean",
 		"cleanall": "gulp cleanall",

From b2a3ad79f2cadd4c39c00f9404c328d1b142b33d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 3 Mar 2018 07:32:18 +0900
Subject: [PATCH 0565/1250] nanka iroiro

Closes #1168, #1169
---
 .gitignore                                        |  2 ++
 package.json                                      |  2 ++
 src/version.ts                                    |  2 +-
 src/web/app/boot.js                               |  4 +++-
 src/web/app/common/scripts/check-for-update.ts    | 10 ++++++----
 src/web/app/common/views/components/switch.vue    |  6 +++++-
 src/web/app/desktop/views/components/settings.vue | 11 ++++++++++-
 webpack/plugins/consts.ts                         |  4 +++-
 webpack/plugins/index.ts                          |  9 +++++++++
 webpack/webpack.config.ts                         |  6 ++++--
 10 files changed, 45 insertions(+), 11 deletions(-)

diff --git a/.gitignore b/.gitignore
index a51e70381..1c05ba5f2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,5 @@ npm-debug.log
 run.bat
 api-docs.json
 package-lock.json
+version.json
+/.cache-loader
diff --git a/package.json b/package.json
index 03db892ec..a4ab2dfeb 100644
--- a/package.json
+++ b/package.json
@@ -124,6 +124,7 @@
 		"gulp-typescript": "3.2.4",
 		"gulp-uglify": "3.0.0",
 		"gulp-util": "3.0.8",
+		"hard-source-webpack-plugin": "^0.6.1",
 		"highlight.js": "9.12.0",
 		"html-minifier": "3.5.9",
 		"inquirer": "5.1.0",
@@ -144,6 +145,7 @@
 		"node-sass": "^4.7.2",
 		"node-sass-json-importer": "^3.1.3",
 		"nprogress": "0.2.0",
+		"on-build-webpack": "^0.1.0",
 		"os-utils": "0.0.14",
 		"progress-bar-webpack-plugin": "^1.11.0",
 		"prominence": "0.2.0",
diff --git a/src/version.ts b/src/version.ts
index 2b4c1320e..d379b57f8 100644
--- a/src/version.ts
+++ b/src/version.ts
@@ -2,6 +2,6 @@
  * Version
  */
 
-const meta = require('../package.json');
+const meta = require('../version.json');
 
 export default meta.version as string;
diff --git a/src/web/app/boot.js b/src/web/app/boot.js
index 211dc2f88..91830b6c0 100644
--- a/src/web/app/boot.js
+++ b/src/web/app/boot.js
@@ -61,11 +61,13 @@
 		app = isMobile ? 'mobile' : 'desktop';
 	}
 
+	const ver = localStorage.getItem('v') || VERSION;
+
 	// Load an app script
 	// Note: 'async' make it possible to load the script asyncly.
 	//       'defer' make it possible to run the script when the dom loaded.
 	const script = document.createElement('script');
-	script.setAttribute('src', `/assets/${app}.${VERSION}.${lang}.js`);
+	script.setAttribute('src', `/assets/${app}.${ver}.${lang}.js`);
 	script.setAttribute('async', 'true');
 	script.setAttribute('defer', 'true');
 	head.appendChild(script);
diff --git a/src/web/app/common/scripts/check-for-update.ts b/src/web/app/common/scripts/check-for-update.ts
index 3e7eb79d8..81c1eb981 100644
--- a/src/web/app/common/scripts/check-for-update.ts
+++ b/src/web/app/common/scripts/check-for-update.ts
@@ -1,11 +1,13 @@
 import MiOS from '../mios';
-import { version } from '../../config';
+import { version as current } from '../../config';
 
 export default async function(mios: MiOS, force = false, silent = false) {
 	const meta = await mios.getMeta(force);
+	const newer = meta.version;
 
-	if (meta.version != version) {
+	if (newer != current) {
 		localStorage.setItem('should-refresh', 'true');
+		localStorage.setItem('v', newer);
 
 		// Clear cache (serive worker)
 		try {
@@ -21,10 +23,10 @@ export default async function(mios: MiOS, force = false, silent = false) {
 		}
 
 		if (!silent) {
-			alert('%i18n:common.update-available%'.replace('{newer}', meta.version).replace('{current}', version));
+			alert('%i18n:common.update-available%'.replace('{newer}', newer).replace('{current}', current));
 		}
 
-		return meta.version;
+		return newer;
 	} else {
 		return null;
 	}
diff --git a/src/web/app/common/views/components/switch.vue b/src/web/app/common/views/components/switch.vue
index fc12e0054..bfb951dfa 100644
--- a/src/web/app/common/views/components/switch.vue
+++ b/src/web/app/common/views/components/switch.vue
@@ -86,6 +86,7 @@ export default Vue.extend({
 <style lang="stylus" scoped>
 .mk-switch
 	display flex
+	margin 8px 0
 	cursor pointer
 	transition all 0.3s
 
@@ -134,7 +135,9 @@ export default Vue.extend({
 		display inline-block
 		margin 0
 		width 40px
+		min-width 40px
 		height 20px
+		min-height 20px
 		background #dcdfe6
 		border 1px solid #dcdfe6
 		outline none
@@ -154,17 +157,18 @@ export default Vue.extend({
 	> .label
 		margin-left 8px
 		display block
-		font-size 14px
 		cursor pointer
 		transition inherit
 
 		> span
+			display block
 			line-height 20px
 			color #4a535a
 			transition inherit
 
 		> p
 			margin 0
+			font-size 90%
 			color #9daab3
 
 </style>
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index 4a9db5f48..0c1968f67 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -94,6 +94,9 @@
 			<mk-switch v-model="debug" text="デバッグモードを有効にする">
 				<span>この設定はアカウントに保存されません。</span>
 			</mk-switch>
+			<mk-switch v-model="enableExperimental" text="実験的機能を有効にする">
+				<span>この設定はアカウントに保存されません。実験的機能を有効にするとMisskeyの動作が不安定になる可能性があります。</span>
+			</mk-switch>
 		</section>
 
 		<section class="other" v-show="page == 'other'">
@@ -126,17 +129,22 @@ export default Vue.extend({
 	data() {
 		return {
 			page: 'profile',
+			meta: null,
 			license,
 			version,
 			latestVersion: undefined,
 			checkingForUpdate: false,
 			showPostFormOnTopOfTl: false,
-			debug: localStorage.getItem('debug') == 'true'
+			debug: localStorage.getItem('debug') == 'true',
+			enableExperimental: localStorage.getItem('enableExperimental') == 'true'
 		};
 	},
 	watch: {
 		debug() {
 			localStorage.setItem('debug', this.debug ? 'true' : 'false');
+		},
+		enableExperimental() {
+			localStorage.setItem('enableExperimental', this.enableExperimental ? 'true' : 'false');
 		}
 	},
 	computed: {
@@ -145,6 +153,7 @@ export default Vue.extend({
 		}
 	},
 	created() {
+		this.meta = (this as any).os.getMeta();
 		this.showPostFormOnTopOfTl = (this as any).os.i.client_settings.showPostFormOnTopOfTl;
 	},
 	methods: {
diff --git a/webpack/plugins/consts.ts b/webpack/plugins/consts.ts
index a01c18af6..cb9ba8e86 100644
--- a/webpack/plugins/consts.ts
+++ b/webpack/plugins/consts.ts
@@ -4,7 +4,9 @@
 
 import * as webpack from 'webpack';
 
-import version from '../../src/version';
+const meta = require('../../package.json');
+const version = meta.version;
+
 const constants = require('../../src/const.json');
 import config from '../../src/conf';
 import { licenseHtml } from '../../src/common/build/license';
diff --git a/webpack/plugins/index.ts b/webpack/plugins/index.ts
index b97cde231..4023cd6cb 100644
--- a/webpack/plugins/index.ts
+++ b/webpack/plugins/index.ts
@@ -1,4 +1,7 @@
+import * as fs from 'fs';
 import * as webpack from 'webpack';
+const WebpackOnBuildPlugin = require('on-build-webpack');
+const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
 const ProgressBarPlugin = require('progress-bar-webpack-plugin');
 import chalk from 'chalk';
 
@@ -11,6 +14,7 @@ const isProduction = env === 'production';
 
 export default (version, lang) => {
 	const plugins = [
+		new HardSourceWebpackPlugin(),
 		new ProgressBarPlugin({
 			format: chalk`  {cyan.bold yes we can} {bold [}:bar{bold ]} {green.bold :percent} {gray (:current/:total)} :elapseds`,
 			clear: false
@@ -20,6 +24,11 @@ export default (version, lang) => {
 			'process.env': {
 				NODE_ENV: JSON.stringify(process.env.NODE_ENV)
 			}
+		}),
+		new WebpackOnBuildPlugin(stats => {
+			fs.writeFileSync('./version.json', JSON.stringify({
+				version
+			}), 'utf-8');
 		})
 	];
 
diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts
index a87341945..cfb129970 100644
--- a/webpack/webpack.config.ts
+++ b/webpack/webpack.config.ts
@@ -12,7 +12,8 @@ const constants = require('../src/const.json');
 import plugins from './plugins';
 
 import langs from '../locales';
-import version from '../src/version';
+const meta = require('../package.json');
+const version = meta.version;
 
 global['faReplacement'] = faReplacement;
 
@@ -59,7 +60,7 @@ module.exports = Object.keys(langs).map(lang => {
 			rules: [{
 				test: /\.vue$/,
 				exclude: /node_modules/,
-				use: [/*'cache-loader', */{
+				use: ['cache-loader', {
 					loader: 'vue-loader',
 					options: {
 						cssSourceMap: false,
@@ -140,6 +141,7 @@ module.exports = Object.keys(langs).map(lang => {
 				use: [{
 					loader: 'ts-loader',
 					options: {
+						happyPackMode: true,
 						configFile: __dirname + '/../src/web/app/tsconfig.json',
 						appendTsSuffixTo: [/\.vue$/]
 					}

From b5d08c5efd14fa38b3c8f6f5847c0e2d1d624154 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 3 Mar 2018 07:32:37 +0900
Subject: [PATCH 0566/1250] v3933

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index a4ab2dfeb..b125139c6 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3930",
+	"version": "0.0.3933",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From ae089052274e52541948cfe0cc16b7afb6ed7042 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 3 Mar 2018 09:49:47 +0900
Subject: [PATCH 0567/1250] nanka iroior

---
 .gitignore                                    |  1 -
 package.json                                  |  2 +-
 src/web/app/boot.js                           |  1 +
 .../app/common/views/components/switch.vue    |  3 +-
 .../app/common/views/widgets/server.info.vue  |  2 +-
 .../app/desktop/views/components/settings.vue | 34 +++++++++++++++----
 .../desktop/views/components/ui.header.vue    |  7 ++--
 .../app/desktop/views/components/window.vue   |  6 ++--
 webpack/webpack.config.ts                     |  4 +--
 9 files changed, 41 insertions(+), 19 deletions(-)

diff --git a/.gitignore b/.gitignore
index 1c05ba5f2..6c8b99c85 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,4 +9,3 @@ run.bat
 api-docs.json
 package-lock.json
 version.json
-/.cache-loader
diff --git a/package.json b/package.json
index b125139c6..98f875b50 100644
--- a/package.json
+++ b/package.json
@@ -85,7 +85,6 @@
 		"bcryptjs": "2.4.3",
 		"body-parser": "1.18.2",
 		"bootstrap-vue": "^2.0.0-rc.1",
-		"cache-loader": "1.2.0",
 		"cafy": "3.2.1",
 		"chai": "4.1.2",
 		"chai-http": "3.0.0",
@@ -180,6 +179,7 @@
 		"typescript-eslint-parser": "13.0.0",
 		"uglify-es": "3.3.9",
 		"uglifyjs-webpack-plugin": "1.2.0",
+		"url-loader": "^0.6.2",
 		"uuid": "3.2.1",
 		"vhost": "3.0.2",
 		"vue": "2.5.13",
diff --git a/src/web/app/boot.js b/src/web/app/boot.js
index 91830b6c0..a6d0f3aad 100644
--- a/src/web/app/boot.js
+++ b/src/web/app/boot.js
@@ -35,6 +35,7 @@
 	// Note: The default language is English
 	let lang = navigator.language.split('-')[0];
 	if (!/^(en|ja)$/.test(lang)) lang = 'en';
+	if (localStorage.getItem('lang')) lang = localStorage.getItem('lang');
 
 	// Detect the user agent
 	const ua = navigator.userAgent.toLowerCase();
diff --git a/src/web/app/common/views/components/switch.vue b/src/web/app/common/views/components/switch.vue
index bfb951dfa..e6cdfa152 100644
--- a/src/web/app/common/views/components/switch.vue
+++ b/src/web/app/common/views/components/switch.vue
@@ -157,6 +157,7 @@ export default Vue.extend({
 	> .label
 		margin-left 8px
 		display block
+		font-size 15px
 		cursor pointer
 		transition inherit
 
@@ -168,7 +169,7 @@ export default Vue.extend({
 
 		> p
 			margin 0
-			font-size 90%
+			//font-size 90%
 			color #9daab3
 
 </style>
diff --git a/src/web/app/common/views/widgets/server.info.vue b/src/web/app/common/views/widgets/server.info.vue
index bed6a1b74..d24362950 100644
--- a/src/web/app/common/views/widgets/server.info.vue
+++ b/src/web/app/common/views/widgets/server.info.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="info">
-	<p>Maintainer: <b>{{ meta.maintainer }}</b></p>
+	<p>Maintainer: <b><a :href="meta.maintainer.url" target="_blank">{{ meta.maintainer.name }}</a></b></p>
 	<p>Machine: {{ meta.machine }}</p>
 	<p>Node: {{ meta.node }}</p>
 </div>
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index 0c1968f67..6f0461bc2 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -23,7 +23,20 @@
 			<div class="div">
 				<button class="ui button" @click="customizeHome">ホームをカスタマイズ</button>
 			</div>
-			<mk-switch v-model="showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="タイムライン上部に投稿フォームを表示する"/>
+			<mk-switch v-model="os.i.client_settings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="タイムライン上部に投稿フォームを表示する"/>
+		</section>
+
+		<section class="web" v-show="page == 'web'">
+			<h1>言語</h1>
+			<el-select v-model="lang" placeholder="言語を選択">
+				<el-option-group label="推奨">
+					<el-option label="自動" value=""/>
+				</el-option-group>
+				<el-option-group label="言語を指定">
+					<el-option label="ja-JP" value="ja"/>
+					<el-option label="en-US" value="en"/>
+				</el-option-group>
+			</el-select>
 		</section>
 
 		<section class="web" v-show="page == 'web'">
@@ -74,6 +87,11 @@
 			<x-api/>
 		</section>
 
+		<section class="other" v-show="page == 'other'">
+			<h1>Misskeyについて</h1>
+			<p v-if="meta">このサーバーの運営者: <i><a :href="meta.maintainer.url" target="_blank">{{ meta.maintainer.name }}</a></i></p>
+		</section>
+
 		<section class="other" v-show="page == 'other'">
 			<h1>Misskey Update</h1>
 			<p>
@@ -134,12 +152,15 @@ export default Vue.extend({
 			version,
 			latestVersion: undefined,
 			checkingForUpdate: false,
-			showPostFormOnTopOfTl: false,
+			lang: localStorage.getItem('lang') || '',
 			debug: localStorage.getItem('debug') == 'true',
 			enableExperimental: localStorage.getItem('enableExperimental') == 'true'
 		};
 	},
 	watch: {
+		lang() {
+			localStorage.setItem('lang', this.lang);
+		},
 		debug() {
 			localStorage.setItem('debug', this.debug ? 'true' : 'false');
 		},
@@ -153,18 +174,19 @@ export default Vue.extend({
 		}
 	},
 	created() {
-		this.meta = (this as any).os.getMeta();
-		this.showPostFormOnTopOfTl = (this as any).os.i.client_settings.showPostFormOnTopOfTl;
+		(this as any).os.getMeta().then(meta => {
+			this.meta = meta;
+		});
 	},
 	methods: {
 		customizeHome() {
 			this.$router.push('/i/customize-home');
 			this.$emit('done');
 		},
-		onChangeShowPostFormOnTopOfTl() {
+		onChangeShowPostFormOnTopOfTl(v) {
 			(this as any).api('i/update_client_setting', {
 				name: 'showPostFormOnTopOfTl',
-				value: this.showPostFormOnTopOfTl
+				value: v
 			});
 		},
 		checkForUpdate() {
diff --git a/src/web/app/desktop/views/components/ui.header.vue b/src/web/app/desktop/views/components/ui.header.vue
index 2cbc33211..5425ec876 100644
--- a/src/web/app/desktop/views/components/ui.header.vue
+++ b/src/web/app/desktop/views/components/ui.header.vue
@@ -99,7 +99,7 @@ export default Vue.extend({
 	position -webkit-sticky
 	position sticky
 	top 0
-	z-index 1024
+	z-index 1000
 	width 100%
 	box-shadow 0 1px 1px rgba(0, 0, 0, 0.075)
 
@@ -109,14 +109,13 @@ export default Vue.extend({
 		> .backdrop
 			position absolute
 			top 0
-			z-index 1023
+			z-index 1000
 			width 100%
 			height 48px
-			backdrop-filter blur(12px)
 			background #f7f7f7
 
 		> .main
-			z-index 1024
+			z-index 1001
 			margin 0
 			padding 0
 			background-clip content-box
diff --git a/src/web/app/desktop/views/components/window.vue b/src/web/app/desktop/views/components/window.vue
index 1dba9a25a..5b08bc87e 100644
--- a/src/web/app/desktop/views/components/window.vue
+++ b/src/web/app/desktop/views/components/window.vue
@@ -431,7 +431,7 @@ export default Vue.extend({
 	> .bg
 		display block
 		position fixed
-		z-index 2048
+		z-index 2000
 		top 0
 		left 0
 		width 100%
@@ -443,7 +443,7 @@ export default Vue.extend({
 	> .main
 		display block
 		position fixed
-		z-index 2048
+		z-index 2000
 		top 15%
 		left 0
 		margin 0
@@ -526,7 +526,7 @@ export default Vue.extend({
 			> header
 				$header-height = 40px
 
-				z-index 128
+				z-index 1001
 				height $header-height
 				overflow hidden
 				white-space nowrap
diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts
index cfb129970..a4ef75d8e 100644
--- a/webpack/webpack.config.ts
+++ b/webpack/webpack.config.ts
@@ -60,7 +60,7 @@ module.exports = Object.keys(langs).map(lang => {
 			rules: [{
 				test: /\.vue$/,
 				exclude: /node_modules/,
-				use: ['cache-loader', {
+				use: [{
 					loader: 'vue-loader',
 					options: {
 						cssSourceMap: false,
@@ -134,7 +134,7 @@ module.exports = Object.keys(langs).map(lang => {
 				]
 			}, {
 				test: /\.(eot|woff|woff2|svg|ttf)([\?]?.*)$/,
-				loader: 'file-loader'
+				loader: 'url-loader'
 			}, {
 				test: /\.ts$/,
 				exclude: /node_modules/,

From e4b43e3e4c71802d70666c2eec6440e82c862586 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 3 Mar 2018 09:50:02 +0900
Subject: [PATCH 0568/1250] v3935

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 98f875b50..f31385c08 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3933",
+	"version": "0.0.3935",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 13f609fb048339c00365357ae4985ba9e24b1ec8 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 3 Mar 2018 11:13:28 +0900
Subject: [PATCH 0569/1250] Update settings.vue

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

diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index 6f0461bc2..e07f060e1 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -37,13 +37,16 @@
 					<el-option label="en-US" value="en"/>
 				</el-option-group>
 			</el-select>
+			<div class="none ui info">
+				<p>%fa:inffo-circle%変更はページの再度読み込み後に反映されます。</p>
+			</div>
 		</section>
 
 		<section class="web" v-show="page == 'web'">
 			<h1>キャッシュ</h1>
 			<button class="ui button" @click="clean">クリーンアップ</button>
-			<div class="none ui info">
-				<p>%fa:info-circle%クリーンアップを行うと、ブラウザに記憶されたアカウント情報のキャッシュ、書きかけの投稿・返信・メッセージ、およびその他のデータ(設定情報含む)が削除されます。クリーンアップを行った後はページを再度読み込みする必要があります。</p>
+			<div class="none ui info warn">
+				<p>%fa:exclamation-triangle%クリーンアップを行うと、ブラウザに記憶されたアカウント情報のキャッシュ、書きかけの投稿・返信・メッセージ、およびその他のデータ(設定情報含む)が削除されます。クリーンアップを行った後はページを再度読み込みする必要があります。</p>
 			</div>
 		</section>
 

From ec940b807dfee516c441cb617be4b6ef2ffd1e2e Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 3 Mar 2018 11:26:46 +0900
Subject: [PATCH 0570/1250] Update ui.header.vue

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

diff --git a/src/web/app/mobile/views/components/ui.header.vue b/src/web/app/mobile/views/components/ui.header.vue
index 1aff555f8..bf7833420 100644
--- a/src/web/app/mobile/views/components/ui.header.vue
+++ b/src/web/app/mobile/views/components/ui.header.vue
@@ -179,7 +179,7 @@ export default Vue.extend({
 				overflow hidden
 				text-overflow ellipsis
 
-				[data-fa]
+				[data-fa], [data-icon]
 					margin-right 8px
 
 				> img

From 3dd4a28fd28f782d5cb28c91578b19207410d34a Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 3 Mar 2018 11:27:05 +0900
Subject: [PATCH 0571/1250] Update drive.vue

---
 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 ea61661cf..200379f22 100644
--- a/src/web/app/mobile/views/pages/drive.vue
+++ b/src/web/app/mobile/views/pages/drive.vue
@@ -2,7 +2,7 @@
 <mk-ui>
 	<span slot="header">
 		<template v-if="folder">%fa:R folder-open%{{ folder.name }}</template>
-		<template v-if="file"><mk-file-type-icon class="icon" :type="file.type"/>{{ file.name }}</template>
+		<template v-if="file"><mk-file-type-icon data-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="func"><button @click="fn">%fa:ellipsis-h%</button></template>

From 741725ea5b7c06bb4ff92a6c4a515bb260ea9420 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 3 Mar 2018 13:47:55 +0900
Subject: [PATCH 0572/1250] :v:

---
 .../common/views/components/autocomplete.vue  |  2 +
 .../views/components/connect-failed.vue       |  2 +
 .../app/common/views/components/forkit.vue    | 48 +++++++++---------
 .../views/components/messaging-room.form.vue  |  2 +
 .../views/components/messaging-room.vue       |  2 +
 .../app/common/views/components/messaging.vue |  2 +
 .../common/views/components/poll-editor.vue   |  2 +
 src/web/app/common/views/components/poll.vue  |  2 +
 .../views/components/reaction-picker.vue      |  2 +
 .../app/common/views/components/signin.vue    |  2 +
 .../app/common/views/components/signup.vue    |  2 +
 .../app/common/views/components/switch.vue    |  2 +
 .../app/common/views/components/uploader.vue  |  2 +
 src/web/app/common/views/widgets/calendar.vue |  2 +
 .../app/desktop/views/components/calendar.vue |  2 +
 .../choose-file-from-drive-window.vue         |  2 +
 .../choose-folder-from-drive-window.vue       |  2 +
 .../views/components/context-menu.menu.vue    |  2 +
 .../desktop/views/components/crop-window.vue  |  2 +
 .../app/desktop/views/components/dialog.vue   |  4 ++
 .../desktop/views/components/drive.file.vue   |  2 +
 .../desktop/views/components/drive.folder.vue |  2 +
 .../app/desktop/views/components/drive.vue    |  2 +
 .../views/components/follow-button.vue        |  2 +
 src/web/app/desktop/views/components/home.vue |  2 +
 .../desktop/views/components/input-dialog.vue |  2 +
 .../app/desktop/views/components/mentions.vue |  2 +
 .../desktop/views/components/post-detail.vue  |  2 +
 .../desktop/views/components/post-form.vue    |  2 +
 .../desktop/views/components/posts.post.vue   |  2 +
 .../views/components/progress-dialog.vue      |  2 +
 .../desktop/views/components/repost-form.vue  |  2 +
 .../app/desktop/views/components/settings.vue |  2 +
 .../views/components/ui.header.account.vue    |  2 +
 .../views/components/ui.header.nav.vue        |  2 +
 .../components/ui.header.notifications.vue    |  2 +
 .../views/components/ui.header.post.vue       |  2 +
 .../views/components/ui.header.search.vue     |  2 +
 .../desktop/views/components/user-preview.vue |  2 +
 .../desktop/views/components/users-list.vue   |  2 +
 .../app/desktop/views/components/window.vue   |  2 +
 .../app/desktop/views/pages/selectdrive.vue   |  2 +
 .../desktop/views/pages/user/user.header.vue  |  2 +
 .../views/pages/user/user.timeline.vue        |  2 +
 src/web/app/desktop/views/pages/welcome.vue   |  2 +
 .../app/desktop/views/widgets/post-form.vue   |  2 +
 .../mobile/views/components/drive.file.vue    |  2 +
 .../mobile/views/components/follow-button.vue |  2 +
 .../mobile/views/components/post-detail.vue   |  2 +
 .../app/mobile/views/components/post-form.vue |  2 +
 .../mobile/views/components/posts.post.vue    |  2 +
 src/web/app/mobile/views/components/posts.vue |  2 +
 .../app/mobile/views/components/ui.header.vue |  2 +
 .../app/mobile/views/components/ui.nav.vue    |  2 +
 .../mobile/views/components/users-list.vue    |  2 +
 .../mobile/views/pages/profile-setting.vue    |  2 +
 src/web/app/mobile/views/pages/user.vue       |  2 +
 webpack/webpack.config.ts                     | 50 ++++++++++---------
 58 files changed, 165 insertions(+), 47 deletions(-)

diff --git a/src/web/app/common/views/components/autocomplete.vue b/src/web/app/common/views/components/autocomplete.vue
index 2ad951b1f..6d7d5cd1b 100644
--- a/src/web/app/common/views/components/autocomplete.vue
+++ b/src/web/app/common/views/components/autocomplete.vue
@@ -223,6 +223,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-autocomplete
 	position fixed
 	z-index 65535
diff --git a/src/web/app/common/views/components/connect-failed.vue b/src/web/app/common/views/components/connect-failed.vue
index b48f7cecb..185250dbd 100644
--- a/src/web/app/common/views/components/connect-failed.vue
+++ b/src/web/app/common/views/components/connect-failed.vue
@@ -39,6 +39,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-connect-failed
 	width 100%
 	padding 32px 18px
diff --git a/src/web/app/common/views/components/forkit.vue b/src/web/app/common/views/components/forkit.vue
index 54fc011d1..6f334b965 100644
--- a/src/web/app/common/views/components/forkit.vue
+++ b/src/web/app/common/views/components/forkit.vue
@@ -9,32 +9,34 @@
 </template>
 
 <style lang="stylus" scoped>
-	.a
+@import '~const.styl'
+
+.a
+	display block
+	position absolute
+	top 0
+	right 0
+
+	> svg
 		display block
-		position absolute
-		top 0
-		right 0
+		//fill #151513
+		//color #fff
+		fill $theme-color
+		color $theme-color-foreground
 
-		> svg
-			display block
-			//fill #151513
-			//color #fff
-			fill $theme-color
-			color $theme-color-foreground
+		.octo-arm
+			transform-origin 130px 106px
 
-			.octo-arm
-				transform-origin 130px 106px
+	&:hover
+		.octo-arm
+			animation octocat-wave 560ms ease-in-out
 
-		&: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)
+	@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/messaging-room.form.vue b/src/web/app/common/views/components/messaging-room.form.vue
index edcda069a..01886b19c 100644
--- a/src/web/app/common/views/components/messaging-room.form.vue
+++ b/src/web/app/common/views/components/messaging-room.form.vue
@@ -195,6 +195,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-messaging-form
 	> textarea
 		cursor auto
diff --git a/src/web/app/common/views/components/messaging-room.vue b/src/web/app/common/views/components/messaging-room.vue
index f7f31c557..0a675ba03 100644
--- a/src/web/app/common/views/components/messaging-room.vue
+++ b/src/web/app/common/views/components/messaging-room.vue
@@ -228,6 +228,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-messaging-room
 	display flex
 	flex 1
diff --git a/src/web/app/common/views/components/messaging.vue b/src/web/app/common/views/components/messaging.vue
index ea75c9081..fcc2dbe31 100644
--- a/src/web/app/common/views/components/messaging.vue
+++ b/src/web/app/common/views/components/messaging.vue
@@ -165,6 +165,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-messaging
 
 	&[data-compact]
diff --git a/src/web/app/common/views/components/poll-editor.vue b/src/web/app/common/views/components/poll-editor.vue
index 20a334d3c..47d901d7b 100644
--- a/src/web/app/common/views/components/poll-editor.vue
+++ b/src/web/app/common/views/components/poll-editor.vue
@@ -67,6 +67,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-poll-editor
 	padding 8px
 
diff --git a/src/web/app/common/views/components/poll.vue b/src/web/app/common/views/components/poll.vue
index 556d8ebf6..8156c8bc5 100644
--- a/src/web/app/common/views/components/poll.vue
+++ b/src/web/app/common/views/components/poll.vue
@@ -66,6 +66,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-poll
 
 	> ul
diff --git a/src/web/app/common/views/components/reaction-picker.vue b/src/web/app/common/views/components/reaction-picker.vue
index f3731cd63..0ba15ba73 100644
--- a/src/web/app/common/views/components/reaction-picker.vue
+++ b/src/web/app/common/views/components/reaction-picker.vue
@@ -106,6 +106,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 $border-color = rgba(27, 31, 35, 0.15)
 
 .mk-reaction-picker
diff --git a/src/web/app/common/views/components/signin.vue b/src/web/app/common/views/components/signin.vue
index 31243e99a..1738d0df7 100644
--- a/src/web/app/common/views/components/signin.vue
+++ b/src/web/app/common/views/components/signin.vue
@@ -53,6 +53,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-signin
 	&.signing
 		&, *
diff --git a/src/web/app/common/views/components/signup.vue b/src/web/app/common/views/components/signup.vue
index 1fdc49a18..2ca1687be 100644
--- a/src/web/app/common/views/components/signup.vue
+++ b/src/web/app/common/views/components/signup.vue
@@ -153,6 +153,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-signup
 	min-width 302px
 
diff --git a/src/web/app/common/views/components/switch.vue b/src/web/app/common/views/components/switch.vue
index e6cdfa152..2ac57be6f 100644
--- a/src/web/app/common/views/components/switch.vue
+++ b/src/web/app/common/views/components/switch.vue
@@ -84,6 +84,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-switch
 	display flex
 	margin 8px 0
diff --git a/src/web/app/common/views/components/uploader.vue b/src/web/app/common/views/components/uploader.vue
index 6367b6997..6465ad35e 100644
--- a/src/web/app/common/views/components/uploader.vue
+++ b/src/web/app/common/views/components/uploader.vue
@@ -81,6 +81,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-uploader
 	overflow auto
 
diff --git a/src/web/app/common/views/widgets/calendar.vue b/src/web/app/common/views/widgets/calendar.vue
index 2bcdb07f9..03f69a759 100644
--- a/src/web/app/common/views/widgets/calendar.vue
+++ b/src/web/app/common/views/widgets/calendar.vue
@@ -107,6 +107,8 @@ export default define({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mkw-calendar
 	padding 16px 0
 	color #777
diff --git a/src/web/app/desktop/views/components/calendar.vue b/src/web/app/desktop/views/components/calendar.vue
index 08b08f8d4..71aab2e8a 100644
--- a/src/web/app/desktop/views/components/calendar.vue
+++ b/src/web/app/desktop/views/components/calendar.vue
@@ -131,6 +131,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-calendar
 	color #777
 	background #fff
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 232282745..9a1e9c958 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
@@ -59,6 +59,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" module>
+@import '~const.styl'
+
 .title
 	> [data-fa]
 		margin-right 4px
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 8111ffcf0..f99533176 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
@@ -37,6 +37,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" module>
+@import '~const.styl'
+
 .title
 	> [data-fa]
 		margin-right 4px
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 e2c34a591..6359dbf1b 100644
--- a/src/web/app/desktop/views/components/context-menu.menu.vue
+++ b/src/web/app/desktop/views/components/context-menu.menu.vue
@@ -29,6 +29,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .menu
 	$width = 240px
 	$item-height = 38px
diff --git a/src/web/app/desktop/views/components/crop-window.vue b/src/web/app/desktop/views/components/crop-window.vue
index 27d89a9ff..eb6a55d95 100644
--- a/src/web/app/desktop/views/components/crop-window.vue
+++ b/src/web/app/desktop/views/components/crop-window.vue
@@ -61,6 +61,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" module>
+@import '~const.styl'
+
 .header
 	> [data-fa]
 		margin-right 4px
diff --git a/src/web/app/desktop/views/components/dialog.vue b/src/web/app/desktop/views/components/dialog.vue
index e89e8654e..fa17e4a9d 100644
--- a/src/web/app/desktop/views/components/dialog.vue
+++ b/src/web/app/desktop/views/components/dialog.vue
@@ -91,6 +91,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-dialog
 	> .bg
 		display block
@@ -151,6 +153,8 @@ export default Vue.extend({
 </style>
 
 <style lang="stylus" module>
+@import '~const.styl'
+
 .header
 	margin 1em 0
 	color $theme-color
diff --git a/src/web/app/desktop/views/components/drive.file.vue b/src/web/app/desktop/views/components/drive.file.vue
index 6390ed351..924ff7052 100644
--- a/src/web/app/desktop/views/components/drive.file.vue
+++ b/src/web/app/desktop/views/components/drive.file.vue
@@ -184,6 +184,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .root.file
 	padding 8px 0 0 0
 	height 180px
diff --git a/src/web/app/desktop/views/components/drive.folder.vue b/src/web/app/desktop/views/components/drive.folder.vue
index d5f4b11ee..a8a9a0137 100644
--- a/src/web/app/desktop/views/components/drive.folder.vue
+++ b/src/web/app/desktop/views/components/drive.folder.vue
@@ -218,6 +218,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .root.folder
 	padding 8px
 	height 64px
diff --git a/src/web/app/desktop/views/components/drive.vue b/src/web/app/desktop/views/components/drive.vue
index 1d84c2409..99036bed8 100644
--- a/src/web/app/desktop/views/components/drive.vue
+++ b/src/web/app/desktop/views/components/drive.vue
@@ -568,6 +568,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-drive
 
 	> nav
diff --git a/src/web/app/desktop/views/components/follow-button.vue b/src/web/app/desktop/views/components/follow-button.vue
index 9056307bb..fc4f87188 100644
--- a/src/web/app/desktop/views/components/follow-button.vue
+++ b/src/web/app/desktop/views/components/follow-button.vue
@@ -92,6 +92,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-follow-button
 	display block
 	cursor pointer
diff --git a/src/web/app/desktop/views/components/home.vue b/src/web/app/desktop/views/components/home.vue
index a7c61f4c5..562b22d11 100644
--- a/src/web/app/desktop/views/components/home.vue
+++ b/src/web/app/desktop/views/components/home.vue
@@ -214,6 +214,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-home
 	display block
 
diff --git a/src/web/app/desktop/views/components/input-dialog.vue b/src/web/app/desktop/views/components/input-dialog.vue
index a735ce0f3..e27bc8da8 100644
--- a/src/web/app/desktop/views/components/input-dialog.vue
+++ b/src/web/app/desktop/views/components/input-dialog.vue
@@ -77,6 +77,8 @@ export default Vue.extend({
 
 
 <style lang="stylus" module>
+@import '~const.styl'
+
 .header
 	> [data-fa]
 		margin-right 4px
diff --git a/src/web/app/desktop/views/components/mentions.vue b/src/web/app/desktop/views/components/mentions.vue
index 28ba59f2b..47066e813 100644
--- a/src/web/app/desktop/views/components/mentions.vue
+++ b/src/web/app/desktop/views/components/mentions.vue
@@ -81,6 +81,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-mentions
 	background #fff
 	border solid 1px rgba(0, 0, 0, 0.075)
diff --git a/src/web/app/desktop/views/components/post-detail.vue b/src/web/app/desktop/views/components/post-detail.vue
index b41053133..32d401351 100644
--- a/src/web/app/desktop/views/components/post-detail.vue
+++ b/src/web/app/desktop/views/components/post-detail.vue
@@ -177,6 +177,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-post-detail
 	margin 0
 	padding 0
diff --git a/src/web/app/desktop/views/components/post-form.vue b/src/web/app/desktop/views/components/post-form.vue
index b94b0f853..1f98aed08 100644
--- a/src/web/app/desktop/views/components/post-form.vue
+++ b/src/web/app/desktop/views/components/post-form.vue
@@ -250,6 +250,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-post-form
 	display block
 	padding 16px
diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index 458e244b3..52531818e 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -256,6 +256,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .post
 	margin 0
 	padding 0
diff --git a/src/web/app/desktop/views/components/progress-dialog.vue b/src/web/app/desktop/views/components/progress-dialog.vue
index ed49b19d7..a4292e1ae 100644
--- a/src/web/app/desktop/views/components/progress-dialog.vue
+++ b/src/web/app/desktop/views/components/progress-dialog.vue
@@ -37,6 +37,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" module>
+@import '~const.styl'
+
 .body
 	padding 18px 24px 24px 24px
 
diff --git a/src/web/app/desktop/views/components/repost-form.vue b/src/web/app/desktop/views/components/repost-form.vue
index 5bf7eaaf0..f2774b817 100644
--- a/src/web/app/desktop/views/components/repost-form.vue
+++ b/src/web/app/desktop/views/components/repost-form.vue
@@ -57,6 +57,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-repost-form
 
 	> .mk-post-preview
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index e07f060e1..04004d2ff 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -222,6 +222,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-settings
 	display flex
 	width 100%
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 ad6533339..2cc2c1867 100644
--- a/src/web/app/desktop/views/components/ui.header.account.vue
+++ b/src/web/app/desktop/views/components/ui.header.account.vue
@@ -84,6 +84,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .account
 	> .header
 		display block
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 c102d5b3f..a5b6ecd6f 100644
--- a/src/web/app/desktop/views/components/ui.header.nav.vue
+++ b/src/web/app/desktop/views/components/ui.header.nav.vue
@@ -86,6 +86,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .nav
 	display inline-block
 	margin 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
index 5467dda85..e829418d1 100644
--- a/src/web/app/desktop/views/components/ui.header.notifications.vue
+++ b/src/web/app/desktop/views/components/ui.header.notifications.vue
@@ -82,6 +82,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .notifications
 
 	> button
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 e8ed380f0..c2f0e07dd 100644
--- a/src/web/app/desktop/views/components/ui.header.post.vue
+++ b/src/web/app/desktop/views/components/ui.header.post.vue
@@ -17,6 +17,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .post
 	display inline-block
 	padding 8px
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 c063de6bb..86215556a 100644
--- a/src/web/app/desktop/views/components/ui.header.search.vue
+++ b/src/web/app/desktop/views/components/ui.header.search.vue
@@ -24,6 +24,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .search
 
 	> [data-fa]
diff --git a/src/web/app/desktop/views/components/user-preview.vue b/src/web/app/desktop/views/components/user-preview.vue
index 2a4bd7cf7..cfb95961d 100644
--- a/src/web/app/desktop/views/components/user-preview.vue
+++ b/src/web/app/desktop/views/components/user-preview.vue
@@ -83,6 +83,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-user-preview
 	position absolute
 	z-index 2048
diff --git a/src/web/app/desktop/views/components/users-list.vue b/src/web/app/desktop/views/components/users-list.vue
index fd15f478d..a08e76f57 100644
--- a/src/web/app/desktop/views/components/users-list.vue
+++ b/src/web/app/desktop/views/components/users-list.vue
@@ -69,6 +69,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-users-list
 	height 100%
 	background #fff
diff --git a/src/web/app/desktop/views/components/window.vue b/src/web/app/desktop/views/components/window.vue
index 5b08bc87e..a92cfd0b6 100644
--- a/src/web/app/desktop/views/components/window.vue
+++ b/src/web/app/desktop/views/components/window.vue
@@ -425,6 +425,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-window
 	display block
 
diff --git a/src/web/app/desktop/views/pages/selectdrive.vue b/src/web/app/desktop/views/pages/selectdrive.vue
index b1f00da2b..4f0b86014 100644
--- a/src/web/app/desktop/views/pages/selectdrive.vue
+++ b/src/web/app/desktop/views/pages/selectdrive.vue
@@ -54,6 +54,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mkp-selectdrive
 	display block
 	position fixed
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 6c8375f16..b2119e52e 100644
--- a/src/web/app/desktop/views/pages/user/user.header.vue
+++ b/src/web/app/desktop/views/pages/user/user.header.vue
@@ -61,6 +61,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .header
 	$banner-height = 320px
 	$footer-height = 58px
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 d8fff6ce6..60eef8951 100644
--- a/src/web/app/desktop/views/pages/user/user.timeline.vue
+++ b/src/web/app/desktop/views/pages/user/user.timeline.vue
@@ -97,6 +97,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .timeline
 	background #fff
 
diff --git a/src/web/app/desktop/views/pages/welcome.vue b/src/web/app/desktop/views/pages/welcome.vue
index f359ce008..9f6930520 100644
--- a/src/web/app/desktop/views/pages/welcome.vue
+++ b/src/web/app/desktop/views/pages/welcome.vue
@@ -57,6 +57,8 @@ export default Vue.extend({
 </style>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-welcome
 	display flex
 	flex-direction column
diff --git a/src/web/app/desktop/views/widgets/post-form.vue b/src/web/app/desktop/views/widgets/post-form.vue
index e51b4f357..cf7fd1f2b 100644
--- a/src/web/app/desktop/views/widgets/post-form.vue
+++ b/src/web/app/desktop/views/widgets/post-form.vue
@@ -54,6 +54,8 @@ export default define({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mkw-post-form
 	background #fff
 	overflow hidden
diff --git a/src/web/app/mobile/views/components/drive.file.vue b/src/web/app/mobile/views/components/drive.file.vue
index dfc69e249..a41d9bcfd 100644
--- a/src/web/app/mobile/views/components/drive.file.vue
+++ b/src/web/app/mobile/views/components/drive.file.vue
@@ -67,6 +67,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .file
 	display block
 	text-decoration none !important
diff --git a/src/web/app/mobile/views/components/follow-button.vue b/src/web/app/mobile/views/components/follow-button.vue
index 2d45ea215..fb6eaa39c 100644
--- a/src/web/app/mobile/views/components/follow-button.vue
+++ b/src/web/app/mobile/views/components/follow-button.vue
@@ -82,6 +82,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-follow-button
 	display block
 	user-select none
diff --git a/src/web/app/mobile/views/components/post-detail.vue b/src/web/app/mobile/views/components/post-detail.vue
index 05ee07123..7e6217f60 100644
--- a/src/web/app/mobile/views/components/post-detail.vue
+++ b/src/web/app/mobile/views/components/post-detail.vue
@@ -175,6 +175,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-post-detail
 	overflow hidden
 	margin 0 auto
diff --git a/src/web/app/mobile/views/components/post-form.vue b/src/web/app/mobile/views/components/post-form.vue
index 7a6eb7741..63b75b92f 100644
--- a/src/web/app/mobile/views/components/post-form.vue
+++ b/src/web/app/mobile/views/components/post-form.vue
@@ -115,6 +115,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-post-form
 	max-width 500px
 	width calc(100% - 16px)
diff --git a/src/web/app/mobile/views/components/posts.post.vue b/src/web/app/mobile/views/components/posts.post.vue
index 3038cdb0e..39bb80457 100644
--- a/src/web/app/mobile/views/components/posts.post.vue
+++ b/src/web/app/mobile/views/components/posts.post.vue
@@ -195,6 +195,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .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 34fb0749a..9a9f9a313 100644
--- a/src/web/app/mobile/views/components/posts.vue
+++ b/src/web/app/mobile/views/components/posts.vue
@@ -49,6 +49,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-posts
 	background #fff
 	border-radius 8px
diff --git a/src/web/app/mobile/views/components/ui.header.vue b/src/web/app/mobile/views/components/ui.header.vue
index bf7833420..d07977cf5 100644
--- a/src/web/app/mobile/views/components/ui.header.vue
+++ b/src/web/app/mobile/views/components/ui.header.vue
@@ -128,6 +128,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .header
 	$height = 48px
 
diff --git a/src/web/app/mobile/views/components/ui.nav.vue b/src/web/app/mobile/views/components/ui.nav.vue
index a3c0042c3..62ea83526 100644
--- a/src/web/app/mobile/views/components/ui.nav.vue
+++ b/src/web/app/mobile/views/components/ui.nav.vue
@@ -109,6 +109,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .nav
 	.backdrop
 		position fixed
diff --git a/src/web/app/mobile/views/components/users-list.vue b/src/web/app/mobile/views/components/users-list.vue
index d6c626135..b11e4549d 100644
--- a/src/web/app/mobile/views/components/users-list.vue
+++ b/src/web/app/mobile/views/components/users-list.vue
@@ -65,6 +65,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-users-list
 
 	> nav
diff --git a/src/web/app/mobile/views/pages/profile-setting.vue b/src/web/app/mobile/views/pages/profile-setting.vue
index 432a850e4..f25d4bbe8 100644
--- a/src/web/app/mobile/views/pages/profile-setting.vue
+++ b/src/web/app/mobile/views/pages/profile-setting.vue
@@ -108,6 +108,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" module>
+@import '~const.styl'
+
 .content
 	margin 8px auto
 	max-width 500px
diff --git a/src/web/app/mobile/views/pages/user.vue b/src/web/app/mobile/views/pages/user.vue
index ae2fbe85d..90f49a99a 100644
--- a/src/web/app/mobile/views/pages/user.vue
+++ b/src/web/app/mobile/views/pages/user.vue
@@ -105,6 +105,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 main
 	> header
 		box-shadow 0 4px 4px rgba(0, 0, 0, 0.3)
diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts
index a4ef75d8e..c4ef4b90f 100644
--- a/webpack/webpack.config.ts
+++ b/webpack/webpack.config.ts
@@ -72,20 +72,6 @@ module.exports = Object.keys(langs).map(lang => {
 						search: /%base64:(.+?)%/g.toString(),
 						replace: 'base64replacement'
 					}
-				}, {
-					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: {
@@ -108,10 +94,16 @@ module.exports = Object.keys(langs).map(lang => {
 			}, {
 				test: /\.styl$/,
 				exclude: /node_modules/,
-				use: [
-					{ loader: 'style-loader' },
-					{ loader: 'css-loader' },
-					{ loader: 'stylus-loader' }
+				use: [{
+					loader: 'style-loader'
+				}, {
+					loader: 'css-loader',
+					options: {
+						minimize: true
+					}
+				}, {
+					loader: 'stylus-loader'
+				}
 				]
 			}, {
 				test: /\.scss$/,
@@ -119,7 +111,10 @@ module.exports = Object.keys(langs).map(lang => {
 				use: [{
 					loader: 'style-loader'
 				}, {
-					loader: 'css-loader'
+					loader: 'css-loader',
+					options: {
+						minimize: true
+					}
 				}, {
 					loader: 'sass-loader',
 					options: {
@@ -128,10 +123,14 @@ module.exports = Object.keys(langs).map(lang => {
 				}]
 			}, {
 				test: /\.css$/,
-				use: [
-					{ loader: 'style-loader' },
-					{ loader: 'css-loader' }
-				]
+				use: [{
+					loader: 'style-loader'
+				}, {
+					loader: 'css-loader',
+					options: {
+						minimize: true
+					}
+				}]
 			}, {
 				test: /\.(eot|woff|woff2|svg|ttf)([\?]?.*)$/,
 				loader: 'url-loader'
@@ -165,7 +164,10 @@ module.exports = Object.keys(langs).map(lang => {
 		resolve: {
 			extensions: [
 				'.js', '.ts', '.json'
-			]
+			],
+			alias: {
+				'const.styl': __dirname + '/../src/web/const.styl'
+			}
 		},
 		resolveLoader: {
 			modules: ['node_modules', './webpack/loaders']

From 3e092a4211f93538b2db0dafe45867202cffd77d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 3 Mar 2018 14:25:36 +0900
Subject: [PATCH 0573/1250] :v:

---
 src/web/app/auth/views/form.vue               |  1 +
 .../app/common/views/components/switch.vue    |  2 +-
 .../views/components/settings.profile.vue     |  2 +-
 .../app/desktop/views/components/settings.vue |  2 +-
 src/web/app/init.ts                           | 22 +++++++++++++------
 src/web/app/reset.styl                        |  1 -
 6 files changed, 19 insertions(+), 11 deletions(-)

diff --git a/src/web/app/auth/views/form.vue b/src/web/app/auth/views/form.vue
index 30ad64ed2..d86ed58b3 100644
--- a/src/web/app/auth/views/form.vue
+++ b/src/web/app/auth/views/form.vue
@@ -123,6 +123,7 @@ export default Vue.extend({
 
 		> button
 			margin 0 8px
+			padding 0
 
 	@media (max-width 600px)
 		> header
diff --git a/src/web/app/common/views/components/switch.vue b/src/web/app/common/views/components/switch.vue
index 2ac57be6f..ffbab843e 100644
--- a/src/web/app/common/views/components/switch.vue
+++ b/src/web/app/common/views/components/switch.vue
@@ -88,7 +88,7 @@ export default Vue.extend({
 
 .mk-switch
 	display flex
-	margin 8px 0
+	margin 12px 0
 	cursor pointer
 	transition all 0.3s
 
diff --git a/src/web/app/desktop/views/components/settings.profile.vue b/src/web/app/desktop/views/components/settings.profile.vue
index b57ac1028..23a166376 100644
--- a/src/web/app/desktop/views/components/settings.profile.vue
+++ b/src/web/app/desktop/views/components/settings.profile.vue
@@ -19,7 +19,7 @@
 	</label>
 	<label class="ui from group">
 		<p>%i18n:desktop.tags.mk-profile-setting.birthday%</p>
-		<input v-model="birthday" type="date" class="ui"/>
+		<el-date-picker v-model="birthday" type="date" value-format="yyyy-MM-dd"/>
 	</label>
 	<button class="ui primary" @click="save">%i18n:desktop.tags.mk-profile-setting.save%</button>
 	<section>
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index 04004d2ff..182a9a1d5 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -38,7 +38,7 @@
 				</el-option-group>
 			</el-select>
 			<div class="none ui info">
-				<p>%fa:inffo-circle%変更はページの再度読み込み後に反映されます。</p>
+				<p>%fa:info-circle%変更はページの再度読み込み後に反映されます。</p>
 			</div>
 		</section>
 
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index dc3057935..716fe45e7 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -6,10 +6,24 @@ import Vue from 'vue';
 import VueRouter from 'vue-router';
 import VModal from 'vue-js-modal';
 import Element from 'element-ui';
+import ElementLocaleEn from 'element-ui/lib/locale/lang/en';
+import ElementLocaleJa from 'element-ui/lib/locale/lang/ja';
+
+import App from './app.vue';
+import checkForUpdate from './common/scripts/check-for-update';
+import MiOS, { API } from './common/mios';
+import { version, host, lang } from './config';
+
+let elementLocale;
+switch (lang) {
+	case 'ja': elementLocale = ElementLocaleJa; break;
+	case 'en': elementLocale = ElementLocaleEn; break;
+	default: elementLocale = ElementLocaleEn; break;
+}
 
 Vue.use(VueRouter);
 Vue.use(VModal);
-Vue.use(Element);
+Vue.use(Element, { locale: elementLocale });
 
 // Register global directives
 require('./common/views/directives');
@@ -29,12 +43,6 @@ Vue.mixin({
 	}
 });
 
-import App from './app.vue';
-
-import checkForUpdate from './common/scripts/check-for-update';
-import MiOS, { API } from './common/mios';
-import { version, host, lang } from './config';
-
 /**
  * APP ENTRY POINT!
  */
diff --git a/src/web/app/reset.styl b/src/web/app/reset.styl
index db4d87486..10bd3113a 100644
--- a/src/web/app/reset.styl
+++ b/src/web/app/reset.styl
@@ -16,7 +16,6 @@ textarea
 
 button
 	margin 0
-	padding 0
 	background transparent
 	border none
 	cursor pointer

From 5bf7a748809aaa26ca6c8741151074971c5084a8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 3 Mar 2018 14:42:25 +0900
Subject: [PATCH 0574/1250] :v:

---
 src/api/stream/home.ts            |  4 +++
 src/web/app/common/mios.ts        | 55 +++++++++++++++++++++++++++++--
 src/web/app/common/scripts/api.ts | 47 --------------------------
 3 files changed, 56 insertions(+), 50 deletions(-)
 delete mode 100644 src/web/app/common/scripts/api.ts

diff --git a/src/api/stream/home.ts b/src/api/stream/home.ts
index 10078337c..cc3fb885e 100644
--- a/src/api/stream/home.ts
+++ b/src/api/stream/home.ts
@@ -66,6 +66,10 @@ export default async function(request: websocket.request, connection: websocket.
 		const msg = JSON.parse(data.utf8Data);
 
 		switch (msg.type) {
+			case 'api':
+				// TODO
+				break;
+
 			case 'alive':
 				// Update lastUsedAt
 				User.update({ _id: user._id }, {
diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
index 3701a24c3..bc83ec0bb 100644
--- a/src/web/app/common/mios.ts
+++ b/src/web/app/common/mios.ts
@@ -2,7 +2,6 @@ import Vue from 'vue';
 import { EventEmitter } from 'eventemitter3';
 
 import { host, apiUrl, swPublickey, version, lang } from '../config';
-import api from './scripts/api';
 import Progress from './scripts/loading';
 import HomeStreamManager from './scripts/streaming/home-stream-manager';
 import DriveStreamManager from './scripts/streaming/drive-stream-manager';
@@ -12,6 +11,11 @@ import MessagingIndexStreamManager from './scripts/streaming/messaging-index-str
 
 import Err from '../common/views/components/connect-failed.vue';
 
+//#region api requests
+let spinner = null;
+let pending = 0;
+//#endregion
+
 export type API = {
 	chooseDriveFile: (opts: {
 		title?: string;
@@ -365,8 +369,53 @@ export default class MiOS extends EventEmitter {
 	 * @param endpoint エンドポイント名
 	 * @param data パラメータ
 	 */
-	public api(endpoint: string, data?: { [x: string]: any }) {
-		return api(this.i, endpoint, data);
+	public api(endpoint: string, data: { [x: string]: any } = {}): Promise<{ [x: string]: any }> {
+		if (++pending === 1) {
+			spinner = document.createElement('div');
+			spinner.setAttribute('id', 'wait');
+			document.body.appendChild(spinner);
+		}
+
+		// Append a credential
+		if (this.isSignedIn) (data as any).i = this.i.token;
+
+		// TODO
+		//const viaStream = localStorage.getItem('enableExperimental') == 'true';
+
+		return new Promise((resolve, reject) => {
+			/*if (viaStream) {
+				const stream = this.stream.borrow();
+				const id = Math.random().toString();
+				stream.once(`api-res:${id}`, res => {
+					resolve(res);
+				});
+				stream.send({
+					type: 'api',
+					id,
+					endpoint,
+					data
+				});
+			} else {*/
+				// Send request
+				fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, {
+					method: 'POST',
+					body: JSON.stringify(data),
+					credentials: endpoint === 'signin' ? 'include' : 'omit',
+					cache: 'no-cache'
+				}).then(res => {
+					if (--pending === 0) spinner.parentNode.removeChild(spinner);
+					if (res.status === 200) {
+						res.json().then(resolve);
+					} else if (res.status === 204) {
+						resolve();
+					} else {
+						res.json().then(err => {
+							reject(err.error);
+						}, reject);
+					}
+				}).catch(reject);
+			/*}*/
+		});
 	}
 
 	/**
diff --git a/src/web/app/common/scripts/api.ts b/src/web/app/common/scripts/api.ts
deleted file mode 100644
index bba838f56..000000000
--- a/src/web/app/common/scripts/api.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * API Request
- */
-
-declare const _API_URL_: string;
-
-let spinner = null;
-let pending = 0;
-
-/**
- * Send a request to API
- * @param  {string|Object} i  Credential
- * @param  {string} endpoint  Endpoint
- * @param  {any} [data={}] Data
- * @return {Promise<any>} Response
- */
-export default (i, endpoint, data = {}): Promise<{ [x: string]: any }> => {
-	if (++pending === 1) {
-		spinner = document.createElement('div');
-		spinner.setAttribute('id', 'wait');
-		document.body.appendChild(spinner);
-	}
-
-	// Append the credential
-	if (i != null) (data as any).i = typeof i === 'object' ? i.token : i;
-
-	return new Promise((resolve, reject) => {
-		// Send request
-		fetch(endpoint.indexOf('://') > -1 ? endpoint : `${_API_URL_}/${endpoint}`, {
-			method: 'POST',
-			body: JSON.stringify(data),
-			credentials: endpoint === 'signin' ? 'include' : 'omit',
-			cache: 'no-cache'
-		}).then(res => {
-			if (--pending === 0) spinner.parentNode.removeChild(spinner);
-			if (res.status === 200) {
-				res.json().then(resolve);
-			} else if (res.status === 204) {
-				resolve();
-			} else {
-				res.json().then(err => {
-					reject(err.error);
-				}, reject);
-			}
-		}).catch(reject);
-	});
-};

From 91619ec6e5ea80d278ec25bd52a44091008abb0b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 3 Mar 2018 16:20:28 +0900
Subject: [PATCH 0575/1250] :art:

---
 src/web/app/desktop/views/components/posts.post.vue | 1 +
 src/web/app/mobile/views/components/posts.post.vue  | 1 +
 2 files changed, 2 insertions(+)

diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index 52531818e..4a95918c4 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -365,6 +365,7 @@ export default Vue.extend({
 
 			> header
 				display flex
+				align-items center
 				margin-bottom 4px
 				white-space nowrap
 
diff --git a/src/web/app/mobile/views/components/posts.post.vue b/src/web/app/mobile/views/components/posts.post.vue
index 39bb80457..3c02e1e99 100644
--- a/src/web/app/mobile/views/components/posts.post.vue
+++ b/src/web/app/mobile/views/components/posts.post.vue
@@ -304,6 +304,7 @@ export default Vue.extend({
 
 			> header
 				display flex
+				align-items center
 				white-space nowrap
 
 				@media (min-width 500px)

From 57e0e4011db4ff418fa65b8c30df386d370d9011 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 3 Mar 2018 16:27:07 +0900
Subject: [PATCH 0576/1250] :art:

---
 src/web/app/common/views/components/messaging.vue | 13 +++++--------
 1 file changed, 5 insertions(+), 8 deletions(-)

diff --git a/src/web/app/common/views/components/messaging.vue b/src/web/app/common/views/components/messaging.vue
index fcc2dbe31..e2fdc502d 100644
--- a/src/web/app/common/views/components/messaging.vue
+++ b/src/web/app/common/views/components/messaging.vue
@@ -368,30 +368,27 @@ export default Vue.extend({
 					clear both
 
 				> header
+					display flex
+					align-items center
 					margin-bottom 2px
 					white-space nowrap
-					overflow hidden
 
 					> .name
-						text-align left
-						display inline
 						margin 0
 						padding 0
+						overflow hidden
+						text-overflow ellipsis
 						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
+						margin 0 0 0 auto
 						color rgba(0, 0, 0, 0.5)
 						font-size 80%
 

From 5ba0ecef1680b53ee10789be26450f3c39719ba9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 3 Mar 2018 16:35:15 +0900
Subject: [PATCH 0577/1250] Fix bug

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

diff --git a/src/web/app/desktop/views/components/post-form.vue b/src/web/app/desktop/views/components/post-form.vue
index 1f98aed08..5cf5cffc6 100644
--- a/src/web/app/desktop/views/components/post-form.vue
+++ b/src/web/app/desktop/views/components/post-form.vue
@@ -455,10 +455,10 @@ export default Vue.extend({
 				from {background-position: 0 0;}
 				to   {background-position: -64px 32px;}
 
-	.upload
-	.drive
-	.kao
-	.poll
+	> .upload
+	> .drive
+	> .kao
+	> .poll
 		display inline-block
 		cursor pointer
 		padding 0

From 93e15cdfddbb080f025bfd5154adc11976d1e824 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 3 Mar 2018 16:39:53 +0900
Subject: [PATCH 0578/1250] :v:

---
 src/web/app/desktop/api/choose-drive-file.ts   |  2 +-
 src/web/app/desktop/views/components/drive.vue | 13 ++++++++++---
 2 files changed, 11 insertions(+), 4 deletions(-)

diff --git a/src/web/app/desktop/api/choose-drive-file.ts b/src/web/app/desktop/api/choose-drive-file.ts
index 892036244..fbda600e6 100644
--- a/src/web/app/desktop/api/choose-drive-file.ts
+++ b/src/web/app/desktop/api/choose-drive-file.ts
@@ -23,7 +23,7 @@ export default function(opts) {
 			};
 
 			window.open(url + '/selectdrive',
-				'drive_window',
+				'choose_drive_window',
 				'height=500, width=800');
 		}
 	});
diff --git a/src/web/app/desktop/views/components/drive.vue b/src/web/app/desktop/views/components/drive.vue
index 99036bed8..0fafa8cf2 100644
--- a/src/web/app/desktop/views/components/drive.vue
+++ b/src/web/app/desktop/views/components/drive.vue
@@ -62,6 +62,7 @@ import XFolder from './drive.folder.vue';
 import XFile from './drive.file.vue';
 import contains from '../../../common/scripts/contains';
 import contextmenu from '../../api/contextmenu';
+import { url } from '../../../config';
 
 export default Vue.extend({
 	components: {
@@ -389,9 +390,15 @@ export default Vue.extend({
 		},
 
 		newWindow(folder) {
-			(this as any).os.new(MkDriveWindow, {
-				folder: folder
-			});
+			if (document.body.clientWidth > 800) {
+				(this as any).os.new(MkDriveWindow, {
+					folder: folder
+				});
+			} else {
+				window.open(url + '/i/drive/folder/' + folder.id,
+					'drive_window',
+					'height=500, width=800');
+			}
 		},
 
 		move(target) {

From f11e5fe829dd1b3f353d92b4a99f8437a3ef11e9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 3 Mar 2018 16:41:43 +0900
Subject: [PATCH 0579/1250] :art:

---
 src/web/app/mobile/views/components/ui.header.vue | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/web/app/mobile/views/components/ui.header.vue b/src/web/app/mobile/views/components/ui.header.vue
index d07977cf5..6d0649e3f 100644
--- a/src/web/app/mobile/views/components/ui.header.vue
+++ b/src/web/app/mobile/views/components/ui.header.vue
@@ -150,7 +150,8 @@ export default Vue.extend({
 			height $height
 			-webkit-backdrop-filter blur(12px)
 			backdrop-filter blur(12px)
-			background-color rgba(#1b2023, 0.75)
+			//background-color rgba(#1b2023, 0.75)
+			background-color #1b2023
 
 		> p
 			display none

From 0e4ae33fdb9700c36639aeb79ed641cf3988405b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 3 Mar 2018 16:41:52 +0900
Subject: [PATCH 0580/1250] v3947

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index f31385c08..2cd5f12b5 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3935",
+	"version": "0.0.3947",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 05c5defad4a67fa9a9341b76bf1a9ff5a22f8efe Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 3 Mar 2018 16:59:00 +0900
Subject: [PATCH 0581/1250] oops

---
 src/web/app/common/views/components/reaction-picker.vue | 1 +
 src/web/app/mobile/views/components/ui.header.vue       | 4 +++-
 2 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/web/app/common/views/components/reaction-picker.vue b/src/web/app/common/views/components/reaction-picker.vue
index 0ba15ba73..df8100f2f 100644
--- a/src/web/app/common/views/components/reaction-picker.vue
+++ b/src/web/app/common/views/components/reaction-picker.vue
@@ -175,6 +175,7 @@ $border-color = rgba(27, 31, 35, 0.15)
 			text-align center
 
 			> button
+				padding 0
 				width 40px
 				height 40px
 				font-size 24px
diff --git a/src/web/app/mobile/views/components/ui.header.vue b/src/web/app/mobile/views/components/ui.header.vue
index 6d0649e3f..f06a35fe7 100644
--- a/src/web/app/mobile/views/components/ui.header.vue
+++ b/src/web/app/mobile/views/components/ui.header.vue
@@ -183,7 +183,7 @@ export default Vue.extend({
 				text-overflow ellipsis
 
 				[data-fa], [data-icon]
-					margin-right 8px
+					margin-right 4px
 
 				> img
 					display inline-block
@@ -198,6 +198,7 @@ export default Vue.extend({
 				position absolute
 				top 0
 				left 0
+				padding 0
 				width $height
 				font-size 1.4em
 				line-height $height
@@ -219,6 +220,7 @@ export default Vue.extend({
 				position absolute
 				top 0
 				right 0
+				padding 0
 				width $height
 				text-align center
 				font-size 1.4em

From 1dd8172f6d89768ea1ec3bc37a3d1b7a1639008e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 3 Mar 2018 16:59:14 +0900
Subject: [PATCH 0582/1250] v3949

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 2cd5f12b5..44acc864d 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3947",
+	"version": "0.0.3949",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From a2246c5e9162d75684e77b97a220bd77e40d9b73 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 3 Mar 2018 17:01:41 +0900
Subject: [PATCH 0583/1250] :art:

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

diff --git a/src/web/app/common/views/components/messaging.vue b/src/web/app/common/views/components/messaging.vue
index e2fdc502d..f6ba601bd 100644
--- a/src/web/app/common/views/components/messaging.vue
+++ b/src/web/app/common/views/components/messaging.vue
@@ -384,7 +384,7 @@ export default Vue.extend({
 						transition all 0.1s ease
 
 					> .username
-						margin 0 0 0 8px
+						margin 0 8px
 						color rgba(0, 0, 0, 0.5)
 
 					> .mk-time

From 075bae834b3a4d48565a7bd49861ac8470735e99 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 4 Mar 2018 09:39:25 +0900
Subject: [PATCH 0584/1250] #172

---
 src/api/endpoints/posts/create.ts             |  5 +++++
 src/api/models/post.ts                        |  1 +
 src/const.json                                |  2 +-
 .../desktop/views/components/posts.post.vue   |  5 +++++
 .../app/desktop/views/components/settings.vue | 11 +++++++++++
 .../app/mobile/views/components/post-form.vue |  4 +++-
 .../mobile/views/components/posts.post.vue    | 19 ++++++++++++++-----
 src/web/docs/api/entities/post.yaml           |  6 ++++++
 8 files changed, 46 insertions(+), 7 deletions(-)

diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index 075e1ac9f..57f98fa81 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -31,6 +31,10 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 	const [text, textErr] = $(params.text).optional.string().pipe(isValidText).$;
 	if (textErr) return rej('invalid text');
 
+	// Get 'via_mobile' parameter
+	const [viaMobile = false, viaMobileErr] = $(params.via_mobile).optional.boolean().$;
+	if (viaMobileErr) return rej('invalid via_mobile');
+
 	// Get 'tags' parameter
 	const [tags = [], tagsErr] = $(params.tags).optional.array('string').unique().eachQ(t => t.range(1, 32)).$;
 	if (tagsErr) return rej('invalid tags');
@@ -239,6 +243,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		tags: tags,
 		user_id: user._id,
 		app_id: app ? app._id : null,
+		via_mobile: viaMobile,
 
 		// 以下非正規化データ
 		_reply: reply ? { user_id: reply.user_id } : undefined,
diff --git a/src/api/models/post.ts b/src/api/models/post.ts
index 0bbacebf6..edb69e0c1 100644
--- a/src/api/models/post.ts
+++ b/src/api/models/post.ts
@@ -31,6 +31,7 @@ export type IPost = {
 	app_id: mongo.ObjectID;
 	category: string;
 	is_category_verified: boolean;
+	via_mobile: boolean;
 };
 
 /**
diff --git a/src/const.json b/src/const.json
index d8fe4fe6c..65dc734fa 100644
--- a/src/const.json
+++ b/src/const.json
@@ -1,5 +1,5 @@
 {
 	"copyright": "Copyright (c) 2014-2018 syuilo",
-	"themeColor": "#ff4e45",
+	"themeColor": "#5cbb2d",
 	"themeColorForeground": "#fff"
 }
diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index 4a95918c4..ce0a31d18 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -26,6 +26,7 @@
 				<span class="username">@{{ p.user.username }}</span>
 				<div class="info">
 					<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
+					<span class="mobile" v-if="p.via_mobile">%fa:mobile-alt%</span>
 					<router-link class="created-at" :to="url">
 						<mk-time :time="p.created_at"/>
 					</router-link>
@@ -399,6 +400,10 @@ export default Vue.extend({
 					margin-left auto
 					font-size 0.9em
 
+					> .mobile
+						margin-right 8px
+						color #ccc
+
 					> .app
 						margin-right 8px
 						padding-right 8px
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index 182a9a1d5..20d7a7771 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -26,6 +26,11 @@
 			<mk-switch v-model="os.i.client_settings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="タイムライン上部に投稿フォームを表示する"/>
 		</section>
 
+		<section class="web" v-show="page == 'web'">
+			<h1>モバイル</h1>
+			<mk-switch v-model="os.i.client_settings.disableViaMobile" @change="onChangeDisableViaMobile" text="モバイルからの投稿とフラグを付けない"/>
+		</section>
+
 		<section class="web" v-show="page == 'web'">
 			<h1>言語</h1>
 			<el-select v-model="lang" placeholder="言語を選択">
@@ -192,6 +197,12 @@ export default Vue.extend({
 				value: v
 			});
 		},
+		onChangeDisableViaMobile(v) {
+			(this as any).api('i/update_client_setting', {
+				name: 'disableViaMobile',
+				value: v
+			});
+		},
 		checkForUpdate() {
 			this.checkingForUpdate = true;
 			checkForUpdate((this as any).os, true, true).then(newer => {
diff --git a/src/web/app/mobile/views/components/post-form.vue b/src/web/app/mobile/views/components/post-form.vue
index 63b75b92f..009012b0b 100644
--- a/src/web/app/mobile/views/components/post-form.vue
+++ b/src/web/app/mobile/views/components/post-form.vue
@@ -91,11 +91,13 @@ export default Vue.extend({
 		},
 		post() {
 			this.posting = true;
+			const viaMobile = (this as any).os.i.client_settings.disableViaMobile !== 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
+				poll: this.poll ? (this.$refs.poll as any).get() : undefined,
+				via_mobile: viaMobile
 			}).then(data => {
 				this.$emit('post');
 				this.$destroy();
diff --git a/src/web/app/mobile/views/components/posts.post.vue b/src/web/app/mobile/views/components/posts.post.vue
index 3c02e1e99..d0a897db6 100644
--- a/src/web/app/mobile/views/components/posts.post.vue
+++ b/src/web/app/mobile/views/components/posts.post.vue
@@ -24,9 +24,12 @@
 				<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>
-				<router-link class="created-at" :to="url">
-					<mk-time :time="p.created_at"/>
-				</router-link>
+				<div class="info">
+					<span class="mobile" v-if="p.via_mobile">%fa:mobile-alt%</span>
+					<router-link class="created-at" :to="url">
+						<mk-time :time="p.created_at"/>
+					</router-link>
+				</div>
 			</header>
 			<div class="body">
 				<div class="text" ref="text">
@@ -336,10 +339,16 @@ export default Vue.extend({
 					margin 0 0.5em 0 0
 					color #ccc
 
-				> .created-at
+				> .info
 					margin-left auto
 					font-size 0.9em
-					color #c0c0c0
+
+					> .mobile
+						margin-right 6px
+						color #c0c0c0
+
+					> .created-at
+						color #c0c0c0
 
 			> .body
 
diff --git a/src/web/docs/api/entities/post.yaml b/src/web/docs/api/entities/post.yaml
index 551f3b7c3..e4359ffd0 100644
--- a/src/web/docs/api/entities/post.yaml
+++ b/src/web/docs/api/entities/post.yaml
@@ -17,6 +17,12 @@ props:
     desc:
       ja: "投稿日時"
       en: "The posted date of this post"
+  - name: "via_mobile"
+    type: "boolean"
+    optional: true
+    desc:
+      ja: "モバイル端末から投稿したか否か(自己申告であることに留意)"
+      en: "Whether this post sent via a mobile device"
   - name: "text"
     type: "string"
     optional: true

From 7f0c503773a7eb52b24fe5603ac4ed8b4af763a4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 4 Mar 2018 09:39:59 +0900
Subject: [PATCH 0585/1250] v3952

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 44acc864d..7022409be 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3949",
+	"version": "0.0.3952",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From d53478b84a90d76057704f5de02e20b3c1a25577 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 4 Mar 2018 09:41:21 +0900
Subject: [PATCH 0586/1250] Update setup.ja.md

---
 docs/setup.ja.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/setup.ja.md b/docs/setup.ja.md
index 564c79097..13dda8f1f 100644
--- a/docs/setup.ja.md
+++ b/docs/setup.ja.md
@@ -68,7 +68,7 @@ web-push generate-vapid-keys
 4. `npm run build`
 
 #### アップデートするには:
-1. `git pull origin master`
+1. `git reset --hard && git pull origin master`
 2. `npm install`
 3. `npm run build`
 

From e7447dd57af2c8234358b0d31264fd3c0c68b0f1 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 4 Mar 2018 09:41:30 +0900
Subject: [PATCH 0587/1250] Update setup.en.md

---
 docs/setup.en.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/setup.en.md b/docs/setup.en.md
index 13b0bdaeb..c9556e8b5 100644
--- a/docs/setup.en.md
+++ b/docs/setup.en.md
@@ -67,7 +67,7 @@ Please install and setup these softwares:
 4. `npm run build`
 
 #### Update
-1. `git pull origin master`
+1. `git reset --hard && git pull origin master`
 2. `npm install`
 3. `npm run build`
 

From 453d39643201c46e4d19a6da920652397ecf39ec Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 4 Mar 2018 12:14:44 +0900
Subject: [PATCH 0588/1250] Fix bug

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

diff --git a/src/web/app/mobile/views/components/post-form.vue b/src/web/app/mobile/views/components/post-form.vue
index 009012b0b..570cd0351 100644
--- a/src/web/app/mobile/views/components/post-form.vue
+++ b/src/web/app/mobile/views/components/post-form.vue
@@ -17,7 +17,7 @@
 				</div>
 			</x-draggable>
 		</div>
-		<mk-poll-editor v-if="poll" ref="poll"/>
+		<mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false"/>
 		<mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/>
 		<button class="upload" @click="chooseFile">%fa:upload%</button>
 		<button class="drive" @click="chooseFileFromDrive">%fa:cloud%</button>

From dc85b638d6ba14795c4df57558ce86c6680ef36c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 4 Mar 2018 12:14:53 +0900
Subject: [PATCH 0589/1250] :v:

---
 src/web/app/common/views/components/file-type-icon.vue | 2 +-
 src/web/app/mobile/views/components/drive.file.vue     | 2 +-
 src/web/app/mobile/views/components/post-form.vue      | 4 +++-
 3 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/src/web/app/common/views/components/file-type-icon.vue b/src/web/app/common/views/components/file-type-icon.vue
index aa2f0ed51..b7e868d1f 100644
--- a/src/web/app/common/views/components/file-type-icon.vue
+++ b/src/web/app/common/views/components/file-type-icon.vue
@@ -1,5 +1,5 @@
 <template>
-<span>
+<span class="mk-file-type-icon">
 	<template v-if="kind == 'image'">%fa:file-image%</template>
 </span>
 </template>
diff --git a/src/web/app/mobile/views/components/drive.file.vue b/src/web/app/mobile/views/components/drive.file.vue
index a41d9bcfd..db7381628 100644
--- a/src/web/app/mobile/views/components/drive.file.vue
+++ b/src/web/app/mobile/views/components/drive.file.vue
@@ -144,7 +144,7 @@ export default Vue.extend({
 					padding 0
 					color #9D9D9D
 
-					> mk-file-type-icon
+					> .mk-file-type-icon
 						margin-right 4px
 
 				> .data-size
diff --git a/src/web/app/mobile/views/components/post-form.vue b/src/web/app/mobile/views/components/post-form.vue
index 570cd0351..d16d5d358 100644
--- a/src/web/app/mobile/views/components/post-form.vue
+++ b/src/web/app/mobile/views/components/post-form.vue
@@ -4,7 +4,7 @@
 		<button class="cancel" @click="cancel">%fa:times%</button>
 		<div>
 			<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>
+			<button class="submit" :disabled="posting" @click="post">{{ reply ? '返信' : '%i18n:mobile.tags.mk-post-form.submit%' }}</button>
 		</div>
 	</header>
 	<div class="form">
@@ -137,6 +137,7 @@ export default Vue.extend({
 		box-shadow 0 1px 0 0 rgba(0, 0, 0, 0.1)
 
 		> .cancel
+			padding 0
 			width 50px
 			line-height 50px
 			font-size 24px
@@ -155,6 +156,7 @@ export default Vue.extend({
 				margin 8px
 				padding 0 16px
 				line-height 34px
+				vertical-align bottom
 				color $theme-color-foreground
 				background $theme-color
 				border-radius 4px

From c857daf15a5800f6bc9cdb22fa20ef9089f8af07 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 4 Mar 2018 14:10:14 +0900
Subject: [PATCH 0590/1250] #1177

---
 src/web/app/desktop/ui.styl                     |  3 +++
 .../app/desktop/views/components/settings.vue   | 17 +++++++++++++++--
 2 files changed, 18 insertions(+), 2 deletions(-)

diff --git a/src/web/app/desktop/ui.styl b/src/web/app/desktop/ui.styl
index 058271876..5a8d1718e 100644
--- a/src/web/app/desktop/ui.styl
+++ b/src/web/app/desktop/ui.styl
@@ -22,6 +22,9 @@ button.ui
 	border-radius 4px
 	outline none
 
+	&.block
+		display block
+
 	&:focus
 		&:after
 			content ""
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index 20d7a7771..cba14f5f9 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -28,7 +28,7 @@
 
 		<section class="web" v-show="page == 'web'">
 			<h1>モバイル</h1>
-			<mk-switch v-model="os.i.client_settings.disableViaMobile" @change="onChangeDisableViaMobile" text="モバイルからの投稿とフラグを付けない"/>
+			<mk-switch v-model="os.i.client_settings.disableViaMobile" @change="onChangeDisableViaMobile" text="「モバイルからの投稿」フラグを付けない"/>
 		</section>
 
 		<section class="web" v-show="page == 'web'">
@@ -109,10 +109,16 @@
 					<span>最新のバージョン: <i>{{ latestVersion ? latestVersion : version }}</i></span>
 				</template>
 			</p>
-			<button class="ui button" @click="checkForUpdate" :disabled="checkingForUpdate">
+			<button class="ui button block" @click="checkForUpdate" :disabled="checkingForUpdate">
 				<template v-if="checkingForUpdate">アップデートを確認中<mk-ellipsis/></template>
 				<template v-else>アップデートを確認</template>
 			</button>
+			<details>
+				<summary>詳細設定</summary>
+				<mk-switch v-model="preventUpdate" text="アップデートを延期する(非推奨)">
+					<span>この設定をオンにしてもアップデートが反映される場合があります。この設定はこのデバイスのみ有効です。</span>
+				</mk-switch>
+			</details>
 		</section>
 
 		<section class="other" v-show="page == 'other'">
@@ -161,6 +167,7 @@ export default Vue.extend({
 			latestVersion: undefined,
 			checkingForUpdate: false,
 			lang: localStorage.getItem('lang') || '',
+			preventUpdate: localStorage.getItem('preventUpdate') == 'true',
 			debug: localStorage.getItem('debug') == 'true',
 			enableExperimental: localStorage.getItem('enableExperimental') == 'true'
 		};
@@ -169,6 +176,9 @@ export default Vue.extend({
 		lang() {
 			localStorage.setItem('lang', this.lang);
 		},
+		preventUpdate() {
+			localStorage.setItem('preventUpdate', this.preventUpdate ? 'true' : 'false');
+		},
 		debug() {
 			localStorage.setItem('debug', this.debug ? 'true' : 'false');
 		},
@@ -285,6 +295,9 @@ export default Vue.extend({
 				border-bottom solid 1px #eee
 
 			&, >>> *
+				.ui.button.block
+					margin 16px 0
+
 				> section
 					margin 32px 0
 

From 26e87f8d26125c73e53106340875f3ca2995c7ee Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 4 Mar 2018 14:33:39 +0900
Subject: [PATCH 0591/1250] :v:

---
 src/web/app/mobile/views/components/index.ts  |   2 +
 .../mobile/views/components/notification.vue  | 159 +++++++-----------
 .../{posts.post.sub.vue => post.sub.vue}      |   0
 .../components/{posts.post.vue => post.vue}   |   2 +-
 src/web/app/mobile/views/components/posts.vue |   6 +-
 5 files changed, 68 insertions(+), 101 deletions(-)
 rename src/web/app/mobile/views/components/{posts.post.sub.vue => post.sub.vue} (100%)
 rename src/web/app/mobile/views/components/{posts.post.vue => post.vue} (99%)

diff --git a/src/web/app/mobile/views/components/index.ts b/src/web/app/mobile/views/components/index.ts
index fe65aab20..19135d08d 100644
--- a/src/web/app/mobile/views/components/index.ts
+++ b/src/web/app/mobile/views/components/index.ts
@@ -2,6 +2,7 @@ import Vue from 'vue';
 
 import ui from './ui.vue';
 import timeline from './timeline.vue';
+import post from './post.vue';
 import posts from './posts.vue';
 import imagesImage from './images-image.vue';
 import drive from './drive.vue';
@@ -23,6 +24,7 @@ import widgetContainer from './widget-container.vue';
 
 Vue.component('mk-ui', ui);
 Vue.component('mk-timeline', timeline);
+Vue.component('mk-post', post);
 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/notification.vue b/src/web/app/mobile/views/components/notification.vue
index 2a0e5c117..506ce3493 100644
--- a/src/web/app/mobile/views/components/notification.vue
+++ b/src/web/app/mobile/views/components/notification.vue
@@ -1,8 +1,7 @@
 <template>
-<div class="mk-notification" :class="notification.type">
-	<mk-time :time="notification.created_at"/>
-
-	<template v-if="notification.type == 'reaction'">
+<div class="mk-notification">
+	<div class="notification reaction" v-if="notification.type == 'reaction'">
+		<mk-time :time="notification.created_at"/>
 		<router-link class="avatar-anchor" :to="`/${notification.user.username}`">
 			<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
@@ -16,9 +15,10 @@
 				%fa:quote-right%
 			</router-link>
 		</div>
-	</template>
+	</div>
 
-	<template v-if="notification.type == 'repost'">
+	<div class="notification repost" v-if="notification.type == 'repost'">
+		<mk-time :time="notification.created_at"/>
 		<router-link class="avatar-anchor" :to="`/${notification.post.user.username}`">
 			<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
@@ -31,22 +31,14 @@
 				%fa:quote-left%{{ getPostSummary(notification.post.repost) }}%fa:quote-right%
 			</router-link>
 		</div>
-	</template>
+	</div>
 
 	<template v-if="notification.type == 'quote'">
-		<router-link class="avatar-anchor" :to="`/${notification.post.user.username}`">
-			<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
-		</router-link>
-		<div class="text">
-			<p>
-				%fa:quote-left%
-				<router-link :to="`/${notification.post.user.username}`">{{ notification.post.user.name }}</router-link>
-			</p>
-			<router-link class="post-preview" :to="`/${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link>
-		</div>
+		<mk-post :post="notification.post"/>
 	</template>
 
-	<template v-if="notification.type == 'follow'">
+	<div class="notification follow" v-if="notification.type == 'follow'">
+		<mk-time :time="notification.created_at"/>
 		<router-link class="avatar-anchor" :to="`/${notification.user.username}`">
 			<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
@@ -56,35 +48,18 @@
 				<router-link :to="`/${notification.user.username}`">{{ notification.user.name }}</router-link>
 			</p>
 		</div>
-	</template>
+	</div>
 
 	<template v-if="notification.type == 'reply'">
-		<router-link class="avatar-anchor" :to="`/${notification.post.user.username}`">
-			<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
-		</router-link>
-		<div class="text">
-			<p>
-				%fa:reply%
-				<router-link :to="`/${notification.post.user.username}`">{{ notification.post.user.name }}</router-link>
-			</p>
-			<router-link class="post-preview" :to="`/${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link>
-		</div>
+		<mk-post :post="notification.post"/>
 	</template>
 
 	<template v-if="notification.type == 'mention'">
-		<router-link class="avatar-anchor" :to="`/${notification.post.user.username}`">
-			<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
-		</router-link>
-		<div class="text">
-			<p>
-				%fa:at%
-				<router-link :to="`/${notification.post.user.username}`">{{ notification.post.user.name }}</router-link>
-			</p>
-			<router-link class="post-preview" :to="`/${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link>
-		</div>
+		<mk-post :post="notification.post"/>
 	</template>
 
-	<template v-if="notification.type == 'poll_vote'">
+	<div class="notification poll_vote" v-if="notification.type == 'poll_vote'">
+		<mk-time :time="notification.created_at"/>
 		<router-link class="avatar-anchor" :to="`/${notification.user.username}`">
 			<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
@@ -97,7 +72,7 @@
 				%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%
 			</router-link>
 		</div>
-	</template>
+	</div>
 </div>
 </template>
 
@@ -117,73 +92,67 @@ export default Vue.extend({
 
 <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
+	> .notification
+		padding 16px
+		overflow-wrap break-word
 
-	&:after
-		content ""
-		display block
-		clear both
+		&:after
+			content ""
+			display block
+			clear both
 
-	.avatar-anchor
-		display block
-		float left
+		> .mk-time
+			display inline
+			position absolute
+			top 16px
+			right 12px
+			vertical-align top
+			color rgba(0, 0, 0, 0.6)
+			font-size 0.9em
 
-		img
-			min-width 36px
-			min-height 36px
-			max-width 36px
-			max-height 36px
-			border-radius 6px
+		> .avatar-anchor
+			display block
+			float left
 
-	.text
-		float right
-		width calc(100% - 36px)
-		padding-left 8px
+			img
+				min-width 36px
+				min-height 36px
+				max-width 36px
+				max-height 36px
+				border-radius 6px
 
-		p
-			margin 0
+		> .text
+			float right
+			width calc(100% - 36px)
+			padding-left 8px
 
-			i, .mk-reaction-icon
-				margin-right 4px
+			p
+				margin 0
 
-	.post-preview
-		color rgba(0, 0, 0, 0.7)
+				i, .mk-reaction-icon
+					margin-right 4px
 
-	.post-ref
-		color rgba(0, 0, 0, 0.7)
+			> .post-preview
+				color rgba(0, 0, 0, 0.7)
 
-		[data-fa]
-			font-size 1em
-			font-weight normal
-			font-style normal
-			display inline-block
-			margin-right 3px
+			> .post-ref
+				color rgba(0, 0, 0, 0.7)
 
-	&.repost, &.quote
-		.text p i
-			color #77B255
+				[data-fa]
+					font-size 1em
+					font-weight normal
+					font-style normal
+					display inline-block
+					margin-right 3px
 
-	&.follow
-		.text p i
-			color #53c7ce
+		&.repost
+			.text p i
+				color #77B255
 
-	&.reply, &.mention
-		.text p i
-			color #555
-
-		.post-preview
-			color rgba(0, 0, 0, 0.7)
+		&.follow
+			.text p i
+				color #53c7ce
 
 </style>
 
diff --git a/src/web/app/mobile/views/components/posts.post.sub.vue b/src/web/app/mobile/views/components/post.sub.vue
similarity index 100%
rename from src/web/app/mobile/views/components/posts.post.sub.vue
rename to src/web/app/mobile/views/components/post.sub.vue
diff --git a/src/web/app/mobile/views/components/posts.post.vue b/src/web/app/mobile/views/components/post.vue
similarity index 99%
rename from src/web/app/mobile/views/components/posts.post.vue
rename to src/web/app/mobile/views/components/post.vue
index d0a897db6..4c8937f51 100644
--- a/src/web/app/mobile/views/components/posts.post.vue
+++ b/src/web/app/mobile/views/components/post.vue
@@ -77,7 +77,7 @@
 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';
+import XSub from './post.sub.vue';
 
 export default Vue.extend({
 	components: {
diff --git a/src/web/app/mobile/views/components/posts.vue b/src/web/app/mobile/views/components/posts.vue
index 9a9f9a313..7e71fa098 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" @update:post="onPostUpdated(i, $event)"/>
+		<mk-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>
@@ -17,12 +17,8 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import XPost from './posts.post.vue';
 
 export default Vue.extend({
-	components: {
-		XPost
-	},
 	props: {
 		posts: {
 			type: Array,

From c73b52ca1f518bfc1c202bc4b84288b1f36a1c98 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 4 Mar 2018 14:34:07 +0900
Subject: [PATCH 0592/1250] v3959

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 7022409be..c8ea2d559 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3952",
+	"version": "0.0.3959",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 5a618dfcdd42e4a93599d973a97c4b5e980e9fb8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 4 Mar 2018 18:14:13 +0900
Subject: [PATCH 0593/1250] Fix bug

---
 src/web/app/common/views/widgets/slideshow.vue | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/web/app/common/views/widgets/slideshow.vue b/src/web/app/common/views/widgets/slideshow.vue
index df6cb900d..e9451663e 100644
--- a/src/web/app/common/views/widgets/slideshow.vue
+++ b/src/web/app/common/views/widgets/slideshow.vue
@@ -81,6 +81,9 @@ export default define({
 				duration: 1000,
 				easing: 'linear',
 				complete: () => {
+					// 既にこのウィジェットがunmountされていたら要素がない
+					if ((this.$refs.slideA as any) == null) return;
+
 					(this.$refs.slideA as any).style.backgroundImage = img;
 					anime({
 						targets: this.$refs.slideB,

From 042aad81857a04ac6bb1611b13e252bb0115b9b4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 4 Mar 2018 18:27:22 +0900
Subject: [PATCH 0594/1250] :art:

---
 src/web/app/common/views/components/switch.vue | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/src/web/app/common/views/components/switch.vue b/src/web/app/common/views/components/switch.vue
index ffbab843e..cfc2f38e2 100644
--- a/src/web/app/common/views/components/switch.vue
+++ b/src/web/app/common/views/components/switch.vue
@@ -133,6 +133,18 @@ export default Vue.extend({
 		opacity 0
 		margin 0
 
+		&:focus + .button
+			&: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 14px
+
 	> .button
 		display inline-block
 		margin 0

From aa7ac47f2d566528c3d066fbeedee68e5674ee69 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 4 Mar 2018 18:50:30 +0900
Subject: [PATCH 0595/1250] make sounds great again

---
 src/web/app/common/mios.ts                            |  7 +++++++
 .../app/common/views/components/messaging-room.vue    |  6 ++++++
 src/web/app/desktop/views/components/settings.vue     | 11 +++++++++++
 src/web/app/desktop/views/components/timeline.vue     |  6 ++++++
 src/web/assets/message.mp3                            |  3 +++
 src/web/assets/post.mp3                               |  3 +++
 6 files changed, 36 insertions(+)
 create mode 100644 src/web/assets/message.mp3
 create mode 100644 src/web/assets/post.mp3

diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
index bc83ec0bb..c5f0d1d4d 100644
--- a/src/web/app/common/mios.ts
+++ b/src/web/app/common/mios.ts
@@ -94,6 +94,13 @@ export default class MiOS extends EventEmitter {
 		return localStorage.getItem('debug') == 'true';
 	}
 
+	/**
+	 * Whether enable sounds
+	 */
+	public get isEnableSounds() {
+		return localStorage.getItem('enableSounds') == 'true';
+	}
+
 	public apis: API;
 
 	/**
diff --git a/src/web/app/common/views/components/messaging-room.vue b/src/web/app/common/views/components/messaging-room.vue
index 0a675ba03..e15e10ec7 100644
--- a/src/web/app/common/views/components/messaging-room.vue
+++ b/src/web/app/common/views/components/messaging-room.vue
@@ -29,6 +29,7 @@ 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';
+import { url } from '../../../config';
 
 export default Vue.extend({
 	components: {
@@ -147,6 +148,11 @@ export default Vue.extend({
 		},
 
 		onMessage(message) {
+			// サウンドを再生する
+			if ((this as any).os.isEnableSounds) {
+				new Audio(`${url}/assets/message.mp3`).play();
+			}
+
 			const isBottom = this.isBottom();
 
 			this.messages.push(message);
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index cba14f5f9..a0ffc4e0a 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -26,6 +26,13 @@
 			<mk-switch v-model="os.i.client_settings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="タイムライン上部に投稿フォームを表示する"/>
 		</section>
 
+		<section class="web" v-show="page == 'web'">
+			<h1>サウンド</h1>
+			<mk-switch v-model="enableSounds" text="サウンドを有効にする">
+				<span>投稿やメッセージを送受信したときなどにサウンドを再生します。この設定はブラウザに記憶されます。</span>
+			</mk-switch>
+		</section>
+
 		<section class="web" v-show="page == 'web'">
 			<h1>モバイル</h1>
 			<mk-switch v-model="os.i.client_settings.disableViaMobile" @change="onChangeDisableViaMobile" text="「モバイルからの投稿」フラグを付けない"/>
@@ -166,6 +173,7 @@ export default Vue.extend({
 			version,
 			latestVersion: undefined,
 			checkingForUpdate: false,
+			enableSounds: localStorage.getItem('enableSounds') == 'true',
 			lang: localStorage.getItem('lang') || '',
 			preventUpdate: localStorage.getItem('preventUpdate') == 'true',
 			debug: localStorage.getItem('debug') == 'true',
@@ -173,6 +181,9 @@ export default Vue.extend({
 		};
 	},
 	watch: {
+		enableSounds() {
+			localStorage.setItem('enableSounds', this.enableSounds ? 'true' : 'false');
+		},
 		lang() {
 			localStorage.setItem('lang', this.lang);
 		},
diff --git a/src/web/app/desktop/views/components/timeline.vue b/src/web/app/desktop/views/components/timeline.vue
index 0d16d60df..c35baa159 100644
--- a/src/web/app/desktop/views/components/timeline.vue
+++ b/src/web/app/desktop/views/components/timeline.vue
@@ -18,6 +18,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import { url } from '../../../config';
 
 export default Vue.extend({
 	data() {
@@ -93,6 +94,11 @@ export default Vue.extend({
 			});
 		},
 		onPost(post) {
+			// サウンドを再生する
+			if ((this as any).os.isEnableSounds) {
+				new Audio(`${url}/assets/post.mp3`).play();
+			}
+
 			this.posts.unshift(post);
 		},
 		onChangeFollowing() {
diff --git a/src/web/assets/message.mp3 b/src/web/assets/message.mp3
new file mode 100644
index 000000000..8ef34914f
--- /dev/null
+++ b/src/web/assets/message.mp3
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9bdfc57945d5102e57b05f2cf4d857b79b6ae2674d4c09ae9d238641f281eab2
+size 4584
diff --git a/src/web/assets/post.mp3 b/src/web/assets/post.mp3
new file mode 100644
index 000000000..d4ab1c640
--- /dev/null
+++ b/src/web/assets/post.mp3
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8a370ac84df90f11d59113e8faa81dd769d11432fc15de6ca40fc9ae7e40c04b
+size 2506

From 1cc075f1768b4eca772b00c1ec515886e549d98b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 4 Mar 2018 18:51:03 +0900
Subject: [PATCH 0596/1250] v3963

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index c8ea2d559..a4e47b35d 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3959",
+	"version": "0.0.3963",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From eecb0a1644e24eeaee71b91441bfc1ed9e3d03c6 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 4 Mar 2018 19:29:21 +0900
Subject: [PATCH 0597/1250] Fix bug

---
 src/web/app/boot.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/app/boot.js b/src/web/app/boot.js
index a6d0f3aad..2d2e27df3 100644
--- a/src/web/app/boot.js
+++ b/src/web/app/boot.js
@@ -89,7 +89,7 @@
 		const meta = await res.json();
 
 		// Compare versions
-		if (meta.version != VERSION) {
+		if (meta.version != ver) {
 			alert(
 				'Misskeyの新しいバージョンがあります。ページを再度読み込みします。' +
 				'\n\n' +

From 62768d0d6fced40f232d2369c3bf782e4a14ba3b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Mar 2018 07:34:23 +0900
Subject: [PATCH 0598/1250] #304

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

diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index ce0a31d18..8cf21f8a4 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -171,7 +171,7 @@ export default Vue.extend({
 			if ((this as any).os.isSignedIn) {
 				this.connection.send({
 					type: 'capture',
-					id: this.post.id
+					id: this.p.id
 				});
 				if (withHandler) this.connection.on('post-updated', this.onStreamPostUpdated);
 			}
@@ -180,7 +180,7 @@ export default Vue.extend({
 			if ((this as any).os.isSignedIn) {
 				this.connection.send({
 					type: 'decapture',
-					id: this.post.id
+					id: this.p.id
 				});
 				if (withHandler) this.connection.off('post-updated', this.onStreamPostUpdated);
 			}
@@ -192,6 +192,8 @@ export default Vue.extend({
 			const post = data.post;
 			if (post.id == this.post.id) {
 				this.$emit('update:post', post);
+			} else if (post.id == this.post.repost_id) {
+				this.post.repost = post;
 			}
 		},
 		reply() {
diff --git a/src/web/app/mobile/views/components/post.vue b/src/web/app/mobile/views/components/post.vue
index 4c8937f51..fafc1429c 100644
--- a/src/web/app/mobile/views/components/post.vue
+++ b/src/web/app/mobile/views/components/post.vue
@@ -146,7 +146,7 @@ export default Vue.extend({
 			if ((this as any).os.isSignedIn) {
 				this.connection.send({
 					type: 'capture',
-					id: this.post.id
+					id: this.p.id
 				});
 				if (withHandler) this.connection.on('post-updated', this.onStreamPostUpdated);
 			}
@@ -155,7 +155,7 @@ export default Vue.extend({
 			if ((this as any).os.isSignedIn) {
 				this.connection.send({
 					type: 'decapture',
-					id: this.post.id
+					id: this.p.id
 				});
 				if (withHandler) this.connection.off('post-updated', this.onStreamPostUpdated);
 			}
@@ -167,6 +167,8 @@ export default Vue.extend({
 			const post = data.post;
 			if (post.id == this.post.id) {
 				this.$emit('update:post', post);
+			} else if (post.id == this.post.repost_id) {
+				this.post.repost = post;
 			}
 		},
 		reply() {

From 628fb5756f3a83161a8c183a25827c0eabb65085 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Mar 2018 08:07:09 +0900
Subject: [PATCH 0599/1250] #1183

---
 src/api/endpoints/i/update.ts                   |  8 +++++++-
 src/api/endpoints/posts/create.ts               |  6 +++---
 src/api/endpoints/posts/polls/vote.ts           |  6 +++---
 src/api/endpoints/posts/reactions/create.ts     |  6 +++---
 src/api/models/user.ts                          |  2 ++
 src/api/private/signup.ts                       |  4 +++-
 .../app/desktop/views/components/settings.vue   | 17 +++++++++++++++++
 7 files changed, 38 insertions(+), 11 deletions(-)

diff --git a/src/api/endpoints/i/update.ts b/src/api/endpoints/i/update.ts
index 2a5dce64a..76bad2d15 100644
--- a/src/api/endpoints/i/update.ts
+++ b/src/api/endpoints/i/update.ts
@@ -51,6 +51,11 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re
 	if (isBotErr) return rej('invalid is_bot param');
 	if (isBot != null) user.is_bot = isBot;
 
+	// Get 'auto_watch' parameter
+	const [autoWatch, autoWatchErr] = $(params.auto_watch).optional.boolean().$;
+	if (autoWatchErr) return rej('invalid auto_watch param');
+	if (autoWatch != null) user.settings.auto_watch = autoWatch;
+
 	await User.update(user._id, {
 		$set: {
 			name: user.name,
@@ -58,7 +63,8 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re
 			avatar_id: user.avatar_id,
 			banner_id: user.banner_id,
 			profile: user.profile,
-			is_bot: user.is_bot
+			is_bot: user.is_bot,
+			settings: user.settings
 		}
 	});
 
diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index 57f98fa81..a9d52fd12 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -377,9 +377,9 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 			});
 
 		// この投稿をWatchする
-		// TODO: ユーザーが「返信したときに自動でWatchする」設定を
-		//       オフにしていた場合はしない
-		watch(user._id, reply);
+		if (user.settings.auto_watch !== false) {
+			watch(user._id, reply);
+		}
 
 		// Add mention
 		addMention(reply.user_id, 'reply');
diff --git a/src/api/endpoints/posts/polls/vote.ts b/src/api/endpoints/posts/polls/vote.ts
index 5a4fd1c26..8222fe532 100644
--- a/src/api/endpoints/posts/polls/vote.ts
+++ b/src/api/endpoints/posts/polls/vote.ts
@@ -100,9 +100,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		});
 
 	// この投稿をWatchする
-	// TODO: ユーザーが「投票したときに自動でWatchする」設定を
-	//       オフにしていた場合はしない
-	watch(user._id, post);
+	if (user.settings.auto_watch !== false) {
+		watch(user._id, post);
+	}
 });
 
 function findWithAttr(array, attr, value) {
diff --git a/src/api/endpoints/posts/reactions/create.ts b/src/api/endpoints/posts/reactions/create.ts
index 0b0e0e294..93d9756d0 100644
--- a/src/api/endpoints/posts/reactions/create.ts
+++ b/src/api/endpoints/posts/reactions/create.ts
@@ -116,7 +116,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		});
 
 	// この投稿をWatchする
-	// TODO: ユーザーが「リアクションしたときに自動でWatchする」設定を
-	//       オフにしていた場合はしない
-	watch(user._id, post);
+	if (user.settings.auto_watch !== false) {
+		watch(user._id, post);
+	}
 });
diff --git a/src/api/models/user.ts b/src/api/models/user.ts
index 278b949db..2fea0566b 100644
--- a/src/api/models/user.ts
+++ b/src/api/models/user.ts
@@ -81,6 +81,8 @@ export type IUser = {
 	keywords: string[];
 	two_factor_secret: string;
 	two_factor_enabled: boolean;
+	client_settings: any;
+	settings: any;
 };
 
 export function init(user): IUser {
diff --git a/src/api/private/signup.ts b/src/api/private/signup.ts
index 19e331475..1e3e9fb45 100644
--- a/src/api/private/signup.ts
+++ b/src/api/private/signup.ts
@@ -132,7 +132,9 @@ export default async (req: express.Request, res: express.Response) => {
 			location: null,
 			weight: null
 		},
-		settings: {},
+		settings: {
+			auto_watch: true
+		},
 		client_settings: {
 			home: homeData,
 			show_donation: false
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index a0ffc4e0a..0eb18770a 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -62,6 +62,13 @@
 			</div>
 		</section>
 
+		<section class="notification" v-show="page == 'notification'">
+			<h1>通知</h1>
+			<mk-switch v-model="autoWatch" @change="onChangeAutoWatch" text="投稿の自動ウォッチ">
+				<span>リアクションしたり返信したりした投稿に関する通知を自動的に受け取るようにします。</span>
+			</mk-switch>
+		</section>
+
 		<section class="drive" v-show="page == 'drive'">
 			<h1>%i18n:desktop.tags.mk-settings.drive%</h1>
 			<mk-drive-setting/>
@@ -173,6 +180,7 @@ export default Vue.extend({
 			version,
 			latestVersion: undefined,
 			checkingForUpdate: false,
+			autoWatch: true,
 			enableSounds: localStorage.getItem('enableSounds') == 'true',
 			lang: localStorage.getItem('lang') || '',
 			preventUpdate: localStorage.getItem('preventUpdate') == 'true',
@@ -206,12 +214,21 @@ export default Vue.extend({
 		(this as any).os.getMeta().then(meta => {
 			this.meta = meta;
 		});
+
+		if ((this as any).os.i.settings.auto_watch != null) {
+			this.autoWatch = (this as any).os.i.settings.auto_watch;
+		}
 	},
 	methods: {
 		customizeHome() {
 			this.$router.push('/i/customize-home');
 			this.$emit('done');
 		},
+		onChangeAutoWatch(v) {
+			(this as any).api('i/update', {
+				auto_watch: v
+			});
+		},
 		onChangeShowPostFormOnTopOfTl(v) {
 			(this as any).api('i/update_client_setting', {
 				name: 'showPostFormOnTopOfTl',

From 94c62a7fb410a3e622871294108b1c3ddb5f4e20 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Mar 2018 08:44:37 +0900
Subject: [PATCH 0600/1250] wip

---
 src/api/endpoints/posts/create.ts             | 13 ++++++
 src/api/models/post.ts                        |  9 ++++
 .../views/components/post-form-window.vue     | 13 +++++-
 .../desktop/views/components/post-form.vue    | 21 ++++++++-
 .../app/mobile/views/components/post-form.vue | 24 +++++++++--
 src/web/docs/api/entities/post.yaml           | 43 +++++++++++++++++++
 6 files changed, 117 insertions(+), 6 deletions(-)

diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index a9d52fd12..15cbc4845 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -39,6 +39,18 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 	const [tags = [], tagsErr] = $(params.tags).optional.array('string').unique().eachQ(t => t.range(1, 32)).$;
 	if (tagsErr) return rej('invalid tags');
 
+	// Get 'geo' parameter
+	const [geo, geoErr] = $(params.geo).optional.nullable.strict.object()
+		.have('latitude', $().number().range(-180, 180))
+		.have('longitude', $().number().range(-90, 90))
+		.have('altitude', $().nullable.number())
+		.have('accuracy', $().nullable.number())
+		.have('altitudeAccuracy', $().nullable.number())
+		.have('heading', $().nullable.number().range(0, 360))
+		.have('speed', $().nullable.number())
+		.$;
+	if (geoErr) return rej('invalid geo');
+
 	// Get 'media_ids' parameter
 	const [mediaIds, mediaIdsErr] = $(params.media_ids).optional.array('id').unique().range(1, 4).$;
 	if (mediaIdsErr) return rej('invalid media_ids');
@@ -244,6 +256,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		user_id: user._id,
 		app_id: app ? app._id : null,
 		via_mobile: viaMobile,
+		geo,
 
 		// 以下非正規化データ
 		_reply: reply ? { user_id: reply.user_id } : undefined,
diff --git a/src/api/models/post.ts b/src/api/models/post.ts
index edb69e0c1..c37c8371c 100644
--- a/src/api/models/post.ts
+++ b/src/api/models/post.ts
@@ -32,6 +32,15 @@ export type IPost = {
 	category: string;
 	is_category_verified: boolean;
 	via_mobile: boolean;
+	geo: {
+		latitude: number;
+		longitude: number;
+		altitude: number;
+		accuracy: number;
+		altitudeAccuracy: number;
+		heading: number;
+		speed: number;
+	};
 };
 
 /**
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 4427f5982..31a07a890 100644
--- a/src/web/app/desktop/views/components/post-form-window.vue
+++ b/src/web/app/desktop/views/components/post-form-window.vue
@@ -1,6 +1,7 @@
 <template>
 <mk-window ref="window" is-modal @closed="$destroy">
 	<span slot="header">
+		<span :class="$style.icon" v-if="geo">%fa:map-marker-alt%</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>
@@ -12,7 +13,8 @@
 		:reply="reply"
 		@posted="onPosted"
 		@change-uploadings="onChangeUploadings"
-		@change-attached-media="onChangeMedia"/>
+		@change-attached-media="onChangeMedia"
+		@geo-attached="onGeoAttached"/>
 </mk-window>
 </template>
 
@@ -24,7 +26,8 @@ export default Vue.extend({
 	data() {
 		return {
 			uploadings: [],
-			media: []
+			media: [],
+			geo: null
 		};
 	},
 	mounted() {
@@ -39,6 +42,9 @@ export default Vue.extend({
 		onChangeMedia(media) {
 			this.media = media;
 		},
+		onGeoAttached(geo) {
+			this.geo = geo;
+		},
 		onPosted() {
 			(this.$refs.window as any).close();
 		}
@@ -47,6 +53,9 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" module>
+.icon
+	margin-right 8px
+
 .count
 	margin-left 8px
 	opacity 0.8
diff --git a/src/web/app/desktop/views/components/post-form.vue b/src/web/app/desktop/views/components/post-form.vue
index 5cf5cffc6..6e334e7ca 100644
--- a/src/web/app/desktop/views/components/post-form.vue
+++ b/src/web/app/desktop/views/components/post-form.vue
@@ -27,6 +27,7 @@
 	<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>
+	<button class="geo" title="位置情報を添付する" @click="setGeo">%fa:map-marker-alt%</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 }" class="submit" :disabled="!canPost" @click="post">
 		{{ posting ? '%i18n:desktop.tags.mk-post-form.posting%' : submitText }}<mk-ellipsis v-if="posting"/>
@@ -53,6 +54,7 @@ export default Vue.extend({
 			files: [],
 			uploadings: [],
 			poll: false,
+			geo: null,
 			autocomplete: null,
 			draghover: false
 		};
@@ -193,6 +195,21 @@ export default Vue.extend({
 			}
 			//#endregion
 		},
+		setGeo() {
+			if (navigator.geolocation == null) {
+				alert('お使いの端末は位置情報に対応していません');
+				return;
+			}
+
+			navigator.geolocation.getCurrentPosition(pos => {
+				this.geo = pos.coords;
+				this.$emit('geo-attached', this.geo);
+			}, err => {
+				alert('エラー: ' + err.message);
+			}, {
+				enableHighAccuracy: true
+			});
+		},
 		post() {
 			this.posting = true;
 
@@ -201,7 +218,8 @@ export default Vue.extend({
 				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
+				poll: this.poll ? (this.$refs.poll as any).get() : undefined,
+				geo: this.geo,
 			}).then(data => {
 				this.clear();
 				this.deleteDraft();
@@ -459,6 +477,7 @@ export default Vue.extend({
 	> .drive
 	> .kao
 	> .poll
+	> .geo
 		display inline-block
 		cursor pointer
 		padding 0
diff --git a/src/web/app/mobile/views/components/post-form.vue b/src/web/app/mobile/views/components/post-form.vue
index d16d5d358..559a6c1c4 100644
--- a/src/web/app/mobile/views/components/post-form.vue
+++ b/src/web/app/mobile/views/components/post-form.vue
@@ -23,6 +23,7 @@
 		<button class="drive" @click="chooseFileFromDrive">%fa:cloud%</button>
 		<button class="kao" @click="kao">%fa:R smile%</button>
 		<button class="poll" @click="poll = true">%fa:chart-pie%</button>
+		<button class="geo" @click="setGeo">%fa:map-marker-alt%</button>
 		<input ref="file" class="file" type="file" accept="image/*" multiple="multiple" @change="onChangeFile"/>
 	</div>
 </div>
@@ -44,7 +45,8 @@ export default Vue.extend({
 			text: '',
 			uploadings: [],
 			files: [],
-			poll: false
+			poll: false,
+			geo: null
 		};
 	},
 	mounted() {
@@ -83,6 +85,20 @@ export default Vue.extend({
 		onChangeUploadings(uploads) {
 			this.$emit('change-uploadings', uploads);
 		},
+		setGeo() {
+			if (navigator.geolocation == null) {
+				alert('お使いの端末は位置情報に対応していません');
+				return;
+			}
+
+			navigator.geolocation.getCurrentPosition(pos => {
+				this.geo = pos.coords;
+			}, err => {
+				alert('エラー: ' + err.message);
+			}, {
+				enableHighAccuracy: true
+			});
+		},
 		clear() {
 			this.text = '';
 			this.files = [];
@@ -97,6 +113,7 @@ export default Vue.extend({
 				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,
+				geo: this.geo,
 				via_mobile: viaMobile
 			}).then(data => {
 				this.$emit('post');
@@ -223,8 +240,9 @@ export default Vue.extend({
 
 		> .upload
 		> .drive
-		.kao
-		.poll
+		> .kao
+		> .poll
+		> .geo
 			display inline-block
 			padding 0
 			margin 0
diff --git a/src/web/docs/api/entities/post.yaml b/src/web/docs/api/entities/post.yaml
index e4359ffd0..451d2579d 100644
--- a/src/web/docs/api/entities/post.yaml
+++ b/src/web/docs/api/entities/post.yaml
@@ -128,3 +128,46 @@ props:
             desc:
               ja: "この選択肢に投票された数"
               en: "The number voted for this choice"
+  - name: "geo"
+    type: "object"
+    optional: true
+    desc:
+      ja: "位置情報"
+      en: "Geo location"
+    defName: "geo"
+    def:
+      - name: "latitude"
+        type: "number"
+        optional: false
+        desc:
+          ja: "緯度。-180〜180で表す。"
+      - name: "longitude"
+        type: "number"
+        optional: false
+        desc:
+          ja: "経度。-90〜90で表す。"
+      - name: "altitude"
+        type: "number"
+        optional: false
+        desc:
+          ja: "高度。メートル単位で表す。"
+      - name: "accuracy"
+        type: "number"
+        optional: false
+        desc:
+          ja: "緯度、経度の精度。メートル単位で表す。"
+      - name: "altitudeAccuracy"
+        type: "number"
+        optional: false
+        desc:
+          ja: "高度の精度。メートル単位で表す。"
+      - name: "heading"
+        type: "number"
+        optional: false
+        desc:
+          ja: "方角。0〜360の角度で表す。0が北、90が東、180が南、270が西。"
+      - name: "speed"
+        type: "number"
+        optional: false
+        desc:
+          ja: "速度。メートル / 秒数で表す。"

From 2f1d3bdbdc00fe48b96bfceaf7445fe79111a8fa Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Mar 2018 08:45:27 +0900
Subject: [PATCH 0601/1250] v3968

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index a4e47b35d..5269273e7 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3963",
+	"version": "0.0.3968",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 38c205cdd223c10941255adad433adc350867371 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Mar 2018 09:03:54 +0900
Subject: [PATCH 0602/1250] Fix bug

---
 src/web/app/desktop/views/components/post-form.vue | 10 +++++++++-
 src/web/app/mobile/views/components/post-form.vue  | 10 +++++++++-
 2 files changed, 18 insertions(+), 2 deletions(-)

diff --git a/src/web/app/desktop/views/components/post-form.vue b/src/web/app/desktop/views/components/post-form.vue
index 6e334e7ca..22ed52b39 100644
--- a/src/web/app/desktop/views/components/post-form.vue
+++ b/src/web/app/desktop/views/components/post-form.vue
@@ -219,7 +219,15 @@ export default Vue.extend({
 				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,
-				geo: this.geo,
+				geo: this.geo ? {
+					latitude: this.geo.latitude,
+					longitude: this.geo.longitude,
+					altitude: this.geo.altitude,
+					accuracy: this.geo.accuracy,
+					altitudeAccuracy: this.geo.altitudeAccuracy,
+					heading: isNaN(this.geo.heading) ? null : this.geo.heading,
+					speed: this.geo.speed,
+				} : null
 			}).then(data => {
 				this.clear();
 				this.deleteDraft();
diff --git a/src/web/app/mobile/views/components/post-form.vue b/src/web/app/mobile/views/components/post-form.vue
index 559a6c1c4..35e581f31 100644
--- a/src/web/app/mobile/views/components/post-form.vue
+++ b/src/web/app/mobile/views/components/post-form.vue
@@ -113,7 +113,15 @@ export default Vue.extend({
 				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,
-				geo: this.geo,
+				geo: this.geo ? {
+					latitude: this.geo.latitude,
+					longitude: this.geo.longitude,
+					altitude: this.geo.altitude,
+					accuracy: this.geo.accuracy,
+					altitudeAccuracy: this.geo.altitudeAccuracy,
+					heading: isNaN(this.geo.heading) ? null : this.geo.heading,
+					speed: this.geo.speed,
+				} : null,
 				via_mobile: viaMobile
 			}).then(data => {
 				this.$emit('post');

From a9cc80f066a5192ba5e76fa179634db9e524b7ce Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Mar 2018 09:05:45 +0900
Subject: [PATCH 0603/1250] :art:

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

diff --git a/src/web/app/common/views/components/messaging.vue b/src/web/app/common/views/components/messaging.vue
index f6ba601bd..a94a99668 100644
--- a/src/web/app/common/views/components/messaging.vue
+++ b/src/web/app/common/views/components/messaging.vue
@@ -372,6 +372,7 @@ export default Vue.extend({
 					align-items center
 					margin-bottom 2px
 					white-space nowrap
+					overflow hidden
 
 					> .name
 						margin 0

From 32e876677ce26839c2be0a0200818b0d0cafa164 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Mar 2018 09:10:23 +0900
Subject: [PATCH 0604/1250] :v:

---
 src/web/app/desktop/views/components/posts.post.vue | 6 ++++++
 src/web/app/mobile/views/components/post.vue        | 6 ++++++
 2 files changed, 12 insertions(+)

diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index 8cf21f8a4..93af3d54d 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -43,6 +43,7 @@
 						<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
 					</div>
 					<a class="rp" v-if="p.repost">RP:</a>
+					<p class="location" v-if="p.geo">%fa:map-marker-alt% 位置情報</p>
 					<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 				</div>
 				<div class="media" v-if="p.media">
@@ -447,6 +448,11 @@ export default Vue.extend({
 						font-style oblique
 						color #a0bf46
 
+					> .location
+						margin 4px 0
+						font-size 12px
+						color #ccc
+
 					> .tags
 						margin 4px 0 0 0
 
diff --git a/src/web/app/mobile/views/components/post.vue b/src/web/app/mobile/views/components/post.vue
index fafc1429c..bd8179dd2 100644
--- a/src/web/app/mobile/views/components/post.vue
+++ b/src/web/app/mobile/views/components/post.vue
@@ -48,6 +48,7 @@
 					<mk-images :images="p.media"/>
 				</div>
 				<mk-poll v-if="p.poll" :post="p" ref="pollViewer"/>
+				<p class="location" v-if="p.geo">%fa:map-marker-alt% 位置情報</p>
 				<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"/>
@@ -423,6 +424,11 @@ export default Vue.extend({
 						display block
 						max-width 100%
 
+				> .location
+					margin 4px 0
+					font-size 12px
+					color #ccc
+
 				> .app
 					font-size 12px
 					color #ccc

From 8d7d0c2e6fe265a905537c8759aec57675d717f0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Mar 2018 09:10:42 +0900
Subject: [PATCH 0605/1250] v3972

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 5269273e7..e3e05fb8e 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3968",
+	"version": "0.0.3972",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From f9663018add1608d8506d7dab8a163235ddd2276 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Mar 2018 09:22:28 +0900
Subject: [PATCH 0606/1250] oops

---
 src/api/endpoints/posts/create.ts   | 4 ++--
 src/web/docs/api/entities/post.yaml | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index 15cbc4845..1c3ab5345 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -41,8 +41,8 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 
 	// Get 'geo' parameter
 	const [geo, geoErr] = $(params.geo).optional.nullable.strict.object()
-		.have('latitude', $().number().range(-180, 180))
-		.have('longitude', $().number().range(-90, 90))
+		.have('latitude', $().number().range(-90, 90))
+		.have('longitude', $().number().range(-180, 180))
 		.have('altitude', $().nullable.number())
 		.have('accuracy', $().nullable.number())
 		.have('altitudeAccuracy', $().nullable.number())
diff --git a/src/web/docs/api/entities/post.yaml b/src/web/docs/api/entities/post.yaml
index 451d2579d..f78026314 100644
--- a/src/web/docs/api/entities/post.yaml
+++ b/src/web/docs/api/entities/post.yaml
@@ -140,12 +140,12 @@ props:
         type: "number"
         optional: false
         desc:
-          ja: "緯度。-180〜180で表す。"
+          ja: "緯度。-90〜90で表す。"
       - name: "longitude"
         type: "number"
         optional: false
         desc:
-          ja: "経度。-90〜90で表す。"
+          ja: "経度。-180〜180で表す。"
       - name: "altitude"
         type: "number"
         optional: false

From 1bb527ba5dbaa836c9af050cf7823e4028054343 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Mar 2018 11:33:44 +0900
Subject: [PATCH 0607/1250] :v:

---
 .../desktop/views/components/posts.post.vue   | 88 +++++++++----------
 src/web/app/mobile/views/components/post.vue  | 77 ++++++++--------
 2 files changed, 82 insertions(+), 83 deletions(-)

diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index 93af3d54d..667263a02 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -33,26 +33,26 @@
 				</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>
+				<div class="text">
 					<a class="reply" v-if="p.reply">%fa:reply%</a>
 					<mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i" :class="$style.text"/>
-					<div class="tags" v-if="p.tags && p.tags.length > 0">
-						<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
-					</div>
 					<a class="rp" v-if="p.repost">RP:</a>
-					<p class="location" v-if="p.geo">%fa:map-marker-alt% 位置情報</p>
-					<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"/>
 				</div>
 				<mk-poll v-if="p.poll" :post="p" ref="pollViewer"/>
+				<div class="tags" v-if="p.tags && p.tags.length > 0">
+					<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
+				</div>
+				<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.latitude},${p.geo.longitude}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
 				<div class="repost" v-if="p.repost">%fa:quote-right -flip-h%
 					<mk-post-preview class="repost" :post="p.repost"/>
 				</div>
+				<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 			</div>
 			<footer>
 				<mk-reactions-viewer :post="p" ref="reactionsViewer"/>
@@ -433,12 +433,6 @@ export default Vue.extend({
 						color #aaa
 						border-left solid 3px #eee
 
-					.mk-url-preview
-						margin-top 8px
-
-					> .channel
-						margin 0
-
 					> .reply
 						margin-right 8px
 						color #717171
@@ -448,39 +442,45 @@ export default Vue.extend({
 						font-style oblique
 						color #a0bf46
 
-					> .location
-						margin 4px 0
-						font-size 12px
-						color #ccc
+				> .location
+					margin 4px 0
+					font-size 12px
+					color #ccc
 
-					> .tags
-						margin 4px 0 0 0
+				> .tags
+					margin 4px 0 0 0
 
-						> *
-							display inline-block
-							margin 0 8px 0 0
-							padding 2px 8px 2px 16px
-							font-size 90%
-							color #8d969e
-							background #edf0f3
-							border-radius 4px
+					> *
+						display inline-block
+						margin 0 8px 0 0
+						padding 2px 8px 2px 16px
+						font-size 90%
+						color #8d969e
+						background #edf0f3
+						border-radius 4px
 
-							&:before
-								content ""
-								display block
-								position absolute
-								top 0
-								bottom 0
-								left 4px
-								width 8px
-								height 8px
-								margin auto 0
-								background #fff
-								border-radius 100%
+						&:before
+							content ""
+							display block
+							position absolute
+							top 0
+							bottom 0
+							left 4px
+							width 8px
+							height 8px
+							margin auto 0
+							background #fff
+							border-radius 100%
 
-							&:hover
-								text-decoration none
-								background #e2e7ec
+						&:hover
+							text-decoration none
+							background #e2e7ec
+
+				.mk-url-preview
+					margin-top 8px
+
+				> .channel
+					margin 0
 
 				> .mk-poll
 					font-size 80%
diff --git a/src/web/app/mobile/views/components/post.vue b/src/web/app/mobile/views/components/post.vue
index bd8179dd2..5ddaa605b 100644
--- a/src/web/app/mobile/views/components/post.vue
+++ b/src/web/app/mobile/views/components/post.vue
@@ -32,23 +32,23 @@
 				</div>
 			</header>
 			<div class="body">
-				<div class="text" ref="text">
-					<p class="channel" v-if="p.channel != null"><a target="_blank">{{ p.channel.title }}</a>:</p>
+				<p class="channel" v-if="p.channel != null"><a target="_blank">{{ p.channel.title }}</a>:</p>
+				<div class="text">
 					<a class="reply" v-if="p.reply">
 						%fa:reply%
 					</a>
 					<mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i" :class="$style.text"/>
-					<div class="tags" v-if="p.tags && p.tags.length > 0">
-						<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
-					</div>
-					<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 					<a class="rp" 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"/>
-				<p class="location" v-if="p.geo">%fa:map-marker-alt% 位置情報</p>
+				<div class="tags" v-if="p.tags && p.tags.length > 0">
+					<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
+				</div>
+				<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
+				<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.latitude},${p.geo.longitude}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
 				<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"/>
@@ -356,7 +356,6 @@ export default Vue.extend({
 			> .body
 
 				> .text
-					cursor default
 					display block
 					margin 0
 					padding 0
@@ -370,12 +369,6 @@ export default Vue.extend({
 						color #aaa
 						border-left solid 3px #eee
 
-					.mk-url-preview
-						margin-top 8px
-
-					> .channel
-						margin 0
-
 					> .reply
 						margin-right 8px
 						color #717171
@@ -385,31 +378,6 @@ export default Vue.extend({
 						font-style oblique
 						color #a0bf46
 
-					> .tags
-						margin 4px 0 0 0
-
-						> *
-							display inline-block
-							margin 0 8px 0 0
-							padding 2px 8px 2px 16px
-							font-size 90%
-							color #8d969e
-							background #edf0f3
-							border-radius 4px
-
-							&:before
-								content ""
-								display block
-								position absolute
-								top 0
-								bottom 0
-								left 4px
-								width 8px
-								height 8px
-								margin auto 0
-								background #fff
-								border-radius 100%
-
 					[data-is-me]:after
 						content "you"
 						padding 0 4px
@@ -419,6 +387,37 @@ export default Vue.extend({
 						background $theme-color
 						border-radius 4px
 
+				.mk-url-preview
+					margin-top 8px
+
+				> .channel
+					margin 0
+
+				> .tags
+					margin 4px 0 0 0
+
+					> *
+						display inline-block
+						margin 0 8px 0 0
+						padding 2px 8px 2px 16px
+						font-size 90%
+						color #8d969e
+						background #edf0f3
+						border-radius 4px
+
+						&:before
+							content ""
+							display block
+							position absolute
+							top 0
+							bottom 0
+							left 4px
+							width 8px
+							height 8px
+							margin auto 0
+							background #fff
+							border-radius 100%
+
 				> .media
 					> img
 						display block

From 38f298e21a795ec3318e1dd9deee1bb17d8a60bd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Mar 2018 11:35:58 +0900
Subject: [PATCH 0608/1250] :art:

---
 src/web/app/desktop/views/components/posts.post.vue | 11 +----------
 src/web/app/mobile/views/components/post.vue        | 11 +----------
 2 files changed, 2 insertions(+), 20 deletions(-)

diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index 667263a02..e73f1445e 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -49,7 +49,7 @@
 					<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
 				</div>
 				<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.latitude},${p.geo.longitude}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
-				<div class="repost" v-if="p.repost">%fa:quote-right -flip-h%
+				<div class="repost" v-if="p.repost">
 					<mk-post-preview class="repost" :post="p.repost"/>
 				</div>
 				<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
@@ -488,15 +488,6 @@ export default Vue.extend({
 				> .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
diff --git a/src/web/app/mobile/views/components/post.vue b/src/web/app/mobile/views/components/post.vue
index 5ddaa605b..7cd6393fe 100644
--- a/src/web/app/mobile/views/components/post.vue
+++ b/src/web/app/mobile/views/components/post.vue
@@ -50,7 +50,7 @@
 				<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 				<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.latitude},${p.geo.longitude}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
 				<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%
+				<div class="repost" v-if="p.repost">
 					<mk-post-preview class="repost" :post="p.repost"/>
 				</div>
 			</div>
@@ -438,15 +438,6 @@ export default Vue.extend({
 				> .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

From f0b858de3cf983241d0a3e12d05d65fb17321f33 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Mar 2018 11:36:35 +0900
Subject: [PATCH 0609/1250] v3976

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index e3e05fb8e..3d0b9c772 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3972",
+	"version": "0.0.3976",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From f0176088ef4305f1267c4b69cd089f44b353902c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Mar 2018 13:40:21 +0900
Subject: [PATCH 0610/1250] Show google maps when geo location attached

---
 src/config.ts                                 |  2 ++
 src/web/app/common/mios.ts                    | 31 +++++++++++++++++--
 src/web/app/config.ts                         |  2 ++
 .../desktop/views/components/post-detail.vue  | 28 ++++++++++++++++-
 .../desktop/views/components/posts.post.vue   | 20 ++++++++++++
 .../mobile/views/components/post-detail.vue   | 28 ++++++++++++++++-
 src/web/app/mobile/views/components/post.vue  | 20 ++++++++++++
 webpack/plugins/consts.ts                     |  3 +-
 8 files changed, 128 insertions(+), 6 deletions(-)

diff --git a/src/config.ts b/src/config.ts
index 3ffefe278..e327cb0ba 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -89,6 +89,8 @@ type Source = {
 		public_key: string;
 		private_key: string;
 	};
+
+	google_maps_api_key: string;
 };
 
 /**
diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
index c5f0d1d4d..bf04588bc 100644
--- a/src/web/app/common/mios.ts
+++ b/src/web/app/common/mios.ts
@@ -1,7 +1,7 @@
 import Vue from 'vue';
 import { EventEmitter } from 'eventemitter3';
 
-import { host, apiUrl, swPublickey, version, lang } from '../config';
+import { host, apiUrl, swPublickey, version, lang, googleMapsApiKey } from '../config';
 import Progress from './scripts/loading';
 import HomeStreamManager from './scripts/streaming/home-stream-manager';
 import DriveStreamManager from './scripts/streaming/drive-stream-manager';
@@ -170,8 +170,33 @@ export default class MiOS extends EventEmitter {
 			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;
+		//#region load google maps api
+		(window as any).initGoogleMaps = () => {
+			this.emit('init-google-maps');
+		};
+		const head = document.getElementsByTagName('head')[0];
+		const script = document.createElement('script');
+		script.setAttribute('src', `https://maps.googleapis.com/maps/api/js?key=${googleMapsApiKey}&callback=initGoogleMaps`);
+		script.setAttribute('async', 'true');
+		script.setAttribute('defer', 'true');
+		head.appendChild(script);
+		//#endregion
+
+		if (this.debug) {
+			(window as any).os = this;
+		}
+	}
+
+	public getGoogleMaps() {
+		return new Promise((res, rej) => {
+			if ((window as any).google && (window as any).google.maps) {
+				res((window as any).google.maps);
+			} else {
+				this.once('init-google-maps', () => {
+					res((window as any).google.maps);
+				});
+			}
+		});
 	}
 
 	public log(...args) {
diff --git a/src/web/app/config.ts b/src/web/app/config.ts
index b51279192..24158c3ae 100644
--- a/src/web/app/config.ts
+++ b/src/web/app/config.ts
@@ -13,6 +13,7 @@ declare const _THEME_COLOR_: string;
 declare const _COPYRIGHT_: string;
 declare const _VERSION_: string;
 declare const _LICENSE_: string;
+declare const _GOOGLE_MAPS_API_KEY_: string;
 
 export const host = _HOST_;
 export const url = _URL_;
@@ -29,3 +30,4 @@ export const themeColor = _THEME_COLOR_;
 export const copyright = _COPYRIGHT_;
 export const version = _VERSION_;
 export const license = _LICENSE_;
+export const googleMapsApiKey = _GOOGLE_MAPS_API_KEY_;
diff --git a/src/web/app/desktop/views/components/post-detail.vue b/src/web/app/desktop/views/components/post-detail.vue
index 32d401351..fdf97282b 100644
--- a/src/web/app/desktop/views/components/post-detail.vue
+++ b/src/web/app/desktop/views/components/post-detail.vue
@@ -39,14 +39,16 @@
 		</header>
 		<div class="body">
 			<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"/>
 			</div>
 			<mk-poll v-if="p.poll" :post="p"/>
+			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 			<div class="tags" v-if="p.tags && p.tags.length > 0">
 				<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
 			</div>
+			<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.latitude},${p.geo.longitude}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
+			<div class="map" v-if="p.geo" ref="map"></div>
 		</div>
 		<footer>
 			<mk-reactions-viewer :post="p"/>
@@ -137,6 +139,21 @@ export default Vue.extend({
 				this.replies = replies;
 			});
 		}
+
+		// Draw map
+		if (this.p.geo) {
+			(this as any).os.getGoogleMaps().then(maps => {
+				const uluru = new maps.LatLng(this.p.geo.latitude, this.p.geo.longitude);
+				const map = new maps.Map(this.$refs.map, {
+					center: uluru,
+					zoom: 15
+				});
+				new maps.Marker({
+					position: uluru,
+					map: map
+				});
+			});
+		}
 	},
 	methods: {
 		fetchContext() {
@@ -308,6 +325,15 @@ export default Vue.extend({
 		> .body
 			padding 8px 0
 
+			> .location
+				margin 4px 0
+				font-size 12px
+				color #ccc
+
+			> .map
+				width 100%
+				height 300px
+
 			> .mk-url-preview
 				margin-top 8px
 
diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index e73f1445e..11bf3c3b5 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -49,6 +49,7 @@
 					<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
 				</div>
 				<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.latitude},${p.geo.longitude}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
+				<div class="map" v-if="p.geo" ref="map"></div>
 				<div class="repost" v-if="p.repost">
 					<mk-post-preview class="repost" :post="p.repost"/>
 				</div>
@@ -158,6 +159,21 @@ export default Vue.extend({
 		if ((this as any).os.isSignedIn) {
 			this.connection.on('_connected_', this.onStreamConnected);
 		}
+
+		// Draw map
+		if (this.p.geo) {
+			(this as any).os.getGoogleMaps().then(maps => {
+				const uluru = new maps.LatLng(this.p.geo.latitude, this.p.geo.longitude);
+				const map = new maps.Map(this.$refs.map, {
+					center: uluru,
+					zoom: 15
+				});
+				new maps.Marker({
+					position: uluru,
+					map: map
+				});
+			});
+		}
 	},
 	beforeDestroy() {
 		this.decapture(true);
@@ -447,6 +463,10 @@ export default Vue.extend({
 					font-size 12px
 					color #ccc
 
+				> .map
+					width 100%
+					height 300px
+
 				> .tags
 					margin 4px 0 0 0
 
diff --git a/src/web/app/mobile/views/components/post-detail.vue b/src/web/app/mobile/views/components/post-detail.vue
index 7e6217f60..554cfa07a 100644
--- a/src/web/app/mobile/views/components/post-detail.vue
+++ b/src/web/app/mobile/views/components/post-detail.vue
@@ -42,11 +42,13 @@
 			<div class="tags" v-if="p.tags && p.tags.length > 0">
 				<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
 			</div>
-			<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"/>
+			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
+			<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.latitude},${p.geo.longitude}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
+			<div class="map" v-if="p.geo" ref="map"></div>
 		</div>
 		<router-link class="time" :to="`/${p.user.username}/${p.id}`">
 			<mk-time :time="p.created_at" mode="detail"/>
@@ -133,6 +135,21 @@ export default Vue.extend({
 				this.replies = replies;
 			});
 		}
+
+		// Draw map
+		if (this.p.geo) {
+			(this as any).os.getGoogleMaps().then(maps => {
+				const uluru = new maps.LatLng(this.p.geo.latitude, this.p.geo.longitude);
+				const map = new maps.Map(this.$refs.map, {
+					center: uluru,
+					zoom: 15
+				});
+				new maps.Marker({
+					position: uluru,
+					map: map
+				});
+			});
+		}
 	},
 	methods: {
 		fetchContext() {
@@ -309,6 +326,15 @@ export default Vue.extend({
 		> .body
 			padding 8px 0
 
+			> .location
+				margin 4px 0
+				font-size 12px
+				color #ccc
+
+			> .map
+				width 100%
+				height 200px
+
 			> .mk-url-preview
 				margin-top 8px
 
diff --git a/src/web/app/mobile/views/components/post.vue b/src/web/app/mobile/views/components/post.vue
index 7cd6393fe..2026fe53b 100644
--- a/src/web/app/mobile/views/components/post.vue
+++ b/src/web/app/mobile/views/components/post.vue
@@ -49,6 +49,7 @@
 				</div>
 				<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 				<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.latitude},${p.geo.longitude}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
+				<div class="map" v-if="p.geo" ref="map"></div>
 				<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
 				<div class="repost" v-if="p.repost">
 					<mk-post-preview class="repost" :post="p.repost"/>
@@ -133,6 +134,21 @@ export default Vue.extend({
 		if ((this as any).os.isSignedIn) {
 			this.connection.on('_connected_', this.onStreamConnected);
 		}
+
+		// Draw map
+		if (this.p.geo) {
+			(this as any).os.getGoogleMaps().then(maps => {
+				const uluru = new maps.LatLng(this.p.geo.latitude, this.p.geo.longitude);
+				const map = new maps.Map(this.$refs.map, {
+					center: uluru,
+					zoom: 15
+				});
+				new maps.Marker({
+					position: uluru,
+					map: map
+				});
+			});
+		}
 	},
 	beforeDestroy() {
 		this.decapture(true);
@@ -428,6 +444,10 @@ export default Vue.extend({
 					font-size 12px
 					color #ccc
 
+				> .map
+					width 100%
+					height 200px
+
 				> .app
 					font-size 12px
 					color #ccc
diff --git a/webpack/plugins/consts.ts b/webpack/plugins/consts.ts
index cb9ba8e86..643589323 100644
--- a/webpack/plugins/consts.ts
+++ b/webpack/plugins/consts.ts
@@ -27,7 +27,8 @@ export default lang => {
 		_LANG_: lang,
 		_HOST_: config.host,
 		_URL_: config.url,
-		_LICENSE_: licenseHtml
+		_LICENSE_: licenseHtml,
+		_GOOGLE_MAPS_API_KEY_: config.google_maps_api_key
 	};
 
 	const _consts = {};

From 4cbc9d5bd29b9ed82cef98dc46cb7a569882ac34 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Mar 2018 13:46:54 +0900
Subject: [PATCH 0611/1250] Fix bug

---
 .../app/desktop/views/components/post-detail.vue | 16 +++++++++++++++-
 .../app/desktop/views/components/posts.post.vue  |  2 +-
 .../app/mobile/views/components/post-detail.vue  | 16 +++++++++++++++-
 src/web/app/mobile/views/components/post.vue     |  2 +-
 4 files changed, 32 insertions(+), 4 deletions(-)

diff --git a/src/web/app/desktop/views/components/post-detail.vue b/src/web/app/desktop/views/components/post-detail.vue
index fdf97282b..e33120c7c 100644
--- a/src/web/app/desktop/views/components/post-detail.vue
+++ b/src/web/app/desktop/views/components/post-detail.vue
@@ -49,6 +49,9 @@
 			</div>
 			<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.latitude},${p.geo.longitude}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
 			<div class="map" v-if="p.geo" ref="map"></div>
+			<div class="repost" v-if="p.repost">
+				<mk-post-preview :post="p.repost"/>
+			</div>
 		</div>
 		<footer>
 			<mk-reactions-viewer :post="p"/>
@@ -104,7 +107,10 @@ export default Vue.extend({
 	},
 	computed: {
 		isRepost(): boolean {
-			return this.post.repost != null;
+			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;
@@ -325,6 +331,14 @@ export default Vue.extend({
 		> .body
 			padding 8px 0
 
+			> .repost
+				margin 8px 0
+
+				> .mk-post-preview
+					padding 16px
+					border dashed 1px #c0dac6
+					border-radius 8px
+
 			> .location
 				margin 4px 0
 				font-size 12px
diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index 11bf3c3b5..b88f016ad 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -51,7 +51,7 @@
 				<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.latitude},${p.geo.longitude}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
 				<div class="map" v-if="p.geo" ref="map"></div>
 				<div class="repost" v-if="p.repost">
-					<mk-post-preview class="repost" :post="p.repost"/>
+					<mk-post-preview :post="p.repost"/>
 				</div>
 				<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 			</div>
diff --git a/src/web/app/mobile/views/components/post-detail.vue b/src/web/app/mobile/views/components/post-detail.vue
index 554cfa07a..bf8ce4f6f 100644
--- a/src/web/app/mobile/views/components/post-detail.vue
+++ b/src/web/app/mobile/views/components/post-detail.vue
@@ -49,6 +49,9 @@
 			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 			<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.latitude},${p.geo.longitude}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
 			<div class="map" v-if="p.geo" ref="map"></div>
+			<div class="repost" v-if="p.repost">
+				<mk-post-preview :post="p.repost"/>
+			</div>
 		</div>
 		<router-link class="time" :to="`/${p.user.username}/${p.id}`">
 			<mk-time :time="p.created_at" mode="detail"/>
@@ -103,7 +106,10 @@ export default Vue.extend({
 	},
 	computed: {
 		isRepost(): boolean {
-			return this.post.repost != null;
+			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;
@@ -326,6 +332,14 @@ export default Vue.extend({
 		> .body
 			padding 8px 0
 
+			> .repost
+				margin 8px 0
+
+				> .mk-post-preview
+					padding 16px
+					border dashed 1px #c0dac6
+					border-radius 8px
+
 			> .location
 				margin 4px 0
 				font-size 12px
diff --git a/src/web/app/mobile/views/components/post.vue b/src/web/app/mobile/views/components/post.vue
index 2026fe53b..390c6396f 100644
--- a/src/web/app/mobile/views/components/post.vue
+++ b/src/web/app/mobile/views/components/post.vue
@@ -52,7 +52,7 @@
 				<div class="map" v-if="p.geo" ref="map"></div>
 				<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
 				<div class="repost" v-if="p.repost">
-					<mk-post-preview class="repost" :post="p.repost"/>
+					<mk-post-preview :post="p.repost"/>
 				</div>
 			</div>
 			<footer>

From f95515564b3c020fa2e5af787918939ac8f98f9b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Mar 2018 13:47:57 +0900
Subject: [PATCH 0612/1250] :v:

---
 docs/config.md | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/docs/config.md b/docs/config.md
index a9987c9ce..45c6b7cfc 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -49,12 +49,15 @@ sw:
   # VAPIDの秘密鍵
   private_key:
 
+# Google Maps API
+google_maps_api_key:
+
 # Twitterインテグレーションの設定(利用しない場合は省略可能)
 twitter:
   # インテグレーション用アプリのコンシューマーキー
-  consumer_key: 
+  consumer_key:
 
   # インテグレーション用アプリのコンシューマーシークレット
-  consumer_secret: 
+  consumer_secret:
 
 ```

From 3dde5787250c21b5392e086e65bcb7bb8fe8b51f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Mar 2018 13:48:11 +0900
Subject: [PATCH 0613/1250] v3980

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 3d0b9c772..ef06be75f 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3976",
+	"version": "0.0.3980",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From c1156a21847acba2fe6eadbcba6db96fd17f6498 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Mar 2018 14:09:12 +0900
Subject: [PATCH 0614/1250] :v:

---
 .../desktop/views/components/post-form-window.vue    |  6 +++++-
 src/web/app/desktop/views/components/post-form.vue   |  6 +++++-
 src/web/app/mobile/views/components/post-form.vue    | 12 ++++++++++--
 3 files changed, 20 insertions(+), 4 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 31a07a890..d0b115e85 100644
--- a/src/web/app/desktop/views/components/post-form-window.vue
+++ b/src/web/app/desktop/views/components/post-form-window.vue
@@ -14,7 +14,8 @@
 		@posted="onPosted"
 		@change-uploadings="onChangeUploadings"
 		@change-attached-media="onChangeMedia"
-		@geo-attached="onGeoAttached"/>
+		@geo-attached="onGeoAttached"
+		@geo-dettached="onGeoDettached"/>
 </mk-window>
 </template>
 
@@ -45,6 +46,9 @@ export default Vue.extend({
 		onGeoAttached(geo) {
 			this.geo = geo;
 		},
+		onGeoDettached() {
+			this.geo = null;
+		},
 		onPosted() {
 			(this.$refs.window as any).close();
 		}
diff --git a/src/web/app/desktop/views/components/post-form.vue b/src/web/app/desktop/views/components/post-form.vue
index 22ed52b39..42d11709f 100644
--- a/src/web/app/desktop/views/components/post-form.vue
+++ b/src/web/app/desktop/views/components/post-form.vue
@@ -27,7 +27,7 @@
 	<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>
-	<button class="geo" title="位置情報を添付する" @click="setGeo">%fa:map-marker-alt%</button>
+	<button class="geo" title="位置情報を添付する" @click="geo ? removeGeo : setGeo">%fa:map-marker-alt%</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 }" class="submit" :disabled="!canPost" @click="post">
 		{{ posting ? '%i18n:desktop.tags.mk-post-form.posting%' : submitText }}<mk-ellipsis v-if="posting"/>
@@ -210,6 +210,10 @@ export default Vue.extend({
 				enableHighAccuracy: true
 			});
 		},
+		removeGeo() {
+			this.geo = null;
+			this.$emit('geo-dettached');
+		},
 		post() {
 			this.posting = true;
 
diff --git a/src/web/app/mobile/views/components/post-form.vue b/src/web/app/mobile/views/components/post-form.vue
index 35e581f31..9af7b7267 100644
--- a/src/web/app/mobile/views/components/post-form.vue
+++ b/src/web/app/mobile/views/components/post-form.vue
@@ -4,6 +4,7 @@
 		<button class="cancel" @click="cancel">%fa:times%</button>
 		<div>
 			<span class="text-count" :class="{ over: text.length > 1000 }">{{ 1000 - text.length }}</span>
+			<span class="geo" v-if="geo">%fa:map-marker-alt%</span>
 			<button class="submit" :disabled="posting" @click="post">{{ reply ? '返信' : '%i18n:mobile.tags.mk-post-form.submit%' }}</button>
 		</div>
 	</header>
@@ -23,7 +24,7 @@
 		<button class="drive" @click="chooseFileFromDrive">%fa:cloud%</button>
 		<button class="kao" @click="kao">%fa:R smile%</button>
 		<button class="poll" @click="poll = true">%fa:chart-pie%</button>
-		<button class="geo" @click="setGeo">%fa:map-marker-alt%</button>
+		<button class="geo" @click="geo ? removeGeo : setGeo">%fa:map-marker-alt%</button>
 		<input ref="file" class="file" type="file" accept="image/*" multiple="multiple" @change="onChangeFile"/>
 	</div>
 </div>
@@ -99,6 +100,9 @@ export default Vue.extend({
 				enableHighAccuracy: true
 			});
 		},
+		removeGeo() {
+			this.geo = null;
+		},
 		clear() {
 			this.text = '';
 			this.files = [];
@@ -172,10 +176,14 @@ export default Vue.extend({
 			position absolute
 			top 0
 			right 0
+			color #657786
 
 			> .text-count
 				line-height 50px
-				color #657786
+
+			> .geo
+				margin 0 8px
+				line-height 50px
 
 			> .submit
 				margin 8px

From 19aed875c778bbd4e5d77b81c70d9851abb358fa Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Mar 2018 14:09:22 +0900
Subject: [PATCH 0615/1250] v3982

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index ef06be75f..7db021bbf 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3980",
+	"version": "0.0.3982",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 3ed863c5074d666bf477b3252d6d0c8606ca3ee6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Mar 2018 14:18:00 +0900
Subject: [PATCH 0616/1250] oops

---
 src/web/app/desktop/views/components/post-form.vue | 2 +-
 src/web/app/mobile/views/components/post-form.vue  | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/web/app/desktop/views/components/post-form.vue b/src/web/app/desktop/views/components/post-form.vue
index 42d11709f..78f6d445a 100644
--- a/src/web/app/desktop/views/components/post-form.vue
+++ b/src/web/app/desktop/views/components/post-form.vue
@@ -27,7 +27,7 @@
 	<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>
-	<button class="geo" title="位置情報を添付する" @click="geo ? removeGeo : setGeo">%fa:map-marker-alt%</button>
+	<button class="geo" title="位置情報を添付する" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</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 }" class="submit" :disabled="!canPost" @click="post">
 		{{ posting ? '%i18n:desktop.tags.mk-post-form.posting%' : submitText }}<mk-ellipsis v-if="posting"/>
diff --git a/src/web/app/mobile/views/components/post-form.vue b/src/web/app/mobile/views/components/post-form.vue
index 9af7b7267..5b47caebc 100644
--- a/src/web/app/mobile/views/components/post-form.vue
+++ b/src/web/app/mobile/views/components/post-form.vue
@@ -24,7 +24,7 @@
 		<button class="drive" @click="chooseFileFromDrive">%fa:cloud%</button>
 		<button class="kao" @click="kao">%fa:R smile%</button>
 		<button class="poll" @click="poll = true">%fa:chart-pie%</button>
-		<button class="geo" @click="geo ? removeGeo : setGeo">%fa:map-marker-alt%</button>
+		<button class="geo" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button>
 		<input ref="file" class="file" type="file" accept="image/*" multiple="multiple" @change="onChangeFile"/>
 	</div>
 </div>

From 50068758e75b5671e3374301c5c29d2eda64d28d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Mar 2018 14:18:08 +0900
Subject: [PATCH 0617/1250] v3984

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 7db021bbf..5efd04b41 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3982",
+	"version": "0.0.3984",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 1c184ed4a7c269ecdb7ffef607bfe807bed73343 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Mar 2018 18:11:07 +0900
Subject: [PATCH 0618/1250] :v:

---
 package.json                                  |   1 +
 src/web/app/common/-tags/authorized-apps.tag  |  34 -----
 src/web/app/common/-tags/signin-history.tag   | 116 ------------------
 .../views/components/settings.apps.vue        |  39 ++++++
 .../views/components/settings.signins.vue     |  98 +++++++++++++++
 .../app/desktop/views/components/settings.vue |  10 +-
 src/web/app/init.ts                           |   2 +
 7 files changed, 147 insertions(+), 153 deletions(-)
 delete mode 100644 src/web/app/common/-tags/authorized-apps.tag
 delete mode 100644 src/web/app/common/-tags/signin-history.tag
 create mode 100644 src/web/app/desktop/views/components/settings.apps.vue
 create mode 100644 src/web/app/desktop/views/components/settings.signins.vue

diff --git a/package.json b/package.json
index 5efd04b41..d59f342ec 100644
--- a/package.json
+++ b/package.json
@@ -185,6 +185,7 @@
 		"vue": "2.5.13",
 		"vue-cropperjs": "2.2.0",
 		"vue-js-modal": "1.3.12",
+		"vue-json-tree-view": "^2.1.3",
 		"vue-loader": "14.1.1",
 		"vue-router": "3.0.1",
 		"vue-template-compiler": "2.5.13",
diff --git a/src/web/app/common/-tags/authorized-apps.tag b/src/web/app/common/-tags/authorized-apps.tag
deleted file mode 100644
index ed1570650..000000000
--- a/src/web/app/common/-tags/authorized-apps.tag
+++ /dev/null
@@ -1,34 +0,0 @@
-<mk-authorized-apps>
-	<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" v-if="apps.length != 0">
-		<div each={ app in apps }>
-			<p><b>{ app.name }</b></p>
-			<p>{ app.description }</p>
-		</div>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> .apps
-				> div
-					padding 16px 0 0 0
-					border-bottom solid 1px #eee
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.apps = [];
-		this.fetching = true;
-
-		this.on('mount', () => {
-			this.$root.$data.os.api('i/authorized_apps').then(apps => {
-				this.apps = apps;
-				this.fetching = false;
-			});
-		});
-	</script>
-</mk-authorized-apps>
diff --git a/src/web/app/common/-tags/signin-history.tag b/src/web/app/common/-tags/signin-history.tag
deleted file mode 100644
index a347c7c23..000000000
--- a/src/web/app/common/-tags/signin-history.tag
+++ /dev/null
@@ -1,116 +0,0 @@
-<mk-signin-history>
-	<div class="records" v-if="history.length != 0">
-		<mk-signin-record each={ rec in history } rec={ rec }/>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-	</style>
-	<script lang="typescript">
-		this.mixin('i');
-		this.mixin('api');
-
-		this.mixin('stream');
-		this.connection = this.stream.getConnection();
-		this.connectionId = this.stream.use();
-
-		this.history = [];
-		this.fetching = true;
-
-		this.on('mount', () => {
-			this.$root.$data.os.api('i/signin_history').then(history => {
-				this.update({
-					fetching: false,
-					history: history
-				});
-			});
-
-			this.connection.on('signin', this.onSignin);
-		});
-
-		this.on('unmount', () => {
-			this.connection.off('signin', this.onSignin);
-			this.stream.dispose(this.connectionId);
-		});
-
-		this.onSignin = signin => {
-			this.history.unshift(signin);
-			this.update();
-		};
-	</script>
-</mk-signin-history>
-
-<mk-signin-record>
-	<header @click="toggle">
-		<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>
-	<pre ref="headers" class="json" show={ show }>{ JSON.stringify(rec.headers, null, 2) }</pre>
-
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			border-bottom solid 1px #eee
-
-			> header
-				display flex
-				padding 8px 0
-				line-height 32px
-				cursor pointer
-
-				> [data-fa]
-					margin-right 8px
-					text-align left
-
-					&.check
-						color #0fda82
-
-					&.times
-						color #ff3100
-
-				> .ip
-					display inline-block
-					text-align left
-					padding 8px
-					line-height 16px
-					font-family monospace
-					font-size 14px
-					color #444
-					background #f8f8f8
-					border-radius 4px
-
-				> mk-time
-					margin-left auto
-					text-align right
-					color #777
-
-			> pre
-				overflow auto
-				margin 0 0 16px 0
-				max-height 100px
-				white-space pre-wrap
-				word-break break-all
-				color #4a535a
-
-	</style>
-
-	<script lang="typescript">
-		import hljs from 'highlight.js';
-
-		this.rec = this.opts.rec;
-		this.show = false;
-
-		this.on('mount', () => {
-			hljs.highlightBlock(this.$refs.headers);
-		});
-
-		this.toggle = () => {
-			this.update({
-				show: !this.show
-			});
-		};
-	</script>
-</mk-signin-record>
diff --git a/src/web/app/desktop/views/components/settings.apps.vue b/src/web/app/desktop/views/components/settings.apps.vue
new file mode 100644
index 000000000..0503b03ab
--- /dev/null
+++ b/src/web/app/desktop/views/components/settings.apps.vue
@@ -0,0 +1,39 @@
+<template>
+<div class="root">
+	<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" v-if="apps.length != 0">
+		<div v-for="app in apps">
+			<p><b>{{ app.name }}</b></p>
+			<p>{{ app.description }}</p>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	data() {
+		return {
+			fetching: true,
+			apps: []
+		};
+	},
+	mounted() {
+		(this as any).api('i/authorized_apps').then(apps => {
+			this.apps = apps;
+			this.fetching = false;
+		});
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.root
+	> .apps
+		> div
+			padding 16px 0 0 0
+			border-bottom solid 1px #eee
+</style>
diff --git a/src/web/app/desktop/views/components/settings.signins.vue b/src/web/app/desktop/views/components/settings.signins.vue
new file mode 100644
index 000000000..ddc567f06
--- /dev/null
+++ b/src/web/app/desktop/views/components/settings.signins.vue
@@ -0,0 +1,98 @@
+<template>
+<div class="root">
+<div class="signins" v-if="signins.length != 0">
+	<div v-for="signin in signins">
+		<header @click="signin._show = !signin._show">
+			<template v-if="signin.success">%fa:check%</template>
+			<template v-else>%fa:times%</template>
+			<span class="ip">{{ signin.ip }}</span>
+			<mk-time :time="signin.created_at"/>
+		</header>
+		<div class="headers" v-show="signin._show">
+			<tree-view :data="signin.headers"/>
+		</div>
+	</div>
+</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	data() {
+		return {
+			fetching: true,
+			signins: [],
+			connection: null,
+			connectionId: null
+		};
+	},
+	mounted() {
+		(this as any).api('i/signin_history').then(signins => {
+			this.signins = signins;
+			this.fetching = false;
+		});
+
+		this.connection = (this as any).os.stream.getConnection();
+		this.connectionId = (this as any).os.stream.use();
+
+		this.connection.on('signin', this.onSignin);
+	},
+	beforeDestroy() {
+		this.connection.off('signin', this.onSignin);
+		(this as any).os.stream.dispose(this.connectionId);
+	},
+	methods: {
+		onSignin(signin) {
+			this.signins.unshift(signin);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.root
+	> .signins
+		> div
+			border-bottom solid 1px #eee
+
+			> header
+				display flex
+				padding 8px 0
+				line-height 32px
+				cursor pointer
+
+				> [data-fa]
+					margin-right 8px
+					text-align left
+
+					&.check
+						color #0fda82
+
+					&.times
+						color #ff3100
+
+				> .ip
+					display inline-block
+					text-align left
+					padding 8px
+					line-height 16px
+					font-family monospace
+					font-size 14px
+					color #444
+					background #f8f8f8
+					border-radius 4px
+
+				> .mk-time
+					margin-left auto
+					text-align right
+					color #777
+
+			> .headers
+				overflow auto
+				margin 0 0 16px 0
+				max-height 100px
+				white-space pre-wrap
+				word-break break-all
+
+</style>
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index 0eb18770a..096ba57fd 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -81,7 +81,7 @@
 
 		<section class="apps" v-show="page == 'apps'">
 			<h1>アプリケーション</h1>
-			<mk-authorized-apps/>
+			<x-apps/>
 		</section>
 
 		<section class="twitter" v-show="page == 'twitter'">
@@ -101,7 +101,7 @@
 
 		<section class="signin" v-show="page == 'security'">
 			<h1>サインイン履歴</h1>
-			<mk-signin-history/>
+			<x-signins/>
 		</section>
 
 		<section class="api" v-show="page == 'api'">
@@ -161,6 +161,8 @@ import XMute from './settings.mute.vue';
 import XPassword from './settings.password.vue';
 import X2fa from './settings.2fa.vue';
 import XApi from './settings.api.vue';
+import XApps from './settings.apps.vue';
+import XSignins from './settings.signins.vue';
 import { docsUrl, license, lang, version } from '../../../config';
 import checkForUpdate from '../../../common/scripts/check-for-update';
 
@@ -170,7 +172,9 @@ export default Vue.extend({
 		XMute,
 		XPassword,
 		X2fa,
-		XApi
+		XApi,
+		XApps,
+		XSignins
 	},
 	data() {
 		return {
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index 716fe45e7..38b74d450 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -5,6 +5,7 @@
 import Vue from 'vue';
 import VueRouter from 'vue-router';
 import VModal from 'vue-js-modal';
+import * as TreeView from 'vue-json-tree-view';
 import Element from 'element-ui';
 import ElementLocaleEn from 'element-ui/lib/locale/lang/en';
 import ElementLocaleJa from 'element-ui/lib/locale/lang/ja';
@@ -23,6 +24,7 @@ switch (lang) {
 
 Vue.use(VueRouter);
 Vue.use(VModal);
+Vue.use(TreeView);
 Vue.use(Element, { locale: elementLocale });
 
 // Register global directives

From d8b1e0034dedc9058cf5f59a79eb2abb1a43f6e6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Mar 2018 18:20:46 +0900
Subject: [PATCH 0619/1250] v3986

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index d59f342ec..df915f6e8 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3984",
+	"version": "0.0.3986",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 44c0758b1aa8f77cec4d98b2eb8cc374a9b6321d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Mar 2018 18:37:12 +0900
Subject: [PATCH 0620/1250] :v:

---
 .../views/components/settings.drive.vue       | 35 +++++++++++++++++++
 .../app/desktop/views/components/settings.vue |  6 ++--
 2 files changed, 39 insertions(+), 2 deletions(-)
 create mode 100644 src/web/app/desktop/views/components/settings.drive.vue

diff --git a/src/web/app/desktop/views/components/settings.drive.vue b/src/web/app/desktop/views/components/settings.drive.vue
new file mode 100644
index 000000000..8bb0c760a
--- /dev/null
+++ b/src/web/app/desktop/views/components/settings.drive.vue
@@ -0,0 +1,35 @@
+<template>
+<div class="root">
+	<template v-if="!fetching">
+		<el-progress :text-inside="true" :stroke-width="18" :percentage="Math.floor((usage / capacity) * 100)"/>
+		<p><b>{{ capacity | bytes }}</b>中<b>{{ usage | bytes }}</b>使用中</p>
+	</template>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	data() {
+		return {
+			fetching: true,
+			usage: null,
+			capacity: null
+		};
+	},
+	mounted() {
+		(this as any).api('drive').then(info => {
+			this.capacity = info.capacity;
+			this.usage = info.usage;
+			this.fetching = false;
+		});
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.root
+	> p
+		> b
+			margin 0 8px
+</style>
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index 096ba57fd..a2c929152 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -71,7 +71,7 @@
 
 		<section class="drive" v-show="page == 'drive'">
 			<h1>%i18n:desktop.tags.mk-settings.drive%</h1>
-			<mk-drive-setting/>
+			<x-drive/>
 		</section>
 
 		<section class="mute" v-show="page == 'mute'">
@@ -163,6 +163,7 @@ import X2fa from './settings.2fa.vue';
 import XApi from './settings.api.vue';
 import XApps from './settings.apps.vue';
 import XSignins from './settings.signins.vue';
+import XDrive from './settings.drive.vue';
 import { docsUrl, license, lang, version } from '../../../config';
 import checkForUpdate from '../../../common/scripts/check-for-update';
 
@@ -174,7 +175,8 @@ export default Vue.extend({
 		X2fa,
 		XApi,
 		XApps,
-		XSignins
+		XSignins,
+		XDrive
 	},
 	data() {
 		return {

From 4294310b2f3e3ddbab893f6983e4a833ebb7f5e2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Mar 2018 18:37:19 +0900
Subject: [PATCH 0621/1250] v3988

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index df915f6e8..6fbd2fb4c 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3986",
+	"version": "0.0.3988",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From ff0c76be97f6d999340a1910eda7f3f5484acb95 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Mar 2018 20:09:26 +0900
Subject: [PATCH 0622/1250] #1184

---
 .../app/common/views/components/switch.vue    |  1 +
 .../app/desktop/views/components/posts.vue    | 22 ++++++++++++-----
 .../app/desktop/views/components/settings.vue | 24 +++++++++++++++++++
 .../app/desktop/views/components/timeline.vue | 12 ++++++----
 4 files changed, 48 insertions(+), 11 deletions(-)

diff --git a/src/web/app/common/views/components/switch.vue b/src/web/app/common/views/components/switch.vue
index cfc2f38e2..19a4adc3d 100644
--- a/src/web/app/common/views/components/switch.vue
+++ b/src/web/app/common/views/components/switch.vue
@@ -56,6 +56,7 @@ export default Vue.extend({
 	},
 	watch: {
 		value() {
+			(this.$el).style.transition = 'all 0.3s';
 			(this.$refs.input as any).checked = this.checked;
 		}
 	},
diff --git a/src/web/app/desktop/views/components/posts.vue b/src/web/app/desktop/views/components/posts.vue
index ec36889ec..ffceff876 100644
--- a/src/web/app/desktop/views/components/posts.vue
+++ b/src/web/app/desktop/views/components/posts.vue
@@ -69,11 +69,21 @@ export default Vue.extend({
 			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
+		> *
+			display block
+			margin 0
+			padding 16px
+			width 100%
+			text-align center
+			color #ccc
+			border-top solid 1px #eaeaea
+			border-bottom-left-radius 4px
+			border-bottom-right-radius 4px
 
+		> button
+			&:hover
+				background #f5f5f5
+
+			&:active
+				background #eee
 </style>
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index a2c929152..3278efb9c 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -18,6 +18,13 @@
 			<x-profile/>
 		</section>
 
+		<section class="web" v-show="page == 'web'">
+			<h1>動作</h1>
+			<mk-switch v-model="fetchOnScroll" @change="onChangeFetchOnScroll" text="スクロールで自動読み込み">
+				<span>ページを下までスクロールしたときに自動で追加のコンテンツを読み込みます。</span>
+			</mk-switch>
+		</section>
+
 		<section class="web" v-show="page == 'web'">
 			<h1>デザイン</h1>
 			<div class="div">
@@ -186,6 +193,7 @@ export default Vue.extend({
 			version,
 			latestVersion: undefined,
 			checkingForUpdate: false,
+			fetchOnScroll: true,
 			autoWatch: true,
 			enableSounds: localStorage.getItem('enableSounds') == 'true',
 			lang: localStorage.getItem('lang') || '',
@@ -223,6 +231,16 @@ export default Vue.extend({
 
 		if ((this as any).os.i.settings.auto_watch != null) {
 			this.autoWatch = (this as any).os.i.settings.auto_watch;
+			this.$watch('os.i.settings.auto_watch', v => {
+				this.autoWatch = v;
+			});
+		}
+
+		if ((this as any).os.i.client_settings.fetchOnScroll != null) {
+			this.fetchOnScroll = (this as any).os.i.client_settings.fetchOnScroll;
+			this.$watch('os.i.client_settings.fetchOnScroll', v => {
+				this.fetchOnScroll = v;
+			});
 		}
 	},
 	methods: {
@@ -230,6 +248,12 @@ export default Vue.extend({
 			this.$router.push('/i/customize-home');
 			this.$emit('done');
 		},
+		onChangeFetchOnScroll(v) {
+			(this as any).api('i/update_client_setting', {
+				name: 'fetchOnScroll',
+				value: v
+			});
+		},
 		onChangeAutoWatch(v) {
 			(this as any).api('i/update', {
 				auto_watch: v
diff --git a/src/web/app/desktop/views/components/timeline.vue b/src/web/app/desktop/views/components/timeline.vue
index c35baa159..99889c3cc 100644
--- a/src/web/app/desktop/views/components/timeline.vue
+++ b/src/web/app/desktop/views/components/timeline.vue
@@ -8,10 +8,10 @@
 		%fa:R comments%自分の投稿や、自分がフォローしているユーザーの投稿が表示されます。
 	</p>
 	<mk-posts :posts="posts" ref="timeline">
-		<div slot="footer">
-			<template v-if="!moreFetching">%fa:comments%</template>
+		<button slot="footer" @click="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
+			<template v-if="!moreFetching">もっと見る</template>
 			<template v-if="moreFetching">%fa:spinner .pulse .fw%</template>
-		</div>
+		</button>
 	</mk-posts>
 </div>
 </template>
@@ -105,8 +105,10 @@ export default Vue.extend({
 			this.fetch();
 		},
 		onScroll() {
-			const current = window.scrollY + window.innerHeight;
-			if (current > document.body.offsetHeight - 8) this.more();
+			if ((this as any).os.i.client_settings.fetchOnScroll !== false) {
+				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') {

From 3f757beacb3764aabc5cd66baa7fa0ae350ff3c6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Mar 2018 20:09:58 +0900
Subject: [PATCH 0623/1250] v3990

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 6fbd2fb4c..bc4938282 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3988",
+	"version": "0.0.3990",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 38129afbfa03c1b453dc024fd2a5f29ecdbed4b1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 6 Mar 2018 07:39:47 +0900
Subject: [PATCH 0624/1250] #1189

---
 src/web/app/common/mios.ts | 29 +++++++++++++++++------------
 1 file changed, 17 insertions(+), 12 deletions(-)

diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
index bf04588bc..da1d9746a 100644
--- a/src/web/app/common/mios.ts
+++ b/src/web/app/common/mios.ts
@@ -170,23 +170,13 @@ export default class MiOS extends EventEmitter {
 			this.streams.messagingIndexStream = new MessagingIndexStreamManager(this.i);
 		});
 
-		//#region load google maps api
-		(window as any).initGoogleMaps = () => {
-			this.emit('init-google-maps');
-		};
-		const head = document.getElementsByTagName('head')[0];
-		const script = document.createElement('script');
-		script.setAttribute('src', `https://maps.googleapis.com/maps/api/js?key=${googleMapsApiKey}&callback=initGoogleMaps`);
-		script.setAttribute('async', 'true');
-		script.setAttribute('defer', 'true');
-		head.appendChild(script);
-		//#endregion
-
 		if (this.debug) {
 			(window as any).os = this;
 		}
 	}
 
+	private googleMapsIniting = false;
+
 	public getGoogleMaps() {
 		return new Promise((res, rej) => {
 			if ((window as any).google && (window as any).google.maps) {
@@ -195,6 +185,21 @@ export default class MiOS extends EventEmitter {
 				this.once('init-google-maps', () => {
 					res((window as any).google.maps);
 				});
+
+				//#region load google maps api
+				if (!this.googleMapsIniting) {
+					this.googleMapsIniting = true;
+					(window as any).initGoogleMaps = () => {
+						this.emit('init-google-maps');
+					};
+					const head = document.getElementsByTagName('head')[0];
+					const script = document.createElement('script');
+					script.setAttribute('src', `https://maps.googleapis.com/maps/api/js?key=${googleMapsApiKey}&callback=initGoogleMaps`);
+					script.setAttribute('async', 'true');
+					script.setAttribute('defer', 'true');
+					head.appendChild(script);
+				}
+				//#endregion
 			}
 		});
 	}

From ad88344ff1eafc65cf3cc15fb72ec14f48d306cd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 6 Mar 2018 08:35:25 +0900
Subject: [PATCH 0625/1250] nanka iroiro

Closes #1188
---
 package.json                                  |  1 +
 src/api/models/user.ts                        |  2 +-
 src/api/private/signup.ts                     |  3 +-
 src/web/app/common/mios.ts                    | 10 ++++++-
 .../common/scripts/streaming/home-stream.ts   |  7 ++++-
 .../desktop/views/components/post-detail.vue  | 26 ++++++++++-------
 .../desktop/views/components/posts.post.vue   | 26 ++++++++++-------
 .../app/desktop/views/components/settings.vue | 29 +++++++------------
 .../mobile/views/components/post-detail.vue   | 26 ++++++++++-------
 src/web/app/mobile/views/components/post.vue  | 26 ++++++++++-------
 10 files changed, 93 insertions(+), 63 deletions(-)

diff --git a/package.json b/package.json
index bc4938282..4afc84f3e 100644
--- a/package.json
+++ b/package.json
@@ -144,6 +144,7 @@
 		"node-sass": "^4.7.2",
 		"node-sass-json-importer": "^3.1.3",
 		"nprogress": "0.2.0",
+		"object-assign-deep": "^0.3.1",
 		"on-build-webpack": "^0.1.0",
 		"os-utils": "0.0.14",
 		"progress-bar-webpack-plugin": "^1.11.0",
diff --git a/src/api/models/user.ts b/src/api/models/user.ts
index 2fea0566b..ba2765c79 100644
--- a/src/api/models/user.ts
+++ b/src/api/models/user.ts
@@ -118,7 +118,6 @@ export const pack = (
 	let _user: any;
 
 	const fields = opts.detail ? {
-		settings: false
 	} : {
 		settings: false,
 		client_settings: false,
@@ -173,6 +172,7 @@ export const pack = (
 	// Visible via only the official client
 	if (!opts.includeSecrets) {
 		delete _user.email;
+		delete _user.settings;
 		delete _user.client_settings;
 	}
 
diff --git a/src/api/private/signup.ts b/src/api/private/signup.ts
index 1e3e9fb45..3df00ae42 100644
--- a/src/api/private/signup.ts
+++ b/src/api/private/signup.ts
@@ -136,8 +136,7 @@ export default async (req: express.Request, res: express.Response) => {
 			auto_watch: true
 		},
 		client_settings: {
-			home: homeData,
-			show_donation: false
+			home: homeData
 		}
 	});
 
diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
index da1d9746a..bbe28960f 100644
--- a/src/web/app/common/mios.ts
+++ b/src/web/app/common/mios.ts
@@ -1,5 +1,6 @@
 import Vue from 'vue';
 import { EventEmitter } from 'eventemitter3';
+import * as merge from 'object-assign-deep';
 
 import { host, apiUrl, swPublickey, version, lang, googleMapsApiKey } from '../config';
 import Progress from './scripts/loading';
@@ -284,6 +285,13 @@ export default class MiOS extends EventEmitter {
 		// フェッチが完了したとき
 		const fetched = me => {
 			if (me) {
+				// デフォルトの設定をマージ
+				me.client_settings = Object.assign({
+					fetchOnScroll: true,
+					showMaps: true,
+					showPostFormOnTopOfTl: false
+				}, me.client_settings);
+
 				// ローカルストレージにキャッシュ
 				localStorage.setItem('me', JSON.stringify(me));
 			}
@@ -313,7 +321,7 @@ export default class MiOS extends EventEmitter {
 
 			// 後から新鮮なデータをフェッチ
 			fetchme(cachedMe.token, freshData => {
-				Object.assign(cachedMe, freshData);
+				merge(cachedMe, freshData);
 			});
 		} 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 57bf0ec2a..3516705e2 100644
--- a/src/web/app/common/scripts/streaming/home-stream.ts
+++ b/src/web/app/common/scripts/streaming/home-stream.ts
@@ -1,3 +1,5 @@
+import * as merge from 'object-assign-deep';
+
 import Stream from './stream';
 import MiOS from '../../mios';
 
@@ -18,7 +20,10 @@ export default class Connection extends Stream {
 
 		// 自分の情報が更新されたとき
 		this.on('i_updated', i => {
-			Object.assign(me, i);
+			if (os.debug) {
+				console.log('I updated:', i);
+			}
+			merge(me, i);
 		});
 
 		// トークンが再生成されたとき
diff --git a/src/web/app/desktop/views/components/post-detail.vue b/src/web/app/desktop/views/components/post-detail.vue
index e33120c7c..e454c2870 100644
--- a/src/web/app/desktop/views/components/post-detail.vue
+++ b/src/web/app/desktop/views/components/post-detail.vue
@@ -148,17 +148,20 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			(this as any).os.getGoogleMaps().then(maps => {
-				const uluru = new maps.LatLng(this.p.geo.latitude, this.p.geo.longitude);
-				const map = new maps.Map(this.$refs.map, {
-					center: uluru,
-					zoom: 15
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.client_settings.showMaps : true;
+			if (shouldShowMap) {
+				(this as any).os.getGoogleMaps().then(maps => {
+					const uluru = new maps.LatLng(this.p.geo.latitude, this.p.geo.longitude);
+					const map = new maps.Map(this.$refs.map, {
+						center: uluru,
+						zoom: 15
+					});
+					new maps.Marker({
+						position: uluru,
+						map: map
+					});
 				});
-				new maps.Marker({
-					position: uluru,
-					map: map
-				});
-			});
+			}
 		}
 	},
 	methods: {
@@ -348,6 +351,9 @@ export default Vue.extend({
 				width 100%
 				height 300px
 
+				&:empty
+					display none
+
 			> .mk-url-preview
 				margin-top 8px
 
diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index b88f016ad..1a5f7c3b0 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -162,17 +162,20 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			(this as any).os.getGoogleMaps().then(maps => {
-				const uluru = new maps.LatLng(this.p.geo.latitude, this.p.geo.longitude);
-				const map = new maps.Map(this.$refs.map, {
-					center: uluru,
-					zoom: 15
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.client_settings.showMaps : true;
+			if (shouldShowMap) {
+				(this as any).os.getGoogleMaps().then(maps => {
+					const uluru = new maps.LatLng(this.p.geo.latitude, this.p.geo.longitude);
+					const map = new maps.Map(this.$refs.map, {
+						center: uluru,
+						zoom: 15
+					});
+					new maps.Marker({
+						position: uluru,
+						map: map
+					});
 				});
-				new maps.Marker({
-					position: uluru,
-					map: map
-				});
-			});
+			}
 		}
 	},
 	beforeDestroy() {
@@ -467,6 +470,9 @@ export default Vue.extend({
 					width 100%
 					height 300px
 
+					&:empty
+						display none
+
 				> .tags
 					margin 4px 0 0 0
 
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index 3278efb9c..524e055c3 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -20,7 +20,7 @@
 
 		<section class="web" v-show="page == 'web'">
 			<h1>動作</h1>
-			<mk-switch v-model="fetchOnScroll" @change="onChangeFetchOnScroll" text="スクロールで自動読み込み">
+			<mk-switch v-model="os.i.client_settings.fetchOnScroll" @change="onChangeFetchOnScroll" text="スクロールで自動読み込み">
 				<span>ページを下までスクロールしたときに自動で追加のコンテンツを読み込みます。</span>
 			</mk-switch>
 		</section>
@@ -31,6 +31,9 @@
 				<button class="ui button" @click="customizeHome">ホームをカスタマイズ</button>
 			</div>
 			<mk-switch v-model="os.i.client_settings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="タイムライン上部に投稿フォームを表示する"/>
+			<mk-switch v-model="os.i.client_settings.showMaps" @change="onChangeShowMaps" text="マップの自動展開">
+				<span>位置情報が添付された投稿のマップを自動的に展開します。</span>
+			</mk-switch>
 		</section>
 
 		<section class="web" v-show="page == 'web'">
@@ -71,7 +74,7 @@
 
 		<section class="notification" v-show="page == 'notification'">
 			<h1>通知</h1>
-			<mk-switch v-model="autoWatch" @change="onChangeAutoWatch" text="投稿の自動ウォッチ">
+			<mk-switch v-model="os.i.settings.auto_watch" @change="onChangeAutoWatch" text="投稿の自動ウォッチ">
 				<span>リアクションしたり返信したりした投稿に関する通知を自動的に受け取るようにします。</span>
 			</mk-switch>
 		</section>
@@ -193,8 +196,6 @@ export default Vue.extend({
 			version,
 			latestVersion: undefined,
 			checkingForUpdate: false,
-			fetchOnScroll: true,
-			autoWatch: true,
 			enableSounds: localStorage.getItem('enableSounds') == 'true',
 			lang: localStorage.getItem('lang') || '',
 			preventUpdate: localStorage.getItem('preventUpdate') == 'true',
@@ -228,20 +229,6 @@ export default Vue.extend({
 		(this as any).os.getMeta().then(meta => {
 			this.meta = meta;
 		});
-
-		if ((this as any).os.i.settings.auto_watch != null) {
-			this.autoWatch = (this as any).os.i.settings.auto_watch;
-			this.$watch('os.i.settings.auto_watch', v => {
-				this.autoWatch = v;
-			});
-		}
-
-		if ((this as any).os.i.client_settings.fetchOnScroll != null) {
-			this.fetchOnScroll = (this as any).os.i.client_settings.fetchOnScroll;
-			this.$watch('os.i.client_settings.fetchOnScroll', v => {
-				this.fetchOnScroll = v;
-			});
-		}
 	},
 	methods: {
 		customizeHome() {
@@ -265,6 +252,12 @@ export default Vue.extend({
 				value: v
 			});
 		},
+		onChangeShowMaps(v) {
+			(this as any).api('i/update_client_setting', {
+				name: 'showMaps',
+				value: v
+			});
+		},
 		onChangeDisableViaMobile(v) {
 			(this as any).api('i/update_client_setting', {
 				name: 'disableViaMobile',
diff --git a/src/web/app/mobile/views/components/post-detail.vue b/src/web/app/mobile/views/components/post-detail.vue
index bf8ce4f6f..d51bfbc0e 100644
--- a/src/web/app/mobile/views/components/post-detail.vue
+++ b/src/web/app/mobile/views/components/post-detail.vue
@@ -144,17 +144,20 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			(this as any).os.getGoogleMaps().then(maps => {
-				const uluru = new maps.LatLng(this.p.geo.latitude, this.p.geo.longitude);
-				const map = new maps.Map(this.$refs.map, {
-					center: uluru,
-					zoom: 15
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.client_settings.showMaps : true;
+			if (shouldShowMap) {
+				(this as any).os.getGoogleMaps().then(maps => {
+					const uluru = new maps.LatLng(this.p.geo.latitude, this.p.geo.longitude);
+					const map = new maps.Map(this.$refs.map, {
+						center: uluru,
+						zoom: 15
+					});
+					new maps.Marker({
+						position: uluru,
+						map: map
+					});
 				});
-				new maps.Marker({
-					position: uluru,
-					map: map
-				});
-			});
+			}
 		}
 	},
 	methods: {
@@ -349,6 +352,9 @@ export default Vue.extend({
 				width 100%
 				height 200px
 
+				&:empty
+					display none
+
 			> .mk-url-preview
 				margin-top 8px
 
diff --git a/src/web/app/mobile/views/components/post.vue b/src/web/app/mobile/views/components/post.vue
index 390c6396f..3b31b827f 100644
--- a/src/web/app/mobile/views/components/post.vue
+++ b/src/web/app/mobile/views/components/post.vue
@@ -137,17 +137,20 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			(this as any).os.getGoogleMaps().then(maps => {
-				const uluru = new maps.LatLng(this.p.geo.latitude, this.p.geo.longitude);
-				const map = new maps.Map(this.$refs.map, {
-					center: uluru,
-					zoom: 15
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.client_settings.showMaps : true;
+			if (shouldShowMap) {
+				(this as any).os.getGoogleMaps().then(maps => {
+					const uluru = new maps.LatLng(this.p.geo.latitude, this.p.geo.longitude);
+					const map = new maps.Map(this.$refs.map, {
+						center: uluru,
+						zoom: 15
+					});
+					new maps.Marker({
+						position: uluru,
+						map: map
+					});
 				});
-				new maps.Marker({
-					position: uluru,
-					map: map
-				});
-			});
+			}
 		}
 	},
 	beforeDestroy() {
@@ -448,6 +451,9 @@ export default Vue.extend({
 					width 100%
 					height 200px
 
+					&:empty
+						display none
+
 				> .app
 					font-size 12px
 					color #ccc

From 3f46bef1550428f0b3e4752ae93d9e34f051e0e5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 6 Mar 2018 08:36:05 +0900
Subject: [PATCH 0626/1250] v3993

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 4afc84f3e..ca01d57cc 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3990",
+	"version": "0.0.3993",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 6140708efaf91b704196a686c3417649e2848836 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Tue, 6 Mar 2018 09:58:34 +0900
Subject: [PATCH 0627/1250] Update settings.vue

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

diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index 524e055c3..3e2d6850b 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -26,7 +26,7 @@
 		</section>
 
 		<section class="web" v-show="page == 'web'">
-			<h1>デザイン</h1>
+			<h1>デザインと表示</h1>
 			<div class="div">
 				<button class="ui button" @click="customizeHome">ホームをカスタマイズ</button>
 			</div>

From 8953ba333c7a2da832f1b34e9003d22bb2e0e4ad Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 6 Mar 2018 14:32:15 +0900
Subject: [PATCH 0628/1250] :v:

---
 src/web/app/init.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index 38b74d450..52d2ecf99 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -52,7 +52,7 @@ Vue.mixin({
 console.info(`Misskey v${version} (葵 aoi)`);
 console.info(
 	'%cここにコードを入力したり張り付けたりしないでください。アカウントが不正利用される可能性があります。',
-	'color: red; background: yellow; font-size: 16px;');
+	'color: red; background: yellow; font-size: 16px; font-weight: bold;');
 
 // BootTimer解除
 window.clearTimeout((window as any).mkBootTimer);

From 83af78cbae5f395adc1ec110fcf7ec09f3f41f1b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 6 Mar 2018 14:33:01 +0900
Subject: [PATCH 0629/1250] #1191

---
 src/web/app/common/mios.ts                       |  3 ++-
 .../app/desktop/views/components/settings.vue    |  7 +++++++
 .../views/components/widget-container.vue        | 15 ++++++++++++++-
 src/web/app/desktop/views/components/window.vue  | 16 +++++++++++++++-
 4 files changed, 38 insertions(+), 3 deletions(-)

diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
index bbe28960f..cbb4400a7 100644
--- a/src/web/app/common/mios.ts
+++ b/src/web/app/common/mios.ts
@@ -289,7 +289,8 @@ export default class MiOS extends EventEmitter {
 				me.client_settings = Object.assign({
 					fetchOnScroll: true,
 					showMaps: true,
-					showPostFormOnTopOfTl: false
+					showPostFormOnTopOfTl: false,
+					gradientWindowHeader: false
 				}, me.client_settings);
 
 				// ローカルストレージにキャッシュ
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index 3e2d6850b..d09b32532 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -34,6 +34,7 @@
 			<mk-switch v-model="os.i.client_settings.showMaps" @change="onChangeShowMaps" text="マップの自動展開">
 				<span>位置情報が添付された投稿のマップを自動的に展開します。</span>
 			</mk-switch>
+			<mk-switch v-model="os.i.client_settings.gradientWindowHeader" @change="onChangeGradientWindowHeader" text="ウィンドウのタイトルバーにグラデーションを使用"/>
 		</section>
 
 		<section class="web" v-show="page == 'web'">
@@ -258,6 +259,12 @@ export default Vue.extend({
 				value: v
 			});
 		},
+		onChangeGradientWindowHeader(v) {
+			(this as any).api('i/update_client_setting', {
+				name: 'gradientWindowHeader',
+				value: v
+			});
+		},
 		onChangeDisableViaMobile(v) {
 			(this as any).api('i/update_client_setting', {
 				name: 'disableViaMobile',
diff --git a/src/web/app/desktop/views/components/widget-container.vue b/src/web/app/desktop/views/components/widget-container.vue
index 7b4e1f55f..c08e58e21 100644
--- a/src/web/app/desktop/views/components/widget-container.vue
+++ b/src/web/app/desktop/views/components/widget-container.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="mk-widget-container" :class="{ naked }">
-	<header v-if="showHeader">
+	<header :class="{ withGradient }" v-if="showHeader">
 		<div class="title"><slot name="header"></slot></div>
 		<slot name="func"></slot>
 	</header>
@@ -20,6 +20,15 @@ export default Vue.extend({
 			type: Boolean,
 			default: false
 		}
+	},
+	computed: {
+		withGradient(): boolean {
+			return (this as any).os.isSignedIn
+				? (this as any).os.i.client_settings.gradientWindowHeader != null
+					? (this as any).os.i.client_settings.gradientWindowHeader
+					: false
+				: false;
+		}
 	}
 });
 </script>
@@ -69,4 +78,8 @@ export default Vue.extend({
 			&:active
 				color #999
 
+		&.withGradient
+			> .title
+				background linear-gradient(to bottom, #fff, #ececec)
+				box-shadow 0 1px rgba(#000, 0.11)
 </style>
diff --git a/src/web/app/desktop/views/components/window.vue b/src/web/app/desktop/views/components/window.vue
index a92cfd0b6..f525d6962 100644
--- a/src/web/app/desktop/views/components/window.vue
+++ b/src/web/app/desktop/views/components/window.vue
@@ -3,7 +3,10 @@
 	<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" @contextmenu.prevent="() => {}" @mousedown.prevent="onHeaderMousedown">
+			<header ref="header"
+				:class="{ withGradient }"
+				@contextmenu.prevent="() => {}" @mousedown.prevent="onHeaderMousedown"
+			>
 				<h1><slot name="header"></slot></h1>
 				<div>
 					<button class="popout" v-if="popoutUrl" @mousedown.stop="() => {}" @click="popout" title="ポップアウト">%fa:R window-restore%</button>
@@ -75,6 +78,13 @@ export default Vue.extend({
 		},
 		canResize(): boolean {
 			return !this.isFlexible;
+		},
+		withGradient(): boolean {
+			return (this as any).os.isSignedIn
+				? (this as any).os.i.client_settings.gradientWindowHeader != null
+					? (this as any).os.i.client_settings.gradientWindowHeader
+					: false
+				: false;
 		}
 	},
 
@@ -537,6 +547,10 @@ export default Vue.extend({
 				border-radius 6px 6px 0 0
 				box-shadow 0 1px 0 rgba(#000, 0.1)
 
+				&.withGradient
+					background linear-gradient(to bottom, #fff, #ececec)
+					box-shadow 0 1px 0 rgba(#000, 0.15)
+
 				&, *
 					user-select none
 

From d02f084c054d8bb567718635287046d7edc74888 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 6 Mar 2018 14:33:31 +0900
Subject: [PATCH 0630/1250] v3997

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index ca01d57cc..c4f8266a7 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3993",
+	"version": "0.0.3997",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 3e642a3b681b6a53ce32160f67c9a43d0c8a761f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 6 Mar 2018 18:06:45 +0900
Subject: [PATCH 0631/1250] #1195

---
 .../views/components/messaging-room.vue       |  4 +++-
 .../app/desktop/views/components/settings.vue | 19 ++++++++++++++++++-
 .../app/desktop/views/components/timeline.vue |  4 +++-
 3 files changed, 24 insertions(+), 3 deletions(-)

diff --git a/src/web/app/common/views/components/messaging-room.vue b/src/web/app/common/views/components/messaging-room.vue
index e15e10ec7..637fa9cd6 100644
--- a/src/web/app/common/views/components/messaging-room.vue
+++ b/src/web/app/common/views/components/messaging-room.vue
@@ -150,7 +150,9 @@ export default Vue.extend({
 		onMessage(message) {
 			// サウンドを再生する
 			if ((this as any).os.isEnableSounds) {
-				new Audio(`${url}/assets/message.mp3`).play();
+				const sound = new Audio(`${url}/assets/message.mp3`);
+				sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 1;
+				sound.play();
 			}
 
 			const isBottom = this.isBottom();
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index d09b32532..01c41194e 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -42,6 +42,14 @@
 			<mk-switch v-model="enableSounds" text="サウンドを有効にする">
 				<span>投稿やメッセージを送受信したときなどにサウンドを再生します。この設定はブラウザに記憶されます。</span>
 			</mk-switch>
+			<label>ボリューム</label>
+			<el-slider
+				v-model="soundVolume"
+				:show-input="true"
+				:format-tooltip="v => `${v}%`"
+				:disabled="!enableSounds"
+			/>
+			<button class="ui button" @click="soundTest">%fa:volume-up% テスト</button>
 		</section>
 
 		<section class="web" v-show="page == 'web'">
@@ -175,7 +183,7 @@ import XApi from './settings.api.vue';
 import XApps from './settings.apps.vue';
 import XSignins from './settings.signins.vue';
 import XDrive from './settings.drive.vue';
-import { docsUrl, license, lang, version } from '../../../config';
+import { url, docsUrl, license, lang, version } from '../../../config';
 import checkForUpdate from '../../../common/scripts/check-for-update';
 
 export default Vue.extend({
@@ -198,6 +206,7 @@ export default Vue.extend({
 			latestVersion: undefined,
 			checkingForUpdate: false,
 			enableSounds: localStorage.getItem('enableSounds') == 'true',
+			soundVolume: localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) : 100,
 			lang: localStorage.getItem('lang') || '',
 			preventUpdate: localStorage.getItem('preventUpdate') == 'true',
 			debug: localStorage.getItem('debug') == 'true',
@@ -208,6 +217,9 @@ export default Vue.extend({
 		enableSounds() {
 			localStorage.setItem('enableSounds', this.enableSounds ? 'true' : 'false');
 		},
+		soundVolume() {
+			localStorage.setItem('soundVolume', this.soundVolume.toString());
+		},
 		lang() {
 			localStorage.setItem('lang', this.lang);
 		},
@@ -295,6 +307,11 @@ export default Vue.extend({
 				title: 'キャッシュを削除しました',
 				text: 'ページを再度読み込みしてください。'
 			});
+		},
+		soundTest() {
+			const sound = new Audio(`${url}/assets/message.mp3`);
+			sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 1;
+			sound.play();
 		}
 	}
 });
diff --git a/src/web/app/desktop/views/components/timeline.vue b/src/web/app/desktop/views/components/timeline.vue
index 99889c3cc..b6b28c352 100644
--- a/src/web/app/desktop/views/components/timeline.vue
+++ b/src/web/app/desktop/views/components/timeline.vue
@@ -96,7 +96,9 @@ export default Vue.extend({
 		onPost(post) {
 			// サウンドを再生する
 			if ((this as any).os.isEnableSounds) {
-				new Audio(`${url}/assets/post.mp3`).play();
+				const sound = new Audio(`${url}/assets/post.mp3`);
+				sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 1;
+				sound.play();
 			}
 
 			this.posts.unshift(post);

From 715a0cc26f8074e7e8faf73f39dc86e9f9920caf Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 6 Mar 2018 18:07:01 +0900
Subject: [PATCH 0632/1250] v3999

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index c4f8266a7..cc072ee1b 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3997",
+	"version": "0.0.3999",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From a49777c76c7e6812d5f76197a5367086bc4e6ba3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 7 Mar 2018 01:54:56 +0900
Subject: [PATCH 0633/1250] wip

---
 src/api/endpoints/othello/sessions/create.ts  | 18 ++++++++++
 src/api/endpoints/othello/sessions/in.ts      | 34 +++++++++++++++++++
 src/api/models/othello-game.ts                | 33 ++++++++++++++++++
 src/api/models/othello-session.ts             | 29 ++++++++++++++++
 src/web/app/common/views/components/index.ts  |  2 ++
 .../app/common/views/components/othello.vue   | 32 +++++++++++++++++
 6 files changed, 148 insertions(+)
 create mode 100644 src/api/endpoints/othello/sessions/create.ts
 create mode 100644 src/api/endpoints/othello/sessions/in.ts
 create mode 100644 src/api/models/othello-game.ts
 create mode 100644 src/api/models/othello-session.ts
 create mode 100644 src/web/app/common/views/components/othello.vue

diff --git a/src/api/endpoints/othello/sessions/create.ts b/src/api/endpoints/othello/sessions/create.ts
new file mode 100644
index 000000000..09c3cff62
--- /dev/null
+++ b/src/api/endpoints/othello/sessions/create.ts
@@ -0,0 +1,18 @@
+import rndstr from 'rndstr';
+import Session, { pack } from '../../../models/othello-session';
+
+module.exports = (params, user) => new Promise(async (res, rej) => {
+	// 以前のセッションはすべて削除しておく
+	await Session.remove({
+		user_id: user._id
+	});
+
+	// セッションを作成
+	const session = await Session.insert({
+		user_id: user._id,
+		code: rndstr('a-z0-9', 3)
+	});
+
+	// Reponse
+	res(await pack(session));
+});
diff --git a/src/api/endpoints/othello/sessions/in.ts b/src/api/endpoints/othello/sessions/in.ts
new file mode 100644
index 000000000..d4b95bc4f
--- /dev/null
+++ b/src/api/endpoints/othello/sessions/in.ts
@@ -0,0 +1,34 @@
+import $ from 'cafy';
+import Session from '../../../models/othello-session';
+import Game, { pack } from '../../../models/othello-game';
+
+module.exports = (params, user) => new Promise(async (res, rej) => {
+	// Get 'code' parameter
+	const [code, codeErr] = $(params.code).string().$;
+	if (codeErr) return rej('invalid code param');
+
+	// Fetch session
+	const session = await Session.findOne({ code });
+
+	if (session == null) {
+		return rej('session not found');
+	}
+
+	// Destroy session
+	Session.remove({
+		_id: session._id
+	});
+
+	const parentIsBlack = Math.random() > 0.5;
+
+	// Start game
+	const game = await Game.insert({
+		created_at: new Date(),
+		black_user_id: parentIsBlack ? session.user_id : user._id,
+		white_user_id: parentIsBlack ? user._id : session.user_id,
+		logs: []
+	});
+
+	// Reponse
+	res(await pack(game));
+});
diff --git a/src/api/models/othello-game.ts b/src/api/models/othello-game.ts
new file mode 100644
index 000000000..a6beaaf9c
--- /dev/null
+++ b/src/api/models/othello-game.ts
@@ -0,0 +1,33 @@
+import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
+import db from '../../db/mongodb';
+
+const Game = db.get<IGame>('othello_games');
+export default Game;
+
+export interface IGame {
+	_id: mongo.ObjectID;
+	created_at: Date;
+	black_user_id: mongo.ObjectID;
+	white_user_id: mongo.ObjectID;
+	logs: any[];
+}
+
+/**
+ * Pack an othello game for API response
+ *
+ * @param {any} game
+ * @return {Promise<any>}
+ */
+export const pack = (
+	game: any
+) => new Promise<any>(async (resolve, reject) => {
+
+	const _game = deepcopy(game);
+
+	// Rename _id to id
+	_game.id = _game._id;
+	delete _game._id;
+
+	resolve(_game);
+});
diff --git a/src/api/models/othello-session.ts b/src/api/models/othello-session.ts
new file mode 100644
index 000000000..0aa1d01e5
--- /dev/null
+++ b/src/api/models/othello-session.ts
@@ -0,0 +1,29 @@
+import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
+import db from '../../db/mongodb';
+
+const Session = db.get<ISession>('othello_sessions');
+export default Session;
+
+export interface ISession {
+	_id: mongo.ObjectID;
+	code: string;
+	user_id: mongo.ObjectID;
+}
+
+/**
+ * Pack an othello session for API response
+ *
+ * @param {any} session
+ * @return {Promise<any>}
+ */
+export const pack = (
+	session: any
+) => new Promise<any>(async (resolve, reject) => {
+
+	const _session = deepcopy(session);
+
+	delete _session._id;
+
+	resolve(_session);
+});
diff --git a/src/web/app/common/views/components/index.ts b/src/web/app/common/views/components/index.ts
index 527492022..98fc2352f 100644
--- a/src/web/app/common/views/components/index.ts
+++ b/src/web/app/common/views/components/index.ts
@@ -21,6 +21,7 @@ import urlPreview from './url-preview.vue';
 import twitterSetting from './twitter-setting.vue';
 import fileTypeIcon from './file-type-icon.vue';
 import Switch from './switch.vue';
+import Othello from './othello.vue';
 
 Vue.component('mk-signin', signin);
 Vue.component('mk-signup', signup);
@@ -43,3 +44,4 @@ Vue.component('mk-url-preview', urlPreview);
 Vue.component('mk-twitter-setting', twitterSetting);
 Vue.component('mk-file-type-icon', fileTypeIcon);
 Vue.component('mk-switch', Switch);
+Vue.component('mk-othello', Othello);
diff --git a/src/web/app/common/views/components/othello.vue b/src/web/app/common/views/components/othello.vue
new file mode 100644
index 000000000..136046db2
--- /dev/null
+++ b/src/web/app/common/views/components/othello.vue
@@ -0,0 +1,32 @@
+<template>
+<div>
+	<div v-if="session">
+		<h1>相手を待っています<mk-ellipsis/></h1>
+		<p>セッションID:<code>{{ session.code }}</code></p>
+		<p>対戦したい相手に上記のセッションIDを伝えてください。相手が「セッションイン」でセッションIDを入力すると、対局が開始されます。</p>
+	</div>
+	<div v-else>
+		<h1>Misskey Othello</h1>
+		<p>他のMisskeyユーザーとオセロで対戦しよう。</p>
+		<button>フリーマッチ(準備中)</button>
+		<button @click="inSession">セッションイン</button>
+		<button @click="createSession">セッションを作成する</button>
+		<section>
+			<h2>過去の対局</h2>
+		</section>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	methods: {
+		createSession() {
+			(this as any).api('othello/sessions/create');
+
+		}
+	}
+});
+</script>
+

From 0b42ca88cb8edab0cb4001a2d9dc0af69c5320b6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 7 Mar 2018 11:40:40 +0900
Subject: [PATCH 0634/1250] wip

---
 src/api/endpoints/othello/match.ts            | 80 ++++++++++++++++++
 src/api/endpoints/othello/sessions/create.ts  | 18 ----
 src/api/endpoints/othello/sessions/in.ts      | 34 --------
 src/api/event.ts                              |  6 ++
 src/api/models/othello-matching.ts            | 11 +++
 src/api/models/othello-session.ts             | 29 -------
 src/api/stream/messaging.ts                   |  2 +-
 src/api/stream/othello-game.ts                | 12 +++
 src/api/stream/othello-matching.ts            | 12 +++
 src/api/stream/requests.ts                    |  2 +-
 src/api/stream/server.ts                      |  2 +-
 src/api/streaming.ts                          |  4 +
 .../app/common/views/components/messaging.vue |  2 +-
 .../common/views/components/othello.game.vue  | 29 +++++++
 .../app/common/views/components/othello.vue   | 82 ++++++++++++++++---
 15 files changed, 230 insertions(+), 95 deletions(-)
 create mode 100644 src/api/endpoints/othello/match.ts
 delete mode 100644 src/api/endpoints/othello/sessions/create.ts
 delete mode 100644 src/api/endpoints/othello/sessions/in.ts
 create mode 100644 src/api/models/othello-matching.ts
 delete mode 100644 src/api/models/othello-session.ts
 create mode 100644 src/api/stream/othello-game.ts
 create mode 100644 src/api/stream/othello-matching.ts
 create mode 100644 src/web/app/common/views/components/othello.game.vue

diff --git a/src/api/endpoints/othello/match.ts b/src/api/endpoints/othello/match.ts
new file mode 100644
index 000000000..2dc22d11f
--- /dev/null
+++ b/src/api/endpoints/othello/match.ts
@@ -0,0 +1,80 @@
+import $ from 'cafy';
+import Matching from '../../models/othello-matchig';
+import Game, { pack } from '../../models/othello-game';
+import User from '../../models/user';
+import { publishOthelloStream } from '../../event';
+
+module.exports = (params, user) => new Promise(async (res, rej) => {
+	// Get 'user_id' parameter
+	const [childId, childIdErr] = $(params.user_id).id().$;
+	if (childIdErr) return rej('invalid user_id param');
+
+	// Myself
+	if (childId.equals(user._id)) {
+		return rej('invalid user_id param');
+	}
+
+	// Find session
+	const exist = await Matching.findOne({
+		parent_id: childId,
+		child_id: user._id
+	});
+
+	if (exist) {
+		// Destroy session
+		Matching.remove({
+			_id: exist._id
+		});
+
+		const parentIsBlack = Math.random() > 0.5;
+
+		// Start game
+		const game = await Game.insert({
+			created_at: new Date(),
+			black_user_id: parentIsBlack ? exist.parent_id : user._id,
+			white_user_id: parentIsBlack ? user._id : exist.parent_id,
+			logs: []
+		});
+
+		const packedGame = await pack(game);
+
+		// Reponse
+		res(packedGame);
+
+		publishOthelloStream(exist.parent_id, 'matched', {
+			game
+		});
+	} else {
+		// Fetch child
+		const child = await User.findOne({
+			_id: childId
+		}, {
+			fields: {
+				_id: true
+			}
+		});
+
+		if (child === null) {
+			return rej('user not found');
+		}
+
+		// 以前のセッションはすべて削除しておく
+		await Matching.remove({
+			parent_id: user._id
+		});
+
+		// セッションを作成
+		await Matching.insert({
+			parent_id: user._id,
+			child_id: child._id
+		});
+
+		// Reponse
+		res(204);
+
+		// 招待
+		publishOthelloStream(child._id, 'invited', {
+			user_id: user._id
+		});
+	}
+});
diff --git a/src/api/endpoints/othello/sessions/create.ts b/src/api/endpoints/othello/sessions/create.ts
deleted file mode 100644
index 09c3cff62..000000000
--- a/src/api/endpoints/othello/sessions/create.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import rndstr from 'rndstr';
-import Session, { pack } from '../../../models/othello-session';
-
-module.exports = (params, user) => new Promise(async (res, rej) => {
-	// 以前のセッションはすべて削除しておく
-	await Session.remove({
-		user_id: user._id
-	});
-
-	// セッションを作成
-	const session = await Session.insert({
-		user_id: user._id,
-		code: rndstr('a-z0-9', 3)
-	});
-
-	// Reponse
-	res(await pack(session));
-});
diff --git a/src/api/endpoints/othello/sessions/in.ts b/src/api/endpoints/othello/sessions/in.ts
deleted file mode 100644
index d4b95bc4f..000000000
--- a/src/api/endpoints/othello/sessions/in.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import $ from 'cafy';
-import Session from '../../../models/othello-session';
-import Game, { pack } from '../../../models/othello-game';
-
-module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'code' parameter
-	const [code, codeErr] = $(params.code).string().$;
-	if (codeErr) return rej('invalid code param');
-
-	// Fetch session
-	const session = await Session.findOne({ code });
-
-	if (session == null) {
-		return rej('session not found');
-	}
-
-	// Destroy session
-	Session.remove({
-		_id: session._id
-	});
-
-	const parentIsBlack = Math.random() > 0.5;
-
-	// Start game
-	const game = await Game.insert({
-		created_at: new Date(),
-		black_user_id: parentIsBlack ? session.user_id : user._id,
-		white_user_id: parentIsBlack ? user._id : session.user_id,
-		logs: []
-	});
-
-	// Reponse
-	res(await pack(game));
-});
diff --git a/src/api/event.ts b/src/api/event.ts
index 4a2e4e453..e68082f0a 100644
--- a/src/api/event.ts
+++ b/src/api/event.ts
@@ -38,6 +38,10 @@ class MisskeyEvent {
 		this.publish(`messaging-index-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
+	public publishOthelloStream(userId: ID, type: string, value?: any): void {
+		this.publish(`othello-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
+	}
+
 	public publishChannelStream(channelId: ID, type: string, value?: any): void {
 		this.publish(`channel-stream:${channelId}`, type, typeof value === 'undefined' ? null : value);
 	}
@@ -65,4 +69,6 @@ export const publishMessagingStream = ev.publishMessagingStream.bind(ev);
 
 export const publishMessagingIndexStream = ev.publishMessagingIndexStream.bind(ev);
 
+export const publishOthelloStream = ev.publishOthelloStream.bind(ev);
+
 export const publishChannelStream = ev.publishChannelStream.bind(ev);
diff --git a/src/api/models/othello-matching.ts b/src/api/models/othello-matching.ts
new file mode 100644
index 000000000..bd7aeef3c
--- /dev/null
+++ b/src/api/models/othello-matching.ts
@@ -0,0 +1,11 @@
+import * as mongo from 'mongodb';
+import db from '../../db/mongodb';
+
+const Matching = db.get<IMatching>('othello_matchings');
+export default Matching;
+
+export interface IMatching {
+	_id: mongo.ObjectID;
+	parent_id: mongo.ObjectID;
+	child_id: mongo.ObjectID;
+}
diff --git a/src/api/models/othello-session.ts b/src/api/models/othello-session.ts
deleted file mode 100644
index 0aa1d01e5..000000000
--- a/src/api/models/othello-session.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import * as mongo from 'mongodb';
-import deepcopy = require('deepcopy');
-import db from '../../db/mongodb';
-
-const Session = db.get<ISession>('othello_sessions');
-export default Session;
-
-export interface ISession {
-	_id: mongo.ObjectID;
-	code: string;
-	user_id: mongo.ObjectID;
-}
-
-/**
- * Pack an othello session for API response
- *
- * @param {any} session
- * @return {Promise<any>}
- */
-export const pack = (
-	session: any
-) => new Promise<any>(async (resolve, reject) => {
-
-	const _session = deepcopy(session);
-
-	delete _session._id;
-
-	resolve(_session);
-});
diff --git a/src/api/stream/messaging.ts b/src/api/stream/messaging.ts
index 3f505cfaf..a4a12426a 100644
--- a/src/api/stream/messaging.ts
+++ b/src/api/stream/messaging.ts
@@ -2,7 +2,7 @@ import * as websocket from 'websocket';
 import * as redis from 'redis';
 import read from '../common/read-messaging-message';
 
-export default function messagingStream(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void {
+export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void {
 	const otherparty = request.resourceURL.query.otherparty;
 
 	// Subscribe messaging stream
diff --git a/src/api/stream/othello-game.ts b/src/api/stream/othello-game.ts
new file mode 100644
index 000000000..ab91ef642
--- /dev/null
+++ b/src/api/stream/othello-game.ts
@@ -0,0 +1,12 @@
+import * as websocket from 'websocket';
+import * as redis from 'redis';
+
+export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient): void {
+	const game = request.resourceURL.query.game;
+
+	// Subscribe game stream
+	subscriber.subscribe(`misskey:othello-game-stream:${game}`);
+	subscriber.on('message', (_, data) => {
+		connection.send(data);
+	});
+}
diff --git a/src/api/stream/othello-matching.ts b/src/api/stream/othello-matching.ts
new file mode 100644
index 000000000..f30ce6eb0
--- /dev/null
+++ b/src/api/stream/othello-matching.ts
@@ -0,0 +1,12 @@
+import * as websocket from 'websocket';
+import * as redis from 'redis';
+
+export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void {
+	const otherparty = request.resourceURL.query.otherparty;
+
+	// Subscribe matching stream
+	subscriber.subscribe(`misskey:othello-matching:${user._id}-${otherparty}`);
+	subscriber.on('message', (_, data) => {
+		connection.send(data);
+	});
+}
diff --git a/src/api/stream/requests.ts b/src/api/stream/requests.ts
index 2c36e58b6..d7bb5e6c5 100644
--- a/src/api/stream/requests.ts
+++ b/src/api/stream/requests.ts
@@ -3,7 +3,7 @@ import Xev from 'xev';
 
 const ev = new Xev();
 
-export default function homeStream(request: websocket.request, connection: websocket.connection): void {
+export default function(request: websocket.request, connection: websocket.connection): void {
 	const onRequest = request => {
 		connection.send(JSON.stringify({
 			type: 'request',
diff --git a/src/api/stream/server.ts b/src/api/stream/server.ts
index 0db6643d4..4ca2ad1b1 100644
--- a/src/api/stream/server.ts
+++ b/src/api/stream/server.ts
@@ -3,7 +3,7 @@ import Xev from 'xev';
 
 const ev = new Xev();
 
-export default function homeStream(request: websocket.request, connection: websocket.connection): void {
+export default function(request: websocket.request, connection: websocket.connection): void {
 	const onStats = stats => {
 		connection.send(JSON.stringify({
 			type: 'stats',
diff --git a/src/api/streaming.ts b/src/api/streaming.ts
index c06d64c24..66c2e0cec 100644
--- a/src/api/streaming.ts
+++ b/src/api/streaming.ts
@@ -10,6 +10,8 @@ import homeStream from './stream/home';
 import driveStream from './stream/drive';
 import messagingStream from './stream/messaging';
 import messagingIndexStream from './stream/messaging-index';
+import othelloGameStream from './stream/othello-game';
+import othelloMatchingStream from './stream/othello-matching';
 import serverStream from './stream/server';
 import requestsStream from './stream/requests';
 import channelStream from './stream/channel';
@@ -62,6 +64,8 @@ module.exports = (server: http.Server) => {
 			request.resourceURL.pathname === '/drive' ? driveStream :
 			request.resourceURL.pathname === '/messaging' ? messagingStream :
 			request.resourceURL.pathname === '/messaging-index' ? messagingIndexStream :
+			request.resourceURL.pathname === '/othello-game' ? othelloGameStream :
+			request.resourceURL.pathname === '/othello-matching' ? othelloMatchingStream :
 			null;
 
 		if (channel !== null) {
diff --git a/src/web/app/common/views/components/messaging.vue b/src/web/app/common/views/components/messaging.vue
index a94a99668..2ec488c24 100644
--- a/src/web/app/common/views/components/messaging.vue
+++ b/src/web/app/common/views/components/messaging.vue
@@ -89,7 +89,7 @@ export default Vue.extend({
 	beforeDestroy() {
 		this.connection.off('message', this.onMessage);
 		this.connection.off('read', this.onRead);
-		(this as any).os.stream.dispose(this.connectionId);
+		(this as any).streams.messagingIndexStream.dispose(this.connectionId);
 	},
 	methods: {
 		isMe(message) {
diff --git a/src/web/app/common/views/components/othello.game.vue b/src/web/app/common/views/components/othello.game.vue
new file mode 100644
index 000000000..3d3ffb2c0
--- /dev/null
+++ b/src/web/app/common/views/components/othello.game.vue
@@ -0,0 +1,29 @@
+<template>
+<div>
+	<header>黒:{{ game.black_user.name }} 白:{{ game.white_user.name }}</header>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['game'],
+	data() {
+		return {
+			game: null,
+			connection: null,
+			connectionId: null
+		};
+	},
+	mounted() {
+		this.connection = (this as any).os.streams.othelloGameStream.getConnection();
+		this.connectionId = (this as any).os.streams.othelloGameStream.use();
+
+		this.connection.on('set', this.onSet);
+	},
+	beforeDestroy() {
+		this.connection.off('set', this.onSet);
+		(this as any).streams.othelloGameStream.dispose(this.connectionId);
+	},
+});
+</script>
diff --git a/src/web/app/common/views/components/othello.vue b/src/web/app/common/views/components/othello.vue
index 136046db2..f5abcfb10 100644
--- a/src/web/app/common/views/components/othello.vue
+++ b/src/web/app/common/views/components/othello.vue
@@ -1,16 +1,19 @@
 <template>
 <div>
-	<div v-if="session">
-		<h1>相手を待っています<mk-ellipsis/></h1>
-		<p>セッションID:<code>{{ session.code }}</code></p>
-		<p>対戦したい相手に上記のセッションIDを伝えてください。相手が「セッションイン」でセッションIDを入力すると、対局が開始されます。</p>
+	<div v-if="game">
+		<x-game :game="game"/>
+	</div>
+	<div v-if="matching">
+		<h1><b>{{ matching.name }}</b>を待っています<mk-ellipsis/></h1>
 	</div>
 	<div v-else>
 		<h1>Misskey Othello</h1>
 		<p>他のMisskeyユーザーとオセロで対戦しよう。</p>
 		<button>フリーマッチ(準備中)</button>
-		<button @click="inSession">セッションイン</button>
-		<button @click="createSession">セッションを作成する</button>
+		<button @click="match">指名</button>
+		<section>
+			<h2>対局の招待があります:</h2>
+		</section>
 		<section>
 			<h2>過去の対局</h2>
 		</section>
@@ -20,11 +23,70 @@
 
 <script lang="ts">
 import Vue from 'vue';
-export default Vue.extend({
-	methods: {
-		createSession() {
-			(this as any).api('othello/sessions/create');
+import XGame from './othello.game.vue';
 
+export default Vue.extend({
+	components: {
+		XGame
+	},
+	data() {
+		return {
+			game: null,
+			games: [],
+			gamesFetching: true,
+			gamesMoreFetching: false,
+			matching: false,
+			invitations: [],
+			connection: null,
+			connectionId: null
+		};
+	},
+	mounted() {
+		this.connection = (this as any).os.streams.othelloStream.getConnection();
+		this.connectionId = (this as any).os.streams.othelloStream.use();
+
+		this.connection.on('macthed', this.onMatched);
+		this.connection.on('invited', this.onInvited);
+
+		(this as any).api('othello/games').then(games => {
+			this.games = games;
+			this.gamesFetching = false;
+		});
+
+		(this as any).api('othello/invitations').then(invitations => {
+			this.invitations = this.invitations.concat(invitations);
+		});
+	},
+	beforeDestroy() {
+		this.connection.off('macthed', this.onMatched);
+		this.connection.off('invited', this.onInvited);
+		(this as any).streams.othelloStream.dispose(this.connectionId);
+	},
+	methods: {
+		match() {
+			(this as any).apis.input({
+				title: 'ユーザー名を入力してください'
+			}).then(username => {
+				(this as any).api('users/show', {
+					username
+				}).then(user => {
+					(this as any).api('othello/match', {
+						user_id: user.id
+					}).then(res => {
+						if (res == null) {
+							this.matching = user;
+						} else {
+							this.game = res;
+						}
+					});
+				});
+			});
+		},
+		onMatched(game) {
+			this.game = game;
+		},
+		onInvited(invite) {
+			this.invitations.unshift(invite);
 		}
 	}
 });

From 6201fa664d7e839520049c0e7b88090a18bf67ea Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 7 Mar 2018 17:48:32 +0900
Subject: [PATCH 0635/1250] wip

---
 src/api/endpoints.ts                          |  20 ++
 src/api/endpoints/othello/games.ts            |  22 ++
 src/api/endpoints/othello/invitations.ts      |  11 +
 src/api/endpoints/othello/match.ts            |  22 +-
 src/api/endpoints/othello/match/cancel.ts     |   9 +
 src/api/event.ts                              |   6 +
 src/api/models/othello-game.ts                |  20 +-
 src/api/models/othello-matching.ts            |  31 +++
 src/api/stream/othello-game.ts                |  63 +++++-
 .../{othello-matching.ts => othello.ts}       |   6 +-
 src/api/streaming.ts                          |   4 +-
 src/common/othello.ts                         |  66 ++++--
 src/web/app/common/mios.ts                    |  16 +-
 .../{channel-stream.ts => channel.ts}         |   0
 .../scripts/streaming/drive-stream-manager.ts |  20 --
 .../common/scripts/streaming/drive-stream.ts  |  12 --
 src/web/app/common/scripts/streaming/drive.ts |  31 +++
 .../scripts/streaming/home-stream-manager.ts  |  23 --
 .../streaming/{home-stream.ts => home.ts}     |  23 +-
 .../messaging-index-stream-manager.ts         |  20 --
 .../streaming/messaging-index-stream.ts       |  12 --
 .../scripts/streaming/messaging-index.ts      |  31 +++
 .../{messaging-stream.ts => messaging.ts}     |   2 +-
 .../common/scripts/streaming/othello-game.ts  |  10 +
 .../app/common/scripts/streaming/othello.ts   |  28 +++
 .../streaming/requests-stream-manager.ts      |  12 --
 .../scripts/streaming/requests-stream.ts      |  10 -
 .../app/common/scripts/streaming/requests.ts  |  21 ++
 .../streaming/server-stream-manager.ts        |  12 --
 .../common/scripts/streaming/server-stream.ts |  10 -
 .../app/common/scripts/streaming/server.ts    |  21 ++
 .../views/components/messaging-room.vue       |   4 +-
 .../common/views/components/othello.game.vue  | 120 ++++++++++-
 .../app/common/views/components/othello.vue   | 203 ++++++++++++++++--
 .../desktop/views/components/game-window.vue  |  24 +++
 .../views/components/ui.header.nav.vue        |  19 +-
 .../desktop/views/widgets/channel.channel.vue |   2 +-
 37 files changed, 747 insertions(+), 219 deletions(-)
 create mode 100644 src/api/endpoints/othello/games.ts
 create mode 100644 src/api/endpoints/othello/invitations.ts
 create mode 100644 src/api/endpoints/othello/match/cancel.ts
 rename src/api/stream/{othello-matching.ts => othello.ts} (62%)
 rename src/web/app/common/scripts/streaming/{channel-stream.ts => channel.ts} (100%)
 delete mode 100644 src/web/app/common/scripts/streaming/drive-stream-manager.ts
 delete mode 100644 src/web/app/common/scripts/streaming/drive-stream.ts
 create mode 100644 src/web/app/common/scripts/streaming/drive.ts
 delete mode 100644 src/web/app/common/scripts/streaming/home-stream-manager.ts
 rename src/web/app/common/scripts/streaming/{home-stream.ts => home.ts} (66%)
 delete mode 100644 src/web/app/common/scripts/streaming/messaging-index-stream-manager.ts
 delete mode 100644 src/web/app/common/scripts/streaming/messaging-index-stream.ts
 create mode 100644 src/web/app/common/scripts/streaming/messaging-index.ts
 rename src/web/app/common/scripts/streaming/{messaging-stream.ts => messaging.ts} (83%)
 create mode 100644 src/web/app/common/scripts/streaming/othello-game.ts
 create mode 100644 src/web/app/common/scripts/streaming/othello.ts
 delete mode 100644 src/web/app/common/scripts/streaming/requests-stream-manager.ts
 delete mode 100644 src/web/app/common/scripts/streaming/requests-stream.ts
 create mode 100644 src/web/app/common/scripts/streaming/requests.ts
 delete mode 100644 src/web/app/common/scripts/streaming/server-stream-manager.ts
 delete mode 100644 src/web/app/common/scripts/streaming/server-stream.ts
 create mode 100644 src/web/app/common/scripts/streaming/server.ts
 create mode 100644 src/web/app/desktop/views/components/game-window.vue

diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts
index cbc016f20..fad666711 100644
--- a/src/api/endpoints.ts
+++ b/src/api/endpoints.ts
@@ -233,6 +233,26 @@ const endpoints: Endpoint[] = [
 		kind: 'notification-read'
 	},
 
+	{
+		name: 'othello/match',
+		withCredential: true
+	},
+
+	{
+		name: 'othello/match/cancel',
+		withCredential: true
+	},
+
+	{
+		name: 'othello/invitations',
+		withCredential: true
+	},
+
+	{
+		name: 'othello/games',
+		withCredential: true
+	},
+
 	{
 		name: 'mute/create',
 		withCredential: true,
diff --git a/src/api/endpoints/othello/games.ts b/src/api/endpoints/othello/games.ts
new file mode 100644
index 000000000..62388b01d
--- /dev/null
+++ b/src/api/endpoints/othello/games.ts
@@ -0,0 +1,22 @@
+import $ from 'cafy';
+import Game, { pack } from '../../models/othello-game';
+
+module.exports = (params, user) => new Promise(async (res, rej) => {
+	// Get 'my' parameter
+	const [my = false, myErr] = $(params.my).boolean().$;
+	if (myErr) return rej('invalid my param');
+
+	const q = my ? {
+		$or: [{
+			black_user_id: user._id
+		}, {
+			white_user_id: user._id
+		}]
+	} : {};
+
+	// Fetch games
+	const games = await Game.find(q);
+
+	// Reponse
+	res(Promise.all(games.map(async (g) => await pack(g, user))));
+});
diff --git a/src/api/endpoints/othello/invitations.ts b/src/api/endpoints/othello/invitations.ts
new file mode 100644
index 000000000..f462ef0bf
--- /dev/null
+++ b/src/api/endpoints/othello/invitations.ts
@@ -0,0 +1,11 @@
+import Matching, { pack as packMatching } from '../../models/othello-matching';
+
+module.exports = (params, user) => new Promise(async (res, rej) => {
+	// Find session
+	const invitations = await Matching.find({
+		child_id: user._id
+	});
+
+	// Reponse
+	res(Promise.all(invitations.map(async (i) => await packMatching(i, user))));
+});
diff --git a/src/api/endpoints/othello/match.ts b/src/api/endpoints/othello/match.ts
index 2dc22d11f..65243a557 100644
--- a/src/api/endpoints/othello/match.ts
+++ b/src/api/endpoints/othello/match.ts
@@ -1,6 +1,6 @@
 import $ from 'cafy';
-import Matching from '../../models/othello-matchig';
-import Game, { pack } from '../../models/othello-game';
+import Matching, { pack as packMatching } from '../../models/othello-matching';
+import Game, { pack as packGame } from '../../models/othello-game';
 import User from '../../models/user';
 import { publishOthelloStream } from '../../event';
 
@@ -33,17 +33,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 			created_at: new Date(),
 			black_user_id: parentIsBlack ? exist.parent_id : user._id,
 			white_user_id: parentIsBlack ? user._id : exist.parent_id,
+			turn_user_id: parentIsBlack ? exist.parent_id : user._id,
 			logs: []
 		});
 
-		const packedGame = await pack(game);
-
 		// Reponse
-		res(packedGame);
+		res(await packGame(game, user));
 
-		publishOthelloStream(exist.parent_id, 'matched', {
-			game
-		});
+		publishOthelloStream(exist.parent_id, 'matched', await packGame(game, exist.parent_id));
 	} else {
 		// Fetch child
 		const child = await User.findOne({
@@ -64,17 +61,16 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		});
 
 		// セッションを作成
-		await Matching.insert({
+		const matching = await Matching.insert({
+			created_at: new Date(),
 			parent_id: user._id,
 			child_id: child._id
 		});
 
 		// Reponse
-		res(204);
+		res();
 
 		// 招待
-		publishOthelloStream(child._id, 'invited', {
-			user_id: user._id
-		});
+		publishOthelloStream(child._id, 'invited', await packMatching(matching, child));
 	}
 });
diff --git a/src/api/endpoints/othello/match/cancel.ts b/src/api/endpoints/othello/match/cancel.ts
new file mode 100644
index 000000000..6f751ef83
--- /dev/null
+++ b/src/api/endpoints/othello/match/cancel.ts
@@ -0,0 +1,9 @@
+import Matching from '../../../models/othello-matching';
+
+module.exports = (params, user) => new Promise(async (res, rej) => {
+	await Matching.remove({
+		parent_id: user._id
+	});
+
+	res();
+});
diff --git a/src/api/event.ts b/src/api/event.ts
index e68082f0a..4c9cc18e4 100644
--- a/src/api/event.ts
+++ b/src/api/event.ts
@@ -42,6 +42,10 @@ class MisskeyEvent {
 		this.publish(`othello-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
+	public publishOthelloGameStream(gameId: ID, type: string, value?: any): void {
+		this.publish(`othello-game-stream:${gameId}`, type, typeof value === 'undefined' ? null : value);
+	}
+
 	public publishChannelStream(channelId: ID, type: string, value?: any): void {
 		this.publish(`channel-stream:${channelId}`, type, typeof value === 'undefined' ? null : value);
 	}
@@ -71,4 +75,6 @@ export const publishMessagingIndexStream = ev.publishMessagingIndexStream.bind(e
 
 export const publishOthelloStream = ev.publishOthelloStream.bind(ev);
 
+export const publishOthelloGameStream = ev.publishOthelloGameStream.bind(ev);
+
 export const publishChannelStream = ev.publishChannelStream.bind(ev);
diff --git a/src/api/models/othello-game.ts b/src/api/models/othello-game.ts
index a6beaaf9c..b9fd94ebc 100644
--- a/src/api/models/othello-game.ts
+++ b/src/api/models/othello-game.ts
@@ -1,6 +1,7 @@
 import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
 import db from '../../db/mongodb';
+import { IUser, pack as packUser } from './user';
 
 const Game = db.get<IGame>('othello_games');
 export default Game;
@@ -15,19 +16,30 @@ export interface IGame {
 
 /**
  * Pack an othello game for API response
- *
- * @param {any} game
- * @return {Promise<any>}
  */
 export const pack = (
-	game: any
+	game: any,
+	me?: string | mongo.ObjectID | IUser
 ) => new Promise<any>(async (resolve, reject) => {
 
+	// Me
+	const meId: mongo.ObjectID = me
+		? mongo.ObjectID.prototype.isPrototypeOf(me)
+			? me as mongo.ObjectID
+			: typeof me === 'string'
+				? new mongo.ObjectID(me)
+				: (me as IUser)._id
+		: null;
+
 	const _game = deepcopy(game);
 
 	// Rename _id to id
 	_game.id = _game._id;
 	delete _game._id;
 
+	// Populate user
+	_game.black_user = await packUser(_game.black_user_id, meId);
+	_game.white_user = await packUser(_game.white_user_id, meId);
+
 	resolve(_game);
 });
diff --git a/src/api/models/othello-matching.ts b/src/api/models/othello-matching.ts
index bd7aeef3c..89fcd6df6 100644
--- a/src/api/models/othello-matching.ts
+++ b/src/api/models/othello-matching.ts
@@ -1,11 +1,42 @@
 import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
 import db from '../../db/mongodb';
+import { IUser, pack as packUser } from './user';
 
 const Matching = db.get<IMatching>('othello_matchings');
 export default Matching;
 
 export interface IMatching {
 	_id: mongo.ObjectID;
+	created_at: Date;
 	parent_id: mongo.ObjectID;
 	child_id: mongo.ObjectID;
 }
+
+/**
+ * Pack an othello matching for API response
+ */
+export const pack = (
+	matching: any,
+	me?: string | mongo.ObjectID | IUser
+) => new Promise<any>(async (resolve, reject) => {
+
+	// Me
+	const meId: mongo.ObjectID = me
+		? mongo.ObjectID.prototype.isPrototypeOf(me)
+			? me as mongo.ObjectID
+			: typeof me === 'string'
+				? new mongo.ObjectID(me)
+				: (me as IUser)._id
+		: null;
+
+	const _matching = deepcopy(matching);
+
+	delete _matching._id;
+
+	// Populate user
+	_matching.parent = await packUser(_matching.parent_id, meId);
+	_matching.child = await packUser(_matching.child_id, meId);
+
+	resolve(_matching);
+});
diff --git a/src/api/stream/othello-game.ts b/src/api/stream/othello-game.ts
index ab91ef642..17cdd3a9e 100644
--- a/src/api/stream/othello-game.ts
+++ b/src/api/stream/othello-game.ts
@@ -1,12 +1,69 @@
 import * as websocket from 'websocket';
 import * as redis from 'redis';
+import Game from '../models/othello-game';
+import { publishOthelloGameStream } from '../event';
+import Othello from '../../common/othello';
 
-export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient): void {
-	const game = request.resourceURL.query.game;
+export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void {
+	const gameId = request.resourceURL.query.game;
 
 	// Subscribe game stream
-	subscriber.subscribe(`misskey:othello-game-stream:${game}`);
+	subscriber.subscribe(`misskey:othello-game-stream:${gameId}`);
 	subscriber.on('message', (_, data) => {
 		connection.send(data);
 	});
+
+	connection.on('message', async (data) => {
+		const msg = JSON.parse(data.utf8Data);
+
+		switch (msg.type) {
+			case 'set':
+				if (msg.pos == null) return;
+				const pos = msg.pos;
+
+				const game = await Game.findOne({ _id: gameId });
+
+				const o = new Othello();
+
+				game.logs.forEach(log => {
+					o.set(log.color, log.pos);
+				});
+
+				const myColor = game.black_user_id.equals(user._id) ? 'black' : 'white';
+				const opColor = myColor == 'black' ? 'white' : 'black';
+
+				if (!o.canReverse(myColor, pos)) return;
+				o.set(myColor, pos);
+
+				let turn;
+				if (o.getPattern(opColor).length > 0) {
+					turn = myColor == 'black' ? game.white_user_id : game.black_user_id;
+				} else {
+					turn = myColor == 'black' ? game.black_user_id : game.white_user_id;
+				}
+
+				const log = {
+					at: new Date(),
+					color: myColor,
+					pos
+				};
+
+				await Game.update({
+					_id: gameId
+				}, {
+					$set: {
+						turn_user_id: turn
+					},
+					$push: {
+						logs: log
+					}
+				});
+
+				publishOthelloGameStream(gameId, 'set', {
+					color: myColor,
+					pos
+				});
+				break;
+		}
+	});
 }
diff --git a/src/api/stream/othello-matching.ts b/src/api/stream/othello.ts
similarity index 62%
rename from src/api/stream/othello-matching.ts
rename to src/api/stream/othello.ts
index f30ce6eb0..5056eb535 100644
--- a/src/api/stream/othello-matching.ts
+++ b/src/api/stream/othello.ts
@@ -2,10 +2,8 @@ import * as websocket from 'websocket';
 import * as redis from 'redis';
 
 export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void {
-	const otherparty = request.resourceURL.query.otherparty;
-
-	// Subscribe matching stream
-	subscriber.subscribe(`misskey:othello-matching:${user._id}-${otherparty}`);
+	// Subscribe othello stream
+	subscriber.subscribe(`misskey:othello-stream:${user._id}`);
 	subscriber.on('message', (_, data) => {
 		connection.send(data);
 	});
diff --git a/src/api/streaming.ts b/src/api/streaming.ts
index 66c2e0cec..7d67ba957 100644
--- a/src/api/streaming.ts
+++ b/src/api/streaming.ts
@@ -11,7 +11,7 @@ import driveStream from './stream/drive';
 import messagingStream from './stream/messaging';
 import messagingIndexStream from './stream/messaging-index';
 import othelloGameStream from './stream/othello-game';
-import othelloMatchingStream from './stream/othello-matching';
+import othelloStream from './stream/othello';
 import serverStream from './stream/server';
 import requestsStream from './stream/requests';
 import channelStream from './stream/channel';
@@ -65,7 +65,7 @@ module.exports = (server: http.Server) => {
 			request.resourceURL.pathname === '/messaging' ? messagingStream :
 			request.resourceURL.pathname === '/messaging-index' ? messagingIndexStream :
 			request.resourceURL.pathname === '/othello-game' ? othelloGameStream :
-			request.resourceURL.pathname === '/othello-matching' ? othelloMatchingStream :
+			request.resourceURL.pathname === '/othello' ? othelloStream :
 			null;
 
 		if (channel !== null) {
diff --git a/src/common/othello.ts b/src/common/othello.ts
index 858fc3315..fc27d72dc 100644
--- a/src/common/othello.ts
+++ b/src/common/othello.ts
@@ -1,37 +1,38 @@
 const BOARD_SIZE = 8;
 
 export default class Othello {
-	public board: Array<Array<'black' | 'white'>>;
+	public board: Array<'black' | 'white'>;
 
 	/**
 	 * ゲームを初期化します
 	 */
 	constructor() {
 		this.board = [
-			[null, null, null, null, null, null, null, null],
-			[null, null, null, null, null, null, null, null],
-			[null, null, null, null, null, null, null, null],
-			[null, null, null, 'black', 'white', null, null, null],
-			[null, null, null, 'white', 'black', null, null, null],
-			[null, null, null, null, null, null, null, null],
-			[null, null, null, null, null, null, null, null],
-			[null, null, null, null, null, null, null, null]
+			null, null, null, null, null, null, null, null,
+			null, null, null, null, null, null, null, null,
+			null, null, null, null, null, null, null, null,
+			null, null, null, 'white', 'black', null, null, null,
+			null, null, null, 'black', 'white', null, null, null,
+			null, null, null, null, null, null, null, null,
+			null, null, null, null, null, null, null, null,
+			null, null, null, null, null, null, null, null
 		];
 	}
 
 	public setByNumber(color, n) {
 		const ps = this.getPattern(color);
-		this.set(color, ps[n][0], ps[n][1]);
+		this.set2(color, ps[n][0], ps[n][1]);
 	}
 
 	private write(color, x, y) {
-		this.board[y][x] = color;
+		const pos = x + (y * 8);
+		this.board[pos] = color;
 	}
 
 	/**
 	 * 石を配置します
 	 */
-	public set(color, x, y) {
+	public set2(color, x, y) {
 		this.write(color, x, y);
 
 		const reverses = this.getReverse(color, x, y);
@@ -89,24 +90,42 @@ export default class Othello {
 		});
 	}
 
+	public set(color, pos) {
+		const x = pos % BOARD_SIZE;
+		const y = Math.floor(pos / BOARD_SIZE);
+		this.set2(color, x, y);
+	}
+
+	public get(x, y) {
+		const pos = x + (y * 8);
+		return this.board[pos];
+	}
+
 	/**
 	 * 打つことができる場所を取得します
 	 */
 	public getPattern(myColor): number[][] {
 		const result = [];
-		this.board.forEach((stones, y) => stones.forEach((stone, x) => {
+		this.board.forEach((stone, i) => {
 			if (stone != null) return;
-			if (this.canReverse(myColor, x, y)) result.push([x, y]);
-		}));
+			const x = i % BOARD_SIZE;
+			const y = Math.floor(i / BOARD_SIZE);
+			if (this.canReverse2(myColor, x, y)) result.push([x, y]);
+		});
 		return result;
 	}
 
 	/**
 	 * 指定の位置に石を打つことができるかどうか(相手の石を1つでも反転させられるか)を取得します
 	 */
-	public canReverse(myColor, targetx, targety): boolean {
+	public canReverse2(myColor, targetx, targety): boolean {
 		return this.getReverse(myColor, targetx, targety) !== null;
 	}
+	public canReverse(myColor, pos): boolean {
+		const x = pos % BOARD_SIZE;
+		const y = Math.floor(pos / BOARD_SIZE);
+		return this.getReverse(myColor, x, y) !== null;
+	}
 
 	private getReverse(myColor, targetx, targety): number[] {
 		const opponentColor = myColor == 'black' ? 'white' : 'black';
@@ -117,11 +136,11 @@ export default class Othello {
 			return (x, y): any => {
 				if (breaked) {
 					return;
-				} else if (this.board[y][x] == myColor && opponentStoneFound) {
+				} else if (this.get(x, y) == myColor && opponentStoneFound) {
 					return true;
-				} else if (this.board[y][x] == myColor && !opponentStoneFound) {
+				} else if (this.get(x, y) == myColor && !opponentStoneFound) {
 					breaked = true;
-				} else if (this.board[y][x] == opponentColor) {
+				} else if (this.get(x, y) == opponentColor) {
 					opponentStoneFound = true;
 				} else {
 					breaked = true;
@@ -210,12 +229,13 @@ export default class Othello {
 
 	public toString(): string {
 		//return this.board.map(row => row.map(state => state === 'black' ? '●' : state === 'white' ? '○' : '┼').join('')).join('\n');
-		return this.board.map(row => row.map(state => state === 'black' ? '⚫️' : state === 'white' ? '⚪️' : '🔹').join('')).join('\n');
+		//return this.board.map(row => row.map(state => state === 'black' ? '⚫️' : state === 'white' ? '⚪️' : '🔹').join('')).join('\n');
+		return 'wip';
 	}
 
 	public toPatternString(color): string {
 		//const num = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
-		const num = ['0️⃣', '1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣', '🔟', '🍏', '🍎', '🍐', '🍊', '🍋', '🍌', '🍉', '🍇', '🍓', '🍈', '🍒', '🍑', '🍍'];
+		/*const num = ['0️⃣', '1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣', '🔟', '🍏', '🍎', '🍐', '🍊', '🍋', '🍌', '🍉', '🍇', '🍓', '🍈', '🍒', '🍑', '🍍'];
 
 		const pattern = this.getPattern(color);
 
@@ -223,7 +243,9 @@ export default class Othello {
 			const i = pattern.findIndex(p => p[0] == x && p[1] == y);
 			//return state === 'black' ? '●' : state === 'white' ? '○' : i != -1 ? num[i] : '┼';
 			return state === 'black' ? '⚫️' : state === 'white' ? '⚪️' : i != -1 ? num[i] : '🔹';
-		}).join('')).join('\n');
+		}).join('')).join('\n');*/
+
+		return 'wip';
 	}
 }
 
diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
index cbb4400a7..3690d3171 100644
--- a/src/web/app/common/mios.ts
+++ b/src/web/app/common/mios.ts
@@ -4,11 +4,12 @@ import * as merge from 'object-assign-deep';
 
 import { host, apiUrl, swPublickey, version, lang, googleMapsApiKey } from '../config';
 import Progress from './scripts/loading';
-import HomeStreamManager from './scripts/streaming/home-stream-manager';
-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';
+import { HomeStreamManager } from './scripts/streaming/home';
+import { DriveStreamManager } from './scripts/streaming/drive';
+import { ServerStreamManager } from './scripts/streaming/server';
+import { RequestsStreamManager } from './scripts/streaming/requests';
+import { MessagingIndexStreamManager } from './scripts/streaming/messaging-index';
+import { OthelloStreamManager } from './scripts/streaming/othello';
 
 import Err from '../common/views/components/connect-failed.vue';
 
@@ -117,11 +118,13 @@ export default class MiOS extends EventEmitter {
 		serverStream: ServerStreamManager;
 		requestsStream: RequestsStreamManager;
 		messagingIndexStream: MessagingIndexStreamManager;
+		othelloStream: OthelloStreamManager;
 	} = {
 		driveStream: null,
 		serverStream: null,
 		requestsStream: null,
-		messagingIndexStream: null
+		messagingIndexStream: null,
+		othelloStream: null
 	};
 
 	/**
@@ -169,6 +172,7 @@ export default class MiOS extends EventEmitter {
 			// Init other stream manager
 			this.streams.driveStream = new DriveStreamManager(this.i);
 			this.streams.messagingIndexStream = new MessagingIndexStreamManager(this.i);
+			this.streams.othelloStream = new OthelloStreamManager(this.i);
 		});
 
 		if (this.debug) {
diff --git a/src/web/app/common/scripts/streaming/channel-stream.ts b/src/web/app/common/scripts/streaming/channel.ts
similarity index 100%
rename from src/web/app/common/scripts/streaming/channel-stream.ts
rename to src/web/app/common/scripts/streaming/channel.ts
diff --git a/src/web/app/common/scripts/streaming/drive-stream-manager.ts b/src/web/app/common/scripts/streaming/drive-stream-manager.ts
deleted file mode 100644
index 8acdd7cbb..000000000
--- a/src/web/app/common/scripts/streaming/drive-stream-manager.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import StreamManager from './stream-manager';
-import Connection from './drive-stream';
-
-export default class DriveStreamManager extends StreamManager<Connection> {
-	private me;
-
-	constructor(me) {
-		super();
-
-		this.me = me;
-	}
-
-	public getConnection() {
-		if (this.connection == null) {
-			this.connection = new Connection(this.me);
-		}
-
-		return this.connection;
-	}
-}
diff --git a/src/web/app/common/scripts/streaming/drive-stream.ts b/src/web/app/common/scripts/streaming/drive-stream.ts
deleted file mode 100644
index 0da3f1255..000000000
--- a/src/web/app/common/scripts/streaming/drive-stream.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import Stream from './stream';
-
-/**
- * Drive stream connection
- */
-export default class Connection extends Stream {
-	constructor(me) {
-		super('drive', {
-			i: me.token
-		});
-	}
-}
diff --git a/src/web/app/common/scripts/streaming/drive.ts b/src/web/app/common/scripts/streaming/drive.ts
new file mode 100644
index 000000000..5805e5803
--- /dev/null
+++ b/src/web/app/common/scripts/streaming/drive.ts
@@ -0,0 +1,31 @@
+import Stream from './stream';
+import StreamManager from './stream-manager';
+
+/**
+ * Drive stream connection
+ */
+export class DriveStream extends Stream {
+	constructor(me) {
+		super('drive', {
+			i: me.token
+		});
+	}
+}
+
+export class DriveStreamManager extends StreamManager<DriveStream> {
+	private me;
+
+	constructor(me) {
+		super();
+
+		this.me = me;
+	}
+
+	public getConnection() {
+		if (this.connection == null) {
+			this.connection = new DriveStream(this.me);
+		}
+
+		return this.connection;
+	}
+}
diff --git a/src/web/app/common/scripts/streaming/home-stream-manager.ts b/src/web/app/common/scripts/streaming/home-stream-manager.ts
deleted file mode 100644
index ab56d5a73..000000000
--- a/src/web/app/common/scripts/streaming/home-stream-manager.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import StreamManager from './stream-manager';
-import Connection from './home-stream';
-import MiOS from '../../mios';
-
-export default class HomeStreamManager extends StreamManager<Connection> {
-	private me;
-	private os: MiOS;
-
-	constructor(os: MiOS, me) {
-		super();
-
-		this.me = me;
-		this.os = os;
-	}
-
-	public getConnection() {
-		if (this.connection == null) {
-			this.connection = new Connection(this.os, this.me);
-		}
-
-		return this.connection;
-	}
-}
diff --git a/src/web/app/common/scripts/streaming/home-stream.ts b/src/web/app/common/scripts/streaming/home.ts
similarity index 66%
rename from src/web/app/common/scripts/streaming/home-stream.ts
rename to src/web/app/common/scripts/streaming/home.ts
index 3516705e2..1f110bfd3 100644
--- a/src/web/app/common/scripts/streaming/home-stream.ts
+++ b/src/web/app/common/scripts/streaming/home.ts
@@ -1,12 +1,13 @@
 import * as merge from 'object-assign-deep';
 
 import Stream from './stream';
+import StreamManager from './stream-manager';
 import MiOS from '../../mios';
 
 /**
  * Home stream connection
  */
-export default class Connection extends Stream {
+export class HomeStream extends Stream {
 	constructor(os: MiOS, me) {
 		super('', {
 			i: me.token
@@ -34,3 +35,23 @@ export default class Connection extends Stream {
 		});
 	}
 }
+
+export class HomeStreamManager extends StreamManager<HomeStream> {
+	private me;
+	private os: MiOS;
+
+	constructor(os: MiOS, me) {
+		super();
+
+		this.me = me;
+		this.os = os;
+	}
+
+	public getConnection() {
+		if (this.connection == null) {
+			this.connection = new HomeStream(this.os, this.me);
+		}
+
+		return this.connection;
+	}
+}
diff --git a/src/web/app/common/scripts/streaming/messaging-index-stream-manager.ts b/src/web/app/common/scripts/streaming/messaging-index-stream-manager.ts
deleted file mode 100644
index 0f08b0148..000000000
--- a/src/web/app/common/scripts/streaming/messaging-index-stream-manager.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import StreamManager from './stream-manager';
-import Connection from './messaging-index-stream';
-
-export default class MessagingIndexStreamManager extends StreamManager<Connection> {
-	private me;
-
-	constructor(me) {
-		super();
-
-		this.me = me;
-	}
-
-	public getConnection() {
-		if (this.connection == null) {
-			this.connection = new Connection(this.me);
-		}
-
-		return this.connection;
-	}
-}
diff --git a/src/web/app/common/scripts/streaming/messaging-index-stream.ts b/src/web/app/common/scripts/streaming/messaging-index-stream.ts
deleted file mode 100644
index 8015c840b..000000000
--- a/src/web/app/common/scripts/streaming/messaging-index-stream.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import Stream from './stream';
-
-/**
- * Messaging index stream connection
- */
-export default class Connection extends Stream {
-	constructor(me) {
-		super('messaging-index', {
-			i: me.token
-		});
-	}
-}
diff --git a/src/web/app/common/scripts/streaming/messaging-index.ts b/src/web/app/common/scripts/streaming/messaging-index.ts
new file mode 100644
index 000000000..69758416d
--- /dev/null
+++ b/src/web/app/common/scripts/streaming/messaging-index.ts
@@ -0,0 +1,31 @@
+import Stream from './stream';
+import StreamManager from './stream-manager';
+
+/**
+ * Messaging index stream connection
+ */
+export class MessagingIndexStream extends Stream {
+	constructor(me) {
+		super('messaging-index', {
+			i: me.token
+		});
+	}
+}
+
+export class MessagingIndexStreamManager extends StreamManager<MessagingIndexStream> {
+	private me;
+
+	constructor(me) {
+		super();
+
+		this.me = me;
+	}
+
+	public getConnection() {
+		if (this.connection == null) {
+			this.connection = new MessagingIndexStream(this.me);
+		}
+
+		return this.connection;
+	}
+}
diff --git a/src/web/app/common/scripts/streaming/messaging-stream.ts b/src/web/app/common/scripts/streaming/messaging.ts
similarity index 83%
rename from src/web/app/common/scripts/streaming/messaging-stream.ts
rename to src/web/app/common/scripts/streaming/messaging.ts
index 68dfc5ec0..1fff2286b 100644
--- a/src/web/app/common/scripts/streaming/messaging-stream.ts
+++ b/src/web/app/common/scripts/streaming/messaging.ts
@@ -3,7 +3,7 @@ import Stream from './stream';
 /**
  * Messaging stream connection
  */
-export default class Connection extends Stream {
+export class MessagingStream extends Stream {
 	constructor(me, otherparty) {
 		super('messaging', {
 			i: me.token,
diff --git a/src/web/app/common/scripts/streaming/othello-game.ts b/src/web/app/common/scripts/streaming/othello-game.ts
new file mode 100644
index 000000000..51a435541
--- /dev/null
+++ b/src/web/app/common/scripts/streaming/othello-game.ts
@@ -0,0 +1,10 @@
+import Stream from './stream';
+
+export class OthelloGameStream extends Stream {
+	constructor(me, game) {
+		super('othello-game', {
+			i: me.token,
+			game: game.id
+		});
+	}
+}
diff --git a/src/web/app/common/scripts/streaming/othello.ts b/src/web/app/common/scripts/streaming/othello.ts
new file mode 100644
index 000000000..febc5d498
--- /dev/null
+++ b/src/web/app/common/scripts/streaming/othello.ts
@@ -0,0 +1,28 @@
+import StreamManager from './stream-manager';
+import Stream from './stream';
+
+export class OthelloStream extends Stream {
+	constructor(me) {
+		super('othello', {
+			i: me.token
+		});
+	}
+}
+
+export class OthelloStreamManager extends StreamManager<OthelloStream> {
+	private me;
+
+	constructor(me) {
+		super();
+
+		this.me = me;
+	}
+
+	public getConnection() {
+		if (this.connection == null) {
+			this.connection = new OthelloStream(this.me);
+		}
+
+		return this.connection;
+	}
+}
diff --git a/src/web/app/common/scripts/streaming/requests-stream-manager.ts b/src/web/app/common/scripts/streaming/requests-stream-manager.ts
deleted file mode 100644
index 44db913e7..000000000
--- a/src/web/app/common/scripts/streaming/requests-stream-manager.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import StreamManager from './stream-manager';
-import Connection from './requests-stream';
-
-export default class RequestsStreamManager extends StreamManager<Connection> {
-	public getConnection() {
-		if (this.connection == null) {
-			this.connection = new Connection();
-		}
-
-		return this.connection;
-	}
-}
diff --git a/src/web/app/common/scripts/streaming/requests-stream.ts b/src/web/app/common/scripts/streaming/requests-stream.ts
deleted file mode 100644
index 22ecea6c0..000000000
--- a/src/web/app/common/scripts/streaming/requests-stream.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import Stream from './stream';
-
-/**
- * Requests stream connection
- */
-export default class Connection extends Stream {
-	constructor() {
-		super('requests');
-	}
-}
diff --git a/src/web/app/common/scripts/streaming/requests.ts b/src/web/app/common/scripts/streaming/requests.ts
new file mode 100644
index 000000000..5d199a074
--- /dev/null
+++ b/src/web/app/common/scripts/streaming/requests.ts
@@ -0,0 +1,21 @@
+import Stream from './stream';
+import StreamManager from './stream-manager';
+
+/**
+ * Requests stream connection
+ */
+export class RequestsStream extends Stream {
+	constructor() {
+		super('requests');
+	}
+}
+
+export class RequestsStreamManager extends StreamManager<RequestsStream> {
+	public getConnection() {
+		if (this.connection == null) {
+			this.connection = new RequestsStream();
+		}
+
+		return this.connection;
+	}
+}
diff --git a/src/web/app/common/scripts/streaming/server-stream-manager.ts b/src/web/app/common/scripts/streaming/server-stream-manager.ts
deleted file mode 100644
index a170daebb..000000000
--- a/src/web/app/common/scripts/streaming/server-stream-manager.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import StreamManager from './stream-manager';
-import Connection from './server-stream';
-
-export default class ServerStreamManager extends StreamManager<Connection> {
-	public getConnection() {
-		if (this.connection == null) {
-			this.connection = new Connection();
-		}
-
-		return this.connection;
-	}
-}
diff --git a/src/web/app/common/scripts/streaming/server-stream.ts b/src/web/app/common/scripts/streaming/server-stream.ts
deleted file mode 100644
index b9e068446..000000000
--- a/src/web/app/common/scripts/streaming/server-stream.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import Stream from './stream';
-
-/**
- * Server stream connection
- */
-export default class Connection extends Stream {
-	constructor() {
-		super('server');
-	}
-}
diff --git a/src/web/app/common/scripts/streaming/server.ts b/src/web/app/common/scripts/streaming/server.ts
new file mode 100644
index 000000000..b12198d2f
--- /dev/null
+++ b/src/web/app/common/scripts/streaming/server.ts
@@ -0,0 +1,21 @@
+import Stream from './stream';
+import StreamManager from './stream-manager';
+
+/**
+ * Server stream connection
+ */
+export class ServerStream extends Stream {
+	constructor() {
+		super('server');
+	}
+}
+
+export class ServerStreamManager extends StreamManager<ServerStream> {
+	public getConnection() {
+		if (this.connection == null) {
+			this.connection = new ServerStream();
+		}
+
+		return this.connection;
+	}
+}
diff --git a/src/web/app/common/views/components/messaging-room.vue b/src/web/app/common/views/components/messaging-room.vue
index 637fa9cd6..547e9494e 100644
--- a/src/web/app/common/views/components/messaging-room.vue
+++ b/src/web/app/common/views/components/messaging-room.vue
@@ -26,7 +26,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import MessagingStreamConnection from '../../scripts/streaming/messaging-stream';
+import { MessagingStream } from '../../scripts/streaming/messaging';
 import XMessage from './messaging-room.message.vue';
 import XForm from './messaging-room.form.vue';
 import { url } from '../../../config';
@@ -66,7 +66,7 @@ export default Vue.extend({
 	},
 
 	mounted() {
-		this.connection = new MessagingStreamConnection((this as any).os.i, this.user.id);
+		this.connection = new MessagingStream((this as any).os.i, this.user.id);
 
 		this.connection.on('message', this.onMessage);
 		this.connection.on('read', this.onRead);
diff --git a/src/web/app/common/views/components/othello.game.vue b/src/web/app/common/views/components/othello.game.vue
index 3d3ffb2c0..cb006a289 100644
--- a/src/web/app/common/views/components/othello.game.vue
+++ b/src/web/app/common/views/components/othello.game.vue
@@ -1,29 +1,131 @@
 <template>
-<div>
-	<header>黒:{{ game.black_user.name }} 白:{{ game.white_user.name }}</header>
+<div class="root">
+	<header><b>{{ game.black_user.name }}</b>(黒) vs <b>{{ game.white_user.name }}</b>(白)</header>
+	<p class="turn">{{ turn ? 'あなたのターンです' : '相手のターンです' }}<mk-ellipsis v-if="!turn"/></p>
+	<div>
+		<div v-for="(stone, i) in o.board"
+			:class="{ empty: stone == null, myTurn: turn, can: o.canReverse(turn ? myColor : opColor, i) }"
+			@click="set(i)"
+		>
+			<img v-if="stone == 'black'" :src="`${game.black_user.avatar_url}?thumbnail&size=64`" alt="">
+			<img v-if="stone == 'white'" :src="`${game.white_user.avatar_url}?thumbnail&size=64`" alt="">
+		</div>
+	</div>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import { OthelloGameStream } from '../../scripts/streaming/othello-game';
+import Othello from '../../../../../common/othello';
+
 export default Vue.extend({
 	props: ['game'],
 	data() {
 		return {
-			game: null,
-			connection: null,
-			connectionId: null
+			o: new Othello(),
+			turn: null,
+			connection: null
 		};
 	},
-	mounted() {
-		this.connection = (this as any).os.streams.othelloGameStream.getConnection();
-		this.connectionId = (this as any).os.streams.othelloGameStream.use();
+	computed: {
+		myColor(): string {
+			return this.game.black_user_id == (this as any).os.i.id ? 'black' : 'white';
+		},
+		opColor(): string {
+			return this.myColor == 'black' ? 'white' : 'black';
+		}
+	},
+	created() {
+		this.game.logs.forEach(log => {
+			this.o.set(log.color, log.pos);
+		});
 
+		this.turn = this.game.turn_user_id == (this as any).os.i.id;
+	},
+	mounted() {
+		this.connection = new OthelloGameStream((this as any).os.i, this.game);
 		this.connection.on('set', this.onSet);
 	},
 	beforeDestroy() {
 		this.connection.off('set', this.onSet);
-		(this as any).streams.othelloGameStream.dispose(this.connectionId);
+		this.connection.close();
 	},
+	methods: {
+		set(pos) {
+			if (!this.turn) return;
+			if (!this.o.canReverse(this.myColor, pos)) return;
+			this.o.set(this.myColor, pos);
+			if (this.o.getPattern(this.opColor).length > 0) {
+				this.turn = !this.turn;
+			}
+			this.connection.send({
+				type: 'set',
+				pos
+			});
+			this.$forceUpdate();
+		},
+		onSet(x) {
+			this.o.set(x.color, x.pos);
+			if (this.o.getPattern(this.myColor).length > 0) {
+				this.turn = true;
+			}
+			this.$forceUpdate();
+		}
+	}
 });
 </script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+.root
+	text-align center
+
+	> header
+		padding 8px
+		border-bottom dashed 1px #c4cdd4
+
+	> div
+		display grid
+		grid-template-rows repeat(8, 1fr)
+		grid-template-columns repeat(8, 1fr)
+		grid-gap 4px
+		width 300px
+		height 300px
+		margin 0 auto
+
+		> div
+			background transparent
+			border-radius 6px
+			overflow hidden
+
+			*
+				pointer-events none
+				user-select none
+
+			&.empty
+				border solid 2px #f5f5f5
+
+			&.empty.can
+				background #f5f5f5
+
+			&.empty.myTurn
+				border-color #eee
+
+				&.can
+					background #eee
+					cursor pointer
+
+					&:hover
+						border-color darken($theme-color, 10%)
+						background $theme-color
+
+					&:active
+						background darken($theme-color, 10%)
+
+			> img
+				display block
+				width 100%
+				height 100%
+</style>
diff --git a/src/web/app/common/views/components/othello.vue b/src/web/app/common/views/components/othello.vue
index f5abcfb10..f409f162f 100644
--- a/src/web/app/common/views/components/othello.vue
+++ b/src/web/app/common/views/components/othello.vue
@@ -1,21 +1,53 @@
 <template>
-<div>
+<div class="mk-othello">
 	<div v-if="game">
 		<x-game :game="game"/>
 	</div>
-	<div v-if="matching">
+	<div class="matching" v-else-if="matching">
 		<h1><b>{{ matching.name }}</b>を待っています<mk-ellipsis/></h1>
+		<div class="cancel">
+			<el-button round @click="cancel">キャンセル</el-button>
+		</div>
 	</div>
-	<div v-else>
-		<h1>Misskey Othello</h1>
-		<p>他のMisskeyユーザーとオセロで対戦しよう。</p>
-		<button>フリーマッチ(準備中)</button>
-		<button @click="match">指名</button>
-		<section>
-			<h2>対局の招待があります:</h2>
+	<div class="index" v-else>
+		<h1>Misskey %fa:circle%thell%fa:circle R%</h1>
+		<p>他のMisskeyユーザーとオセロで対戦しよう</p>
+		<div class="play">
+			<el-button round>フリーマッチ(準備中)</el-button>
+			<el-button type="primary" round @click="match">指名</el-button>
+			<details>
+				<summary>遊び方</summary>
+				<div>
+					<p>オセロは、相手と交互に石をボードに置いてゆき、相手の石を挟んでひっくり返しながら、最終的に残った石が多い方が勝ちというボードゲームです。</p>
+					<dl>
+						<dt><b>フリーマッチ</b></dt>
+						<dd>ランダムなユーザーと対戦するモードです。</dd>
+						<dt><b>指名</b></dt>
+						<dd>指定したユーザーと対戦するモードです。</dd>
+					</dl>
+				</div>
+			</details>
+		</div>
+		<section v-if="invitations.length > 0">
+			<h2>対局の招待があります!:</h2>
+			<div class="invitation" v-for="i in invitations" tabindex="-1" @click="accept(i)">
+				<img :src="`${i.parent.avatar_url}?thumbnail&size=32`" alt="">
+				<span class="name"><b>{{ i.parent.name }}</b></span>
+				<span class="username">@{{ i.parent.username }}</span>
+				<mk-time :time="i.created_at"/>
+			</div>
+		</section>
+		<section v-if="myGames.length > 0">
+			<h2>自分の対局</h2>
+			<div class="game" v-for="g in myGames" tabindex="-1" @click="game = g">
+				<img :src="`${g.black_user.avatar_url}?thumbnail&size=32`" alt="">
+				<img :src="`${g.white_user.avatar_url}?thumbnail&size=32`" alt="">
+				<span><b>{{ g.black_user.name }}</b> vs <b>{{ g.white_user.name }}</b></span>
+				<span class="state">{{ g.winner ? '終了' : '進行中' }}</span>
+			</div>
 		</section>
 		<section>
-			<h2>過去の対局</h2>
+			<h2>みんなの対局</h2>
 		</section>
 	</div>
 </div>
@@ -35,7 +67,8 @@ export default Vue.extend({
 			games: [],
 			gamesFetching: true,
 			gamesMoreFetching: false,
-			matching: false,
+			myGames: [],
+			matching: null,
 			invitations: [],
 			connection: null,
 			connectionId: null
@@ -45,9 +78,15 @@ export default Vue.extend({
 		this.connection = (this as any).os.streams.othelloStream.getConnection();
 		this.connectionId = (this as any).os.streams.othelloStream.use();
 
-		this.connection.on('macthed', this.onMatched);
+		this.connection.on('matched', this.onMatched);
 		this.connection.on('invited', this.onInvited);
 
+		(this as any).api('othello/games', {
+			my: true
+		}).then(games => {
+			this.myGames = games;
+		});
+
 		(this as any).api('othello/games').then(games => {
 			this.games = games;
 			this.gamesFetching = false;
@@ -58,9 +97,9 @@ export default Vue.extend({
 		});
 	},
 	beforeDestroy() {
-		this.connection.off('macthed', this.onMatched);
+		this.connection.off('matched', this.onMatched);
 		this.connection.off('invited', this.onInvited);
-		(this as any).streams.othelloStream.dispose(this.connectionId);
+		(this as any).os.streams.othelloStream.dispose(this.connectionId);
 	},
 	methods: {
 		match() {
@@ -82,6 +121,19 @@ export default Vue.extend({
 				});
 			});
 		},
+		cancel() {
+			this.matching = null;
+			(this as any).api('othello/match/cancel');
+		},
+		accept(invitation) {
+			(this as any).api('othello/match', {
+				user_id: invitation.parent.id
+			}).then(game => {
+				if (game) {
+					this.game = game;
+				}
+			});
+		},
 		onMatched(game) {
 			this.game = game;
 		},
@@ -92,3 +144,126 @@ export default Vue.extend({
 });
 </script>
 
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+.mk-othello
+	color #677f84
+
+	> .matching
+		> h1
+			margin 0
+			padding 24px
+			font-size 20px
+			text-align center
+			font-weight normal
+
+		> .cancel
+			margin 0 auto
+			padding 24px 0 0 0
+			max-width 200px
+			text-align center
+			border-top dashed 1px #c4cdd4
+
+	> .index
+		> h1
+			margin 0
+			padding 24px
+			font-size 24px
+			text-align center
+			font-weight normal
+			color #fff
+			background linear-gradient(to bottom, #8bca3e, #d6cf31)
+
+			& + p
+				margin 0
+				padding 12px
+				margin-bottom 12px
+				text-align center
+				font-size 14px
+				border-bottom solid 1px #d3d9dc
+
+		> .play
+			margin 0 auto
+			padding 0 16px
+			max-width 500px
+			text-align center
+
+			> details
+				margin 8px 0
+
+				> div
+					padding 16px
+					font-size 14px
+					text-align left
+					background #f5f5f5
+					border-radius 8px
+
+		> section
+			margin 0 auto
+			padding 0 16px 16px 16px
+			max-width 500px
+			border-top solid 1px #d3d9dc
+
+			> h2
+				margin 0
+				padding 16px 0 8px 0
+				font-size 16px
+				font-weight bold
+
+	.invitation
+		margin 8px 0
+		padding 8px
+		border solid 1px #e1e5e8
+		border-radius 6px
+		cursor pointer
+
+		*
+			pointer-events none
+			user-select none
+
+		&:focus
+			border-color $theme-color
+
+		&:hover
+			background #f5f5f5
+
+		&:active
+			background #eee
+
+		> img
+			vertical-align bottom
+			border-radius 100%
+
+		> span
+			margin 0 8px
+			line-height 32px
+
+	.game
+		margin 8px 0
+		padding 8px
+		border solid 1px #e1e5e8
+		border-radius 6px
+		cursor pointer
+
+		*
+			pointer-events none
+			user-select none
+
+		&:focus
+			border-color $theme-color
+
+		&:hover
+			background #f5f5f5
+
+		&:active
+			background #eee
+
+		> img
+			vertical-align bottom
+			border-radius 100%
+
+		> span
+			margin 0 8px
+			line-height 32px
+</style>
diff --git a/src/web/app/desktop/views/components/game-window.vue b/src/web/app/desktop/views/components/game-window.vue
new file mode 100644
index 000000000..bf339092a
--- /dev/null
+++ b/src/web/app/desktop/views/components/game-window.vue
@@ -0,0 +1,24 @@
+<template>
+<mk-window ref="window" width="500px" height="560px" @closed="$destroy">
+	<span slot="header" :class="$style.header">%fa:gamepad%オセロ</span>
+	<mk-othello :class="$style.content"/>
+</mk-window>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+
+});
+</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/ui.header.nav.vue b/src/web/app/desktop/views/components/ui.header.nav.vue
index a5b6ecd6f..54045db8d 100644
--- a/src/web/app/desktop/views/components/ui.header.nav.vue
+++ b/src/web/app/desktop/views/components/ui.header.nav.vue
@@ -15,6 +15,13 @@
 					<template v-if="hasUnreadMessagingMessages">%fa:circle%</template>
 				</a>
 			</li>
+			<li class="game">
+				<a @click="game">
+					%fa:gamepad%
+					<p>ゲーム</p>
+					<template v-if="hasGameInvitations">%fa:circle%</template>
+				</a>
+			</li>
 		</template>
 		<li class="ch">
 			<a :href="chUrl" target="_blank">
@@ -22,12 +29,6 @@
 				<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>
@@ -36,11 +37,13 @@
 import Vue from 'vue';
 import { chUrl } from '../../../config';
 import MkMessagingWindow from './messaging-window.vue';
+import MkGameWindow from './game-window.vue';
 
 export default Vue.extend({
 	data() {
 		return {
 			hasUnreadMessagingMessages: false,
+			hasGameInvitations: false,
 			connection: null,
 			connectionId: null,
 			chUrl
@@ -80,6 +83,10 @@ export default Vue.extend({
 
 		messaging() {
 			(this as any).os.new(MkMessagingWindow);
+		},
+
+		game() {
+			(this as any).os.new(MkGameWindow);
 		}
 	}
 });
diff --git a/src/web/app/desktop/views/widgets/channel.channel.vue b/src/web/app/desktop/views/widgets/channel.channel.vue
index 70dac316c..02cdf6de1 100644
--- a/src/web/app/desktop/views/widgets/channel.channel.vue
+++ b/src/web/app/desktop/views/widgets/channel.channel.vue
@@ -11,7 +11,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import ChannelStream from '../../../common/scripts/streaming/channel-stream';
+import ChannelStream from '../../../common/scripts/streaming/channel';
 import XForm from './channel.channel.form.vue';
 import XPost from './channel.channel.post.vue';
 

From 464734ffea74c7b21cd4554621f5a904afd6bf2f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 7 Mar 2018 18:45:16 +0900
Subject: [PATCH 0636/1250] wip

---
 src/api/bot/core.ts                           |   4 +-
 src/api/endpoints/othello/games.ts            |   2 +-
 src/api/endpoints/othello/match.ts            |   1 +
 src/api/models/othello-game.ts                |   4 +
 src/api/stream/othello-game.ts                | 105 ++++++++++--------
 .../common/views/components/othello.game.vue  |  54 +++++++--
 .../app/common/views/components/othello.vue   |  10 +-
 7 files changed, 124 insertions(+), 56 deletions(-)

diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
index 0a073a312..75564b81e 100644
--- a/src/api/bot/core.ts
+++ b/src/api/bot/core.ts
@@ -475,8 +475,8 @@ class OthelloContext extends Context {
 		othelloAi('white', this.othello);
 		if (this.othello.getPattern('black').length === 0) {
 			this.bot.clearContext();
-			const blackCount = this.othello.board.map(row => row.filter(s => s == 'black').length).reduce((a, b) => a + b);
-			const whiteCount = this.othello.board.map(row => row.filter(s => s == 'white').length).reduce((a, b) => a + b);
+			const blackCount = this.othello.board.filter(s => s == 'black').length;
+			const whiteCount = this.othello.board.filter(s => s == 'white').length;
 			const winner = blackCount == whiteCount ? '引き分け' : blackCount > whiteCount ? '黒の勝ち' : '白の勝ち';
 			return this.othello.toString() + `\n\n~終了~\n\n黒${blackCount}、白${whiteCount}で${winner}です。`;
 		} else {
diff --git a/src/api/endpoints/othello/games.ts b/src/api/endpoints/othello/games.ts
index 62388b01d..da85de1c1 100644
--- a/src/api/endpoints/othello/games.ts
+++ b/src/api/endpoints/othello/games.ts
@@ -3,7 +3,7 @@ import Game, { pack } from '../../models/othello-game';
 
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Get 'my' parameter
-	const [my = false, myErr] = $(params.my).boolean().$;
+	const [my = false, myErr] = $(params.my).optional.boolean().$;
 	if (myErr) return rej('invalid my param');
 
 	const q = my ? {
diff --git a/src/api/endpoints/othello/match.ts b/src/api/endpoints/othello/match.ts
index 65243a557..cb094bbc6 100644
--- a/src/api/endpoints/othello/match.ts
+++ b/src/api/endpoints/othello/match.ts
@@ -34,6 +34,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 			black_user_id: parentIsBlack ? exist.parent_id : user._id,
 			white_user_id: parentIsBlack ? user._id : exist.parent_id,
 			turn_user_id: parentIsBlack ? exist.parent_id : user._id,
+			is_ended: false,
 			logs: []
 		});
 
diff --git a/src/api/models/othello-game.ts b/src/api/models/othello-game.ts
index b9fd94ebc..d75baf356 100644
--- a/src/api/models/othello-game.ts
+++ b/src/api/models/othello-game.ts
@@ -11,6 +11,9 @@ export interface IGame {
 	created_at: Date;
 	black_user_id: mongo.ObjectID;
 	white_user_id: mongo.ObjectID;
+	turn_user_id: mongo.ObjectID;
+	is_ended: boolean;
+	winner_id: mongo.ObjectID;
 	logs: any[];
 }
 
@@ -40,6 +43,7 @@ export const pack = (
 	// Populate user
 	_game.black_user = await packUser(_game.black_user_id, meId);
 	_game.white_user = await packUser(_game.white_user_id, meId);
+	_game.winner = await packUser(_game.winner_id, meId);
 
 	resolve(_game);
 });
diff --git a/src/api/stream/othello-game.ts b/src/api/stream/othello-game.ts
index 17cdd3a9e..59d964777 100644
--- a/src/api/stream/othello-game.ts
+++ b/src/api/stream/othello-game.ts
@@ -19,51 +19,68 @@ export default function(request: websocket.request, connection: websocket.connec
 		switch (msg.type) {
 			case 'set':
 				if (msg.pos == null) return;
-				const pos = msg.pos;
-
-				const game = await Game.findOne({ _id: gameId });
-
-				const o = new Othello();
-
-				game.logs.forEach(log => {
-					o.set(log.color, log.pos);
-				});
-
-				const myColor = game.black_user_id.equals(user._id) ? 'black' : 'white';
-				const opColor = myColor == 'black' ? 'white' : 'black';
-
-				if (!o.canReverse(myColor, pos)) return;
-				o.set(myColor, pos);
-
-				let turn;
-				if (o.getPattern(opColor).length > 0) {
-					turn = myColor == 'black' ? game.white_user_id : game.black_user_id;
-				} else {
-					turn = myColor == 'black' ? game.black_user_id : game.white_user_id;
-				}
-
-				const log = {
-					at: new Date(),
-					color: myColor,
-					pos
-				};
-
-				await Game.update({
-					_id: gameId
-				}, {
-					$set: {
-						turn_user_id: turn
-					},
-					$push: {
-						logs: log
-					}
-				});
-
-				publishOthelloGameStream(gameId, 'set', {
-					color: myColor,
-					pos
-				});
+				set(msg.pos);
 				break;
 		}
 	});
+
+	async function set(pos) {
+		const game = await Game.findOne({ _id: gameId });
+
+		if (game.is_ended) return;
+
+		const o = new Othello();
+
+		game.logs.forEach(log => {
+			o.set(log.color, log.pos);
+		});
+
+		const myColor = game.black_user_id.equals(user._id) ? 'black' : 'white';
+		const opColor = myColor == 'black' ? 'white' : 'black';
+
+		if (!o.canReverse(myColor, pos)) return;
+		o.set(myColor, pos);
+
+		let turn;
+		if (o.getPattern(opColor).length > 0) {
+			turn = myColor == 'black' ? game.white_user_id : game.black_user_id;
+		} else if (o.getPattern(myColor).length > 0) {
+			turn = myColor == 'black' ? game.black_user_id : game.white_user_id;
+		} else {
+			turn = null;
+		}
+
+		const isEnded = turn === null;
+
+		let winner;
+		if (isEnded) {
+			const blackCount = o.board.filter(s => s == 'black').length;
+			const whiteCount = o.board.filter(s => s == 'white').length;
+			winner = blackCount == whiteCount ? null : blackCount > whiteCount ? game.black_user_id : game.white_user_id;
+		}
+
+		const log = {
+			at: new Date(),
+			color: myColor,
+			pos
+		};
+
+		await Game.update({
+			_id: gameId
+		}, {
+			$set: {
+				turn_user_id: turn,
+				is_ended: isEnded,
+				winner_id: winner
+			},
+			$push: {
+				logs: log
+			}
+		});
+
+		publishOthelloGameStream(gameId, 'set', {
+			color: myColor,
+			pos
+		});
+	}
 }
diff --git a/src/web/app/common/views/components/othello.game.vue b/src/web/app/common/views/components/othello.game.vue
index cb006a289..b7c23e704 100644
--- a/src/web/app/common/views/components/othello.game.vue
+++ b/src/web/app/common/views/components/othello.game.vue
@@ -1,10 +1,15 @@
 <template>
 <div class="root">
 	<header><b>{{ game.black_user.name }}</b>(黒) vs <b>{{ game.white_user.name }}</b>(白)</header>
-	<p class="turn">{{ turn ? 'あなたのターンです' : '相手のターンです' }}<mk-ellipsis v-if="!turn"/></p>
+	<p class="turn" v-if="!iAmPlayer && !isEnded">{{ turn.name }}のターンです<mk-ellipsis/></p>
+	<p class="turn" v-if="iAmPlayer && !isEnded">{{ isMyTurn ? 'あなたのターンです' : '相手のターンです' }}<mk-ellipsis v-if="!isMyTurn"/></p>
+	<p class="result" v-if="isEnded">
+		<template v-if="winner"><b>{{ winner.name }}</b>の勝ち</template>
+		<template v-else>引き分け</template>
+	</p>
 	<div>
 		<div v-for="(stone, i) in o.board"
-			:class="{ empty: stone == null, myTurn: turn, can: o.canReverse(turn ? myColor : opColor, i) }"
+			:class="{ empty: stone == null, myTurn: isMyTurn, can: o.canReverse(turn.id == game.black_user.id ? 'black' : 'white', i) }"
 			@click="set(i)"
 		>
 			<img v-if="stone == 'black'" :src="`${game.black_user.avatar_url}?thumbnail&size=64`" alt="">
@@ -25,10 +30,16 @@ export default Vue.extend({
 		return {
 			o: new Othello(),
 			turn: null,
+			isMyTurn: null,
+			isEnded: false,
+			winner: null,
 			connection: null
 		};
 	},
 	computed: {
+		iAmPlayer(): boolean {
+			return this.game.black_user_id == (this as any).os.i.id || this.game.white_user_id == (this as any).os.i.id;
+		},
 		myColor(): string {
 			return this.game.black_user_id == (this as any).os.i.id ? 'black' : 'white';
 		},
@@ -41,7 +52,10 @@ export default Vue.extend({
 			this.o.set(log.color, log.pos);
 		});
 
-		this.turn = this.game.turn_user_id == (this as any).os.i.id;
+		this.turn = this.game.turn_user_id == this.game.black_user_id ? this.game.black_user : this.game.white_user;
+		this.isMyTurn = this.game.turn_user_id == (this as any).os.i.id;
+		this.isEnded = this.game.is_ended;
+		this.winner = this.game.winner;
 	},
 	mounted() {
 		this.connection = new OthelloGameStream((this as any).os.i, this.game);
@@ -53,11 +67,17 @@ export default Vue.extend({
 	},
 	methods: {
 		set(pos) {
-			if (!this.turn) return;
+			if (!this.isMyTurn) return;
 			if (!this.o.canReverse(this.myColor, pos)) return;
 			this.o.set(this.myColor, pos);
 			if (this.o.getPattern(this.opColor).length > 0) {
-				this.turn = !this.turn;
+				this.isMyTurn = !this.isMyTurn;
+				this.turn = this.myColor == 'black' ? this.game.white_user : this.game.black_user;
+			} else if (this.o.getPattern(this.myColor).length == 0) {
+				this.isEnded = true;
+				const blackCount = this.o.board.filter(s => s == 'black').length;
+				const whiteCount = this.o.board.filter(s => s == 'white').length;
+				this.winner = blackCount == whiteCount ? null : blackCount > whiteCount ? this.game.black_user : this.game.white_user;
 			}
 			this.connection.send({
 				type: 'set',
@@ -67,8 +87,28 @@ export default Vue.extend({
 		},
 		onSet(x) {
 			this.o.set(x.color, x.pos);
-			if (this.o.getPattern(this.myColor).length > 0) {
-				this.turn = true;
+			if (this.o.getPattern('black').length == 0 && this.o.getPattern('white').length == 0) {
+				this.isEnded = true;
+				const blackCount = this.o.board.filter(s => s == 'black').length;
+				const whiteCount = this.o.board.filter(s => s == 'white').length;
+				this.winner = blackCount == whiteCount ? null : blackCount > whiteCount ? this.game.black_user : this.game.white_user;
+			} else {
+				if (this.iAmPlayer && this.o.getPattern(this.myColor).length > 0) {
+					this.isMyTurn = true;
+				}
+
+				if (x.color == 'black' && this.o.getPattern('white').length > 0) {
+					this.turn = this.game.white_user;
+				}
+				if (x.color == 'black' && this.o.getPattern('white').length == 0) {
+					this.turn = this.game.black_user;
+				}
+				if (x.color == 'white' && this.o.getPattern('black').length > 0) {
+					this.turn = this.game.black_user;
+				}
+				if (x.color == 'white' && this.o.getPattern('black').length == 0) {
+					this.turn = this.game.white_user;
+				}
 			}
 			this.$forceUpdate();
 		}
diff --git a/src/web/app/common/views/components/othello.vue b/src/web/app/common/views/components/othello.vue
index f409f162f..326a8d80c 100644
--- a/src/web/app/common/views/components/othello.vue
+++ b/src/web/app/common/views/components/othello.vue
@@ -43,11 +43,17 @@
 				<img :src="`${g.black_user.avatar_url}?thumbnail&size=32`" alt="">
 				<img :src="`${g.white_user.avatar_url}?thumbnail&size=32`" alt="">
 				<span><b>{{ g.black_user.name }}</b> vs <b>{{ g.white_user.name }}</b></span>
-				<span class="state">{{ g.winner ? '終了' : '進行中' }}</span>
+				<span class="state">{{ g.is_ended ? '終了' : '進行中' }}</span>
 			</div>
 		</section>
-		<section>
+		<section v-if="games.length > 0">
 			<h2>みんなの対局</h2>
+			<div class="game" v-for="g in games" tabindex="-1" @click="game = g">
+				<img :src="`${g.black_user.avatar_url}?thumbnail&size=32`" alt="">
+				<img :src="`${g.white_user.avatar_url}?thumbnail&size=32`" alt="">
+				<span><b>{{ g.black_user.name }}</b> vs <b>{{ g.white_user.name }}</b></span>
+				<span class="state">{{ g.is_ended ? '終了' : '進行中' }}</span>
+			</div>
 		</section>
 	</div>
 </div>

From b7eb998feb5c441cfe81e7370200dcb8750500d3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 7 Mar 2018 18:55:02 +0900
Subject: [PATCH 0637/1250] wip

---
 src/api/models/othello-game.ts                 |  6 +++++-
 src/web/app/mobile/api/input.ts                |  5 ++++-
 src/web/app/mobile/script.ts                   |  3 +++
 src/web/app/mobile/views/components/ui.nav.vue |  1 +
 src/web/app/mobile/views/pages/othello.vue     | 16 ++++++++++++++++
 5 files changed, 29 insertions(+), 2 deletions(-)
 create mode 100644 src/web/app/mobile/views/pages/othello.vue

diff --git a/src/api/models/othello-game.ts b/src/api/models/othello-game.ts
index d75baf356..73a5c94b2 100644
--- a/src/api/models/othello-game.ts
+++ b/src/api/models/othello-game.ts
@@ -43,7 +43,11 @@ export const pack = (
 	// Populate user
 	_game.black_user = await packUser(_game.black_user_id, meId);
 	_game.white_user = await packUser(_game.white_user_id, meId);
-	_game.winner = await packUser(_game.winner_id, meId);
+	if (_game.winner_id) {
+		_game.winner = await packUser(_game.winner_id, meId);
+	} else {
+		_game.winner = null;
+	}
 
 	resolve(_game);
 });
diff --git a/src/web/app/mobile/api/input.ts b/src/web/app/mobile/api/input.ts
index fcff68cfb..38d0fb61e 100644
--- a/src/web/app/mobile/api/input.ts
+++ b/src/web/app/mobile/api/input.ts
@@ -1,5 +1,8 @@
 export default function(opts) {
 	return new Promise<string>((res, rej) => {
-		alert('input not implemented yet');
+		const x = window.prompt(opts.title);
+		if (x) {
+			res(x);
+		}
 	});
 }
diff --git a/src/web/app/mobile/script.ts b/src/web/app/mobile/script.ts
index eeadfd92b..27c18c5ae 100644
--- a/src/web/app/mobile/script.ts
+++ b/src/web/app/mobile/script.ts
@@ -4,6 +4,7 @@
 
 // Style
 import './style.styl';
+import '../../element.scss';
 
 import init from '../init';
 
@@ -28,6 +29,7 @@ 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';
+import MkOthello from './views/pages/othello.vue';
 
 /**
  * init
@@ -67,6 +69,7 @@ init((launch) => {
 		{ path: '/i/drive/file/:file', component: MkDrive },
 		{ path: '/selectdrive', component: MkSelectDrive },
 		{ path: '/search', component: MkSearch },
+		{ path: '/game/othello', component: MkOthello },
 		{ path: '/:user', component: MkUser },
 		{ path: '/:user/followers', component: MkFollowers },
 		{ path: '/:user/following', component: MkFollowing },
diff --git a/src/web/app/mobile/views/components/ui.nav.vue b/src/web/app/mobile/views/components/ui.nav.vue
index 62ea83526..a58225a17 100644
--- a/src/web/app/mobile/views/components/ui.nav.vue
+++ b/src/web/app/mobile/views/components/ui.nav.vue
@@ -18,6 +18,7 @@
 					<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>
+					<li><router-link to="/game/othello">%fa:gamepad%ゲーム%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>
diff --git a/src/web/app/mobile/views/pages/othello.vue b/src/web/app/mobile/views/pages/othello.vue
new file mode 100644
index 000000000..67f4add07
--- /dev/null
+++ b/src/web/app/mobile/views/pages/othello.vue
@@ -0,0 +1,16 @@
+<template>
+<mk-ui>
+	<span slot="header">%fa:gamepad%オセロ</span>
+	<mk-othello/>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	mounted() {
+		document.title = 'Misskey オセロ';
+		document.documentElement.style.background = '#fff';
+	}
+});
+</script>

From 25242f357fd91d63791f25659dca369a04c5f2a2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 7 Mar 2018 18:56:55 +0900
Subject: [PATCH 0638/1250] wip

---
 src/api/endpoints/othello/games.ts       | 6 +++++-
 src/api/endpoints/othello/invitations.ts | 4 ++++
 2 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/src/api/endpoints/othello/games.ts b/src/api/endpoints/othello/games.ts
index da85de1c1..39963fcd2 100644
--- a/src/api/endpoints/othello/games.ts
+++ b/src/api/endpoints/othello/games.ts
@@ -15,7 +15,11 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	} : {};
 
 	// Fetch games
-	const games = await Game.find(q);
+	const games = await Game.find(q, {
+		sort: {
+			_id: -1
+		}
+	});
 
 	// Reponse
 	res(Promise.all(games.map(async (g) => await pack(g, user))));
diff --git a/src/api/endpoints/othello/invitations.ts b/src/api/endpoints/othello/invitations.ts
index f462ef0bf..02fb421fb 100644
--- a/src/api/endpoints/othello/invitations.ts
+++ b/src/api/endpoints/othello/invitations.ts
@@ -4,6 +4,10 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Find session
 	const invitations = await Matching.find({
 		child_id: user._id
+	}, {
+		sort: {
+			_id: -1
+		}
 	});
 
 	// Reponse

From f7002041545e6985a08d9bf22dc68d28924a5f49 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 7 Mar 2018 18:58:19 +0900
Subject: [PATCH 0639/1250] oops

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

diff --git a/src/web/app/common/views/components/messaging.vue b/src/web/app/common/views/components/messaging.vue
index 2ec488c24..db60e9259 100644
--- a/src/web/app/common/views/components/messaging.vue
+++ b/src/web/app/common/views/components/messaging.vue
@@ -89,7 +89,7 @@ export default Vue.extend({
 	beforeDestroy() {
 		this.connection.off('message', this.onMessage);
 		this.connection.off('read', this.onRead);
-		(this as any).streams.messagingIndexStream.dispose(this.connectionId);
+		(this as any).os.streams.messagingIndexStream.dispose(this.connectionId);
 	},
 	methods: {
 		isMe(message) {

From 4666c088983da07809cc7e0fb1fa5470d163e30f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 7 Mar 2018 19:00:53 +0900
Subject: [PATCH 0640/1250] v4008

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index cc072ee1b..57048f01e 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.3999",
+	"version": "0.0.4008",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 1bcb0733aded39e7fcef374b04cb92aa6386e868 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 7 Mar 2018 19:03:32 +0900
Subject: [PATCH 0641/1250] Fix bug

---
 src/api/stream/othello-game.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/api/stream/othello-game.ts b/src/api/stream/othello-game.ts
index 59d964777..a5fb379e8 100644
--- a/src/api/stream/othello-game.ts
+++ b/src/api/stream/othello-game.ts
@@ -28,6 +28,7 @@ export default function(request: websocket.request, connection: websocket.connec
 		const game = await Game.findOne({ _id: gameId });
 
 		if (game.is_ended) return;
+		if (!game.black_user_id.equals(user._id) && !game.white_user_id.equals(user._id)) return;
 
 		const o = new Othello();
 

From ff2ecdfe4a538d5fb007f770cc1313e8e9dbc41f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 7 Mar 2018 21:57:06 +0900
Subject: [PATCH 0642/1250] :v:

---
 src/api/stream/othello-game.ts                |  9 +-
 src/common/othello.ts                         | 40 +++++++-
 .../common/views/components/othello.game.vue  | 97 +++++++++++++++++--
 3 files changed, 126 insertions(+), 20 deletions(-)

diff --git a/src/api/stream/othello-game.ts b/src/api/stream/othello-game.ts
index a5fb379e8..d08647815 100644
--- a/src/api/stream/othello-game.ts
+++ b/src/api/stream/othello-game.ts
@@ -55,9 +55,7 @@ export default function(request: websocket.request, connection: websocket.connec
 
 		let winner;
 		if (isEnded) {
-			const blackCount = o.board.filter(s => s == 'black').length;
-			const whiteCount = o.board.filter(s => s == 'white').length;
-			winner = blackCount == whiteCount ? null : blackCount > whiteCount ? game.black_user_id : game.white_user_id;
+			winner = o.blackCount == o.whiteCount ? null : o.blackCount > o.whiteCount ? game.black_user_id : game.white_user_id;
 		}
 
 		const log = {
@@ -79,9 +77,6 @@ export default function(request: websocket.request, connection: websocket.connec
 			}
 		});
 
-		publishOthelloGameStream(gameId, 'set', {
-			color: myColor,
-			pos
-		});
+		publishOthelloGameStream(gameId, 'set', log);
 	}
 }
diff --git a/src/common/othello.ts b/src/common/othello.ts
index fc27d72dc..1057e6b9a 100644
--- a/src/common/othello.ts
+++ b/src/common/othello.ts
@@ -3,6 +3,11 @@ const BOARD_SIZE = 8;
 export default class Othello {
 	public board: Array<'black' | 'white'>;
 
+	public stats: Array<{
+		b: number;
+		w: number;
+	}> = [];
+
 	/**
 	 * ゲームを初期化します
 	 */
@@ -17,6 +22,27 @@ export default class Othello {
 			null, null, null, null, null, null, null, null,
 			null, null, null, null, null, null, null, null
 		];
+
+		this.stats.push({
+			b: 0.5,
+			w: 0.5
+		});
+	}
+
+	public get blackCount() {
+		return this.board.filter(s => s == 'black').length;
+	}
+
+	public get whiteCount() {
+		return this.board.filter(s => s == 'white').length;
+	}
+
+	public get blackP() {
+		return this.blackCount / (this.blackCount + this.whiteCount);
+	}
+
+	public get whiteP() {
+		return this.whiteCount / (this.blackCount + this.whiteCount);
 	}
 
 	public setByNumber(color, n) {
@@ -88,6 +114,11 @@ export default class Othello {
 					break;
 				}
 		});
+
+		this.stats.push({
+			b: this.blackP,
+			w: this.whiteP
+		});
 	}
 
 	public set(color, pos) {
@@ -118,10 +149,11 @@ export default class Othello {
 	/**
 	 * 指定の位置に石を打つことができるかどうか(相手の石を1つでも反転させられるか)を取得します
 	 */
-	public canReverse2(myColor, targetx, targety): boolean {
-		return this.getReverse(myColor, targetx, targety) !== null;
+	public canReverse2(myColor, x, y): boolean {
+		return this.canReverse(myColor, x + (y * 8));
 	}
 	public canReverse(myColor, pos): boolean {
+		if (this.board[pos] != null) return false;
 		const x = pos % BOARD_SIZE;
 		const y = Math.floor(pos / BOARD_SIZE);
 		return this.getReverse(myColor, x, y) !== null;
@@ -181,7 +213,7 @@ export default class Othello {
 
 		// 右下
 		iterate = createIterater();
-		for (let c = 0, i = 1; i < Math.min(BOARD_SIZE - targetx, BOARD_SIZE - targety); c++, i++) {
+		for (let c = 0, i = 1; i <= Math.min(BOARD_SIZE - targetx, BOARD_SIZE - targety); c++, i++) {
 			if (iterate(targetx + i, targety + i)) {
 				res.push([3, c]);
 				break;
@@ -199,7 +231,7 @@ export default class Othello {
 
 		// 左下
 		iterate = createIterater();
-		for (let c = 0, i = 1; i < Math.min(targetx, BOARD_SIZE - targety); c++, i++) {
+		for (let c = 0, i = 1; i <= Math.min(targetx, BOARD_SIZE - targety); c++, i++) {
 			if (iterate(targetx - i, targety + i)) {
 				res.push([5, c]);
 				break;
diff --git a/src/web/app/common/views/components/othello.game.vue b/src/web/app/common/views/components/othello.game.vue
index b7c23e704..70c9965ee 100644
--- a/src/web/app/common/views/components/othello.game.vue
+++ b/src/web/app/common/views/components/othello.game.vue
@@ -2,12 +2,13 @@
 <div class="root">
 	<header><b>{{ game.black_user.name }}</b>(黒) vs <b>{{ game.white_user.name }}</b>(白)</header>
 	<p class="turn" v-if="!iAmPlayer && !isEnded">{{ turn.name }}のターンです<mk-ellipsis/></p>
+	<p class="turn" v-if="logPos != logs.length">{{ turn.name }}のターン</p>
 	<p class="turn" v-if="iAmPlayer && !isEnded">{{ isMyTurn ? 'あなたのターンです' : '相手のターンです' }}<mk-ellipsis v-if="!isMyTurn"/></p>
-	<p class="result" v-if="isEnded">
+	<p class="result" v-if="isEnded && logPos == logs.length">
 		<template v-if="winner"><b>{{ winner.name }}</b>の勝ち</template>
 		<template v-else>引き分け</template>
 	</p>
-	<div>
+	<div class="board">
 		<div v-for="(stone, i) in o.board"
 			:class="{ empty: stone == null, myTurn: isMyTurn, can: o.canReverse(turn.id == game.black_user.id ? 'black' : 'white', i) }"
 			@click="set(i)"
@@ -16,6 +17,22 @@
 			<img v-if="stone == 'white'" :src="`${game.white_user.avatar_url}?thumbnail&size=64`" alt="">
 		</div>
 	</div>
+	<p>黒:{{ o.blackCount }} 白:{{ o.whiteCount }} 合計:{{ o.blackCount + o.whiteCount }}</p>
+	<div class="graph">
+		<div v-for="n in 61 - o.stats.length">
+		</div>
+		<div v-for="data in o.stats">
+			<div :style="{ height: `${ Math.floor(data.b * 100) }%` }"></div>
+			<div :style="{ height: `${ Math.floor(data.w * 100) }%` }"></div>
+		</div>
+	</div>
+	<div class="player" v-if="isEnded">
+		<el-button type="primary" @click="logPos = 0" :disabled="logPos == 0">%fa:fast-backward%</el-button>
+		<el-button type="primary" @click="logPos--" :disabled="logPos == 0">%fa:backward%</el-button>
+		<span>{{ logPos }} / {{ logs.length }}</span>
+		<el-button type="primary" @click="logPos++" :disabled="logPos == logs.length">%fa:forward%</el-button>
+		<el-button type="primary" @click="logPos = logs.length" :disabled="logPos == logs.length">%fa:fast-forward%</el-button>
+	</div>
 </div>
 </template>
 
@@ -26,9 +43,12 @@ import Othello from '../../../../../common/othello';
 
 export default Vue.extend({
 	props: ['game'],
+
 	data() {
 		return {
 			o: new Othello(),
+			logs: [],
+			logPos: 0,
 			turn: null,
 			isMyTurn: null,
 			isEnded: false,
@@ -36,6 +56,7 @@ export default Vue.extend({
 			connection: null
 		};
 	},
+
 	computed: {
 		iAmPlayer(): boolean {
 			return this.game.black_user_id == (this as any).os.i.id || this.game.white_user_id == (this as any).os.i.id;
@@ -47,24 +68,57 @@ export default Vue.extend({
 			return this.myColor == 'black' ? 'white' : 'black';
 		}
 	},
+
+	watch: {
+		logPos(v) {
+			if (!this.isEnded) return;
+			this.o = new Othello();
+			this.turn = this.game.black_user;
+			this.logs.forEach((log, i) => {
+				if (i < v) {
+					this.o.set(log.color, log.pos);
+
+					if (log.color == 'black' && this.o.getPattern('white').length > 0) {
+						this.turn = this.game.white_user;
+					}
+					if (log.color == 'black' && this.o.getPattern('white').length == 0) {
+						this.turn = this.game.black_user;
+					}
+					if (log.color == 'white' && this.o.getPattern('black').length > 0) {
+						this.turn = this.game.black_user;
+					}
+					if (log.color == 'white' && this.o.getPattern('black').length == 0) {
+						this.turn = this.game.white_user;
+					}
+				}
+			});
+			this.$forceUpdate();
+		}
+	},
+
 	created() {
 		this.game.logs.forEach(log => {
 			this.o.set(log.color, log.pos);
 		});
 
+		this.logs = this.game.logs;
+		this.logPos = this.logs.length;
 		this.turn = this.game.turn_user_id == this.game.black_user_id ? this.game.black_user : this.game.white_user;
 		this.isMyTurn = this.game.turn_user_id == (this as any).os.i.id;
 		this.isEnded = this.game.is_ended;
 		this.winner = this.game.winner;
 	},
+
 	mounted() {
 		this.connection = new OthelloGameStream((this as any).os.i, this.game);
 		this.connection.on('set', this.onSet);
 	},
+
 	beforeDestroy() {
 		this.connection.off('set', this.onSet);
 		this.connection.close();
 	},
+
 	methods: {
 		set(pos) {
 			if (!this.isMyTurn) return;
@@ -75,9 +129,7 @@ export default Vue.extend({
 				this.turn = this.myColor == 'black' ? this.game.white_user : this.game.black_user;
 			} else if (this.o.getPattern(this.myColor).length == 0) {
 				this.isEnded = true;
-				const blackCount = this.o.board.filter(s => s == 'black').length;
-				const whiteCount = this.o.board.filter(s => s == 'white').length;
-				this.winner = blackCount == whiteCount ? null : blackCount > whiteCount ? this.game.black_user : this.game.white_user;
+				this.winner = this.o.blackCount == this.o.whiteCount ? null : this.o.blackCount > this.o.whiteCount ? this.game.black_user : this.game.white_user;
 			}
 			this.connection.send({
 				type: 'set',
@@ -85,13 +137,15 @@ export default Vue.extend({
 			});
 			this.$forceUpdate();
 		},
+
 		onSet(x) {
+			this.logs.push(x);
+			this.logPos++;
 			this.o.set(x.color, x.pos);
+
 			if (this.o.getPattern('black').length == 0 && this.o.getPattern('white').length == 0) {
 				this.isEnded = true;
-				const blackCount = this.o.board.filter(s => s == 'black').length;
-				const whiteCount = this.o.board.filter(s => s == 'white').length;
-				this.winner = blackCount == whiteCount ? null : blackCount > whiteCount ? this.game.black_user : this.game.white_user;
+				this.winner = this.o.blackCount == this.o.whiteCount ? null : this.o.blackCount > this.o.whiteCount ? this.game.black_user : this.game.white_user;
 			} else {
 				if (this.iAmPlayer && this.o.getPattern(this.myColor).length > 0) {
 					this.isMyTurn = true;
@@ -126,7 +180,7 @@ export default Vue.extend({
 		padding 8px
 		border-bottom dashed 1px #c4cdd4
 
-	> div
+	> .board
 		display grid
 		grid-template-rows repeat(8, 1fr)
 		grid-template-columns repeat(8, 1fr)
@@ -168,4 +222,29 @@ export default Vue.extend({
 				display block
 				width 100%
 				height 100%
+
+	> .graph
+		display grid
+		grid-template-columns repeat(61, 1fr)
+		width 300px
+		height 38px
+		margin 0 auto 16px auto
+
+		> div
+			&:not(:empty)
+				background #ccc
+
+			> div:first-child
+				background #333
+
+			> div:last-child
+				background #ccc
+
+	> .player
+		margin-bottom 16px
+
+		> span
+			display inline-block
+			margin 0 8px
+			min-width 70px
 </style>

From 9a62606aa4d26d3f2e7aeb003c8a70931909e2ae Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 7 Mar 2018 21:58:15 +0900
Subject: [PATCH 0643/1250] v4010

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 57048f01e..1d437b1dc 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4008",
+	"version": "0.0.4010",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From cb9641f24ce5682bfc3d5b9c0a8542b6e1204b1b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 7 Mar 2018 22:25:26 +0900
Subject: [PATCH 0644/1250] :v:

---
 src/common/othello.ts                                | 3 +++
 src/web/app/common/views/components/othello.game.vue | 5 ++++-
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/common/othello.ts b/src/common/othello.ts
index 1057e6b9a..5d95fd2fe 100644
--- a/src/common/othello.ts
+++ b/src/common/othello.ts
@@ -29,6 +29,8 @@ export default class Othello {
 		});
 	}
 
+	public prevPos = -1;
+
 	public get blackCount() {
 		return this.board.filter(s => s == 'black').length;
 	}
@@ -59,6 +61,7 @@ export default class Othello {
 	 * 石を配置します
 	 */
 	public set2(color, x, y) {
+		this.prevPos = x + (y * 8);
 		this.write(color, x, y);
 
 		const reverses = this.getReverse(color, x, y);
diff --git a/src/web/app/common/views/components/othello.game.vue b/src/web/app/common/views/components/othello.game.vue
index 70c9965ee..1cb2400f7 100644
--- a/src/web/app/common/views/components/othello.game.vue
+++ b/src/web/app/common/views/components/othello.game.vue
@@ -10,7 +10,7 @@
 	</p>
 	<div class="board">
 		<div v-for="(stone, i) in o.board"
-			:class="{ empty: stone == null, myTurn: isMyTurn, can: o.canReverse(turn.id == game.black_user.id ? 'black' : 'white', i) }"
+			:class="{ empty: stone == null, myTurn: isMyTurn, can: o.canReverse(turn.id == game.black_user.id ? 'black' : 'white', i), prev: o.prevPos == i }"
 			@click="set(i)"
 		>
 			<img v-if="stone == 'black'" :src="`${game.black_user.avatar_url}?thumbnail&size=64`" alt="">
@@ -218,6 +218,9 @@ export default Vue.extend({
 					&:active
 						background darken($theme-color, 10%)
 
+			&.prev
+				box-shadow 0 0 0 4px rgba($theme-color, 0.7)
+
 			> img
 				display block
 				width 100%

From 1d68091dcba1dd0a1b41164d1649ea25eb07ca9d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 7 Mar 2018 22:25:52 +0900
Subject: [PATCH 0645/1250] v4013

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 1d437b1dc..95ee4e18e 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4010",
+	"version": "0.0.4013",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From ac98858ae631edb2a1465daaf70d37f0805d93dc Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 7 Mar 2018 23:08:42 +0900
Subject: [PATCH 0646/1250] Fix bug

---
 src/common/othello.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/common/othello.ts b/src/common/othello.ts
index 5d95fd2fe..1da8ad36d 100644
--- a/src/common/othello.ts
+++ b/src/common/othello.ts
@@ -216,7 +216,7 @@ export default class Othello {
 
 		// 右下
 		iterate = createIterater();
-		for (let c = 0, i = 1; i <= Math.min(BOARD_SIZE - targetx, BOARD_SIZE - targety); c++, i++) {
+		for (let c = 0, i = 1; i < Math.min(BOARD_SIZE - targetx, BOARD_SIZE - targety); c++, i++) {
 			if (iterate(targetx + i, targety + i)) {
 				res.push([3, c]);
 				break;

From 8fad927a69252bbba7196a7f5c0e0d69ef9e40e6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 8 Mar 2018 17:57:57 +0900
Subject: [PATCH 0647/1250] #1200

---
 src/api/endpoints/othello/games.ts            |   9 +-
 src/api/endpoints/othello/match.ts            |  20 +-
 src/api/models/othello-game.ts                |  43 ++-
 src/api/stream/othello-game.ts                | 142 ++++++--
 src/common/othello.ts                         | 325 ------------------
 src/common/othello/ai.ts                      |  42 +++
 src/common/othello/core.ts                    | 239 +++++++++++++
 src/common/othello/maps.ts                    | 217 ++++++++++++
 .../common/views/components/othello.game.vue  | 164 ++++-----
 .../views/components/othello.gameroom.vue     |  42 +++
 .../common/views/components/othello.room.vue  | 152 ++++++++
 .../app/common/views/components/othello.vue   |  18 +-
 12 files changed, 956 insertions(+), 457 deletions(-)
 delete mode 100644 src/common/othello.ts
 create mode 100644 src/common/othello/ai.ts
 create mode 100644 src/common/othello/core.ts
 create mode 100644 src/common/othello/maps.ts
 create mode 100644 src/web/app/common/views/components/othello.gameroom.vue
 create mode 100644 src/web/app/common/views/components/othello.room.vue

diff --git a/src/api/endpoints/othello/games.ts b/src/api/endpoints/othello/games.ts
index 39963fcd2..dd9fb5ef5 100644
--- a/src/api/endpoints/othello/games.ts
+++ b/src/api/endpoints/othello/games.ts
@@ -7,12 +7,15 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	if (myErr) return rej('invalid my param');
 
 	const q = my ? {
+		is_started: true,
 		$or: [{
-			black_user_id: user._id
+			user1_id: user._id
 		}, {
-			white_user_id: user._id
+			user2_id: user._id
 		}]
-	} : {};
+	} : {
+		is_started: true
+	};
 
 	// Fetch games
 	const games = await Game.find(q, {
diff --git a/src/api/endpoints/othello/match.ts b/src/api/endpoints/othello/match.ts
index cb094bbc6..05b87a541 100644
--- a/src/api/endpoints/othello/match.ts
+++ b/src/api/endpoints/othello/match.ts
@@ -3,6 +3,7 @@ import Matching, { pack as packMatching } from '../../models/othello-matching';
 import Game, { pack as packGame } from '../../models/othello-game';
 import User from '../../models/user';
 import { publishOthelloStream } from '../../event';
+import { eighteight } from '../../../common/othello/maps';
 
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Get 'user_id' parameter
@@ -26,16 +27,21 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 			_id: exist._id
 		});
 
-		const parentIsBlack = Math.random() > 0.5;
-
-		// Start game
+		// Create game
 		const game = await Game.insert({
 			created_at: new Date(),
-			black_user_id: parentIsBlack ? exist.parent_id : user._id,
-			white_user_id: parentIsBlack ? user._id : exist.parent_id,
-			turn_user_id: parentIsBlack ? exist.parent_id : user._id,
+			user1_id: exist.parent_id,
+			user2_id: user._id,
+			user1_accepted: false,
+			user2_accepted: false,
+			is_started: false,
 			is_ended: false,
-			logs: []
+			logs: [],
+			settings: {
+				map: eighteight,
+				bw: 'random',
+				is_llotheo: false
+			}
 		});
 
 		// Reponse
diff --git a/src/api/models/othello-game.ts b/src/api/models/othello-game.ts
index 73a5c94b2..de7c804c4 100644
--- a/src/api/models/othello-game.ts
+++ b/src/api/models/othello-game.ts
@@ -2,6 +2,7 @@ import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
 import db from '../../db/mongodb';
 import { IUser, pack as packUser } from './user';
+import { Map } from '../../common/othello/maps';
 
 const Game = db.get<IGame>('othello_games');
 export default Game;
@@ -9,12 +10,28 @@ export default Game;
 export interface IGame {
 	_id: mongo.ObjectID;
 	created_at: Date;
-	black_user_id: mongo.ObjectID;
-	white_user_id: mongo.ObjectID;
-	turn_user_id: mongo.ObjectID;
+	started_at: Date;
+	user1_id: mongo.ObjectID;
+	user2_id: mongo.ObjectID;
+	user1_accepted: boolean;
+	user2_accepted: boolean;
+
+	/**
+	 * どちらのプレイヤーが先行(黒)か
+	 * 1 ... user1
+	 * 2 ... user2
+	 */
+	black: number;
+
+	is_started: boolean;
 	is_ended: boolean;
 	winner_id: mongo.ObjectID;
 	logs: any[];
+	settings: {
+		map: Map;
+		bw: string | number;
+		is_llotheo: boolean;
+	};
 }
 
 /**
@@ -24,6 +41,20 @@ export const pack = (
 	game: any,
 	me?: string | mongo.ObjectID | IUser
 ) => new Promise<any>(async (resolve, reject) => {
+	let _game: any;
+
+	// Populate the game if 'game' is ID
+	if (mongo.ObjectID.prototype.isPrototypeOf(game)) {
+		_game = await Game.findOne({
+			_id: game
+		});
+	} else if (typeof game === 'string') {
+		_game = await Game.findOne({
+			_id: new mongo.ObjectID(game)
+		});
+	} else {
+		_game = deepcopy(game);
+	}
 
 	// Me
 	const meId: mongo.ObjectID = me
@@ -34,15 +65,13 @@ export const pack = (
 				: (me as IUser)._id
 		: null;
 
-	const _game = deepcopy(game);
-
 	// Rename _id to id
 	_game.id = _game._id;
 	delete _game._id;
 
 	// Populate user
-	_game.black_user = await packUser(_game.black_user_id, meId);
-	_game.white_user = await packUser(_game.white_user_id, meId);
+	_game.user1 = await packUser(_game.user1_id, meId);
+	_game.user2 = await packUser(_game.user2_id, meId);
 	if (_game.winner_id) {
 		_game.winner = await packUser(_game.winner_id, meId);
 	} else {
diff --git a/src/api/stream/othello-game.ts b/src/api/stream/othello-game.ts
index d08647815..1dcd37efa 100644
--- a/src/api/stream/othello-game.ts
+++ b/src/api/stream/othello-game.ts
@@ -1,8 +1,8 @@
 import * as websocket from 'websocket';
 import * as redis from 'redis';
-import Game from '../models/othello-game';
+import Game, { pack } from '../models/othello-game';
 import { publishOthelloGameStream } from '../event';
-import Othello from '../../common/othello';
+import Othello from '../../common/othello/core';
 
 export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void {
 	const gameId = request.resourceURL.query.game;
@@ -17,6 +17,19 @@ export default function(request: websocket.request, connection: websocket.connec
 		const msg = JSON.parse(data.utf8Data);
 
 		switch (msg.type) {
+			case 'accept':
+				accept(true);
+				break;
+
+			case 'cancel-accept':
+				accept(false);
+				break;
+
+			case 'update-settings':
+				if (msg.settings == null) return;
+				updateSettings(msg.settings);
+				break;
+
 			case 'set':
 				if (msg.pos == null) return;
 				set(msg.pos);
@@ -24,38 +37,118 @@ export default function(request: websocket.request, connection: websocket.connec
 		}
 	});
 
+	async function updateSettings(settings) {
+		const game = await Game.findOne({ _id: gameId });
+
+		if (game.is_started) return;
+		if (!game.user1_id.equals(user._id) && !game.user2_id.equals(user._id)) return;
+		if (game.user1_id.equals(user._id) && game.user1_accepted) return;
+		if (game.user2_id.equals(user._id) && game.user2_accepted) return;
+
+		await Game.update({ _id: gameId }, {
+			$set: {
+				settings
+			}
+		});
+
+		publishOthelloGameStream(gameId, 'update-settings', settings);
+	}
+
+	async function accept(accept: boolean) {
+		const game = await Game.findOne({ _id: gameId });
+
+		if (game.is_started) return;
+
+		let bothAccepted = false;
+
+		if (game.user1_id.equals(user._id)) {
+			await Game.update({ _id: gameId }, {
+				$set: {
+					user1_accepted: accept
+				}
+			});
+
+			publishOthelloGameStream(gameId, 'change-accepts', {
+				user1: accept,
+				user2: game.user2_accepted
+			});
+
+			if (accept && game.user2_accepted) bothAccepted = true;
+		} else if (game.user2_id.equals(user._id)) {
+			await Game.update({ _id: gameId }, {
+				$set: {
+					user2_accepted: accept
+				}
+			});
+
+			publishOthelloGameStream(gameId, 'change-accepts', {
+				user1: game.user1_accepted,
+				user2: accept
+			});
+
+			if (accept && game.user1_accepted) bothAccepted = true;
+		} else {
+			return;
+		}
+
+		if (bothAccepted) {
+			// 3秒後、まだacceptされていたらゲーム開始
+			setTimeout(async () => {
+				const freshGame = await Game.findOne({ _id: gameId });
+				if (freshGame == null || freshGame.is_started || freshGame.is_ended) return;
+
+				let bw: number;
+				if (freshGame.settings.bw == 'random') {
+					bw = Math.random() > 0.5 ? 1 : 2;
+				} else {
+					bw = freshGame.settings.bw as number;
+				}
+
+				await Game.update({ _id: gameId }, {
+					$set: {
+						started_at: new Date(),
+						is_started: true,
+						black: bw
+					}
+				});
+
+				publishOthelloGameStream(gameId, 'started', await pack(gameId));
+			}, 3000);
+		}
+	}
+
 	async function set(pos) {
 		const game = await Game.findOne({ _id: gameId });
 
+		if (!game.is_started) return;
 		if (game.is_ended) return;
-		if (!game.black_user_id.equals(user._id) && !game.white_user_id.equals(user._id)) return;
+		if (!game.user1_id.equals(user._id) && !game.user2_id.equals(user._id)) return;
 
-		const o = new Othello();
-
-		game.logs.forEach(log => {
-			o.set(log.color, log.pos);
+		const o = new Othello(game.settings.map, {
+			isLlotheo: game.settings.is_llotheo
 		});
 
-		const myColor = game.black_user_id.equals(user._id) ? 'black' : 'white';
-		const opColor = myColor == 'black' ? 'white' : 'black';
+		game.logs.forEach(log => {
+			o.put(log.color, log.pos);
+		});
 
-		if (!o.canReverse(myColor, pos)) return;
-		o.set(myColor, pos);
+		const myColor =
+			(game.user1_id.equals(user._id) && game.black == 1) || (game.user2_id.equals(user._id) && game.black == 2)
+				? 'black'
+				: 'white';
 
-		let turn;
-		if (o.getPattern(opColor).length > 0) {
-			turn = myColor == 'black' ? game.white_user_id : game.black_user_id;
-		} else if (o.getPattern(myColor).length > 0) {
-			turn = myColor == 'black' ? game.black_user_id : game.white_user_id;
-		} else {
-			turn = null;
-		}
-
-		const isEnded = turn === null;
+		if (!o.canPut(myColor, pos)) return;
+		o.put(myColor, pos);
 
 		let winner;
-		if (isEnded) {
-			winner = o.blackCount == o.whiteCount ? null : o.blackCount > o.whiteCount ? game.black_user_id : game.white_user_id;
+		if (o.isEnded) {
+			if (o.winner == 'black') {
+				winner = game.black == 1 ? game.user1_id : game.user2_id;
+			} else if (o.winner == 'white') {
+				winner = game.black == 1 ? game.user2_id : game.user1_id;
+			} else {
+				winner = null;
+			}
 		}
 
 		const log = {
@@ -68,8 +161,7 @@ export default function(request: websocket.request, connection: websocket.connec
 			_id: gameId
 		}, {
 			$set: {
-				turn_user_id: turn,
-				is_ended: isEnded,
+				is_ended: o.isEnded,
 				winner_id: winner
 			},
 			$push: {
diff --git a/src/common/othello.ts b/src/common/othello.ts
deleted file mode 100644
index 1da8ad36d..000000000
--- a/src/common/othello.ts
+++ /dev/null
@@ -1,325 +0,0 @@
-const BOARD_SIZE = 8;
-
-export default class Othello {
-	public board: Array<'black' | 'white'>;
-
-	public stats: Array<{
-		b: number;
-		w: number;
-	}> = [];
-
-	/**
-	 * ゲームを初期化します
-	 */
-	constructor() {
-		this.board = [
-			null, null, null, null, null, null, null, null,
-			null, null, null, null, null, null, null, null,
-			null, null, null, null, null, null, null, null,
-			null, null, null, 'white', 'black', null, null, null,
-			null, null, null, 'black', 'white', null, null, null,
-			null, null, null, null, null, null, null, null,
-			null, null, null, null, null, null, null, null,
-			null, null, null, null, null, null, null, null
-		];
-
-		this.stats.push({
-			b: 0.5,
-			w: 0.5
-		});
-	}
-
-	public prevPos = -1;
-
-	public get blackCount() {
-		return this.board.filter(s => s == 'black').length;
-	}
-
-	public get whiteCount() {
-		return this.board.filter(s => s == 'white').length;
-	}
-
-	public get blackP() {
-		return this.blackCount / (this.blackCount + this.whiteCount);
-	}
-
-	public get whiteP() {
-		return this.whiteCount / (this.blackCount + this.whiteCount);
-	}
-
-	public setByNumber(color, n) {
-		const ps = this.getPattern(color);
-		this.set2(color, ps[n][0], ps[n][1]);
-	}
-
-	private write(color, x, y) {
-		const pos = x + (y * 8);
-		this.board[pos] = color;
-	}
-
-	/**
-	 * 石を配置します
-	 */
-	public set2(color, x, y) {
-		this.prevPos = x + (y * 8);
-		this.write(color, x, y);
-
-		const reverses = this.getReverse(color, x, y);
-
-		reverses.forEach(r => {
-			switch (r[0]) {
-				case 0: // 上
-					for (let c = 0, _y = y - 1; c < r[1]; c++, _y--) {
-						this.write(color, x, _y);
-					}
-					break;
-
-				case 1: // 右上
-					for (let c = 0, i = 1; c < r[1]; c++, i++) {
-						this.write(color, x + i, y - i);
-					}
-					break;
-
-				case 2: // 右
-					for (let c = 0, _x = x + 1; c < r[1]; c++, _x++) {
-						this.write(color, _x, y);
-					}
-					break;
-
-				case 3: // 右下
-					for (let c = 0, i = 1; c < r[1]; c++, i++) {
-						this.write(color, x + i, y + i);
-					}
-					break;
-
-				case 4: // 下
-					for (let c = 0, _y = y + 1; c < r[1]; c++, _y++) {
-						this.write(color, x, _y);
-					}
-					break;
-
-				case 5: // 左下
-					for (let c = 0, i = 1; c < r[1]; c++, i++) {
-						this.write(color, x - i, y + i);
-					}
-					break;
-
-				case 6: // 左
-					for (let c = 0, _x = x - 1; c < r[1]; c++, _x--) {
-						this.write(color, _x, y);
-					}
-					break;
-
-				case 7: // 左上
-					for (let c = 0, i = 1; c < r[1]; c++, i++) {
-						this.write(color, x - i, y - i);
-					}
-					break;
-				}
-		});
-
-		this.stats.push({
-			b: this.blackP,
-			w: this.whiteP
-		});
-	}
-
-	public set(color, pos) {
-		const x = pos % BOARD_SIZE;
-		const y = Math.floor(pos / BOARD_SIZE);
-		this.set2(color, x, y);
-	}
-
-	public get(x, y) {
-		const pos = x + (y * 8);
-		return this.board[pos];
-	}
-
-	/**
-	 * 打つことができる場所を取得します
-	 */
-	public getPattern(myColor): number[][] {
-		const result = [];
-		this.board.forEach((stone, i) => {
-			if (stone != null) return;
-			const x = i % BOARD_SIZE;
-			const y = Math.floor(i / BOARD_SIZE);
-			if (this.canReverse2(myColor, x, y)) result.push([x, y]);
-		});
-		return result;
-	}
-
-	/**
-	 * 指定の位置に石を打つことができるかどうか(相手の石を1つでも反転させられるか)を取得します
-	 */
-	public canReverse2(myColor, x, y): boolean {
-		return this.canReverse(myColor, x + (y * 8));
-	}
-	public canReverse(myColor, pos): boolean {
-		if (this.board[pos] != null) return false;
-		const x = pos % BOARD_SIZE;
-		const y = Math.floor(pos / BOARD_SIZE);
-		return this.getReverse(myColor, x, y) !== null;
-	}
-
-	private getReverse(myColor, targetx, targety): number[] {
-		const opponentColor = myColor == 'black' ? 'white' : 'black';
-
-		const createIterater = () => {
-			let opponentStoneFound = false;
-			let breaked = false;
-			return (x, y): any => {
-				if (breaked) {
-					return;
-				} else if (this.get(x, y) == myColor && opponentStoneFound) {
-					return true;
-				} else if (this.get(x, y) == myColor && !opponentStoneFound) {
-					breaked = true;
-				} else if (this.get(x, y) == opponentColor) {
-					opponentStoneFound = true;
-				} else {
-					breaked = true;
-				}
-			};
-		};
-
-		const res = [];
-
-		let iterate;
-
-		// 上
-		iterate = createIterater();
-		for (let c = 0, y = targety - 1; y >= 0; c++, y--) {
-			if (iterate(targetx, y)) {
-				res.push([0, c]);
-				break;
-			}
-		}
-
-		// 右上
-		iterate = createIterater();
-		for (let c = 0, i = 1; i <= Math.min(BOARD_SIZE - targetx, targety); c++, i++) {
-			if (iterate(targetx + i, targety - i)) {
-				res.push([1, c]);
-				break;
-			}
-		}
-
-		// 右
-		iterate = createIterater();
-		for (let c = 0, x = targetx + 1; x < BOARD_SIZE; c++, x++) {
-			if (iterate(x, targety)) {
-				res.push([2, c]);
-				break;
-			}
-		}
-
-		// 右下
-		iterate = createIterater();
-		for (let c = 0, i = 1; i < Math.min(BOARD_SIZE - targetx, BOARD_SIZE - targety); c++, i++) {
-			if (iterate(targetx + i, targety + i)) {
-				res.push([3, c]);
-				break;
-			}
-		}
-
-		// 下
-		iterate = createIterater();
-		for (let c = 0, y = targety + 1; y < BOARD_SIZE; c++, y++) {
-			if (iterate(targetx, y)) {
-				res.push([4, c]);
-				break;
-			}
-		}
-
-		// 左下
-		iterate = createIterater();
-		for (let c = 0, i = 1; i <= Math.min(targetx, BOARD_SIZE - targety); c++, i++) {
-			if (iterate(targetx - i, targety + i)) {
-				res.push([5, c]);
-				break;
-			}
-		}
-
-		// 左
-		iterate = createIterater();
-		for (let c = 0, x = targetx - 1; x >= 0; c++, x--) {
-			if (iterate(x, targety)) {
-				res.push([6, c]);
-				break;
-			}
-		}
-
-		// 左上
-		iterate = createIterater();
-		for (let c = 0, i = 1; i <= Math.min(targetx, targety); c++, i++) {
-			if (iterate(targetx - i, targety - i)) {
-				res.push([7, c]);
-				break;
-			}
-		}
-
-		return res.length === 0 ? null : res;
-	}
-
-	public toString(): string {
-		//return this.board.map(row => row.map(state => state === 'black' ? '●' : state === 'white' ? '○' : '┼').join('')).join('\n');
-		//return this.board.map(row => row.map(state => state === 'black' ? '⚫️' : state === 'white' ? '⚪️' : '🔹').join('')).join('\n');
-		return 'wip';
-	}
-
-	public toPatternString(color): string {
-		//const num = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
-		/*const num = ['0️⃣', '1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣', '🔟', '🍏', '🍎', '🍐', '🍊', '🍋', '🍌', '🍉', '🍇', '🍓', '🍈', '🍒', '🍑', '🍍'];
-
-		const pattern = this.getPattern(color);
-
-		return this.board.map((row, y) => row.map((state, x) => {
-			const i = pattern.findIndex(p => p[0] == x && p[1] == y);
-			//return state === 'black' ? '●' : state === 'white' ? '○' : i != -1 ? num[i] : '┼';
-			return state === 'black' ? '⚫️' : state === 'white' ? '⚪️' : i != -1 ? num[i] : '🔹';
-		}).join('')).join('\n');*/
-
-		return 'wip';
-	}
-}
-
-export function ai(color: string, othello: Othello) {
-	const opponentColor = color == 'black' ? 'white' : 'black';
-
-	function think() {
-		// 打てる場所を取得
-		const ps = othello.getPattern(color);
-
-		if (ps.length > 0) { // 打てる場所がある場合
-			// 角を取得
-			const corners = ps.filter(p =>
-				// 左上
-				(p[0] == 0 && p[1] == 0) ||
-				// 右上
-				(p[0] == (BOARD_SIZE - 1) && p[1] == 0) ||
-				// 右下
-				(p[0] == (BOARD_SIZE - 1) && p[1] == (BOARD_SIZE - 1)) ||
-				// 左下
-				(p[0] == 0 && p[1] == (BOARD_SIZE - 1))
-			);
-
-			if (corners.length > 0) { // どこかしらの角に打てる場合
-				// 打てる角からランダムに選択して打つ
-				const p = corners[Math.floor(Math.random() * corners.length)];
-				othello.set(color, p[0], p[1]);
-			} else { // 打てる角がない場合
-				// 打てる場所からランダムに選択して打つ
-				const p = ps[Math.floor(Math.random() * ps.length)];
-				othello.set(color, p[0], p[1]);
-			}
-
-			// 相手の打つ場所がない場合続けてAIのターン
-			if (othello.getPattern(opponentColor).length === 0) {
-				think();
-			}
-		}
-	}
-
-	think();
-}
diff --git a/src/common/othello/ai.ts b/src/common/othello/ai.ts
new file mode 100644
index 000000000..3943d04bf
--- /dev/null
+++ b/src/common/othello/ai.ts
@@ -0,0 +1,42 @@
+import Othello, { Color } from './core';
+
+export function ai(color: Color, othello: Othello) {
+	//const opponentColor = color == 'black' ? 'white' : 'black';
+/* wip
+
+	function think() {
+		// 打てる場所を取得
+		const ps = othello.canPutSomewhere(color);
+
+		if (ps.length > 0) { // 打てる場所がある場合
+			// 角を取得
+			const corners = ps.filter(p =>
+				// 左上
+				(p[0] == 0 && p[1] == 0) ||
+				// 右上
+				(p[0] == (BOARD_SIZE - 1) && p[1] == 0) ||
+				// 右下
+				(p[0] == (BOARD_SIZE - 1) && p[1] == (BOARD_SIZE - 1)) ||
+				// 左下
+				(p[0] == 0 && p[1] == (BOARD_SIZE - 1))
+			);
+
+			if (corners.length > 0) { // どこかしらの角に打てる場合
+				// 打てる角からランダムに選択して打つ
+				const p = corners[Math.floor(Math.random() * corners.length)];
+				othello.set(color, p[0], p[1]);
+			} else { // 打てる角がない場合
+				// 打てる場所からランダムに選択して打つ
+				const p = ps[Math.floor(Math.random() * ps.length)];
+				othello.set(color, p[0], p[1]);
+			}
+
+			// 相手の打つ場所がない場合続けてAIのターン
+			if (othello.getPattern(opponentColor).length === 0) {
+				think();
+			}
+		}
+	}
+
+	think();*/
+}
diff --git a/src/common/othello/core.ts b/src/common/othello/core.ts
new file mode 100644
index 000000000..b76586031
--- /dev/null
+++ b/src/common/othello/core.ts
@@ -0,0 +1,239 @@
+import { Map } from './maps';
+
+export type Color = 'black' | 'white';
+export type MapPixel = 'null' | 'empty';
+
+export type Options = {
+	isLlotheo: boolean;
+};
+
+/**
+ * オセロエンジン
+ */
+export default class Othello {
+	public map: Map;
+	public mapData: MapPixel[];
+	public board: Color[];
+	public turn: Color = 'black';
+	public opts: Options;
+
+	public stats: Array<{
+		b: number;
+		w: number;
+	}>;
+
+	/**
+	 * ゲームを初期化します
+	 */
+	constructor(map: Map, opts: Options) {
+		this.map = map;
+		this.opts = opts;
+
+		// Parse map data
+		this.board = this.map.data.split('').map(d => {
+			if (d == '-') return null;
+			if (d == 'b') return 'black';
+			if (d == 'w') return 'white';
+			return undefined;
+		});
+		this.mapData = this.map.data.split('').map(d => {
+			if (d == '-' || d == 'b' || d == 'w') return 'empty';
+			return 'null';
+		});
+
+		// Init stats
+		this.stats = [{
+			b: this.blackP,
+			w: this.whiteP
+		}];
+	}
+
+	public prevPos = -1;
+
+	/**
+	 * 黒石の数
+	 */
+	public get blackCount() {
+		return this.board.filter(x => x == 'black').length;
+	}
+
+	/**
+	 * 白石の数
+	 */
+	public get whiteCount() {
+		return this.board.filter(x => x == 'white').length;
+	}
+
+	/**
+	 * 黒石の比率
+	 */
+	public get blackP() {
+		return this.blackCount / (this.blackCount + this.whiteCount);
+	}
+
+	/**
+	 * 白石の比率
+	 */
+	public get whiteP() {
+		return this.whiteCount / (this.blackCount + this.whiteCount);
+	}
+
+	public transformPosToXy(pos: number): number[] {
+		const x = pos % this.map.size;
+		const y = Math.floor(pos / this.map.size);
+		return [x, y];
+	}
+
+	public transformXyToPos(x: number, y: number): number {
+		return x + (y * this.map.size);
+	}
+
+	/**
+	 * 指定のマスに石を書き込みます
+	 * @param color 石の色
+	 * @param pos 位置
+	 */
+	private write(color: Color, pos: number) {
+		this.board[pos] = color;
+	}
+
+	/**
+	 * 指定のマスに石を打ちます
+	 * @param color 石の色
+	 * @param pos 位置
+	 */
+	public put(color: Color, pos: number) {
+		if (!this.canPut(color, pos)) return;
+
+		this.prevPos = pos;
+		this.write(color, pos);
+
+		// 反転させられる石を取得
+		const reverses = this.effects(color, pos);
+
+		// 反転させる
+		reverses.forEach(pos => {
+			this.write(color, pos);
+		});
+
+		this.stats.push({
+			b: this.blackP,
+			w: this.whiteP
+		});
+
+		// ターン計算
+		const opColor = color == 'black' ? 'white' : 'black';
+		if (this.canPutSomewhere(opColor).length > 0) {
+			this.turn = color == 'black' ? 'white' : 'black';
+		} else if (this.canPutSomewhere(color).length > 0) {
+			this.turn = color == 'black' ? 'black' : 'white';
+		} else {
+			this.turn = null;
+		}
+	}
+
+	/**
+	 * 指定したマスの状態を取得します
+	 * @param pos 位置
+	 */
+	public get(pos: number) {
+		return this.board[pos];
+	}
+
+	/**
+	 * 指定した位置のマップデータのマスを取得します
+	 * @param pos 位置
+	 */
+	public mapDataGet(pos: number): MapPixel {
+		if (pos < 0 || pos >= this.mapData.length) return 'null';
+		return this.mapData[pos];
+	}
+
+	/**
+	 * 打つことができる場所を取得します
+	 */
+	public canPutSomewhere(color: Color): number[] {
+		const result = [];
+
+		this.board.forEach((x, i) => {
+			if (this.canPut(color, i)) result.push(i);
+		});
+
+		return result;
+	}
+
+	/**
+	 * 指定のマスに石を打つことができるかどうか(相手の石を1つでも反転させられるか)を取得します
+	 * @param color 自分の色
+	 * @param pos 位置
+	 */
+	public canPut(color: Color, pos: number): boolean {
+		// 既に石が置いてある場所には打てない
+		if (this.get(pos) !== null) return false;
+		return this.effects(color, pos).length !== 0;
+	}
+
+	/**
+	 * 指定のマスに石を置いた時の、反転させられる石を取得します
+	 * @param color 自分の色
+	 * @param pos 位置
+	 */
+	private effects(color: Color, pos: number): number[] {
+		const enemyColor = color == 'black' ? 'white' : 'black';
+		const [x, y] = this.transformPosToXy(pos);
+		let stones = [];
+
+		const iterate = (fn: (i: number) => number[]) => {
+			let i = 1;
+			const found = [];
+			while (true) {
+				const [x, y] = fn(i);
+				if (x < 0 || y < 0 || x >= this.map.size || y >= this.map.size) break;
+				const pos = this.transformXyToPos(x, y);
+				const pixel = this.mapDataGet(pos);
+				if (pixel == 'null') break;
+				const stone = this.get(pos);
+				if (stone == null) break;
+				if (stone == enemyColor) found.push(pos);
+				if (stone == color) {
+					stones = stones.concat(found);
+					break;
+				}
+				i++;
+			}
+		};
+
+		iterate(i => [x    , y - i]); // 上
+		iterate(i => [x + i, y - i]); // 右上
+		iterate(i => [x + i, y    ]); // 右
+		iterate(i => [x + i, y + i]); // 右下
+		iterate(i => [x    , y + i]); // 下
+		iterate(i => [x - i, y + i]); // 左下
+		iterate(i => [x - i, y    ]); // 左
+		iterate(i => [x - i, y - i]); // 左上
+
+		return stones;
+	}
+
+	/**
+	 * ゲームが終了したか否か
+	 */
+	public get isEnded(): boolean {
+		return this.turn === null;
+	}
+
+	/**
+	 * ゲームの勝者 (null = 引き分け)
+	 */
+	public get winner(): Color {
+		if (!this.isEnded) return undefined;
+
+		if (this.blackCount == this.whiteCount) return null;
+
+		if (this.opts.isLlotheo) {
+			return this.blackCount > this.whiteCount ? 'white' : 'black';
+		} else {
+			return this.blackCount > this.whiteCount ? 'black' : 'white';
+		}
+	}
+}
diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
new file mode 100644
index 000000000..e6f3f409e
--- /dev/null
+++ b/src/common/othello/maps.ts
@@ -0,0 +1,217 @@
+/**
+ * 組み込みマップ定義
+ *
+ * データ値:
+ * (スペース) ... マス無し
+ * - ... マス
+ * b ... 初期配置される黒石
+ * w ... 初期配置される白石
+ */
+
+export type Map = {
+	name?: string;
+	size: number;
+	data: string;
+};
+
+export const fourfour: Map = {
+	name: '4x4',
+	size: 4,
+	data:
+		'----' +
+		'-wb-' +
+		'-bw-' +
+		'----'
+};
+
+export const sixsix: Map = {
+	name: '6x6',
+	size: 6,
+	data:
+		'------' +
+		'------' +
+		'--wb--' +
+		'--bw--' +
+		'------' +
+		'------'
+};
+
+export const eighteight: Map = {
+	name: '8x8',
+	size: 8,
+	data:
+		'--------' +
+		'--------' +
+		'--------' +
+		'---wb---' +
+		'---bw---' +
+		'--------' +
+		'--------' +
+		'--------'
+};
+
+export const roundedEighteight: Map = {
+	name: '8x8 rounded',
+	size: 8,
+	data:
+		' ------ ' +
+		'--------' +
+		'--------' +
+		'---wb---' +
+		'---bw---' +
+		'--------' +
+		'--------' +
+		' ------ '
+};
+
+export const roundedEighteight2: Map = {
+	name: '8x8 rounded 2',
+	size: 8,
+	data:
+		'  ----  ' +
+		' ------ ' +
+		'--------' +
+		'---wb---' +
+		'---bw---' +
+		'--------' +
+		' ------ ' +
+		'  ----  '
+};
+
+export const eighteightWithNotch: Map = {
+	name: '8x8 with notch',
+	size: 8,
+	data:
+		'---  ---' +
+		'--------' +
+		'--------' +
+		' --wb-- ' +
+		' --bw-- ' +
+		'--------' +
+		'--------' +
+		'---  ---'
+};
+
+export const eighteightWithSomeHoles: Map = {
+	name: '8x8 with some holes',
+	size: 8,
+	data:
+		'--- ----' +
+		'----- --' +
+		'-- -----' +
+		'---wb---' +
+		'---bw- -' +
+		' -------' +
+		'--- ----' +
+		'--------'
+};
+
+export const sixeight: Map = {
+	name: '6x8',
+	size: 8,
+	data:
+		' ------ ' +
+		' ------ ' +
+		' ------ ' +
+		' --wb-- ' +
+		' --bw-- ' +
+		' ------ ' +
+		' ------ ' +
+		' ------ '
+};
+
+export const tenthtenth: Map = {
+	name: '10x10',
+	size: 10,
+	data:
+		'----------' +
+		'----------' +
+		'----------' +
+		'----------' +
+		'----wb----' +
+		'----bw----' +
+		'----------' +
+		'----------' +
+		'----------' +
+		'----------'
+};
+
+export const hole: Map = {
+	name: 'hole',
+	size: 10,
+	data:
+		'----------' +
+		'----------' +
+		'--wb--wb--' +
+		'--bw--bw--' +
+		'----  ----' +
+		'----  ----' +
+		'--wb--wb--' +
+		'--bw--bw--' +
+		'----------' +
+		'----------'
+};
+
+export const spark: Map = {
+	name: 'spark',
+	size: 10,
+	data:
+		' -      - ' +
+		'----------' +
+		' -------- ' +
+		' -------- ' +
+		' ---wb--- ' +
+		' ---bw--- ' +
+		' -------- ' +
+		' -------- ' +
+		'----------' +
+		' -      - '
+};
+
+export const islands: Map = {
+	name: 'islands',
+	size: 10,
+	data:
+		'--------  ' +
+		'---wb---  ' +
+		'---bw---  ' +
+		'--------  ' +
+		'  -    -  ' +
+		'  -    -  ' +
+		'  --------' +
+		'  ---bw---' +
+		'  ---wb---' +
+		'  --------'
+};
+
+export const grid: Map = {
+	name: 'grid',
+	size: 10,
+	data:
+		'----------' +
+		'- - -- - -' +
+		'----------' +
+		'- - -- - -' +
+		'----wb----' +
+		'----bw----' +
+		'- - -- - -' +
+		'----------' +
+		'- - -- - -' +
+		'----------'
+};
+
+export const iphonex: Map = {
+	name: 'iPhone X',
+	size: 10,
+	data:
+		'  --  --  ' +
+		' -------- ' +
+		' -------- ' +
+		' -------- ' +
+		' ---wb--- ' +
+		' ---bw--- ' +
+		' -------- ' +
+		' -------- ' +
+		' -------- ' +
+		'  ------  '
+};
diff --git a/src/web/app/common/views/components/othello.game.vue b/src/web/app/common/views/components/othello.game.vue
index 1cb2400f7..2ef6b645c 100644
--- a/src/web/app/common/views/components/othello.game.vue
+++ b/src/web/app/common/views/components/othello.game.vue
@@ -1,23 +1,27 @@
 <template>
 <div class="root">
-	<header><b>{{ game.black_user.name }}</b>(黒) vs <b>{{ game.white_user.name }}</b>(白)</header>
-	<p class="turn" v-if="!iAmPlayer && !isEnded">{{ turn.name }}のターンです<mk-ellipsis/></p>
-	<p class="turn" v-if="logPos != logs.length">{{ turn.name }}のターン</p>
-	<p class="turn" v-if="iAmPlayer && !isEnded">{{ isMyTurn ? 'あなたのターンです' : '相手のターンです' }}<mk-ellipsis v-if="!isMyTurn"/></p>
-	<p class="result" v-if="isEnded && logPos == logs.length">
-		<template v-if="winner"><b>{{ winner.name }}</b>の勝ち</template>
+	<header><b>{{ blackUser.name }}</b>(黒) vs <b>{{ whiteUser.name }}</b>(白)</header>
+
+	<p class="turn" v-if="!iAmPlayer && !game.is_ended">{{ turnUser.name }}のターンです<mk-ellipsis/></p>
+	<p class="turn" v-if="logPos != logs.length">{{ turnUser.name }}のターン</p>
+	<p class="turn" v-if="iAmPlayer && !game.is_ended">{{ isMyTurn ? 'あなたのターンです' : '相手のターンです' }}<mk-ellipsis v-if="!isMyTurn"/></p>
+	<p class="result" v-if="game.is_ended && logPos == logs.length">
+		<template v-if="game.winner"><b>{{ game.winner.name }}</b>の勝ち{{ game.settings.is_llotheo ? ' (ロセオ)' : '' }}</template>
 		<template v-else>引き分け</template>
 	</p>
-	<div class="board">
+
+	<div class="board" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.size }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map.size }, 1fr)` }">
 		<div v-for="(stone, i) in o.board"
-			:class="{ empty: stone == null, myTurn: isMyTurn, can: o.canReverse(turn.id == game.black_user.id ? 'black' : 'white', i), prev: o.prevPos == i }"
+			:class="{ empty: stone == null, none: o.map.data[i] == ' ', myTurn: isMyTurn, can: turnUser ? o.canPut(turnUser.id == blackUser.id ? 'black' : 'white', i) : null, prev: o.prevPos == i }"
 			@click="set(i)"
 		>
-			<img v-if="stone == 'black'" :src="`${game.black_user.avatar_url}?thumbnail&size=64`" alt="">
-			<img v-if="stone == 'white'" :src="`${game.white_user.avatar_url}?thumbnail&size=64`" alt="">
+			<img v-if="stone == 'black'" :src="`${blackUser.avatar_url}?thumbnail&size=128`" alt="">
+			<img v-if="stone == 'white'" :src="`${whiteUser.avatar_url}?thumbnail&size=128`" alt="">
 		</div>
 	</div>
+
 	<p>黒:{{ o.blackCount }} 白:{{ o.whiteCount }} 合計:{{ o.blackCount + o.whiteCount }}</p>
+
 	<div class="graph">
 		<div v-for="n in 61 - o.stats.length">
 		</div>
@@ -26,7 +30,8 @@
 			<div :style="{ height: `${ Math.floor(data.w * 100) }%` }"></div>
 		</div>
 	</div>
-	<div class="player" v-if="isEnded">
+
+	<div class="player" v-if="game.is_ended">
 		<el-button type="primary" @click="logPos = 0" :disabled="logPos == 0">%fa:fast-backward%</el-button>
 		<el-button type="primary" @click="logPos--" :disabled="logPos == 0">%fa:backward%</el-button>
 		<span>{{ logPos }} / {{ logs.length }}</span>
@@ -38,58 +43,63 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import { OthelloGameStream } from '../../scripts/streaming/othello-game';
-import Othello from '../../../../../common/othello';
+import Othello, { Color } from '../../../../../common/othello/core';
 
 export default Vue.extend({
-	props: ['game'],
+	props: ['game', 'connection'],
 
 	data() {
 		return {
-			o: new Othello(),
+			o: null as Othello,
 			logs: [],
-			logPos: 0,
-			turn: null,
-			isMyTurn: null,
-			isEnded: false,
-			winner: null,
-			connection: null
+			logPos: 0
 		};
 	},
 
 	computed: {
 		iAmPlayer(): boolean {
-			return this.game.black_user_id == (this as any).os.i.id || this.game.white_user_id == (this as any).os.i.id;
+			return this.game.user1_id == (this as any).os.i.id || this.game.user2_id == (this as any).os.i.id;
 		},
-		myColor(): string {
-			return this.game.black_user_id == (this as any).os.i.id ? 'black' : 'white';
+		myColor(): Color {
+			if (!this.iAmPlayer) return null;
+			if (this.game.user1_id == (this as any).os.i.id && this.game.black == 1) return 'black';
+			if (this.game.user2_id == (this as any).os.i.id && this.game.black == 2) return 'black';
+			return 'white';
 		},
-		opColor(): string {
+		opColor(): Color {
+			if (!this.iAmPlayer) return null;
 			return this.myColor == 'black' ? 'white' : 'black';
+		},
+		blackUser(): any {
+			return this.game.black == 1 ? this.game.user1 : this.game.user2;
+		},
+		whiteUser(): any {
+			return this.game.black == 1 ? this.game.user2 : this.game.user1;
+		},
+		turnUser(): any {
+			if (this.o.turn == 'black') {
+				return this.game.black == 1 ? this.game.user1 : this.game.user2;
+			} else if (this.o.turn == 'white') {
+				return this.game.black == 1 ? this.game.user2 : this.game.user1;
+			} else {
+				return null;
+			}
+		},
+		isMyTurn(): boolean {
+			if (this.turnUser == null) return null;
+			return this.turnUser.id == (this as any).os.i.id;
 		}
 	},
 
 	watch: {
 		logPos(v) {
-			if (!this.isEnded) return;
-			this.o = new Othello();
-			this.turn = this.game.black_user;
+			if (!this.game.is_ended) return;
+			this.o = new Othello(this.game.settings.map, {
+				isLlotheo: this.game.settings.is_llotheo
+			});
 			this.logs.forEach((log, i) => {
 				if (i < v) {
-					this.o.set(log.color, log.pos);
-
-					if (log.color == 'black' && this.o.getPattern('white').length > 0) {
-						this.turn = this.game.white_user;
-					}
-					if (log.color == 'black' && this.o.getPattern('white').length == 0) {
-						this.turn = this.game.black_user;
-					}
-					if (log.color == 'white' && this.o.getPattern('black').length > 0) {
-						this.turn = this.game.black_user;
-					}
-					if (log.color == 'white' && this.o.getPattern('black').length == 0) {
-						this.turn = this.game.white_user;
-					}
+					this.o.put(log.color, log.pos);
 				}
 			});
 			this.$forceUpdate();
@@ -97,74 +107,65 @@ export default Vue.extend({
 	},
 
 	created() {
+		this.o = new Othello(this.game.settings.map, {
+			isLlotheo: this.game.settings.is_llotheo
+		});
+
 		this.game.logs.forEach(log => {
-			this.o.set(log.color, log.pos);
+			this.o.put(log.color, log.pos);
 		});
 
 		this.logs = this.game.logs;
 		this.logPos = this.logs.length;
-		this.turn = this.game.turn_user_id == this.game.black_user_id ? this.game.black_user : this.game.white_user;
-		this.isMyTurn = this.game.turn_user_id == (this as any).os.i.id;
-		this.isEnded = this.game.is_ended;
-		this.winner = this.game.winner;
 	},
 
 	mounted() {
-		this.connection = new OthelloGameStream((this as any).os.i, this.game);
 		this.connection.on('set', this.onSet);
 	},
 
 	beforeDestroy() {
 		this.connection.off('set', this.onSet);
-		this.connection.close();
 	},
 
 	methods: {
 		set(pos) {
 			if (!this.isMyTurn) return;
-			if (!this.o.canReverse(this.myColor, pos)) return;
-			this.o.set(this.myColor, pos);
-			if (this.o.getPattern(this.opColor).length > 0) {
-				this.isMyTurn = !this.isMyTurn;
-				this.turn = this.myColor == 'black' ? this.game.white_user : this.game.black_user;
-			} else if (this.o.getPattern(this.myColor).length == 0) {
-				this.isEnded = true;
-				this.winner = this.o.blackCount == this.o.whiteCount ? null : this.o.blackCount > this.o.whiteCount ? this.game.black_user : this.game.white_user;
-			}
+			if (!this.o.canPut(this.myColor, pos)) return;
+
+			this.o.put(this.myColor, pos);
+
 			this.connection.send({
 				type: 'set',
 				pos
 			});
+
+			this.checkEnd();
+
 			this.$forceUpdate();
 		},
 
 		onSet(x) {
 			this.logs.push(x);
 			this.logPos++;
-			this.o.set(x.color, x.pos);
+			this.o.put(x.color, x.pos);
+			this.checkEnd();
+			this.$forceUpdate();
+		},
 
-			if (this.o.getPattern('black').length == 0 && this.o.getPattern('white').length == 0) {
-				this.isEnded = true;
-				this.winner = this.o.blackCount == this.o.whiteCount ? null : this.o.blackCount > this.o.whiteCount ? this.game.black_user : this.game.white_user;
-			} else {
-				if (this.iAmPlayer && this.o.getPattern(this.myColor).length > 0) {
-					this.isMyTurn = true;
-				}
-
-				if (x.color == 'black' && this.o.getPattern('white').length > 0) {
-					this.turn = this.game.white_user;
-				}
-				if (x.color == 'black' && this.o.getPattern('white').length == 0) {
-					this.turn = this.game.black_user;
-				}
-				if (x.color == 'white' && this.o.getPattern('black').length > 0) {
-					this.turn = this.game.black_user;
-				}
-				if (x.color == 'white' && this.o.getPattern('black').length == 0) {
-					this.turn = this.game.white_user;
+		checkEnd() {
+			this.game.is_ended = this.o.isEnded;
+			if (this.game.is_ended) {
+				if (this.o.winner == 'black') {
+					this.game.winner_id = this.game.black == 1 ? this.game.user1_id : this.game.user2_id;
+					this.game.winner = this.game.black == 1 ? this.game.user1 : this.game.user2;
+				} else if (this.o.winner == 'white') {
+					this.game.winner_id = this.game.black == 1 ? this.game.user2_id : this.game.user1_id;
+					this.game.winner = this.game.black == 1 ? this.game.user2 : this.game.user1;
+				} else {
+					this.game.winner_id = null;
+					this.game.winner = null;
 				}
 			}
-			this.$forceUpdate();
 		}
 	}
 });
@@ -182,8 +183,6 @@ export default Vue.extend({
 
 	> .board
 		display grid
-		grid-template-rows repeat(8, 1fr)
-		grid-template-columns repeat(8, 1fr)
 		grid-gap 4px
 		width 300px
 		height 300px
@@ -221,6 +220,9 @@ export default Vue.extend({
 			&.prev
 				box-shadow 0 0 0 4px rgba($theme-color, 0.7)
 
+			&.none
+				border-color transparent !important
+
 			> img
 				display block
 				width 100%
diff --git a/src/web/app/common/views/components/othello.gameroom.vue b/src/web/app/common/views/components/othello.gameroom.vue
new file mode 100644
index 000000000..9f4037515
--- /dev/null
+++ b/src/web/app/common/views/components/othello.gameroom.vue
@@ -0,0 +1,42 @@
+<template>
+<div>
+	<x-room v-if="!g.is_started" :game="g" :connection="connection"/>
+	<x-game v-else :game="g" :connection="connection"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XGame from './othello.game.vue';
+import XRoom from './othello.room.vue';
+import { OthelloGameStream } from '../../scripts/streaming/othello-game';
+
+export default Vue.extend({
+	components: {
+		XGame,
+		XRoom
+	},
+	props: ['game'],
+	data() {
+		return {
+			connection: null,
+			g: null
+		};
+	},
+	created() {
+		this.g = this.game;
+		this.connection = new OthelloGameStream((this as any).os.i, this.game);
+		this.connection.on('started', this.onStarted);
+	},
+	beforeDestroy() {
+		this.connection.off('started', this.onStarted);
+		this.connection.close();
+	},
+	methods: {
+		onStarted(game) {
+			Object.assign(this.g, game);
+			this.$forceUpdate();
+		}
+	}
+});
+</script>
diff --git a/src/web/app/common/views/components/othello.room.vue b/src/web/app/common/views/components/othello.room.vue
new file mode 100644
index 000000000..b41d97ec6
--- /dev/null
+++ b/src/web/app/common/views/components/othello.room.vue
@@ -0,0 +1,152 @@
+<template>
+<div class="root">
+	<header><b>{{ game.user1.name }}</b> vs <b>{{ game.user2.name }}</b></header>
+
+	<p>ゲームの設定</p>
+
+	<el-select v-model="mapName" placeholder="マップを選択" @change="onMapChange">
+		<el-option v-for="m in maps" :key="m.name" :label="m.name" :value="m.name"/>
+	</el-select>
+
+	<div class="board" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.size }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map.size }, 1fr)` }">
+		<div v-for="(x, i) in game.settings.map.data"
+			:class="{ none: x == ' ' }"
+		>
+			<template v-if="x == 'b'">%fa:circle%</template>
+			<template v-if="x == 'w'">%fa:circle R%</template>
+		</div>
+	</div>
+
+	<div class="rules">
+		<mk-switch v-model="game.settings.is_llotheo" @change="onIsLlotheoChange" text="石の少ない方が勝ち(ロセオ)"/>
+	</div>
+
+	<div class="actions">
+		<el-button @click="exit">キャンセル</el-button>
+		<el-button type="primary" @click="accept" v-if="!isAccepted">準備完了</el-button>
+		<el-button type="primary" @click="cancel" v-if="isAccepted">準備続行</el-button>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as maps from '../../../../../common/othello/maps';
+
+export default Vue.extend({
+	props: ['game', 'connection'],
+
+	data() {
+		return {
+			o: null,
+			isLlotheo: false,
+			mapName: maps.eighteight.name,
+			maps: maps
+		};
+	},
+
+	computed: {
+		isAccepted(): boolean {
+			if (this.game.user1_id == (this as any).os.i.id && this.game.user1_accepted) return true;
+			if (this.game.user2_id == (this as any).os.i.id && this.game.user2_accepted) return true;
+			return false;
+		}
+	},
+
+	created() {
+		this.connection.on('change-accepts', this.onChangeAccepts);
+		this.connection.on('update-settings', this.onUpdateSettings);
+	},
+
+	beforeDestroy() {
+		this.connection.off('change-accepts', this.onChangeAccepts);
+		this.connection.off('update-settings', this.onUpdateSettings);
+	},
+
+	methods: {
+		exit() {
+
+		},
+
+		accept() {
+			this.connection.send({
+				type: 'accept'
+			});
+		},
+
+		cancel() {
+			this.connection.send({
+				type: 'cancel-accept'
+			});
+		},
+
+		onChangeAccepts(accepts) {
+			this.game.user1_accepted = accepts.user1;
+			this.game.user2_accepted = accepts.user2;
+			this.$forceUpdate();
+		},
+
+		onUpdateSettings(settings) {
+			this.game.settings = settings;
+			this.mapName = this.game.settings.map.name;
+		},
+
+		onMapChange(v) {
+			this.game.settings.map = Object.entries(maps).find(x => x[1].name == v)[1];
+			this.connection.send({
+				type: 'update-settings',
+				settings: this.game.settings
+			});
+			this.$forceUpdate();
+		},
+
+		onIsLlotheoChange(v) {
+			this.connection.send({
+				type: 'update-settings',
+				settings: this.game.settings
+			});
+			this.$forceUpdate();
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+.root
+	text-align center
+
+	> header
+		padding 8px
+		border-bottom dashed 1px #c4cdd4
+
+	> .board
+		display grid
+		grid-gap 4px
+		width 300px
+		height 300px
+		margin 16px auto
+
+		> div
+			background transparent
+			border solid 2px #eee
+			border-radius 6px
+			overflow hidden
+
+			*
+				pointer-events none
+				user-select none
+				width 100%
+				height 100%
+
+			&.none
+				border-color transparent
+
+	> .rules
+		max-width 300px
+		margin 0 auto
+
+	> .actions
+		margin-bottom 16px
+</style>
diff --git a/src/web/app/common/views/components/othello.vue b/src/web/app/common/views/components/othello.vue
index 326a8d80c..31858fca1 100644
--- a/src/web/app/common/views/components/othello.vue
+++ b/src/web/app/common/views/components/othello.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mk-othello">
 	<div v-if="game">
-		<x-game :game="game"/>
+		<x-gameroom :game="game"/>
 	</div>
 	<div class="matching" v-else-if="matching">
 		<h1><b>{{ matching.name }}</b>を待っています<mk-ellipsis/></h1>
@@ -40,18 +40,18 @@
 		<section v-if="myGames.length > 0">
 			<h2>自分の対局</h2>
 			<div class="game" v-for="g in myGames" tabindex="-1" @click="game = g">
-				<img :src="`${g.black_user.avatar_url}?thumbnail&size=32`" alt="">
-				<img :src="`${g.white_user.avatar_url}?thumbnail&size=32`" alt="">
-				<span><b>{{ g.black_user.name }}</b> vs <b>{{ g.white_user.name }}</b></span>
+				<img :src="`${g.user1.avatar_url}?thumbnail&size=32`" alt="">
+				<img :src="`${g.user2.avatar_url}?thumbnail&size=32`" alt="">
+				<span><b>{{ g.user1.name }}</b> vs <b>{{ g.user2.name }}</b></span>
 				<span class="state">{{ g.is_ended ? '終了' : '進行中' }}</span>
 			</div>
 		</section>
 		<section v-if="games.length > 0">
 			<h2>みんなの対局</h2>
 			<div class="game" v-for="g in games" tabindex="-1" @click="game = g">
-				<img :src="`${g.black_user.avatar_url}?thumbnail&size=32`" alt="">
-				<img :src="`${g.white_user.avatar_url}?thumbnail&size=32`" alt="">
-				<span><b>{{ g.black_user.name }}</b> vs <b>{{ g.white_user.name }}</b></span>
+				<img :src="`${g.user1.avatar_url}?thumbnail&size=32`" alt="">
+				<img :src="`${g.user2.avatar_url}?thumbnail&size=32`" alt="">
+				<span><b>{{ g.user1.name }}</b> vs <b>{{ g.user2.name }}</b></span>
 				<span class="state">{{ g.is_ended ? '終了' : '進行中' }}</span>
 			</div>
 		</section>
@@ -61,11 +61,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import XGame from './othello.game.vue';
+import XGameroom from './othello.gameroom.vue';
 
 export default Vue.extend({
 	components: {
-		XGame
+		XGameroom
 	},
 	data() {
 		return {

From 7e7be8f975ea745a882cc9448981170ba7d4062a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 8 Mar 2018 17:59:32 +0900
Subject: [PATCH 0648/1250] v4016

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 95ee4e18e..1b3b520f0 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4013",
+	"version": "0.0.4016",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 54896096bf8754a4f758b69b01566dcdcadcdd49 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 8 Mar 2018 18:30:28 +0900
Subject: [PATCH 0649/1250] Fix bug

---
 src/api/stream/othello-game.ts                       |  1 +
 src/web/app/common/views/components/othello.game.vue |  4 +++-
 src/web/app/common/views/components/othello.room.vue | 12 ++++++++++++
 3 files changed, 16 insertions(+), 1 deletion(-)

diff --git a/src/api/stream/othello-game.ts b/src/api/stream/othello-game.ts
index 1dcd37efa..2af1bd97a 100644
--- a/src/api/stream/othello-game.ts
+++ b/src/api/stream/othello-game.ts
@@ -96,6 +96,7 @@ export default function(request: websocket.request, connection: websocket.connec
 			setTimeout(async () => {
 				const freshGame = await Game.findOne({ _id: gameId });
 				if (freshGame == null || freshGame.is_started || freshGame.is_ended) return;
+				if (!freshGame.user1_accepted || !freshGame.user2_accepted) return;
 
 				let bw: number;
 				if (freshGame.settings.bw == 'random') {
diff --git a/src/web/app/common/views/components/othello.game.vue b/src/web/app/common/views/components/othello.game.vue
index 2ef6b645c..f0254a0b7 100644
--- a/src/web/app/common/views/components/othello.game.vue
+++ b/src/web/app/common/views/components/othello.game.vue
@@ -12,7 +12,7 @@
 
 	<div class="board" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.size }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map.size }, 1fr)` }">
 		<div v-for="(stone, i) in o.board"
-			:class="{ empty: stone == null, none: o.map.data[i] == ' ', myTurn: isMyTurn, can: turnUser ? o.canPut(turnUser.id == blackUser.id ? 'black' : 'white', i) : null, prev: o.prevPos == i }"
+			:class="{ empty: stone == null, none: o.map.data[i] == ' ', myTurn: !game.is_ended && isMyTurn, can: turnUser ? o.canPut(turnUser.id == blackUser.id ? 'black' : 'white', i) : null, prev: o.prevPos == i }"
 			@click="set(i)"
 		>
 			<img v-if="stone == 'black'" :src="`${blackUser.avatar_url}?thumbnail&size=128`" alt="">
@@ -129,6 +129,8 @@ export default Vue.extend({
 
 	methods: {
 		set(pos) {
+			if (this.game.is_ended) return;
+			if (!this.iAmPlayer) return;
 			if (!this.isMyTurn) return;
 			if (!this.o.canPut(this.myColor, pos)) return;
 
diff --git a/src/web/app/common/views/components/othello.room.vue b/src/web/app/common/views/components/othello.room.vue
index b41d97ec6..445a0b45d 100644
--- a/src/web/app/common/views/components/othello.room.vue
+++ b/src/web/app/common/views/components/othello.room.vue
@@ -21,6 +21,13 @@
 		<mk-switch v-model="game.settings.is_llotheo" @change="onIsLlotheoChange" text="石の少ない方が勝ち(ロセオ)"/>
 	</div>
 
+	<p class="status">
+		<template v-if="isAccepted && isOpAccepted">ゲームは数秒後に開始されます<mk-ellipsis/></template>
+		<template v-if="isAccepted && !isOpAccepted">相手の準備が完了するのを待っています<mk-ellipsis/></template>
+		<template v-if="!isAccepted && isOpAccepted">あなたの準備が完了するのを待っています</template>
+		<template v-if="!isAccepted && !isOpAccepted">準備中<mk-ellipsis/></template>
+	</p>
+
 	<div class="actions">
 		<el-button @click="exit">キャンセル</el-button>
 		<el-button type="primary" @click="accept" v-if="!isAccepted">準備完了</el-button>
@@ -50,6 +57,11 @@ export default Vue.extend({
 			if (this.game.user1_id == (this as any).os.i.id && this.game.user1_accepted) return true;
 			if (this.game.user2_id == (this as any).os.i.id && this.game.user2_accepted) return true;
 			return false;
+		},
+		isOpAccepted(): boolean {
+			if (this.game.user1_id != (this as any).os.i.id && this.game.user1_accepted) return true;
+			if (this.game.user2_id != (this as any).os.i.id && this.game.user2_accepted) return true;
+			return false;
 		}
 	},
 

From 8a3b1d87e6a56268901155dbfb33d6d65142da0d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 8 Mar 2018 18:30:50 +0900
Subject: [PATCH 0650/1250] v4018

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 1b3b520f0..3d6e039ca 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4016",
+	"version": "0.0.4018",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From f7659b59dd689a995ba483db0aea6990cf7b1aba Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 8 Mar 2018 22:11:08 +0900
Subject: [PATCH 0651/1250] :v:

---
 src/common/othello/maps.ts                    | 194 ++++++++++++++----
 .../common/views/components/othello.game.vue  |  11 +-
 .../common/views/components/othello.room.vue  |  47 +++--
 3 files changed, 194 insertions(+), 58 deletions(-)

diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index e6f3f409e..e34e15bb5 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -10,12 +10,14 @@
 
 export type Map = {
 	name?: string;
+	category?: string;
 	size: number;
 	data: string;
 };
 
 export const fourfour: Map = {
 	name: '4x4',
+	category: '4x4',
 	size: 4,
 	data:
 		'----' +
@@ -26,6 +28,7 @@ export const fourfour: Map = {
 
 export const sixsix: Map = {
 	name: '6x6',
+	category: '6x6',
 	size: 6,
 	data:
 		'------' +
@@ -36,8 +39,35 @@ export const sixsix: Map = {
 		'------'
 };
 
+export const roundedSixsix: Map = {
+	name: '6x6 rounded',
+	category: '6x6',
+	size: 6,
+	data:
+		' ---- ' +
+		'------' +
+		'--wb--' +
+		'--bw--' +
+		'------' +
+		' ---- '
+};
+
+export const roundedSixsix2: Map = {
+	name: '6x6 rounded 2',
+	category: '6x6',
+	size: 6,
+	data:
+		'  --  ' +
+		' ---- ' +
+		'--wb--' +
+		'--bw--' +
+		' -----' +
+		'  --  '
+};
+
 export const eighteight: Map = {
 	name: '8x8',
+	category: '8x8',
 	size: 8,
 	data:
 		'--------' +
@@ -52,6 +82,7 @@ export const eighteight: Map = {
 
 export const roundedEighteight: Map = {
 	name: '8x8 rounded',
+	category: '8x8',
 	size: 8,
 	data:
 		' ------ ' +
@@ -66,6 +97,7 @@ export const roundedEighteight: Map = {
 
 export const roundedEighteight2: Map = {
 	name: '8x8 rounded 2',
+	category: '8x8',
 	size: 8,
 	data:
 		'  ----  ' +
@@ -78,8 +110,24 @@ export const roundedEighteight2: Map = {
 		'  ----  '
 };
 
+export const roundedEighteight3: Map = {
+	name: '8x8 rounded 3',
+	category: '8x8',
+	size: 8,
+	data:
+		'   --   ' +
+		'  ----  ' +
+		' ------ ' +
+		'---wb---' +
+		'---bw---' +
+		' ------ ' +
+		'  ----  ' +
+		'   --   '
+};
+
 export const eighteightWithNotch: Map = {
 	name: '8x8 with notch',
+	category: '8x8',
 	size: 8,
 	data:
 		'---  ---' +
@@ -94,6 +142,7 @@ export const eighteightWithNotch: Map = {
 
 export const eighteightWithSomeHoles: Map = {
 	name: '8x8 with some holes',
+	category: '8x8',
 	size: 8,
 	data:
 		'--- ----' +
@@ -106,22 +155,69 @@ export const eighteightWithSomeHoles: Map = {
 		'--------'
 };
 
-export const sixeight: Map = {
-	name: '6x8',
+export const circle: Map = {
+	name: 'Circle',
+	category: '8x8',
+	size: 8,
+	data:
+		'   --   ' +
+		' ------ ' +
+		' ------ ' +
+		'---wb---' +
+		'---bw---' +
+		' ------ ' +
+		' ------ ' +
+		'   --   '
+};
+
+export const dice: Map = {
+	name: 'Dice',
+	category: '8x8',
+	size: 8,
+	data:
+		'--------' +
+		'-  --  -' +
+		'-  --  -' +
+		'---wb---' +
+		'---bw---' +
+		'-  --  -' +
+		'-  --  -' +
+		'--------'
+};
+
+export const face: Map = {
+	name: 'Face',
+	category: '8x8',
 	size: 8,
 	data:
 		' ------ ' +
-		' ------ ' +
-		' ------ ' +
-		' --wb-- ' +
-		' --bw-- ' +
-		' ------ ' +
-		' ------ ' +
+		'--------' +
+		'-- -- --' +
+		'---wb---' +
+		'-- bw --' +
+		'---  ---' +
+		'--------' +
 		' ------ '
 };
 
+export const window: Map = {
+	name: 'Window',
+	category: '8x8',
+	size: 8,
+	data:
+		'--------' +
+		'-  --  -' +
+		'-  --  -' +
+		'---wb---' +
+		'---bw---' +
+		'-  --  -' +
+		'-  --  -' +
+		'--------'
+};
+
 export const tenthtenth: Map = {
 	name: '10x10',
+	category: '10x10',
 	size: 10,
 	data:
 		'----------' +
@@ -137,7 +233,8 @@ export const tenthtenth: Map = {
 };
 
 export const hole: Map = {
-	name: 'hole',
+	name: 'The Hole',
+	category: '10x10',
 	size: 10,
 	data:
 		'----------' +
@@ -152,8 +249,41 @@ export const hole: Map = {
 		'----------'
 };
 
+export const grid: Map = {
+	name: 'Grid',
+	category: '10x10',
+	size: 10,
+	data:
+		'----------' +
+		'- - -- - -' +
+		'----------' +
+		'- - -- - -' +
+		'----wb----' +
+		'----bw----' +
+		'- - -- - -' +
+		'----------' +
+		'- - -- - -' +
+		'----------'
+};
+
+export const sixeight: Map = {
+	name: '6x8',
+	category: 'special',
+	size: 8,
+	data:
+		' ------ ' +
+		' ------ ' +
+		' ------ ' +
+		' --wb-- ' +
+		' --bw-- ' +
+		' ------ ' +
+		' ------ ' +
+		' ------ '
+};
+
 export const spark: Map = {
-	name: 'spark',
+	name: 'Spark',
+	category: 'special',
 	size: 10,
 	data:
 		' -      - ' +
@@ -169,7 +299,8 @@ export const spark: Map = {
 };
 
 export const islands: Map = {
-	name: 'islands',
+	name: 'Islands',
+	category: 'special',
 	size: 10,
 	data:
 		'--------  ' +
@@ -184,34 +315,21 @@ export const islands: Map = {
 		'  --------'
 };
 
-export const grid: Map = {
-	name: 'grid',
-	size: 10,
-	data:
-		'----------' +
-		'- - -- - -' +
-		'----------' +
-		'- - -- - -' +
-		'----wb----' +
-		'----bw----' +
-		'- - -- - -' +
-		'----------' +
-		'- - -- - -' +
-		'----------'
-};
-
 export const iphonex: Map = {
 	name: 'iPhone X',
-	size: 10,
+	category: 'special',
+	size: 12,
 	data:
-		'  --  --  ' +
-		' -------- ' +
-		' -------- ' +
-		' -------- ' +
-		' ---wb--- ' +
-		' ---bw--- ' +
-		' -------- ' +
-		' -------- ' +
-		' -------- ' +
-		'  ------  '
+		'   --  --   ' +
+		'  --------  ' +
+		'  --------  ' +
+		'  --------  ' +
+		'  --------  ' +
+		'  ---wb---  ' +
+		'  ---bw---  ' +
+		'  --------  ' +
+		'  --------  ' +
+		'  --------  ' +
+		'  --------  ' +
+		'   ------   '
 };
diff --git a/src/web/app/common/views/components/othello.game.vue b/src/web/app/common/views/components/othello.game.vue
index f0254a0b7..b50d709d4 100644
--- a/src/web/app/common/views/components/othello.game.vue
+++ b/src/web/app/common/views/components/othello.game.vue
@@ -12,7 +12,7 @@
 
 	<div class="board" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.size }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map.size }, 1fr)` }">
 		<div v-for="(stone, i) in o.board"
-			:class="{ empty: stone == null, none: o.map.data[i] == ' ', myTurn: !game.is_ended && isMyTurn, can: turnUser ? o.canPut(turnUser.id == blackUser.id ? 'black' : 'white', i) : null, prev: o.prevPos == i }"
+			:class="{ empty: stone == null, none: o.map.data[i] == ' ', isEnded: game.is_ended, myTurn: !game.is_ended && isMyTurn, can: turnUser ? o.canPut(turnUser.id == blackUser.id ? 'black' : 'white', i) : null, prev: o.prevPos == i }"
 			@click="set(i)"
 		>
 			<img v-if="stone == 'black'" :src="`${blackUser.avatar_url}?thumbnail&size=128`" alt="">
@@ -200,13 +200,13 @@ export default Vue.extend({
 				user-select none
 
 			&.empty
-				border solid 2px #f5f5f5
+				border solid 2px #eee
 
 			&.empty.can
-				background #f5f5f5
+				background #eee
 
 			&.empty.myTurn
-				border-color #eee
+				border-color #ddd
 
 				&.can
 					background #eee
@@ -222,6 +222,9 @@ export default Vue.extend({
 			&.prev
 				box-shadow 0 0 0 4px rgba($theme-color, 0.7)
 
+			&.isEnded
+				border-color #ddd
+
 			&.none
 				border-color transparent !important
 
diff --git a/src/web/app/common/views/components/othello.room.vue b/src/web/app/common/views/components/othello.room.vue
index 445a0b45d..bcae37b22 100644
--- a/src/web/app/common/views/components/othello.room.vue
+++ b/src/web/app/common/views/components/othello.room.vue
@@ -5,7 +5,9 @@
 	<p>ゲームの設定</p>
 
 	<el-select v-model="mapName" placeholder="マップを選択" @change="onMapChange">
-		<el-option v-for="m in maps" :key="m.name" :label="m.name" :value="m.name"/>
+		<el-option-group v-for="c in mapCategories" :key="c" :label="c">
+			<el-option v-for="m in maps" v-if="m.category == c" :key="m.name" :label="m.name" :value="m.name"/>
+		</el-option-group>
 	</el-select>
 
 	<div class="board" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.size }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map.size }, 1fr)` }">
@@ -21,18 +23,20 @@
 		<mk-switch v-model="game.settings.is_llotheo" @change="onIsLlotheoChange" text="石の少ない方が勝ち(ロセオ)"/>
 	</div>
 
-	<p class="status">
-		<template v-if="isAccepted && isOpAccepted">ゲームは数秒後に開始されます<mk-ellipsis/></template>
-		<template v-if="isAccepted && !isOpAccepted">相手の準備が完了するのを待っています<mk-ellipsis/></template>
-		<template v-if="!isAccepted && isOpAccepted">あなたの準備が完了するのを待っています</template>
-		<template v-if="!isAccepted && !isOpAccepted">準備中<mk-ellipsis/></template>
-	</p>
+	<footer>
+		<p class="status">
+			<template v-if="isAccepted && isOpAccepted">ゲームは数秒後に開始されます<mk-ellipsis/></template>
+			<template v-if="isAccepted && !isOpAccepted">相手の準備が完了するのを待っています<mk-ellipsis/></template>
+			<template v-if="!isAccepted && isOpAccepted">あなたの準備が完了するのを待っています</template>
+			<template v-if="!isAccepted && !isOpAccepted">準備中<mk-ellipsis/></template>
+		</p>
 
-	<div class="actions">
-		<el-button @click="exit">キャンセル</el-button>
-		<el-button type="primary" @click="accept" v-if="!isAccepted">準備完了</el-button>
-		<el-button type="primary" @click="cancel" v-if="isAccepted">準備続行</el-button>
-	</div>
+		<div class="actions">
+			<el-button @click="exit">キャンセル</el-button>
+			<el-button type="primary" @click="accept" v-if="!isAccepted">準備完了</el-button>
+			<el-button type="primary" @click="cancel" v-if="isAccepted">準備続行</el-button>
+		</div>
+	</footer>
 </div>
 </template>
 
@@ -53,6 +57,10 @@ export default Vue.extend({
 	},
 
 	computed: {
+		mapCategories(): string[] {
+			const categories = Object.entries(maps).map(x => x[1].category);
+			return categories.filter((item, pos) => categories.indexOf(item) == pos);
+		},
 		isAccepted(): boolean {
 			if (this.game.user1_id == (this as any).os.i.id && this.game.user1_accepted) return true;
 			if (this.game.user2_id == (this as any).os.i.id && this.game.user2_accepted) return true;
@@ -142,7 +150,7 @@ export default Vue.extend({
 
 		> div
 			background transparent
-			border solid 2px #eee
+			border solid 2px #ddd
 			border-radius 6px
 			overflow hidden
 
@@ -157,8 +165,15 @@ export default Vue.extend({
 
 	> .rules
 		max-width 300px
-		margin 0 auto
+		margin 0 auto 32px auto
 
-	> .actions
-		margin-bottom 16px
+	> footer
+		position sticky
+		bottom 0
+		padding 16px
+		background rgba(255, 255, 255, 0.9)
+		border-top solid 1px #c4cdd4
+
+		> .status
+			margin 0 0 16px 0
 </style>

From 261a04d4b7ee8cc5665be5fe6b2647a8d37e1e72 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 8 Mar 2018 22:11:19 +0900
Subject: [PATCH 0652/1250] v4020

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 3d6e039ca..fea7db40e 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4018",
+	"version": "0.0.4020",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 03090d549407abdcde35411cec4b38014d0d4f84 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 8 Mar 2018 23:57:11 +0900
Subject: [PATCH 0653/1250] Fix bug

---
 src/web/app/common/views/components/othello.game.vue | 9 ---------
 1 file changed, 9 deletions(-)

diff --git a/src/web/app/common/views/components/othello.game.vue b/src/web/app/common/views/components/othello.game.vue
index b50d709d4..ffda68c8d 100644
--- a/src/web/app/common/views/components/othello.game.vue
+++ b/src/web/app/common/views/components/othello.game.vue
@@ -22,15 +22,6 @@
 
 	<p>黒:{{ o.blackCount }} 白:{{ o.whiteCount }} 合計:{{ o.blackCount + o.whiteCount }}</p>
 
-	<div class="graph">
-		<div v-for="n in 61 - o.stats.length">
-		</div>
-		<div v-for="data in o.stats">
-			<div :style="{ height: `${ Math.floor(data.b * 100) }%` }"></div>
-			<div :style="{ height: `${ Math.floor(data.w * 100) }%` }"></div>
-		</div>
-	</div>
-
 	<div class="player" v-if="game.is_ended">
 		<el-button type="primary" @click="logPos = 0" :disabled="logPos == 0">%fa:fast-backward%</el-button>
 		<el-button type="primary" @click="logPos--" :disabled="logPos == 0">%fa:backward%</el-button>

From ea6245229b909e69ba9492bd85c9fad5b0b5374c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 8 Mar 2018 23:57:33 +0900
Subject: [PATCH 0654/1250] oops

---
 src/common/othello/maps.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index e34e15bb5..2f8dc5ff7 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -61,7 +61,7 @@ export const roundedSixsix2: Map = {
 		' ---- ' +
 		'--wb--' +
 		'--bw--' +
-		' -----' +
+		' ---- ' +
 		'  --  '
 };
 

From b513e5bcdf649324e8b75382dc043d39ca207693 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 8 Mar 2018 23:57:44 +0900
Subject: [PATCH 0655/1250] v4023

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index fea7db40e..2201b5ba4 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4020",
+	"version": "0.0.4023",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From bd93958c0e448bf0254b8a3ab5b48eb50c80b20b Mon Sep 17 00:00:00 2001
From: Aya Morisawa <AyaMorisawa4869@gmail.com>
Date: Fri, 9 Mar 2018 00:47:41 +0900
Subject: [PATCH 0656/1250] Add Big board map

---
 src/common/othello/maps.ts | 23 +++++++++++++++++++++++
 1 file changed, 23 insertions(+)

diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index 2f8dc5ff7..02fe4562f 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -333,3 +333,26 @@ export const iphonex: Map = {
 		'  --------  ' +
 		'   ------   '
 };
+
+export const bigBoard: Map = {
+	name: 'Big board',
+	category: 'special',
+	size: 16,
+	data:
+		'----------------' +
+		'----------------' +
+		'----------------' +
+		'----------------' +
+		'----------------' +
+		'----------------' +
+		'----------------' +
+		'-------wb-------' +
+		'-------bw-------' +
+		'----------------' +
+		'----------------' +
+		'----------------' +
+		'----------------' +
+		'----------------' +
+		'----------------' +
+		'----------------' +
+};

From d8f3fb7f3e7ac127be590addb38486f9226fe98e Mon Sep 17 00:00:00 2001
From: Aya Morisawa <AyaMorisawa4869@gmail.com>
Date: Fri, 9 Mar 2018 00:48:50 +0900
Subject: [PATCH 0657/1250] Remove trailing +

---
 src/common/othello/maps.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index 02fe4562f..67fcb8260 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -354,5 +354,5 @@ export const bigBoard: Map = {
 		'----------------' +
 		'----------------' +
 		'----------------' +
-		'----------------' +
+		'----------------'
 };

From 4bcc29de6462e4805f391c081112224d4748744b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Mar 2018 00:50:56 +0900
Subject: [PATCH 0658/1250] v4027

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 2201b5ba4..6d9d033f9 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4023",
+	"version": "0.0.4027",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From e230e05714ef11b217cf399d51e935b5d563edd6 Mon Sep 17 00:00:00 2001
From: Aya Morisawa <AyaMorisawa4869@gmail.com>
Date: Fri, 9 Mar 2018 01:00:21 +0900
Subject: [PATCH 0659/1250] Add Two board map

---
 src/common/othello/maps.ts | 24 ++++++++++++++++++++++++
 1 file changed, 24 insertions(+)

diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index 67fcb8260..1c9ddc867 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -356,3 +356,27 @@ export const bigBoard: Map = {
 		'----------------' +
 		'----------------'
 };
+
+export const twoBoard: Map = {
+	name: 'Two board',
+	category: 'special',
+	size: 17,
+	data:
+		'-------- --------' +
+		'-------- --------' +
+		'-------- --------' +
+		'---wb--- ---wb---' +
+		'---bw--- ---bw---' +
+		'-------- --------' +
+		'-------- --------' +
+		'-------- --------' +
+		'                 ' +
+		'                 ' +
+		'                 ' +
+		'                 ' +
+		'                 ' +
+		'                 ' +
+		'                 ' +
+		'                 ' +
+		'                 '
+};

From 4d071675646e47a552cb34b27bf04c952f4d51bf Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Mar 2018 01:02:45 +0900
Subject: [PATCH 0660/1250] v4030

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 6d9d033f9..4a3c2990a 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4027",
+	"version": "0.0.4030",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 26fd23106f2ea8f109b174827710e62225400b93 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Mar 2018 02:04:55 +0900
Subject: [PATCH 0661/1250] Remove the duplicatd map

---
 src/common/othello/maps.ts | 15 ---------------
 1 file changed, 15 deletions(-)

diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index 1c9ddc867..e587b584c 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -170,21 +170,6 @@ export const circle: Map = {
 		'   --   '
 };
 
-export const dice: Map = {
-	name: 'Dice',
-	category: '8x8',
-	size: 8,
-	data:
-		'--------' +
-		'-  --  -' +
-		'-  --  -' +
-		'---wb---' +
-		'---bw---' +
-		'-  --  -' +
-		'-  --  -' +
-		'--------'
-};
-
 export const face: Map = {
 	name: 'Face',
 	category: '8x8',

From 6dad21a40bbf5ab5cb8842b8131addb4fdb27dab Mon Sep 17 00:00:00 2001
From: Aya Morisawa <AyaMorisawa4869@gmail.com>
Date: Fri, 9 Mar 2018 02:31:01 +0900
Subject: [PATCH 0662/1250] Add cross map

---
 src/common/othello/maps.ts | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index e587b584c..0a9ff477a 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -251,6 +251,23 @@ export const grid: Map = {
 		'----------'
 };
 
+export const cross: Map = {
+	name: 'Cross',
+	category: '10x10',
+	size: 10,
+	data:
+		'   ----   ' +
+		'   ----   ' +
+		'   ----   ' +
+		'----------' +
+		'----wb----' +
+		'----bw----' +
+		'----------' +
+		'   ----   ' +
+		'   ----   ' +
+		'   ----   '
+};
+
 export const sixeight: Map = {
 	name: '6x8',
 	category: 'special',

From 3edd038be0cd0b4c6ebdc1723fe636a7f6acf644 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Mar 2018 02:31:48 +0900
Subject: [PATCH 0663/1250] v4034

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 4a3c2990a..b07c1ae5e 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4030",
+	"version": "0.0.4034",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 8cf9172c29d8e7d0a9b587df25484b4802033e11 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Mar 2018 02:35:35 +0900
Subject: [PATCH 0664/1250] Fix islands map

---
 src/common/othello/maps.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index 0a9ff477a..77c18e10c 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -312,8 +312,8 @@ export const islands: Map = {
 		'  -    -  ' +
 		'  -    -  ' +
 		'  --------' +
-		'  ---bw---' +
-		'  ---wb---' +
+		'  --------' +
+		'  --------' +
 		'  --------'
 };
 

From 04369b89bf743d14306c7e38e6a78682d7778d19 Mon Sep 17 00:00:00 2001
From: Aya Morisawa <AyaMorisawa4869@gmail.com>
Date: Fri, 9 Mar 2018 02:38:54 +0900
Subject: [PATCH 0665/1250] Add reserved map

---
 src/common/othello/maps.ts | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index 77c18e10c..48583c68b 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -200,6 +200,21 @@ export const window: Map = {
 		'--------'
 };
 
+export const reserved: Map = {
+	name: 'Reserved',
+	category: '8x8',
+	size: 8,
+	data:
+		'w------b' +
+		'--------' +
+		'--------' +
+		'---wb---' +
+		'---bw---' +
+		'--------' +
+		'--------' +
+		'b------w'
+};
+
 export const tenthtenth: Map = {
 	name: '10x10',
 	category: '10x10',

From 6d295356b1cc0876bffd092a5baa3b47fe00d353 Mon Sep 17 00:00:00 2001
From: Aya Morisawa <AyaMorisawa4869@gmail.com>
Date: Fri, 9 Mar 2018 02:54:51 +0900
Subject: [PATCH 0666/1250] Add Walls map

---
 src/common/othello/maps.ts | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index 77c18e10c..9d91219ff 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -268,6 +268,23 @@ export const cross: Map = {
 		'   ----   '
 };
 
+export const walls: Map = {
+	name: 'Walls',
+	category: '10x10',
+	size: 10,
+	data:
+		' bbbbbbbb ' +
+		'w--------w' +
+		'w--------w' +
+		'w--------w' +
+		'w---wb---w' +
+		'w---bw---w' +
+		'w--------w' +
+		'w--------w' +
+		'w--------w' +
+		' bbbbbbbb '
+};
+
 export const sixeight: Map = {
 	name: '6x8',
 	category: 'special',

From a2f53247a86cad169eb372c32c5d93d2ee8ae7f1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Mar 2018 02:55:48 +0900
Subject: [PATCH 0667/1250] v4038

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index b07c1ae5e..5bb472f69 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4034",
+	"version": "0.0.4038",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From dae13008fbc0ed0309720fd44f7c5024027ba2bd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Mar 2018 02:57:06 +0900
Subject: [PATCH 0668/1250] v4041

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 5bb472f69..fe65b3130 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4038",
+	"version": "0.0.4041",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 179fb94d573984f63acaa3c300e7d615e2d99ba9 Mon Sep 17 00:00:00 2001
From: Aya Morisawa <AyaMorisawa4869@gmail.com>
Date: Fri, 9 Mar 2018 03:01:55 +0900
Subject: [PATCH 0669/1250] Add X map

---
 src/common/othello/maps.ts | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index 0672fc86b..977762dea 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -215,6 +215,21 @@ export const reserved: Map = {
 		'b------w'
 };
 
+export const x: Map = {
+	name: 'X',
+	category: '8x8',
+	size: 8,
+	data:
+		'w------b' +
+		'-w----b-' +
+		'--w--b--' +
+		'---wb---' +
+		'---bw---' +
+		'--b--w--' +
+		'-b----w-' +
+		'b------w'
+};
+
 export const tenthtenth: Map = {
 	name: '10x10',
 	category: '10x10',

From ea16f1d479220cd207b65f1224d3d17262987c38 Mon Sep 17 00:00:00 2001
From: Aya Morisawa <AyaMorisawa4869@gmail.com>
Date: Fri, 9 Mar 2018 03:10:19 +0900
Subject: [PATCH 0670/1250] Add checker map

---
 src/common/othello/maps.ts | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index 977762dea..434fd75af 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -315,6 +315,23 @@ export const walls: Map = {
 		' bbbbbbbb '
 };
 
+export const checker: Map = {
+	name: 'Checker',
+	category: '10x10',
+	size: 10,
+	data:
+		'----------' +
+		'----------' +
+		'----------' +
+		'---wbwb---' +
+		'---bwbw---' +
+		'---wbwb---' +
+		'---bwbw---' +
+		'----------' +
+		'----------' +
+		'----------'
+};
+
 export const sixeight: Map = {
 	name: '6x8',
 	category: 'special',

From d496c2e3f725efeddea2c42e5f6d2b0d5b898b8d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Mar 2018 03:11:06 +0900
Subject: [PATCH 0671/1250] v4046

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index fe65b3130..217ddcbcc 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4041",
+	"version": "0.0.4046",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From e03b86751cd882429388e4e0c50533fc7a509f57 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Mar 2018 18:11:10 +0900
Subject: [PATCH 0672/1250] =?UTF-8?q?=E3=81=AA=E3=82=93=E3=81=8B=E3=82=82?=
 =?UTF-8?q?=E3=81=86=E3=82=81=E3=81=A3=E3=81=A1=E3=82=83=E5=A4=89=E3=81=88?=
 =?UTF-8?q?=E3=81=9F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/endpoints/othello/games.ts            |  41 +-
 src/api/endpoints/othello/match.ts            |   2 +-
 src/api/models/othello-game.ts                |  16 +-
 src/common/othello/core.ts                    |  33 +-
 src/common/othello/maps.ts                    | 618 ++++++++++--------
 .../common/views/components/othello.game.vue  |   6 +-
 .../common/views/components/othello.room.vue  |  16 +-
 7 files changed, 425 insertions(+), 307 deletions(-)

diff --git a/src/api/endpoints/othello/games.ts b/src/api/endpoints/othello/games.ts
index dd9fb5ef5..dd3ee523a 100644
--- a/src/api/endpoints/othello/games.ts
+++ b/src/api/endpoints/othello/games.ts
@@ -6,6 +6,23 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [my = false, myErr] = $(params.my).optional.boolean().$;
 	if (myErr) return rej('invalid my param');
 
+	// Get 'limit' parameter
+	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
+	if (limitErr) return rej('invalid limit param');
+
+	// Get 'since_id' parameter
+	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
+	if (sinceIdErr) return rej('invalid since_id param');
+
+	// Get 'until_id' parameter
+	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+	if (untilIdErr) return rej('invalid until_id param');
+
+	// Check if both of since_id and until_id is specified
+	if (sinceId && untilId) {
+		return rej('cannot set since_id and until_id');
+	}
+
 	const q = my ? {
 		is_started: true,
 		$or: [{
@@ -17,13 +34,29 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		is_started: true
 	};
 
+
+	const sort = {
+		_id: -1
+	};
+
+	if (sinceId) {
+		sort._id = 1;
+		q._id = {
+			$gt: sinceId
+		};
+	} else if (untilId) {
+		q._id = {
+			$lt: untilId
+		};
+	}
+
 	// Fetch games
 	const games = await Game.find(q, {
-		sort: {
-			_id: -1
-		}
+		sort
 	});
 
 	// Reponse
-	res(Promise.all(games.map(async (g) => await pack(g, user))));
+	res(Promise.all(games.map(async (g) => await pack(g, user, {
+		detail: false
+	}))));
 });
diff --git a/src/api/endpoints/othello/match.ts b/src/api/endpoints/othello/match.ts
index 05b87a541..640be9cb5 100644
--- a/src/api/endpoints/othello/match.ts
+++ b/src/api/endpoints/othello/match.ts
@@ -38,7 +38,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 			is_ended: false,
 			logs: [],
 			settings: {
-				map: eighteight,
+				map: eighteight.data,
 				bw: 'random',
 				is_llotheo: false
 			}
diff --git a/src/api/models/othello-game.ts b/src/api/models/othello-game.ts
index de7c804c4..7ae48b8aa 100644
--- a/src/api/models/othello-game.ts
+++ b/src/api/models/othello-game.ts
@@ -28,7 +28,7 @@ export interface IGame {
 	winner_id: mongo.ObjectID;
 	logs: any[];
 	settings: {
-		map: Map;
+		map: string[];
 		bw: string | number;
 		is_llotheo: boolean;
 	};
@@ -39,8 +39,15 @@ export interface IGame {
  */
 export const pack = (
 	game: any,
-	me?: string | mongo.ObjectID | IUser
+	me?: string | mongo.ObjectID | IUser,
+	options?: {
+		detail?: boolean
+	}
 ) => new Promise<any>(async (resolve, reject) => {
+	const opts = Object.assign({
+		detail: true
+	}, options);
+
 	let _game: any;
 
 	// Populate the game if 'game' is ID
@@ -69,6 +76,11 @@ export const pack = (
 	_game.id = _game._id;
 	delete _game._id;
 
+	if (opts.detail === false) {
+		delete _game.logs;
+		delete _game.settings.map;
+	}
+
 	// Populate user
 	_game.user1 = await packUser(_game.user1_id, meId);
 	_game.user2 = await packUser(_game.user2_id, meId);
diff --git a/src/common/othello/core.ts b/src/common/othello/core.ts
index b76586031..0b4cb1fc1 100644
--- a/src/common/othello/core.ts
+++ b/src/common/othello/core.ts
@@ -1,5 +1,3 @@
-import { Map } from './maps';
-
 export type Color = 'black' | 'white';
 export type MapPixel = 'null' | 'empty';
 
@@ -11,12 +9,14 @@ export type Options = {
  * オセロエンジン
  */
 export default class Othello {
-	public map: Map;
-	public mapData: MapPixel[];
+	public map: MapPixel[];
+	public mapWidth: number;
+	public mapHeight: number;
 	public board: Color[];
 	public turn: Color = 'black';
 	public opts: Options;
 
+	public prevPos = -1;
 	public stats: Array<{
 		b: number;
 		w: number;
@@ -25,18 +25,21 @@ export default class Othello {
 	/**
 	 * ゲームを初期化します
 	 */
-	constructor(map: Map, opts: Options) {
-		this.map = map;
+	constructor(map: string[], opts: Options) {
 		this.opts = opts;
 
+		this.mapWidth = map[0].length;
+		this.mapHeight = map.length;
+		const mapData = map.join('');
+
 		// Parse map data
-		this.board = this.map.data.split('').map(d => {
+		this.board = mapData.split('').map(d => {
 			if (d == '-') return null;
 			if (d == 'b') return 'black';
 			if (d == 'w') return 'white';
 			return undefined;
 		});
-		this.mapData = this.map.data.split('').map(d => {
+		this.map = mapData.split('').map(d => {
 			if (d == '-' || d == 'b' || d == 'w') return 'empty';
 			return 'null';
 		});
@@ -48,8 +51,6 @@ export default class Othello {
 		}];
 	}
 
-	public prevPos = -1;
-
 	/**
 	 * 黒石の数
 	 */
@@ -79,13 +80,13 @@ export default class Othello {
 	}
 
 	public transformPosToXy(pos: number): number[] {
-		const x = pos % this.map.size;
-		const y = Math.floor(pos / this.map.size);
+		const x = pos % this.mapWidth;
+		const y = Math.floor(pos / this.mapHeight);
 		return [x, y];
 	}
 
 	public transformXyToPos(x: number, y: number): number {
-		return x + (y * this.map.size);
+		return x + (y * this.mapHeight);
 	}
 
 	/**
@@ -145,8 +146,8 @@ export default class Othello {
 	 * @param pos 位置
 	 */
 	public mapDataGet(pos: number): MapPixel {
-		if (pos < 0 || pos >= this.mapData.length) return 'null';
-		return this.mapData[pos];
+		if (pos < 0 || pos >= this.map.length) return 'null';
+		return this.map[pos];
 	}
 
 	/**
@@ -188,7 +189,7 @@ export default class Othello {
 			const found = [];
 			while (true) {
 				const [x, y] = fn(i);
-				if (x < 0 || y < 0 || x >= this.map.size || y >= this.map.size) break;
+				if (x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight) break;
 				const pos = this.transformXyToPos(x, y);
 				const pixel = this.mapDataGet(pos);
 				if (pixel == 'null') break;
diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index 434fd75af..d8f3154a0 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -11,438 +11,502 @@
 export type Map = {
 	name?: string;
 	category?: string;
-	size: number;
-	data: string;
+	author?: string;
+	data: string[];
 };
 
 export const fourfour: Map = {
 	name: '4x4',
 	category: '4x4',
-	size: 4,
-	data:
-		'----' +
-		'-wb-' +
-		'-bw-' +
+	data: [
+		'----',
+		'-wb-',
+		'-bw-',
 		'----'
+	]
 };
 
 export const sixsix: Map = {
 	name: '6x6',
 	category: '6x6',
-	size: 6,
-	data:
-		'------' +
-		'------' +
-		'--wb--' +
-		'--bw--' +
-		'------' +
+	data: [
+		'------',
+		'------',
+		'--wb--',
+		'--bw--',
+		'------',
 		'------'
+	]
 };
 
 export const roundedSixsix: Map = {
 	name: '6x6 rounded',
 	category: '6x6',
-	size: 6,
-	data:
-		' ---- ' +
-		'------' +
-		'--wb--' +
-		'--bw--' +
-		'------' +
+	author: 'syuilo',
+	data: [
+		' ---- ',
+		'------',
+		'--wb--',
+		'--bw--',
+		'------',
 		' ---- '
+	]
 };
 
 export const roundedSixsix2: Map = {
 	name: '6x6 rounded 2',
 	category: '6x6',
-	size: 6,
-	data:
-		'  --  ' +
-		' ---- ' +
-		'--wb--' +
-		'--bw--' +
-		' ---- ' +
+	author: 'syuilo',
+	data: [
+		'  --  ',
+		' ---- ',
+		'--wb--',
+		'--bw--',
+		' ---- ',
 		'  --  '
+	]
 };
 
 export const eighteight: Map = {
 	name: '8x8',
 	category: '8x8',
-	size: 8,
-	data:
-		'--------' +
-		'--------' +
-		'--------' +
-		'---wb---' +
-		'---bw---' +
-		'--------' +
-		'--------' +
+	data: [
+		'--------',
+		'--------',
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------',
+		'--------',
 		'--------'
+	]
 };
 
 export const roundedEighteight: Map = {
 	name: '8x8 rounded',
 	category: '8x8',
-	size: 8,
-	data:
-		' ------ ' +
-		'--------' +
-		'--------' +
-		'---wb---' +
-		'---bw---' +
-		'--------' +
-		'--------' +
+	author: 'syuilo',
+	data: [
+		' ------ ',
+		'--------',
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------',
+		'--------',
 		' ------ '
+	]
 };
 
 export const roundedEighteight2: Map = {
 	name: '8x8 rounded 2',
 	category: '8x8',
-	size: 8,
-	data:
-		'  ----  ' +
-		' ------ ' +
-		'--------' +
-		'---wb---' +
-		'---bw---' +
-		'--------' +
-		' ------ ' +
+	author: 'syuilo',
+	data: [
+		'  ----  ',
+		' ------ ',
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------',
+		' ------ ',
 		'  ----  '
+	]
 };
 
 export const roundedEighteight3: Map = {
 	name: '8x8 rounded 3',
 	category: '8x8',
-	size: 8,
-	data:
-		'   --   ' +
-		'  ----  ' +
-		' ------ ' +
-		'---wb---' +
-		'---bw---' +
-		' ------ ' +
-		'  ----  ' +
+	author: 'syuilo',
+	data: [
+		'   --   ',
+		'  ----  ',
+		' ------ ',
+		'---wb---',
+		'---bw---',
+		' ------ ',
+		'  ----  ',
 		'   --   '
+	]
 };
 
 export const eighteightWithNotch: Map = {
 	name: '8x8 with notch',
 	category: '8x8',
-	size: 8,
-	data:
-		'---  ---' +
-		'--------' +
-		'--------' +
-		' --wb-- ' +
-		' --bw-- ' +
-		'--------' +
-		'--------' +
+	author: 'syuilo',
+	data: [
+		'---  ---',
+		'--------',
+		'--------',
+		' --wb-- ',
+		' --bw-- ',
+		'--------',
+		'--------',
 		'---  ---'
+	]
 };
 
 export const eighteightWithSomeHoles: Map = {
 	name: '8x8 with some holes',
 	category: '8x8',
-	size: 8,
-	data:
-		'--- ----' +
-		'----- --' +
-		'-- -----' +
-		'---wb---' +
-		'---bw- -' +
-		' -------' +
-		'--- ----' +
+	author: 'syuilo',
+	data: [
+		'--- ----',
+		'----- --',
+		'-- -----',
+		'---wb---',
+		'---bw- -',
+		' -------',
+		'--- ----',
 		'--------'
+	]
 };
 
 export const circle: Map = {
 	name: 'Circle',
 	category: '8x8',
-	size: 8,
-	data:
-		'   --   ' +
-		' ------ ' +
-		' ------ ' +
-		'---wb---' +
-		'---bw---' +
-		' ------ ' +
-		' ------ ' +
+	author: 'syuilo',
+	data: [
+		'   --   ',
+		' ------ ',
+		' ------ ',
+		'---wb---',
+		'---bw---',
+		' ------ ',
+		' ------ ',
 		'   --   '
+	]
 };
 
-export const face: Map = {
-	name: 'Face',
+export const smile: Map = {
+	name: 'Smile',
 	category: '8x8',
-	size: 8,
-	data:
-		' ------ ' +
-		'--------' +
-		'-- -- --' +
-		'---wb---' +
-		'-- bw --' +
-		'---  ---' +
-		'--------' +
+	author: 'syuilo',
+	data: [
+		' ------ ',
+		'--------',
+		'-- -- --',
+		'---wb---',
+		'-- bw --',
+		'---  ---',
+		'--------',
 		' ------ '
+	]
 };
 
 export const window: Map = {
 	name: 'Window',
 	category: '8x8',
-	size: 8,
-	data:
-		'--------' +
-		'-  --  -' +
-		'-  --  -' +
-		'---wb---' +
-		'---bw---' +
-		'-  --  -' +
-		'-  --  -' +
+	author: 'syuilo',
+	data: [
+		'--------',
+		'-  --  -',
+		'-  --  -',
+		'---wb---',
+		'---bw---',
+		'-  --  -',
+		'-  --  -',
 		'--------'
+	]
 };
 
 export const reserved: Map = {
 	name: 'Reserved',
 	category: '8x8',
-	size: 8,
-	data:
-		'w------b' +
-		'--------' +
-		'--------' +
-		'---wb---' +
-		'---bw---' +
-		'--------' +
-		'--------' +
+	author: 'Aya',
+	data: [
+		'w------b',
+		'--------',
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------',
+		'--------',
 		'b------w'
+	]
 };
 
 export const x: Map = {
 	name: 'X',
 	category: '8x8',
-	size: 8,
-	data:
-		'w------b' +
-		'-w----b-' +
-		'--w--b--' +
-		'---wb---' +
-		'---bw---' +
-		'--b--w--' +
-		'-b----w-' +
+	author: 'Aya',
+	data: [
+		'w------b',
+		'-w----b-',
+		'--w--b--',
+		'---wb---',
+		'---bw---',
+		'--b--w--',
+		'-b----w-',
 		'b------w'
+	]
+};
+
+export const minesweeper: Map = {
+	name: 'Minesweeper',
+	category: '8x8',
+	author: 'syuilo',
+	data: [
+		'b-b--w-w',
+		'-w-wb-b-',
+		'w-b--w-b',
+		'-b-wb-w-',
+		'-w-bw-b-',
+		'b-w--b-w',
+		'-b-bw-w-',
+		'w-w--b-b'
+	]
 };
 
 export const tenthtenth: Map = {
 	name: '10x10',
 	category: '10x10',
-	size: 10,
-	data:
-		'----------' +
-		'----------' +
-		'----------' +
-		'----------' +
-		'----wb----' +
-		'----bw----' +
-		'----------' +
-		'----------' +
-		'----------' +
+	data: [
+		'----------',
+		'----------',
+		'----------',
+		'----------',
+		'----wb----',
+		'----bw----',
+		'----------',
+		'----------',
+		'----------',
 		'----------'
+	]
 };
 
 export const hole: Map = {
 	name: 'The Hole',
 	category: '10x10',
-	size: 10,
-	data:
-		'----------' +
-		'----------' +
-		'--wb--wb--' +
-		'--bw--bw--' +
-		'----  ----' +
-		'----  ----' +
-		'--wb--wb--' +
-		'--bw--bw--' +
-		'----------' +
+	author: 'syuilo',
+	data: [
+		'----------',
+		'----------',
+		'--wb--wb--',
+		'--bw--bw--',
+		'----  ----',
+		'----  ----',
+		'--wb--wb--',
+		'--bw--bw--',
+		'----------',
 		'----------'
+	]
 };
 
 export const grid: Map = {
 	name: 'Grid',
 	category: '10x10',
-	size: 10,
-	data:
-		'----------' +
-		'- - -- - -' +
-		'----------' +
-		'- - -- - -' +
-		'----wb----' +
-		'----bw----' +
-		'- - -- - -' +
-		'----------' +
-		'- - -- - -' +
+	author: 'syuilo',
+	data: [
+		'----------',
+		'- - -- - -',
+		'----------',
+		'- - -- - -',
+		'----wb----',
+		'----bw----',
+		'- - -- - -',
+		'----------',
+		'- - -- - -',
 		'----------'
+	]
 };
 
 export const cross: Map = {
 	name: 'Cross',
 	category: '10x10',
-	size: 10,
-	data:
-		'   ----   ' +
-		'   ----   ' +
-		'   ----   ' +
-		'----------' +
-		'----wb----' +
-		'----bw----' +
-		'----------' +
-		'   ----   ' +
-		'   ----   ' +
+	author: 'Aya',
+	data: [
+		'   ----   ',
+		'   ----   ',
+		'   ----   ',
+		'----------',
+		'----wb----',
+		'----bw----',
+		'----------',
+		'   ----   ',
+		'   ----   ',
 		'   ----   '
+	]
+};
+
+export const charX: Map = {
+	name: 'Char X',
+	category: '10x10',
+	author: 'syuilo',
+	data: [
+		'---    ---',
+		'----  ----',
+		'----------',
+		' -------- ',
+		'  --wb--  ',
+		'  --bw--  ',
+		' -------- ',
+		'----------',
+		'----  ----',
+		'---    ---'
+	]
+};
+
+export const charY: Map = {
+	name: 'Char Y',
+	category: '10x10',
+	author: 'syuilo',
+	data: [
+		'---    ---',
+		'----  ----',
+		'----------',
+		' -------- ',
+		'  --wb--  ',
+		'  --bw--  ',
+		'  ------  ',
+		'  ------  ',
+		'  ------  ',
+		'  ------  '
+	]
 };
 
 export const walls: Map = {
 	name: 'Walls',
 	category: '10x10',
-	size: 10,
-	data:
-		' bbbbbbbb ' +
-		'w--------w' +
-		'w--------w' +
-		'w--------w' +
-		'w---wb---w' +
-		'w---bw---w' +
-		'w--------w' +
-		'w--------w' +
-		'w--------w' +
+	author: 'Aya',
+	data: [
+		' bbbbbbbb ',
+		'w--------w',
+		'w--------w',
+		'w--------w',
+		'w---wb---w',
+		'w---bw---w',
+		'w--------w',
+		'w--------w',
+		'w--------w',
 		' bbbbbbbb '
+	]
 };
 
 export const checker: Map = {
 	name: 'Checker',
 	category: '10x10',
-	size: 10,
-	data:
-		'----------' +
-		'----------' +
-		'----------' +
-		'---wbwb---' +
-		'---bwbw---' +
-		'---wbwb---' +
-		'---bwbw---' +
-		'----------' +
-		'----------' +
+	author: 'Aya',
+	data: [
+		'----------',
+		'----------',
+		'----------',
+		'---wbwb---',
+		'---bwbw---',
+		'---wbwb---',
+		'---bwbw---',
+		'----------',
+		'----------',
 		'----------'
+	]
 };
 
 export const sixeight: Map = {
 	name: '6x8',
 	category: 'special',
-	size: 8,
-	data:
-		' ------ ' +
-		' ------ ' +
-		' ------ ' +
-		' --wb-- ' +
-		' --bw-- ' +
-		' ------ ' +
-		' ------ ' +
-		' ------ '
+	data: [
+		'------',
+		'------',
+		'------',
+		'--wb--',
+		'--bw--',
+		'------',
+		'------',
+		'------'
+	]
 };
 
 export const spark: Map = {
 	name: 'Spark',
 	category: 'special',
-	size: 10,
-	data:
-		' -      - ' +
-		'----------' +
-		' -------- ' +
-		' -------- ' +
-		' ---wb--- ' +
-		' ---bw--- ' +
-		' -------- ' +
-		' -------- ' +
-		'----------' +
+	author: 'syuilo',
+	data: [
+		' -      - ',
+		'----------',
+		' -------- ',
+		' -------- ',
+		' ---wb--- ',
+		' ---bw--- ',
+		' -------- ',
+		' -------- ',
+		'----------',
 		' -      - '
+	]
 };
 
 export const islands: Map = {
 	name: 'Islands',
 	category: 'special',
-	size: 10,
-	data:
-		'--------  ' +
-		'---wb---  ' +
-		'---bw---  ' +
-		'--------  ' +
-		'  -    -  ' +
-		'  -    -  ' +
-		'  --------' +
-		'  --------' +
-		'  --------' +
+	author: 'syuilo',
+	data: [
+		'--------  ',
+		'---wb---  ',
+		'---bw---  ',
+		'--------  ',
+		'  -    -  ',
+		'  -    -  ',
+		'  --------',
+		'  --------',
+		'  --------',
 		'  --------'
+	]
 };
 
 export const iphonex: Map = {
 	name: 'iPhone X',
 	category: 'special',
-	size: 12,
-	data:
-		'   --  --   ' +
-		'  --------  ' +
-		'  --------  ' +
-		'  --------  ' +
-		'  --------  ' +
-		'  ---wb---  ' +
-		'  ---bw---  ' +
-		'  --------  ' +
-		'  --------  ' +
-		'  --------  ' +
-		'  --------  ' +
-		'   ------   '
+	author: 'syuilo',
+	data: [
+		' --  -- ',
+		'--------',
+		'--------',
+		'--------',
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------',
+		'--------',
+		'--------',
+		'--------',
+		' ------ '
+	]
 };
 
 export const bigBoard: Map = {
 	name: 'Big board',
 	category: 'special',
-	size: 16,
-	data:
-		'----------------' +
-		'----------------' +
-		'----------------' +
-		'----------------' +
-		'----------------' +
-		'----------------' +
-		'----------------' +
-		'-------wb-------' +
-		'-------bw-------' +
-		'----------------' +
-		'----------------' +
-		'----------------' +
-		'----------------' +
-		'----------------' +
-		'----------------' +
+	data: [
+		'----------------',
+		'----------------',
+		'----------------',
+		'----------------',
+		'----------------',
+		'----------------',
+		'----------------',
+		'-------wb-------',
+		'-------bw-------',
+		'----------------',
+		'----------------',
+		'----------------',
+		'----------------',
+		'----------------',
+		'----------------',
 		'----------------'
+	]
 };
 
 export const twoBoard: Map = {
 	name: 'Two board',
 	category: 'special',
-	size: 17,
-	data:
-		'-------- --------' +
-		'-------- --------' +
-		'-------- --------' +
-		'---wb--- ---wb---' +
-		'---bw--- ---bw---' +
-		'-------- --------' +
-		'-------- --------' +
-		'-------- --------' +
-		'                 ' +
-		'                 ' +
-		'                 ' +
-		'                 ' +
-		'                 ' +
-		'                 ' +
-		'                 ' +
-		'                 ' +
-		'                 '
+	author: 'Aya',
+	data: [
+		'-------- --------',
+		'-------- --------',
+		'-------- --------',
+		'---wb--- ---wb---',
+		'---bw--- ---bw---',
+		'-------- --------',
+		'-------- --------',
+		'-------- --------'
+	]
 };
diff --git a/src/web/app/common/views/components/othello.game.vue b/src/web/app/common/views/components/othello.game.vue
index ffda68c8d..248528ed4 100644
--- a/src/web/app/common/views/components/othello.game.vue
+++ b/src/web/app/common/views/components/othello.game.vue
@@ -10,9 +10,9 @@
 		<template v-else>引き分け</template>
 	</p>
 
-	<div class="board" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.size }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map.size }, 1fr)` }">
+	<div class="board" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map[0].length }, 1fr)` }">
 		<div v-for="(stone, i) in o.board"
-			:class="{ empty: stone == null, none: o.map.data[i] == ' ', isEnded: game.is_ended, myTurn: !game.is_ended && isMyTurn, can: turnUser ? o.canPut(turnUser.id == blackUser.id ? 'black' : 'white', i) : null, prev: o.prevPos == i }"
+			:class="{ empty: stone == null, none: o.map[i] == 'null', isEnded: game.is_ended, myTurn: !game.is_ended && isMyTurn, can: turnUser ? o.canPut(turnUser.id == blackUser.id ? 'black' : 'white', i) : null, prev: o.prevPos == i }"
 			@click="set(i)"
 		>
 			<img v-if="stone == 'black'" :src="`${blackUser.avatar_url}?thumbnail&size=128`" alt="">
@@ -106,6 +106,8 @@ export default Vue.extend({
 			this.o.put(log.color, log.pos);
 		});
 
+		console.log(this.o);
+
 		this.logs = this.game.logs;
 		this.logPos = this.logs.length;
 	},
diff --git a/src/web/app/common/views/components/othello.room.vue b/src/web/app/common/views/components/othello.room.vue
index bcae37b22..0fcca8548 100644
--- a/src/web/app/common/views/components/othello.room.vue
+++ b/src/web/app/common/views/components/othello.room.vue
@@ -4,14 +4,17 @@
 
 	<p>ゲームの設定</p>
 
-	<el-select v-model="mapName" placeholder="マップを選択" @change="onMapChange">
+	<el-select class="map" v-model="mapName" placeholder="マップを選択" @change="onMapChange">
 		<el-option-group v-for="c in mapCategories" :key="c" :label="c">
-			<el-option v-for="m in maps" v-if="m.category == c" :key="m.name" :label="m.name" :value="m.name"/>
+			<el-option v-for="m in maps" v-if="m.category == c" :key="m.name" :label="m.name" :value="m.name">
+				<span style="float: left">{{ m.name }}</span>
+				<span style="float: right; color: #8492a6; font-size: 13px" v-if="m.author">(by <i>{{ m.author }}</i>)</span>
+			</el-option>
 		</el-option-group>
 	</el-select>
 
-	<div class="board" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.size }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map.size }, 1fr)` }">
-		<div v-for="(x, i) in game.settings.map.data"
+	<div class="board" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map[0].length }, 1fr)` }">
+		<div v-for="(x, i) in game.settings.map.join('')"
 			:class="{ none: x == ' ' }"
 		>
 			<template v-if="x == 'b'">%fa:circle%</template>
@@ -112,7 +115,7 @@ export default Vue.extend({
 		},
 
 		onMapChange(v) {
-			this.game.settings.map = Object.entries(maps).find(x => x[1].name == v)[1];
+			this.game.settings.map = Object.entries(maps).find(x => x[1].name == v)[1].data;
 			this.connection.send({
 				type: 'update-settings',
 				settings: this.game.settings
@@ -141,6 +144,9 @@ export default Vue.extend({
 		padding 8px
 		border-bottom dashed 1px #c4cdd4
 
+	> .map
+		width 300px
+
 	> .board
 		display grid
 		grid-gap 4px

From e983d8534a1c1e650567e26e9fdb6ac8394e3292 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Mar 2018 18:29:27 +0900
Subject: [PATCH 0673/1250] :v:

---
 src/api/endpoints.ts                             |  5 +++++
 src/api/endpoints/othello/games.ts               |  6 +++---
 src/api/endpoints/othello/games/show.ts          | 16 ++++++++++++++++
 src/api/models/othello-game.ts                   |  6 +++++-
 .../app/common/views/components/othello.game.vue |  2 --
 src/web/app/common/views/components/othello.vue  | 11 +++++++++--
 .../desktop/views/components/input-dialog.vue    |  1 -
 7 files changed, 38 insertions(+), 9 deletions(-)
 create mode 100644 src/api/endpoints/othello/games/show.ts

diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts
index fad666711..1fc9465cd 100644
--- a/src/api/endpoints.ts
+++ b/src/api/endpoints.ts
@@ -253,6 +253,11 @@ const endpoints: Endpoint[] = [
 		withCredential: true
 	},
 
+	{
+		name: 'othello/games/show',
+		withCredential: true
+	},
+
 	{
 		name: 'mute/create',
 		withCredential: true,
diff --git a/src/api/endpoints/othello/games.ts b/src/api/endpoints/othello/games.ts
index dd3ee523a..2a6bbb404 100644
--- a/src/api/endpoints/othello/games.ts
+++ b/src/api/endpoints/othello/games.ts
@@ -23,7 +23,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		return rej('cannot set since_id and until_id');
 	}
 
-	const q = my ? {
+	const q: any = my ? {
 		is_started: true,
 		$or: [{
 			user1_id: user._id
@@ -34,7 +34,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		is_started: true
 	};
 
-
 	const sort = {
 		_id: -1
 	};
@@ -52,7 +51,8 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Fetch games
 	const games = await Game.find(q, {
-		sort
+		sort,
+		limit
 	});
 
 	// Reponse
diff --git a/src/api/endpoints/othello/games/show.ts b/src/api/endpoints/othello/games/show.ts
new file mode 100644
index 000000000..9dc8f2490
--- /dev/null
+++ b/src/api/endpoints/othello/games/show.ts
@@ -0,0 +1,16 @@
+import $ from 'cafy';
+import Game, { pack } from '../../../models/othello-game';
+
+module.exports = (params, user) => new Promise(async (res, rej) => {
+	// Get 'game_id' parameter
+	const [gameId, gameIdErr] = $(params.game_id).id().$;
+	if (gameIdErr) return rej('invalid game_id param');
+
+	const game = await Game.findOne({ _id: gameId });
+
+	if (game == null) {
+		return rej('game not found');
+	}
+
+	res(await pack(game, user));
+});
diff --git a/src/api/models/othello-game.ts b/src/api/models/othello-game.ts
index 7ae48b8aa..82c004210 100644
--- a/src/api/models/othello-game.ts
+++ b/src/api/models/othello-game.ts
@@ -2,7 +2,6 @@ import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
 import db from '../../db/mongodb';
 import { IUser, pack as packUser } from './user';
-import { Map } from '../../common/othello/maps';
 
 const Game = db.get<IGame>('othello_games');
 export default Game;
@@ -79,6 +78,11 @@ export const pack = (
 	if (opts.detail === false) {
 		delete _game.logs;
 		delete _game.settings.map;
+	} else {
+		// 互換性のため
+		if (_game.settings.map.hasOwnProperty('size')) {
+			_game.settings.map = _game.settings.map.data.match(new RegExp(`.{1,${_game.settings.map.size}}`, 'g'));
+		}
 	}
 
 	// Populate user
diff --git a/src/web/app/common/views/components/othello.game.vue b/src/web/app/common/views/components/othello.game.vue
index 248528ed4..6daa7a810 100644
--- a/src/web/app/common/views/components/othello.game.vue
+++ b/src/web/app/common/views/components/othello.game.vue
@@ -106,8 +106,6 @@ export default Vue.extend({
 			this.o.put(log.color, log.pos);
 		});
 
-		console.log(this.o);
-
 		this.logs = this.game.logs;
 		this.logPos = this.logs.length;
 	},
diff --git a/src/web/app/common/views/components/othello.vue b/src/web/app/common/views/components/othello.vue
index 31858fca1..d4157eb76 100644
--- a/src/web/app/common/views/components/othello.vue
+++ b/src/web/app/common/views/components/othello.vue
@@ -39,7 +39,7 @@
 		</section>
 		<section v-if="myGames.length > 0">
 			<h2>自分の対局</h2>
-			<div class="game" v-for="g in myGames" tabindex="-1" @click="game = g">
+			<div class="game" v-for="g in myGames" tabindex="-1" @click="go(g)">
 				<img :src="`${g.user1.avatar_url}?thumbnail&size=32`" alt="">
 				<img :src="`${g.user2.avatar_url}?thumbnail&size=32`" alt="">
 				<span><b>{{ g.user1.name }}</b> vs <b>{{ g.user2.name }}</b></span>
@@ -48,7 +48,7 @@
 		</section>
 		<section v-if="games.length > 0">
 			<h2>みんなの対局</h2>
-			<div class="game" v-for="g in games" tabindex="-1" @click="game = g">
+			<div class="game" v-for="g in games" tabindex="-1" @click="go(g)">
 				<img :src="`${g.user1.avatar_url}?thumbnail&size=32`" alt="">
 				<img :src="`${g.user2.avatar_url}?thumbnail&size=32`" alt="">
 				<span><b>{{ g.user1.name }}</b> vs <b>{{ g.user2.name }}</b></span>
@@ -108,6 +108,13 @@ export default Vue.extend({
 		(this as any).os.streams.othelloStream.dispose(this.connectionId);
 	},
 	methods: {
+		go(game) {
+			(this as any).api('othello/games/show', {
+				game_id: game.id
+			}).then(game => {
+				this.game = game;
+			});
+		},
 		match() {
 			(this as any).apis.input({
 				title: 'ユーザー名を入力してください'
diff --git a/src/web/app/desktop/views/components/input-dialog.vue b/src/web/app/desktop/views/components/input-dialog.vue
index e27bc8da8..e939fc190 100644
--- a/src/web/app/desktop/views/components/input-dialog.vue
+++ b/src/web/app/desktop/views/components/input-dialog.vue
@@ -43,7 +43,6 @@ export default Vue.extend({
 	mounted() {
 		if (this.default) this.text = this.default;
 		this.$nextTick(() => {
-			console.log(this);
 			(this.$refs.text as any).focus();
 		});
 	},

From a01e292791b12bd746c234d9c48afd0f8949d470 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Mar 2018 18:29:58 +0900
Subject: [PATCH 0674/1250] v4049

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 217ddcbcc..9b19eb8ba 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4046",
+	"version": "0.0.4049",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From e469eb02d908bdd6129080eaa5b77204aecca6d5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Mar 2018 18:46:51 +0900
Subject: [PATCH 0675/1250] Fix bug

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

diff --git a/src/web/app/common/views/components/othello.room.vue b/src/web/app/common/views/components/othello.room.vue
index 0fcca8548..60fdb32a4 100644
--- a/src/web/app/common/views/components/othello.room.vue
+++ b/src/web/app/common/views/components/othello.room.vue
@@ -111,7 +111,7 @@ export default Vue.extend({
 
 		onUpdateSettings(settings) {
 			this.game.settings = settings;
-			this.mapName = this.game.settings.map.name;
+			this.mapName = Object.entries(maps).find(x => x[1].data.join('') == this.game.settings.map.join(''))[1].name;
 		},
 
 		onMapChange(v) {

From 1109b7bbaf759a25c95783b2915f17503d76f3b5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Mar 2018 18:47:03 +0900
Subject: [PATCH 0676/1250] v4051

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 9b19eb8ba..5a0ee571f 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4049",
+	"version": "0.0.4051",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 413489cb3d8e56be6ce51733b083dd554350735c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Mar 2018 19:10:11 +0900
Subject: [PATCH 0677/1250] Add new othello map

---
 src/common/othello/maps.ts | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index d8f3154a0..f25342f36 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -242,6 +242,22 @@ export const x: Map = {
 	]
 };
 
+export const squareParty: Map = {
+	name: 'Square Party',
+	category: '8x8',
+	author: 'syuilo',
+	data: [
+		'--------',
+		'-wwwbbb-',
+		'-w-wb-b-',
+		'-wwwbbb-',
+		'-bbbwww-',
+		'-b-bw-w-',
+		'-bbbwww-',
+		'--------'
+	]
+};
+
 export const minesweeper: Map = {
 	name: 'Minesweeper',
 	category: '8x8',

From 77f854b8efca93b58a314fa794e657ca0e5c84c9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Mar 2018 19:10:19 +0900
Subject: [PATCH 0678/1250] v4053

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 5a0ee571f..7f5b18931 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4051",
+	"version": "0.0.4053",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From f340e697f360142420d93193aed9e5bf6df04ff1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Mar 2018 21:51:00 +0900
Subject: [PATCH 0679/1250] Add galaxy map

---
 src/common/othello/maps.ts | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index f25342f36..30d757c54 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -468,6 +468,26 @@ export const islands: Map = {
 	]
 };
 
+export const galaxy: Map = {
+	name: 'Galaxy',
+	category: 'special',
+	author: 'syuilo',
+	data: [
+		'   ------   ',
+		'  --www---  ',
+		' ------w--- ',
+		'---bbb--w---',
+		'--b---b-w-b-',
+		'-b--wwb-w-b-',
+		'-b-w-bww--b-',
+		'-b-w-b---b--',
+		'---w--bbb---',
+		' ---w------ ',
+		'  ---www--  ',
+		'   ------   '
+	]
+};
+
 export const iphonex: Map = {
 	name: 'iPhone X',
 	category: 'special',

From 4f8f5a8ee6649f490617b0f2d896e758fd56bded Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Mar 2018 21:54:47 +0900
Subject: [PATCH 0680/1250] Add triangle map

---
 src/common/othello/maps.ts | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index 30d757c54..181a3b087 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -488,6 +488,24 @@ export const galaxy: Map = {
 	]
 };
 
+export const triangle: Map = {
+	name: 'Triangle',
+	category: 'special',
+	author: 'syuilo',
+	data: [
+		'    --    ',
+		'    --    ',
+		'   ----   ',
+		'   ----   ',
+		'  --wb--  ',
+		'  --bw--  ',
+		' -------- ',
+		' -------- ',
+		'----------',
+		'----------'
+	]
+};
+
 export const iphonex: Map = {
 	name: 'iPhone X',
 	category: 'special',

From c8bacab417b1e38688b23c06e66c7e360d4dc9ea Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Mar 2018 22:05:00 +0900
Subject: [PATCH 0681/1250] Add reactor map

---
 src/common/othello/maps.ts | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index 181a3b087..e4f898fd7 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -417,6 +417,24 @@ export const checker: Map = {
 	]
 };
 
+export const reactor: Map = {
+	name: 'Reactor',
+	category: '10x10',
+	author: 'syuilo',
+	data: [
+		'-w------b-',
+		'b- -  - -w',
+		'- --wb-- -',
+		'---b  w---',
+		'- b wb w -',
+		'- w bw b -',
+		'---w  b---',
+		'- --bw-- -',
+		'w- -  - -b',
+		'-b------w-'
+	]
+};
+
 export const sixeight: Map = {
 	name: '6x8',
 	category: 'special',

From 6c98b11c13db47dffa65c866172b2fd08b46c99b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Mar 2018 22:06:16 +0900
Subject: [PATCH 0682/1250] #1215

---
 src/api/stream/othello-game.ts | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/api/stream/othello-game.ts b/src/api/stream/othello-game.ts
index 2af1bd97a..53cda88b5 100644
--- a/src/api/stream/othello-game.ts
+++ b/src/api/stream/othello-game.ts
@@ -171,5 +171,11 @@ export default function(request: websocket.request, connection: websocket.connec
 		});
 
 		publishOthelloGameStream(gameId, 'set', log);
+
+		if (o.isEnded) {
+			publishOthelloGameStream(gameId, 'ended', {
+				winner_id: winner
+			});
+		}
 	}
 }

From 749359db7612aae06d19ff5e35b30cbae69570d0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Mar 2018 22:06:58 +0900
Subject: [PATCH 0683/1250] v4058

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 7f5b18931..938875cd2 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4053",
+	"version": "0.0.4058",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From b0b90ccbee702ab1a5f1fe241ca07cd5face7f7f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Mar 2018 22:30:02 +0900
Subject: [PATCH 0684/1250] =?UTF-8?q?set=E3=82=A4=E3=83=99=E3=83=B3?=
 =?UTF-8?q?=E3=83=88=E3=81=AB=E6=AC=A1=E3=81=A9=E3=81=A3=E3=81=A1=E3=81=AE?=
 =?UTF-8?q?=E3=82=BF=E3=83=BC=E3=83=B3=E3=81=8B=E3=81=AE=E6=83=85=E5=A0=B1?=
 =?UTF-8?q?=E3=82=92=E5=90=AB=E3=82=81=E3=81=9F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/stream/othello-game.ts | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/api/stream/othello-game.ts b/src/api/stream/othello-game.ts
index 53cda88b5..5c826e5b4 100644
--- a/src/api/stream/othello-game.ts
+++ b/src/api/stream/othello-game.ts
@@ -170,7 +170,9 @@ export default function(request: websocket.request, connection: websocket.connec
 			}
 		});
 
-		publishOthelloGameStream(gameId, 'set', log);
+		publishOthelloGameStream(gameId, 'set', Object.assign(log, {
+			next: o.turn
+		}));
 
 		if (o.isEnded) {
 			publishOthelloGameStream(gameId, 'ended', {

From 01a131adab19ec631adad6ab2540d830a885a012 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Mar 2018 22:54:30 +0900
Subject: [PATCH 0685/1250] Add new othello map

---
 src/common/othello/maps.ts | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index e4f898fd7..2b1b61b2d 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -417,6 +417,24 @@ export const checker: Map = {
 	]
 };
 
+export const arena: Map = {
+	name: 'Arena',
+	category: '10x10',
+	author: 'syuilo',
+	data: [
+		'- - -- - -',
+		' - -  - - ',
+		'- ------ -',
+		' -------- ',
+		'- --wb-- -',
+		'- --bw-- -',
+		' -------- ',
+		'- ------ -',
+		' - -  - - ',
+		'- - -- - -'
+	]
+};
+
 export const reactor: Map = {
 	name: 'Reactor',
 	category: '10x10',

From 7657f6c7ad5c58744ee10725811a5ae0f79b4b46 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Mar 2018 22:54:58 +0900
Subject: [PATCH 0686/1250] v4061

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 938875cd2..4c526866d 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4058",
+	"version": "0.0.4061",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From f05c3513469f48eabcd1a90cf7731c1f63922b55 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 01:48:16 +0900
Subject: [PATCH 0687/1250] :v:

---
 .../app/common/views/components/othello.vue   | 12 +++++
 src/web/app/desktop/script.ts                 |  3 ++
 .../desktop/views/components/game-window.vue  | 19 +++++--
 src/web/app/desktop/views/pages/othello.vue   | 50 +++++++++++++++++++
 src/web/app/mobile/script.ts                  |  3 +-
 .../app/mobile/views/components/ui.nav.vue    |  2 +-
 src/web/app/mobile/views/pages/othello.vue    | 36 ++++++++++++-
 7 files changed, 119 insertions(+), 6 deletions(-)
 create mode 100644 src/web/app/desktop/views/pages/othello.vue

diff --git a/src/web/app/common/views/components/othello.vue b/src/web/app/common/views/components/othello.vue
index d4157eb76..70bb6b2ef 100644
--- a/src/web/app/common/views/components/othello.vue
+++ b/src/web/app/common/views/components/othello.vue
@@ -67,6 +67,7 @@ export default Vue.extend({
 	components: {
 		XGameroom
 	},
+	props: ['initGame'],
 	data() {
 		return {
 			game: null,
@@ -80,6 +81,16 @@ export default Vue.extend({
 			connectionId: null
 		};
 	},
+	watch: {
+		game(g) {
+			this.$emit('gamed', g);
+		}
+	},
+	created() {
+		if (this.initGame) {
+			this.game = this.initGame;
+		}
+	},
 	mounted() {
 		this.connection = (this as any).os.streams.othelloStream.getConnection();
 		this.connectionId = (this as any).os.streams.othelloStream.use();
@@ -162,6 +173,7 @@ export default Vue.extend({
 
 .mk-othello
 	color #677f84
+	background #fff
 
 	> .matching
 		> h1
diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
index 78549741b..25a60d7ec 100644
--- a/src/web/app/desktop/script.ts
+++ b/src/web/app/desktop/script.ts
@@ -28,6 +28,7 @@ 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';
+import MkOthello from './views/pages/othello.vue';
 
 /**
  * init
@@ -80,6 +81,8 @@ init(async (launch) => {
 		{ path: '/i/drive/folder/:folder', component: MkDrive },
 		{ path: '/selectdrive', component: MkSelectDrive },
 		{ path: '/search', component: MkSearch },
+		{ path: '/othello', component: MkOthello },
+		{ path: '/othello/:game', component: MkOthello },
 		{ path: '/:user', component: MkUser },
 		{ path: '/:user/:post', component: MkPost }
 	]);
diff --git a/src/web/app/desktop/views/components/game-window.vue b/src/web/app/desktop/views/components/game-window.vue
index bf339092a..3c8bf40e1 100644
--- a/src/web/app/desktop/views/components/game-window.vue
+++ b/src/web/app/desktop/views/components/game-window.vue
@@ -1,14 +1,27 @@
 <template>
-<mk-window ref="window" width="500px" height="560px" @closed="$destroy">
+<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="$destroy">
 	<span slot="header" :class="$style.header">%fa:gamepad%オセロ</span>
-	<mk-othello :class="$style.content"/>
+	<mk-othello :class="$style.content" @gamed="g => game = g"/>
 </mk-window>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-export default Vue.extend({
+import { url } from '../../../config';
 
+export default Vue.extend({
+	data() {
+		return {
+			game: null
+		};
+	},
+	computed: {
+		popout(): string {
+			return this.game
+				? `${url}/othello/${this.game.id}`
+				: `${url}/othello`;
+		}
+	}
 });
 </script>
 
diff --git a/src/web/app/desktop/views/pages/othello.vue b/src/web/app/desktop/views/pages/othello.vue
new file mode 100644
index 000000000..160dd9a35
--- /dev/null
+++ b/src/web/app/desktop/views/pages/othello.vue
@@ -0,0 +1,50 @@
+<template>
+<component :is="ui ? 'mk-ui' : 'div'">
+	<mk-othello v-if="!fetching" :init-game="game" @gamed="onGamed"/>
+</component>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Progress from '../../../common/scripts/loading';
+
+export default Vue.extend({
+	props: {
+		ui: {
+			default: false
+		}
+	},
+	data() {
+		return {
+			fetching: false,
+			game: null
+		};
+	},
+	watch: {
+		$route: 'fetch'
+	},
+	created() {
+		this.fetch();
+	},
+	methods: {
+		fetch() {
+			if (this.$route.params.game == null) return;
+
+			Progress.start();
+			this.fetching = true;
+
+			(this as any).api('othello/games/show', {
+				game_id: this.$route.params.game
+			}).then(game => {
+				this.game = game;
+				this.fetching = false;
+
+				Progress.done();
+			});
+		},
+		onGamed(game) {
+			history.pushState(null, null, '/othello/' + game.id);
+		}
+	}
+});
+</script>
diff --git a/src/web/app/mobile/script.ts b/src/web/app/mobile/script.ts
index 27c18c5ae..2b57b78ad 100644
--- a/src/web/app/mobile/script.ts
+++ b/src/web/app/mobile/script.ts
@@ -69,7 +69,8 @@ init((launch) => {
 		{ path: '/i/drive/file/:file', component: MkDrive },
 		{ path: '/selectdrive', component: MkSelectDrive },
 		{ path: '/search', component: MkSearch },
-		{ path: '/game/othello', component: MkOthello },
+		{ path: '/othello', component: MkOthello },
+		{ path: '/othello/:game', component: MkOthello },
 		{ path: '/:user', component: MkUser },
 		{ path: '/:user/followers', component: MkFollowers },
 		{ path: '/:user/following', component: MkFollowing },
diff --git a/src/web/app/mobile/views/components/ui.nav.vue b/src/web/app/mobile/views/components/ui.nav.vue
index a58225a17..ba35a2783 100644
--- a/src/web/app/mobile/views/components/ui.nav.vue
+++ b/src/web/app/mobile/views/components/ui.nav.vue
@@ -18,7 +18,7 @@
 					<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>
-					<li><router-link to="/game/othello">%fa:gamepad%ゲーム%fa:angle-right%</router-link></li>
+					<li><router-link to="/othello">%fa:gamepad%ゲーム%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>
diff --git a/src/web/app/mobile/views/pages/othello.vue b/src/web/app/mobile/views/pages/othello.vue
index 67f4add07..b110bf309 100644
--- a/src/web/app/mobile/views/pages/othello.vue
+++ b/src/web/app/mobile/views/pages/othello.vue
@@ -1,16 +1,50 @@
 <template>
 <mk-ui>
 	<span slot="header">%fa:gamepad%オセロ</span>
-	<mk-othello/>
+	<mk-othello v-if="!fetching" :init-game="game" @gamed="onGamed"/>
 </mk-ui>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import Progress from '../../../common/scripts/loading';
+
 export default Vue.extend({
+	data() {
+		return {
+			fetching: false,
+			game: null
+		};
+	},
+	watch: {
+		$route: 'fetch'
+	},
+	created() {
+		this.fetch();
+	},
 	mounted() {
 		document.title = 'Misskey オセロ';
 		document.documentElement.style.background = '#fff';
+	},
+	methods: {
+		fetch() {
+			if (this.$route.params.game == null) return;
+
+			Progress.start();
+			this.fetching = true;
+
+			(this as any).api('othello/games/show', {
+				game_id: this.$route.params.game
+			}).then(game => {
+				this.game = game;
+				this.fetching = false;
+
+				Progress.done();
+			});
+		},
+		onGamed(game) {
+			history.pushState(null, null, '/othello/' + game.id);
+		}
 	}
 });
 </script>

From 3b45465ea7d8a29aca2ec15cea576eb9f72a9c01 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 01:48:20 +0900
Subject: [PATCH 0688/1250] :art:

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

diff --git a/src/web/app/common/views/components/othello.game.vue b/src/web/app/common/views/components/othello.game.vue
index 6daa7a810..31030f614 100644
--- a/src/web/app/common/views/components/othello.game.vue
+++ b/src/web/app/common/views/components/othello.game.vue
@@ -177,8 +177,8 @@ export default Vue.extend({
 	> .board
 		display grid
 		grid-gap 4px
-		width 300px
-		height 300px
+		width 350px
+		height 350px
 		margin 0 auto
 
 		> div

From c7f3cd201a5b9d52004eda8bddb1695f51aacf69 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 01:48:45 +0900
Subject: [PATCH 0689/1250] v4064

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 4c526866d..3a498e99a 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4061",
+	"version": "0.0.4064",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 646141db1744abd0ba8b4d495ed6cfd23c1ad30c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 03:01:01 +0900
Subject: [PATCH 0690/1250] Fix #1218

---
 src/common/othello/core.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/common/othello/core.ts b/src/common/othello/core.ts
index 0b4cb1fc1..8a0af4d50 100644
--- a/src/common/othello/core.ts
+++ b/src/common/othello/core.ts
@@ -81,12 +81,12 @@ export default class Othello {
 
 	public transformPosToXy(pos: number): number[] {
 		const x = pos % this.mapWidth;
-		const y = Math.floor(pos / this.mapHeight);
+		const y = Math.floor(pos / this.mapWidth);
 		return [x, y];
 	}
 
 	public transformXyToPos(x: number, y: number): number {
-		return x + (y * this.mapHeight);
+		return x + (y * this.mapWidth);
 	}
 
 	/**

From dff5fb472f500fe2dceff5b71d1fc894b9ef0ae5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 03:01:33 +0900
Subject: [PATCH 0691/1250] :v:

---
 src/common/othello/maps.ts | 27 +++++++++++++++++++--------
 1 file changed, 19 insertions(+), 8 deletions(-)

diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index 2b1b61b2d..9fc87cfff 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -455,7 +455,7 @@ export const reactor: Map = {
 
 export const sixeight: Map = {
 	name: '6x8',
-	category: 'special',
+	category: 'Special',
 	data: [
 		'------',
 		'------',
@@ -470,7 +470,7 @@ export const sixeight: Map = {
 
 export const spark: Map = {
 	name: 'Spark',
-	category: 'special',
+	category: 'Special',
 	author: 'syuilo',
 	data: [
 		' -      - ',
@@ -488,7 +488,7 @@ export const spark: Map = {
 
 export const islands: Map = {
 	name: 'Islands',
-	category: 'special',
+	category: 'Special',
 	author: 'syuilo',
 	data: [
 		'--------  ',
@@ -506,7 +506,7 @@ export const islands: Map = {
 
 export const galaxy: Map = {
 	name: 'Galaxy',
-	category: 'special',
+	category: 'Special',
 	author: 'syuilo',
 	data: [
 		'   ------   ',
@@ -526,7 +526,7 @@ export const galaxy: Map = {
 
 export const triangle: Map = {
 	name: 'Triangle',
-	category: 'special',
+	category: 'Special',
 	author: 'syuilo',
 	data: [
 		'    --    ',
@@ -544,7 +544,7 @@ export const triangle: Map = {
 
 export const iphonex: Map = {
 	name: 'iPhone X',
-	category: 'special',
+	category: 'Special',
 	author: 'syuilo',
 	data: [
 		' --  -- ',
@@ -564,7 +564,7 @@ export const iphonex: Map = {
 
 export const bigBoard: Map = {
 	name: 'Big board',
-	category: 'special',
+	category: 'Special',
 	data: [
 		'----------------',
 		'----------------',
@@ -587,7 +587,7 @@ export const bigBoard: Map = {
 
 export const twoBoard: Map = {
 	name: 'Two board',
-	category: 'special',
+	category: 'Special',
 	author: 'Aya',
 	data: [
 		'-------- --------',
@@ -600,3 +600,14 @@ export const twoBoard: Map = {
 		'-------- --------'
 	]
 };
+
+export const test: Map = {
+	name: 'Test1',
+	category: 'Test',
+	data: [
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------'
+	]
+};

From ae94cc5f9287bdf91196a44507ef476750e4490f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 03:02:02 +0900
Subject: [PATCH 0692/1250] v4067

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 3a498e99a..9d76468c3 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4064",
+	"version": "0.0.4067",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From a23ad137801af80f7acf9e9c83bdc111db74ded9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 03:24:01 +0900
Subject: [PATCH 0693/1250] =?UTF-8?q?=E5=85=88=E8=A1=8C=E5=BE=8C=E6=94=BB?=
 =?UTF-8?q?=E3=82=92=E6=B1=BA=E3=82=81=E3=82=89=E3=82=8C=E3=82=8B=E3=82=88?=
 =?UTF-8?q?=E3=81=86=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../app/common/views/components/othello.room.vue    | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/src/web/app/common/views/components/othello.room.vue b/src/web/app/common/views/components/othello.room.vue
index 60fdb32a4..49f199f72 100644
--- a/src/web/app/common/views/components/othello.room.vue
+++ b/src/web/app/common/views/components/othello.room.vue
@@ -24,6 +24,11 @@
 
 	<div class="rules">
 		<mk-switch v-model="game.settings.is_llotheo" @change="onIsLlotheoChange" text="石の少ない方が勝ち(ロセオ)"/>
+		<div>
+			<el-radio v-model="game.settings.bw" label="random" @change="onBwChange">ランダム</el-radio>
+			<el-radio v-model="game.settings.bw" :label="1" @change="onBwChange">{{ game.user1.name }}が先行</el-radio>
+			<el-radio v-model="game.settings.bw" :label="2" @change="onBwChange">{{ game.user2.name }}が先行</el-radio>
+		</div>
 	</div>
 
 	<footer>
@@ -129,6 +134,14 @@ export default Vue.extend({
 				settings: this.game.settings
 			});
 			this.$forceUpdate();
+		},
+
+		onBwChange(v) {
+			this.connection.send({
+				type: 'update-settings',
+				settings: this.game.settings
+			});
+			this.$forceUpdate();
 		}
 	}
 });

From d8bfe7992f170d17aeed33c102284fa449ef84e4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 03:25:59 +0900
Subject: [PATCH 0694/1250] Add some handicap maps

---
 src/common/othello/maps.ts | 60 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 60 insertions(+)

diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index 9fc87cfff..48a1f40f3 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -82,6 +82,66 @@ export const eighteight: Map = {
 	]
 };
 
+export const eighteightH1: Map = {
+	name: '8x8 handicap 1',
+	category: '8x8',
+	data: [
+		'b-------',
+		'--------',
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------',
+		'--------',
+		'--------'
+	]
+};
+
+export const eighteightH2: Map = {
+	name: '8x8 handicap 2',
+	category: '8x8',
+	data: [
+		'b-------',
+		'--------',
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------',
+		'--------',
+		'-------b'
+	]
+};
+
+export const eighteightH3: Map = {
+	name: '8x8 handicap 3',
+	category: '8x8',
+	data: [
+		'b------b',
+		'--------',
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------',
+		'--------',
+		'-------b'
+	]
+};
+
+export const eighteightH4: Map = {
+	name: '8x8 handicap 4',
+	category: '8x8',
+	data: [
+		'b------b',
+		'--------',
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------',
+		'--------',
+		'b------b'
+	]
+};
+
 export const roundedEighteight: Map = {
 	name: '8x8 rounded',
 	category: '8x8',

From b0fb70888f021c54119ba3a4f511a3428465fff1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 03:26:28 +0900
Subject: [PATCH 0695/1250] v4070

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 9d76468c3..f8000b157 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4067",
+	"version": "0.0.4070",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From f7e07de24a50117b9a1d9bb09dcbf2832c72094f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 03:29:28 +0900
Subject: [PATCH 0696/1250] Better text

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

diff --git a/src/web/app/common/views/components/othello.room.vue b/src/web/app/common/views/components/othello.room.vue
index 49f199f72..41226057b 100644
--- a/src/web/app/common/views/components/othello.room.vue
+++ b/src/web/app/common/views/components/othello.room.vue
@@ -26,8 +26,8 @@
 		<mk-switch v-model="game.settings.is_llotheo" @change="onIsLlotheoChange" text="石の少ない方が勝ち(ロセオ)"/>
 		<div>
 			<el-radio v-model="game.settings.bw" label="random" @change="onBwChange">ランダム</el-radio>
-			<el-radio v-model="game.settings.bw" :label="1" @change="onBwChange">{{ game.user1.name }}が先行</el-radio>
-			<el-radio v-model="game.settings.bw" :label="2" @change="onBwChange">{{ game.user2.name }}が先行</el-radio>
+			<el-radio v-model="game.settings.bw" :label="1" @change="onBwChange">{{ game.user1.name }}が黒</el-radio>
+			<el-radio v-model="game.settings.bw" :label="2" @change="onBwChange">{{ game.user2.name }}が黒</el-radio>
 		</div>
 	</div>
 

From 47b6e923dc8cbb2974a6d45924723b1e2ac6a978 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 03:33:03 +0900
Subject: [PATCH 0697/1250] Add new handi map

---
 src/common/othello/maps.ts | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index 48a1f40f3..b84d10ab9 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -142,6 +142,21 @@ export const eighteightH4: Map = {
 	]
 };
 
+export const eighteightH28: Map = {
+	name: '8x8 handicap 28',
+	category: '8x8',
+	data: [
+		'bbbbbbbb',
+		'b------b',
+		'b------b',
+		'b--wb--b',
+		'b--bw--b',
+		'b------b',
+		'b------b',
+		'bbbbbbbb'
+	]
+};
+
 export const roundedEighteight: Map = {
 	name: '8x8 rounded',
 	category: '8x8',

From ae2ed6b46cf4242f041ebe1bee9a9ec1ed199a87 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 03:33:24 +0900
Subject: [PATCH 0698/1250] v4073

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index f8000b157..4b8091577 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4070",
+	"version": "0.0.4073",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 2a1b836f10100e0d9537e91eae1c034e39cc3fcd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 03:53:42 +0900
Subject: [PATCH 0699/1250] Add new map

---
 src/common/othello/maps.ts | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index b84d10ab9..ffc0c2cb8 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -492,6 +492,24 @@ export const checker: Map = {
 	]
 };
 
+export const japaneseCurry: Map = {
+	name: 'Japanese curry',
+	category: '10x10',
+	author: 'syuilo',
+	data: [
+		'w-b-b-b-b-',
+		'-w-b-b-b-b',
+		'w-w-b-b-b-',
+		'-w-w-b-b-b',
+		'w-w-wwb-b-',
+		'-w-wbb-b-b',
+		'w-w-w-b-b-',
+		'-w-w-w-b-b',
+		'w-w-w-w-b-',
+		'-w-w-w-w-b'
+	]
+};
+
 export const arena: Map = {
 	name: 'Arena',
 	category: '10x10',

From 49be1b0489db02584d9ff1191527eeae23520e1e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 03:53:54 +0900
Subject: [PATCH 0700/1250] v4075

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 4b8091577..a988e5731 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4073",
+	"version": "0.0.4075",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 4305a513d9da564fc45a5ed17c165dbff6ec3012 Mon Sep 17 00:00:00 2001
From: Aya Morisawa <AyaMorisawa4869@gmail.com>
Date: Fri, 9 Mar 2018 21:14:46 +0000
Subject: [PATCH 0701/1250] Add 8x8 handicap 12 map

---
 src/common/othello/maps.ts | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index ffc0c2cb8..9e5bc087a 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -142,6 +142,21 @@ export const eighteightH4: Map = {
 	]
 };
 
+export const eighteightH12: Map = {
+	name: '8x8 handicap 12',
+	category: '8x8',
+	data: [
+		'bb----bb',
+		'b------b',
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------',
+		'b------b',
+		'bb----bb'
+	]
+};
+
 export const eighteightH28: Map = {
 	name: '8x8 handicap 28',
 	category: '8x8',

From 30b9eb9a2fa13ad11154a51fe4feb228cede600f Mon Sep 17 00:00:00 2001
From: Aya Morisawa <AyaMorisawa4869@gmail.com>
Date: Sat, 10 Mar 2018 11:42:50 +0900
Subject: [PATCH 0702/1250] Add Parallel map

---
 src/common/othello/maps.ts | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index 9e5bc087a..5c6c44dd2 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -332,6 +332,22 @@ export const x: Map = {
 	]
 };
 
+export const parallel: Map = {
+	name: 'Parallel',
+	category: '8x8',
+	author: 'Aya',
+	data: [
+		'--------',
+		'--------',
+		'--------',
+		'---bb---',
+		'---ww---',
+		'--------',
+		'--------',
+		'--------'
+	]
+};
+
 export const squareParty: Map = {
 	name: 'Square Party',
 	category: '8x8',

From 2b3d2a3b40bdb6f2c695e91d63861d91a2e73641 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 12:48:41 +0900
Subject: [PATCH 0703/1250] Implement othello map editing

---
 src/common/othello/core.ts                    |  2 +
 .../common/views/components/othello.room.vue  | 50 +++++++++++--------
 2 files changed, 30 insertions(+), 22 deletions(-)

diff --git a/src/common/othello/core.ts b/src/common/othello/core.ts
index 8a0af4d50..62ec34f41 100644
--- a/src/common/othello/core.ts
+++ b/src/common/othello/core.ts
@@ -69,6 +69,7 @@ export default class Othello {
 	 * 黒石の比率
 	 */
 	public get blackP() {
+		if (this.blackCount == 0 && this.whiteCount == 0) return 0;
 		return this.blackCount / (this.blackCount + this.whiteCount);
 	}
 
@@ -76,6 +77,7 @@ export default class Othello {
 	 * 白石の比率
 	 */
 	public get whiteP() {
+		if (this.blackCount == 0 && this.whiteCount == 0) return 0;
 		return this.whiteCount / (this.blackCount + this.whiteCount);
 	}
 
diff --git a/src/web/app/common/views/components/othello.room.vue b/src/web/app/common/views/components/othello.room.vue
index 41226057b..6c8ce1653 100644
--- a/src/web/app/common/views/components/othello.room.vue
+++ b/src/web/app/common/views/components/othello.room.vue
@@ -16,6 +16,7 @@
 	<div class="board" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map[0].length }, 1fr)` }">
 		<div v-for="(x, i) in game.settings.map.join('')"
 			:class="{ none: x == ' ' }"
+			@click="onPixelClick(i, x)"
 		>
 			<template v-if="x == 'b'">%fa:circle%</template>
 			<template v-if="x == 'w'">%fa:circle R%</template>
@@ -23,11 +24,11 @@
 	</div>
 
 	<div class="rules">
-		<mk-switch v-model="game.settings.is_llotheo" @change="onIsLlotheoChange" text="石の少ない方が勝ち(ロセオ)"/>
+		<mk-switch v-model="game.settings.is_llotheo" @change="updateSettings" text="石の少ない方が勝ち(ロセオ)"/>
 		<div>
-			<el-radio v-model="game.settings.bw" label="random" @change="onBwChange">ランダム</el-radio>
-			<el-radio v-model="game.settings.bw" :label="1" @change="onBwChange">{{ game.user1.name }}が黒</el-radio>
-			<el-radio v-model="game.settings.bw" :label="2" @change="onBwChange">{{ game.user2.name }}が黒</el-radio>
+			<el-radio v-model="game.settings.bw" label="random" @change="updateSettings">ランダム</el-radio>
+			<el-radio v-model="game.settings.bw" :label="1" @change="updateSettings">{{ game.user1.name }}が黒</el-radio>
+			<el-radio v-model="game.settings.bw" :label="2" @change="updateSettings">{{ game.user2.name }}が黒</el-radio>
 		</div>
 	</div>
 
@@ -114,34 +115,38 @@ export default Vue.extend({
 			this.$forceUpdate();
 		},
 
+		updateSettings() {
+			this.connection.send({
+				type: 'update-settings',
+				settings: this.game.settings
+			});
+		},
+
 		onUpdateSettings(settings) {
 			this.game.settings = settings;
-			this.mapName = Object.entries(maps).find(x => x[1].data.join('') == this.game.settings.map.join(''))[1].name;
+			const foundMap = Object.entries(maps).find(x => x[1].data.join('') == this.game.settings.map.join(''));
+			this.mapName = foundMap ? foundMap[1].name : '-Custom-';
 		},
 
 		onMapChange(v) {
 			this.game.settings.map = Object.entries(maps).find(x => x[1].name == v)[1].data;
-			this.connection.send({
-				type: 'update-settings',
-				settings: this.game.settings
-			});
 			this.$forceUpdate();
+			this.updateSettings();
 		},
 
-		onIsLlotheoChange(v) {
-			this.connection.send({
-				type: 'update-settings',
-				settings: this.game.settings
-			});
-			this.$forceUpdate();
-		},
-
-		onBwChange(v) {
-			this.connection.send({
-				type: 'update-settings',
-				settings: this.game.settings
-			});
+		onPixelClick(pos, pixel) {
+			const x = pos % this.game.settings.map[0].length;
+			const y = Math.floor(pos / this.game.settings.map[0].length);
+			const newPixel =
+				pixel == ' ' ? '-' :
+				pixel == '-' ? 'b' :
+				pixel == 'b' ? 'w' :
+				' ';
+			const line = this.game.settings.map[y].split('');
+			line[x] = newPixel;
+			this.$set(this.game.settings.map, y, line.join(''));
 			this.$forceUpdate();
+			this.updateSettings();
 		}
 	}
 });
@@ -172,6 +177,7 @@ export default Vue.extend({
 			border solid 2px #ddd
 			border-radius 6px
 			overflow hidden
+			cursor pointer
 
 			*
 				pointer-events none

From e686151e08b68d4e501faa27fd7795392f7ad8c4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 13:07:17 +0900
Subject: [PATCH 0704/1250] :v:

---
 src/api/stream/othello-game.ts | 30 ++++++++++++++++++++++++++++++
 src/common/othello/core.ts     |  9 +++++++++
 2 files changed, 39 insertions(+)

diff --git a/src/api/stream/othello-game.ts b/src/api/stream/othello-game.ts
index 5c826e5b4..9ca4864a5 100644
--- a/src/api/stream/othello-game.ts
+++ b/src/api/stream/othello-game.ts
@@ -113,6 +113,36 @@ export default function(request: websocket.request, connection: websocket.connec
 					}
 				});
 
+				//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
+				const o = new Othello(freshGame.settings.map, {
+					isLlotheo: freshGame.settings.is_llotheo
+				});
+
+				if (o.isEnded) {
+					let winner;
+					if (o.winner == 'black') {
+						winner = freshGame.black == 1 ? freshGame.user1_id : freshGame.user2_id;
+					} else if (o.winner == 'white') {
+						winner = freshGame.black == 1 ? freshGame.user2_id : freshGame.user1_id;
+					} else {
+						winner = null;
+					}
+
+					await Game.update({
+						_id: gameId
+					}, {
+						$set: {
+							is_ended: true,
+							winner_id: winner
+						}
+					});
+
+					publishOthelloGameStream(gameId, 'ended', {
+						winner_id: winner
+					});
+				}
+				//#endregion
+
 				publishOthelloGameStream(gameId, 'started', await pack(gameId));
 			}, 3000);
 		}
diff --git a/src/common/othello/core.ts b/src/common/othello/core.ts
index 62ec34f41..fc432b2ce 100644
--- a/src/common/othello/core.ts
+++ b/src/common/othello/core.ts
@@ -49,6 +49,15 @@ export default class Othello {
 			b: this.blackP,
 			w: this.whiteP
 		}];
+
+		// ゲームが始まった時点で片方の色の石しかないか、始まった時点で勝敗が決定するようなマップの場合がある
+		if (this.canPutSomewhere('black').length == 0) {
+			if (this.canPutSomewhere('white').length == 0) {
+				this.turn = null;
+			} else {
+				this.turn = 'white';
+			}
+		}
 	}
 
 	/**

From d6c2d97e03f589aa36ae3c678cc43c59a87180fc Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 13:22:20 +0900
Subject: [PATCH 0705/1250] :v:

---
 package.json                                    |  1 +
 .../common/views/components/othello.game.vue    | 17 ++++++++++-------
 src/web/app/common/views/components/othello.vue | 10 ++++++----
 src/web/app/init.ts                             |  2 ++
 4 files changed, 19 insertions(+), 11 deletions(-)

diff --git a/package.json b/package.json
index a988e5731..e4bcae1a2 100644
--- a/package.json
+++ b/package.json
@@ -182,6 +182,7 @@
 		"uglifyjs-webpack-plugin": "1.2.0",
 		"url-loader": "^0.6.2",
 		"uuid": "3.2.1",
+		"v-animate-css": "0.0.2",
 		"vhost": "3.0.2",
 		"vue": "2.5.13",
 		"vue-cropperjs": "2.2.0",
diff --git a/src/web/app/common/views/components/othello.game.vue b/src/web/app/common/views/components/othello.game.vue
index 31030f614..26612daea 100644
--- a/src/web/app/common/views/components/othello.game.vue
+++ b/src/web/app/common/views/components/othello.game.vue
@@ -2,13 +2,16 @@
 <div class="root">
 	<header><b>{{ blackUser.name }}</b>(黒) vs <b>{{ whiteUser.name }}</b>(白)</header>
 
-	<p class="turn" v-if="!iAmPlayer && !game.is_ended">{{ turnUser.name }}のターンです<mk-ellipsis/></p>
-	<p class="turn" v-if="logPos != logs.length">{{ turnUser.name }}のターン</p>
-	<p class="turn" v-if="iAmPlayer && !game.is_ended">{{ isMyTurn ? 'あなたのターンです' : '相手のターンです' }}<mk-ellipsis v-if="!isMyTurn"/></p>
-	<p class="result" v-if="game.is_ended && logPos == logs.length">
-		<template v-if="game.winner"><b>{{ game.winner.name }}</b>の勝ち{{ game.settings.is_llotheo ? ' (ロセオ)' : '' }}</template>
-		<template v-else>引き分け</template>
-	</p>
+	<div style="overflow: hidden">
+		<p class="turn" v-if="!iAmPlayer && !game.is_ended">{{ turnUser.name }}のターンです<mk-ellipsis/></p>
+		<p class="turn" v-if="logPos != logs.length">{{ turnUser.name }}のターン</p>
+		<p class="turn1" v-if="iAmPlayer && !game.is_ended && !isMyTurn">相手のターンです<mk-ellipsis/></p>
+		<p class="turn2" v-if="iAmPlayer && !game.is_ended && isMyTurn" v-animate-css="{ classes: 'tada', iteration: 'infinite' }">あなたのターンです</p>
+		<p class="result" v-if="game.is_ended && logPos == logs.length">
+			<template v-if="game.winner"><b>{{ game.winner.name }}</b>の勝ち{{ game.settings.is_llotheo ? ' (ロセオ)' : '' }}</template>
+			<template v-else>引き分け</template>
+		</p>
+	</div>
 
 	<div class="board" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map[0].length }, 1fr)` }">
 		<div v-for="(stone, i) in o.board"
diff --git a/src/web/app/common/views/components/othello.vue b/src/web/app/common/views/components/othello.vue
index 70bb6b2ef..81da02d1c 100644
--- a/src/web/app/common/views/components/othello.vue
+++ b/src/web/app/common/views/components/othello.vue
@@ -39,21 +39,21 @@
 		</section>
 		<section v-if="myGames.length > 0">
 			<h2>自分の対局</h2>
-			<div class="game" v-for="g in myGames" tabindex="-1" @click="go(g)">
+			<a class="game" v-for="g in myGames" tabindex="-1" @click.prevent="go(g)" :href="`/othello/${g.id}`">
 				<img :src="`${g.user1.avatar_url}?thumbnail&size=32`" alt="">
 				<img :src="`${g.user2.avatar_url}?thumbnail&size=32`" alt="">
 				<span><b>{{ g.user1.name }}</b> vs <b>{{ g.user2.name }}</b></span>
 				<span class="state">{{ g.is_ended ? '終了' : '進行中' }}</span>
-			</div>
+			</a>
 		</section>
 		<section v-if="games.length > 0">
 			<h2>みんなの対局</h2>
-			<div class="game" v-for="g in games" tabindex="-1" @click="go(g)">
+			<a class="game" v-for="g in games" tabindex="-1" @click.prevent="go(g)" :href="`/othello/${g.id}`">
 				<img :src="`${g.user1.avatar_url}?thumbnail&size=32`" alt="">
 				<img :src="`${g.user2.avatar_url}?thumbnail&size=32`" alt="">
 				<span><b>{{ g.user1.name }}</b> vs <b>{{ g.user2.name }}</b></span>
 				<span class="state">{{ g.is_ended ? '終了' : '進行中' }}</span>
-			</div>
+			</a>
 		</section>
 	</div>
 </div>
@@ -265,8 +265,10 @@ export default Vue.extend({
 			line-height 32px
 
 	.game
+		display block
 		margin 8px 0
 		padding 8px
+		color #677f84
 		border solid 1px #e1e5e8
 		border-radius 6px
 		cursor pointer
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index 52d2ecf99..28adfd3b0 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -6,6 +6,7 @@ import Vue from 'vue';
 import VueRouter from 'vue-router';
 import VModal from 'vue-js-modal';
 import * as TreeView from 'vue-json-tree-view';
+import VAnimateCss from 'v-animate-css';
 import Element from 'element-ui';
 import ElementLocaleEn from 'element-ui/lib/locale/lang/en';
 import ElementLocaleJa from 'element-ui/lib/locale/lang/ja';
@@ -25,6 +26,7 @@ switch (lang) {
 Vue.use(VueRouter);
 Vue.use(VModal);
 Vue.use(TreeView);
+Vue.use(VAnimateCss);
 Vue.use(Element, { locale: elementLocale });
 
 // Register global directives

From efc4b17454c26f6b55c0f1f04c49de5e010f8cce Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 13:42:26 +0900
Subject: [PATCH 0706/1250] #1225

---
 src/api/stream/othello-game.ts                  | 14 ++++++++++++--
 .../common/views/components/othello.room.vue    | 17 +++++++++++++----
 2 files changed, 25 insertions(+), 6 deletions(-)

diff --git a/src/api/stream/othello-game.ts b/src/api/stream/othello-game.ts
index 9ca4864a5..cc936805b 100644
--- a/src/api/stream/othello-game.ts
+++ b/src/api/stream/othello-game.ts
@@ -3,6 +3,7 @@ import * as redis from 'redis';
 import Game, { pack } from '../models/othello-game';
 import { publishOthelloGameStream } from '../event';
 import Othello from '../../common/othello/core';
+import * as maps from '../../common/othello/maps';
 
 export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void {
 	const gameId = request.resourceURL.query.game;
@@ -105,16 +106,25 @@ export default function(request: websocket.request, connection: websocket.connec
 					bw = freshGame.settings.bw as number;
 				}
 
+				function getRandomMap() {
+					const mapCount = Object.entries(maps).length;
+					const rnd = Math.floor(Math.random() * mapCount);
+					return Object.entries(maps).find((x, i) => i == rnd)[1].data;
+				}
+
+				const map = freshGame.settings.map != null ? freshGame.settings.map : getRandomMap();
+
 				await Game.update({ _id: gameId }, {
 					$set: {
 						started_at: new Date(),
 						is_started: true,
-						black: bw
+						black: bw,
+						'settings.map': map
 					}
 				});
 
 				//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
-				const o = new Othello(freshGame.settings.map, {
+				const o = new Othello(map, {
 					isLlotheo: freshGame.settings.is_llotheo
 				});
 
diff --git a/src/web/app/common/views/components/othello.room.vue b/src/web/app/common/views/components/othello.room.vue
index 6c8ce1653..745074b17 100644
--- a/src/web/app/common/views/components/othello.room.vue
+++ b/src/web/app/common/views/components/othello.room.vue
@@ -5,6 +5,7 @@
 	<p>ゲームの設定</p>
 
 	<el-select class="map" v-model="mapName" placeholder="マップを選択" @change="onMapChange">
+		<el-option label="ランダム" :value="null"/>
 		<el-option-group v-for="c in mapCategories" :key="c" :label="c">
 			<el-option v-for="m in maps" v-if="m.category == c" :key="m.name" :label="m.name" :value="m.name">
 				<span style="float: left">{{ m.name }}</span>
@@ -13,7 +14,7 @@
 		</el-option-group>
 	</el-select>
 
-	<div class="board" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map[0].length }, 1fr)` }">
+	<div class="board" v-if="game.settings.map != null" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map[0].length }, 1fr)` }">
 		<div v-for="(x, i) in game.settings.map.join('')"
 			:class="{ none: x == ' ' }"
 			@click="onPixelClick(i, x)"
@@ -124,12 +125,20 @@ export default Vue.extend({
 
 		onUpdateSettings(settings) {
 			this.game.settings = settings;
-			const foundMap = Object.entries(maps).find(x => x[1].data.join('') == this.game.settings.map.join(''));
-			this.mapName = foundMap ? foundMap[1].name : '-Custom-';
+			if (this.game.settings.map == null) {
+				this.mapName = null;
+			} else {
+				const foundMap = Object.entries(maps).find(x => x[1].data.join('') == this.game.settings.map.join(''));
+				this.mapName = foundMap ? foundMap[1].name : '-Custom-';
+			}
 		},
 
 		onMapChange(v) {
-			this.game.settings.map = Object.entries(maps).find(x => x[1].name == v)[1].data;
+			if (v == null) {
+				this.game.settings.map = null;
+			} else {
+				this.game.settings.map = Object.entries(maps).find(x => x[1].name == v)[1].data;
+			}
 			this.$forceUpdate();
 			this.updateSettings();
 		},

From 2798f5dfa4074e7752218e9e6ea8bce462668388 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 13:42:55 +0900
Subject: [PATCH 0707/1250] v4084

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index e4bcae1a2..0afa4bfaf 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4075",
+	"version": "0.0.4084",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From d4017c73328e36e49e6ccf3a5790370c7202c19c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 18:04:39 +0900
Subject: [PATCH 0708/1250] Add some othello maps

---
 src/common/othello/maps.ts | 68 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 68 insertions(+)

diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index 5c6c44dd2..a6a1c4604 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -505,6 +505,24 @@ export const walls: Map = {
 	]
 };
 
+export const cpu: Map = {
+	name: 'CPU',
+	category: '10x10',
+	author: 'syuilo',
+	data: [
+		' b b  b b ',
+		'w--------w',
+		' -------- ',
+		'w--------w',
+		' ---wb--- ',
+		' ---bw--- ',
+		'w--------w',
+		' -------- ',
+		'w--------w',
+		' b b  b b '
+	]
+};
+
 export const checker: Map = {
 	name: 'Checker',
 	category: '10x10',
@@ -541,6 +559,24 @@ export const japaneseCurry: Map = {
 	]
 };
 
+export const mosaic: Map = {
+	name: 'Mosaic',
+	category: '10x10',
+	author: 'syuilo',
+	data: [
+		'- - - - - ',
+		' - - - - -',
+		'- - - - - ',
+		' - w w - -',
+		'- - b b - ',
+		' - w w - -',
+		'- - b b - ',
+		' - - - - -',
+		'- - - - - ',
+		' - - - - -',
+	]
+};
+
 export const arena: Map = {
 	name: 'Arena',
 	category: '10x10',
@@ -686,6 +722,38 @@ export const iphonex: Map = {
 	]
 };
 
+export const dealWithIt: Map = {
+	name: 'Deal with it!',
+	category: 'Special',
+	author: 'syuilo',
+	data: [
+		'------------',
+		'--w-b-------',
+		' --b-w------',
+		'  --w-b---- ',
+		'   -------  '
+	]
+};
+
+export const experiment: Map = {
+	name: 'Let\'s experiment',
+	category: 'Special',
+	author: 'syuilo',
+	data: [
+		' ------------ ',
+		'------wb------',
+		'------bw------',
+		'--------------',
+		'    -    -    ',
+		'------  ------',
+		'bbbbbb  wwwwww',
+		'bbbbbb  wwwwww',
+		'bbbbbb  wwwwww',
+		'bbbbbb  wwwwww',
+		'wwwwww  bbbbbb'
+	]
+};
+
 export const bigBoard: Map = {
 	name: 'Big board',
 	category: 'Special',

From 14a450cadd8cd36c5dfdfffabdcdda4a0e7c6157 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 18:04:58 +0900
Subject: [PATCH 0709/1250] v4086

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 0afa4bfaf..51079997a 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4084",
+	"version": "0.0.4086",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 448cb0cda2e9b5ca9555fcd87d53094a0c3eda3a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 18:22:54 +0900
Subject: [PATCH 0710/1250] =?UTF-8?q?=E3=81=A9=E3=81=93=E3=81=A7=E3=82=82?=
 =?UTF-8?q?=E7=BD=AE=E3=81=91=E3=82=8B=E3=83=A2=E3=83=BC=E3=83=89=E5=AE=9F?=
 =?UTF-8?q?=E8=A3=85?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/models/othello-game.ts                |  1 +
 src/api/stream/othello-game.ts                |  6 ++++--
 src/common/othello/core.ts                    | 20 ++++++++++++++++---
 .../common/views/components/othello.game.vue  |  6 ++++--
 .../common/views/components/othello.room.vue  |  1 +
 5 files changed, 27 insertions(+), 7 deletions(-)

diff --git a/src/api/models/othello-game.ts b/src/api/models/othello-game.ts
index 82c004210..b9d33007b 100644
--- a/src/api/models/othello-game.ts
+++ b/src/api/models/othello-game.ts
@@ -30,6 +30,7 @@ export interface IGame {
 		map: string[];
 		bw: string | number;
 		is_llotheo: boolean;
+		can_put_everywhere: boolean;
 	};
 }
 
diff --git a/src/api/stream/othello-game.ts b/src/api/stream/othello-game.ts
index cc936805b..05f244f76 100644
--- a/src/api/stream/othello-game.ts
+++ b/src/api/stream/othello-game.ts
@@ -125,7 +125,8 @@ export default function(request: websocket.request, connection: websocket.connec
 
 				//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
 				const o = new Othello(map, {
-					isLlotheo: freshGame.settings.is_llotheo
+					isLlotheo: freshGame.settings.is_llotheo,
+					canPutEverywhere: freshGame.settings.can_put_everywhere
 				});
 
 				if (o.isEnded) {
@@ -166,7 +167,8 @@ export default function(request: websocket.request, connection: websocket.connec
 		if (!game.user1_id.equals(user._id) && !game.user2_id.equals(user._id)) return;
 
 		const o = new Othello(game.settings.map, {
-			isLlotheo: game.settings.is_llotheo
+			isLlotheo: game.settings.is_llotheo,
+			canPutEverywhere: game.settings.can_put_everywhere
 		});
 
 		game.logs.forEach(log => {
diff --git a/src/common/othello/core.ts b/src/common/othello/core.ts
index fc432b2ce..851f1b79c 100644
--- a/src/common/othello/core.ts
+++ b/src/common/othello/core.ts
@@ -3,6 +3,7 @@ export type MapPixel = 'null' | 'empty';
 
 export type Options = {
 	isLlotheo: boolean;
+	canPutEverywhere: boolean;
 };
 
 /**
@@ -26,23 +27,29 @@ export default class Othello {
 	 * ゲームを初期化します
 	 */
 	constructor(map: string[], opts: Options) {
+		//#region Options
 		this.opts = opts;
+		if (this.opts.isLlotheo == null) this.opts.isLlotheo = false;
+		if (this.opts.canPutEverywhere == null) this.opts.canPutEverywhere = false;
+		//#endregion
 
+		//#region Parse map data
 		this.mapWidth = map[0].length;
 		this.mapHeight = map.length;
 		const mapData = map.join('');
 
-		// Parse map data
 		this.board = mapData.split('').map(d => {
 			if (d == '-') return null;
 			if (d == 'b') return 'black';
 			if (d == 'w') return 'white';
 			return undefined;
 		});
+
 		this.map = mapData.split('').map(d => {
 			if (d == '-' || d == 'b' || d == 'w') return 'empty';
 			return 'null';
 		});
+		//#endregion
 
 		// Init stats
 		this.stats = [{
@@ -175,14 +182,21 @@ export default class Othello {
 	}
 
 	/**
-	 * 指定のマスに石を打つことができるかどうか(相手の石を1つでも反転させられるか)を取得します
+	 * 指定のマスに石を打つことができるかどうかを取得します
 	 * @param color 自分の色
 	 * @param pos 位置
 	 */
 	public canPut(color: Color, pos: number): boolean {
 		// 既に石が置いてある場所には打てない
 		if (this.get(pos) !== null) return false;
-		return this.effects(color, pos).length !== 0;
+
+		if (this.opts.canPutEverywhere) {
+			// 挟んでなくても置けるモード
+			return this.mapDataGet(pos) == 'empty';
+		} else {
+			// 相手の石を1つでも反転させられるか
+			return this.effects(color, pos).length !== 0;
+		}
 	}
 
 	/**
diff --git a/src/web/app/common/views/components/othello.game.vue b/src/web/app/common/views/components/othello.game.vue
index 26612daea..fa3ed8d9a 100644
--- a/src/web/app/common/views/components/othello.game.vue
+++ b/src/web/app/common/views/components/othello.game.vue
@@ -89,7 +89,8 @@ export default Vue.extend({
 		logPos(v) {
 			if (!this.game.is_ended) return;
 			this.o = new Othello(this.game.settings.map, {
-				isLlotheo: this.game.settings.is_llotheo
+				isLlotheo: this.game.settings.is_llotheo,
+				canPutEverywhere: this.game.settings.can_put_everywhere
 			});
 			this.logs.forEach((log, i) => {
 				if (i < v) {
@@ -102,7 +103,8 @@ export default Vue.extend({
 
 	created() {
 		this.o = new Othello(this.game.settings.map, {
-			isLlotheo: this.game.settings.is_llotheo
+			isLlotheo: this.game.settings.is_llotheo,
+			canPutEverywhere: this.game.settings.can_put_everywhere
 		});
 
 		this.game.logs.forEach(log => {
diff --git a/src/web/app/common/views/components/othello.room.vue b/src/web/app/common/views/components/othello.room.vue
index 745074b17..b7c28ae67 100644
--- a/src/web/app/common/views/components/othello.room.vue
+++ b/src/web/app/common/views/components/othello.room.vue
@@ -26,6 +26,7 @@
 
 	<div class="rules">
 		<mk-switch v-model="game.settings.is_llotheo" @change="updateSettings" text="石の少ない方が勝ち(ロセオ)"/>
+		<mk-switch v-model="game.settings.can_put_everywhere" @change="updateSettings" text="どこでも置けるモード"/>
 		<div>
 			<el-radio v-model="game.settings.bw" label="random" @change="updateSettings">ランダム</el-radio>
 			<el-radio v-model="game.settings.bw" :label="1" @change="updateSettings">{{ game.user1.name }}が黒</el-radio>

From d9476562aed327a7ef0a75d3ce2f04f22409cacc Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 18:23:04 +0900
Subject: [PATCH 0711/1250] v4088

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 51079997a..ffd5809c5 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4086",
+	"version": "0.0.4088",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 08b2a8cf631f129a590c18304c3a70c23bb5386e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 21:23:00 +0900
Subject: [PATCH 0712/1250] =?UTF-8?q?=E3=83=AB=E3=83=BC=E3=83=97=E3=83=A2?=
 =?UTF-8?q?=E3=83=BC=E3=83=89=E5=AE=9F=E8=A3=85?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/models/othello-game.ts                |  1 +
 src/api/stream/othello-game.ts                |  6 ++-
 src/common/othello/core.ts                    | 39 +++++++++++++++++--
 src/common/othello/maps.ts                    | 14 ++++++-
 .../common/views/components/othello.game.vue  |  6 ++-
 .../common/views/components/othello.room.vue  |  1 +
 6 files changed, 59 insertions(+), 8 deletions(-)

diff --git a/src/api/models/othello-game.ts b/src/api/models/othello-game.ts
index b9d33007b..a8c302510 100644
--- a/src/api/models/othello-game.ts
+++ b/src/api/models/othello-game.ts
@@ -31,6 +31,7 @@ export interface IGame {
 		bw: string | number;
 		is_llotheo: boolean;
 		can_put_everywhere: boolean;
+		looped_board: boolean;
 	};
 }
 
diff --git a/src/api/stream/othello-game.ts b/src/api/stream/othello-game.ts
index 05f244f76..5f61f0cc2 100644
--- a/src/api/stream/othello-game.ts
+++ b/src/api/stream/othello-game.ts
@@ -126,7 +126,8 @@ export default function(request: websocket.request, connection: websocket.connec
 				//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
 				const o = new Othello(map, {
 					isLlotheo: freshGame.settings.is_llotheo,
-					canPutEverywhere: freshGame.settings.can_put_everywhere
+					canPutEverywhere: freshGame.settings.can_put_everywhere,
+					loopedBoard: freshGame.settings.looped_board
 				});
 
 				if (o.isEnded) {
@@ -168,7 +169,8 @@ export default function(request: websocket.request, connection: websocket.connec
 
 		const o = new Othello(game.settings.map, {
 			isLlotheo: game.settings.is_llotheo,
-			canPutEverywhere: game.settings.can_put_everywhere
+			canPutEverywhere: game.settings.can_put_everywhere,
+			loopedBoard: game.settings.looped_board
 		});
 
 		game.logs.forEach(log => {
diff --git a/src/common/othello/core.ts b/src/common/othello/core.ts
index 851f1b79c..418b461c6 100644
--- a/src/common/othello/core.ts
+++ b/src/common/othello/core.ts
@@ -4,6 +4,7 @@ export type MapPixel = 'null' | 'empty';
 export type Options = {
 	isLlotheo: boolean;
 	canPutEverywhere: boolean;
+	loopedBoard: boolean;
 };
 
 /**
@@ -31,6 +32,7 @@ export default class Othello {
 		this.opts = opts;
 		if (this.opts.isLlotheo == null) this.opts.isLlotheo = false;
 		if (this.opts.canPutEverywhere == null) this.opts.canPutEverywhere = false;
+		if (this.opts.loopedBoard == null) this.opts.loopedBoard = false;
 		//#endregion
 
 		//#region Parse map data
@@ -206,21 +208,50 @@ export default class Othello {
 	 */
 	private effects(color: Color, pos: number): number[] {
 		const enemyColor = color == 'black' ? 'white' : 'black';
-		const [x, y] = this.transformPosToXy(pos);
+
+		// ひっくり返せる石(の位置)リスト
 		let stones = [];
 
+		const initPos = pos;
+
+		// 走査
 		const iterate = (fn: (i: number) => number[]) => {
 			let i = 1;
 			const found = [];
+
 			while (true) {
-				const [x, y] = fn(i);
-				if (x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight) break;
+				let [x, y] = fn(i);
+
+				// 座標が指し示す位置がボード外に出たとき
+				if (this.opts.loopedBoard) {
+					if (x < 0            ) x = this.mapWidth - (-x);
+					if (y < 0            ) y = this.mapHeight - (-y);
+					if (x >= this.mapWidth ) x = x - this.mapWidth;
+					if (y >= this.mapHeight) y = y - this.mapHeight;
+
+					// 一周して自分に帰ってきたら
+					if (this.transformXyToPos(x, y) == initPos) break;
+				} else {
+					if (x == -1 || y == -1 || x == this.mapWidth || y == this.mapHeight) break;
+				}
+
 				const pos = this.transformXyToPos(x, y);
+
+				//#region 「配置不能」マスに当たった場合走査終了
 				const pixel = this.mapDataGet(pos);
 				if (pixel == 'null') break;
+				//#endregion
+
+				// 石取得
 				const stone = this.get(pos);
+
+				// 石が置かれていないマスなら走査終了
 				if (stone == null) break;
+
+				// 相手の石なら「ひっくり返せるかもリスト」に入れておく
 				if (stone == enemyColor) found.push(pos);
+
+				// 自分の石なら「ひっくり返せるかもリスト」を「ひっくり返せるリスト」に入れ、走査終了
 				if (stone == color) {
 					stones = stones.concat(found);
 					break;
@@ -229,6 +260,8 @@ export default class Othello {
 			}
 		};
 
+		const [x, y] = this.transformPosToXy(pos);
+
 		iterate(i => [x    , y - i]); // 上
 		iterate(i => [x + i, y - i]); // 右上
 		iterate(i => [x + i, y    ]); // 右
diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index a6a1c4604..3518259bb 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -793,7 +793,7 @@ export const twoBoard: Map = {
 	]
 };
 
-export const test: Map = {
+export const test1: Map = {
 	name: 'Test1',
 	category: 'Test',
 	data: [
@@ -803,3 +803,15 @@ export const test: Map = {
 		'--------'
 	]
 };
+
+export const test2: Map = {
+	name: 'Test2',
+	category: 'Test',
+	data: [
+		'------',
+		'------',
+		'-b--w-',
+		'-w--b-',
+		'-w--b-'
+	]
+};
diff --git a/src/web/app/common/views/components/othello.game.vue b/src/web/app/common/views/components/othello.game.vue
index fa3ed8d9a..a84dcedd4 100644
--- a/src/web/app/common/views/components/othello.game.vue
+++ b/src/web/app/common/views/components/othello.game.vue
@@ -90,7 +90,8 @@ export default Vue.extend({
 			if (!this.game.is_ended) return;
 			this.o = new Othello(this.game.settings.map, {
 				isLlotheo: this.game.settings.is_llotheo,
-				canPutEverywhere: this.game.settings.can_put_everywhere
+				canPutEverywhere: this.game.settings.can_put_everywhere,
+				loopedBoard: this.game.settings.looped_board
 			});
 			this.logs.forEach((log, i) => {
 				if (i < v) {
@@ -104,7 +105,8 @@ export default Vue.extend({
 	created() {
 		this.o = new Othello(this.game.settings.map, {
 			isLlotheo: this.game.settings.is_llotheo,
-			canPutEverywhere: this.game.settings.can_put_everywhere
+			canPutEverywhere: this.game.settings.can_put_everywhere,
+			loopedBoard: this.game.settings.looped_board
 		});
 
 		this.game.logs.forEach(log => {
diff --git a/src/web/app/common/views/components/othello.room.vue b/src/web/app/common/views/components/othello.room.vue
index b7c28ae67..dfdc43ef9 100644
--- a/src/web/app/common/views/components/othello.room.vue
+++ b/src/web/app/common/views/components/othello.room.vue
@@ -26,6 +26,7 @@
 
 	<div class="rules">
 		<mk-switch v-model="game.settings.is_llotheo" @change="updateSettings" text="石の少ない方が勝ち(ロセオ)"/>
+		<mk-switch v-model="game.settings.looped_board" @change="updateSettings" text="ループマップ"/>
 		<mk-switch v-model="game.settings.can_put_everywhere" @change="updateSettings" text="どこでも置けるモード"/>
 		<div>
 			<el-radio v-model="game.settings.bw" label="random" @change="updateSettings">ランダム</el-radio>

From 9ecfd4d14fc88229d732dfbe8563d44970f626e2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 21:23:11 +0900
Subject: [PATCH 0713/1250] v4090

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index ffd5809c5..51c5024f4 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4088",
+	"version": "0.0.4090",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 045059a159e853b458de3374f4d1377d986a16cb Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 21:29:57 +0900
Subject: [PATCH 0714/1250] Improve readability

---
 src/common/othello/core.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/common/othello/core.ts b/src/common/othello/core.ts
index 418b461c6..e7e725aaf 100644
--- a/src/common/othello/core.ts
+++ b/src/common/othello/core.ts
@@ -224,8 +224,8 @@ export default class Othello {
 
 				// 座標が指し示す位置がボード外に出たとき
 				if (this.opts.loopedBoard) {
-					if (x < 0            ) x = this.mapWidth - (-x);
-					if (y < 0            ) y = this.mapHeight - (-y);
+					if (x <  0             ) x = this.mapWidth - (-x);
+					if (y <  0             ) y = this.mapHeight - (-y);
 					if (x >= this.mapWidth ) x = x - this.mapWidth;
 					if (y >= this.mapHeight) y = y - this.mapHeight;
 

From 8107c73ca7e17d3697ac1be47f0b8f20a28dcd7f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 21:48:20 +0900
Subject: [PATCH 0715/1250] Fix bug

---
 src/common/othello/core.ts | 13 +++++++++----
 src/common/othello/maps.ts | 19 +++++++++++++++++++
 2 files changed, 28 insertions(+), 4 deletions(-)

diff --git a/src/common/othello/core.ts b/src/common/othello/core.ts
index e7e725aaf..3a181a3eb 100644
--- a/src/common/othello/core.ts
+++ b/src/common/othello/core.ts
@@ -224,10 +224,15 @@ export default class Othello {
 
 				// 座標が指し示す位置がボード外に出たとき
 				if (this.opts.loopedBoard) {
-					if (x <  0             ) x = this.mapWidth - (-x);
-					if (y <  0             ) y = this.mapHeight - (-y);
-					if (x >= this.mapWidth ) x = x - this.mapWidth;
-					if (y >= this.mapHeight) y = y - this.mapHeight;
+					if (x <  0             ) x = this.mapWidth - ((-x) % this.mapWidth);
+					if (y <  0             ) y = this.mapHeight - ((-y) % this.mapHeight);
+					if (x >= this.mapWidth ) x = x % this.mapWidth;
+					if (y >= this.mapHeight) y = y % this.mapHeight;
+
+					// for debug
+					//if (x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight) {
+					//	console.log(x, y);
+					//}
 
 					// 一周して自分に帰ってきたら
 					if (this.transformXyToPos(x, y) == initPos) break;
diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index 3518259bb..81b9663c3 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -815,3 +815,22 @@ export const test2: Map = {
 		'-w--b-'
 	]
 };
+
+export const test3: Map = {
+	name: 'Test3',
+	category: 'Test',
+	data: [
+		'-w-',
+		'--w',
+		'w--',
+		'-w-',
+		'--w',
+		'w--',
+		'-w-',
+		'--w',
+		'w--',
+		'-w-',
+		'---',
+		'b--',
+	]
+};

From b02f02c2b3b0da984dff8df15a9efca7d4e0c823 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 21:48:27 +0900
Subject: [PATCH 0716/1250] v4092

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 51c5024f4..cb979e442 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4090",
+	"version": "0.0.4092",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From f762117686fb86afa3d042279b12ad828f92bba2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 22:23:14 +0900
Subject: [PATCH 0717/1250] :v:

---
 src/common/othello/core.ts |  9 ++++++++-
 src/common/othello/maps.ts | 12 ++++++++++++
 2 files changed, 20 insertions(+), 1 deletion(-)

diff --git a/src/common/othello/core.ts b/src/common/othello/core.ts
index 3a181a3eb..bd04ab768 100644
--- a/src/common/othello/core.ts
+++ b/src/common/othello/core.ts
@@ -235,7 +235,14 @@ export default class Othello {
 					//}
 
 					// 一周して自分に帰ってきたら
-					if (this.transformXyToPos(x, y) == initPos) break;
+					if (this.transformXyToPos(x, y) == initPos) {
+						// ↓のコメントアウトを外すと、「現時点で自分の石が隣接していないが、
+						// そこに置いたとするとループして最終的に挟んだことになる」というケースを有効化します。(Test4のマップで違いが分かります)
+						// このケースを有効にした方が良いのか無効にした方が良いのか判断がつかなかったためとりあえず無効としておきます
+						// (あと無効な方がゲームとしておもしろそうだった)
+						//stones = stones.concat(found);
+						break;
+					}
 				} else {
 					if (x == -1 || y == -1 || x == this.mapWidth || y == this.mapHeight) break;
 				}
diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index 81b9663c3..1382cac03 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -834,3 +834,15 @@ export const test3: Map = {
 		'b--',
 	]
 };
+
+export const test4: Map = {
+	name: 'Test4',
+	category: 'Test',
+	data: [
+		'-w--b-',
+		'-w--b-',
+		'------',
+		'-w--b-',
+		'-w--b-'
+	]
+};

From ffe8f419f84bf7ae8be4b42ea9ce735b629cef72 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Mar 2018 22:30:38 +0900
Subject: [PATCH 0718/1250] =?UTF-8?q?othello/games/show=20=E3=81=AB?=
 =?UTF-8?q?=E3=83=9C=E3=83=BC=E3=83=89=E3=81=AE=E7=8A=B6=E6=85=8B=E3=82=84?=
 =?UTF-8?q?=E3=82=BF=E3=83=BC=E3=83=B3=E6=83=85=E5=A0=B1=E3=82=92=E5=90=AB?=
 =?UTF-8?q?=E3=82=81=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/endpoints/othello/games/show.ts | 18 +++++++++++++++++-
 1 file changed, 17 insertions(+), 1 deletion(-)

diff --git a/src/api/endpoints/othello/games/show.ts b/src/api/endpoints/othello/games/show.ts
index 9dc8f2490..2b0db4dd0 100644
--- a/src/api/endpoints/othello/games/show.ts
+++ b/src/api/endpoints/othello/games/show.ts
@@ -1,5 +1,6 @@
 import $ from 'cafy';
 import Game, { pack } from '../../../models/othello-game';
+import Othello from '../../../../common/othello/core';
 
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Get 'game_id' parameter
@@ -12,5 +13,20 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		return rej('game not found');
 	}
 
-	res(await pack(game, user));
+	const o = new Othello(game.settings.map, {
+		isLlotheo: game.settings.is_llotheo,
+		canPutEverywhere: game.settings.can_put_everywhere,
+		loopedBoard: game.settings.looped_board
+	});
+
+	game.logs.forEach(log => {
+		o.put(log.color, log.pos);
+	});
+
+	const packed = await pack(game, user);
+
+	res(Object.assign({
+		board: o.board,
+		turn: o.turn
+	}, packed));
 });

From 4872f5b58dfd9d26a61b220009cd2585dcbf13d3 Mon Sep 17 00:00:00 2001
From: Aya Morisawa <AyaMorisawa4869@gmail.com>
Date: Sat, 10 Mar 2018 23:36:05 +0900
Subject: [PATCH 0719/1250] Add 8x8 handicap 20 map

---
 src/common/othello/maps.ts | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index 1382cac03..a4b8799b4 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -157,6 +157,21 @@ export const eighteightH12: Map = {
 	]
 };
 
+export const eighteightH20: Map = {
+	name: '8x8 handicap 20',
+	category: '8x8',
+	data: [
+		'bbb--bbb',
+		'b------b',
+		'b------b',
+		'---wb---',
+		'---bw---',
+		'b------b',
+		'b------b',
+		'bbb---bb'
+	]
+};
+
 export const eighteightH28: Map = {
 	name: '8x8 handicap 28',
 	category: '8x8',

From 926e5a4da3c2ca4ec5bc2c0cff5dfee26f016c04 Mon Sep 17 00:00:00 2001
From: Aya Morisawa <AyaMorisawa4869@gmail.com>
Date: Sun, 11 Mar 2018 00:04:25 +0900
Subject: [PATCH 0720/1250] Add 8x8 handicap 16 map

---
 src/common/othello/maps.ts | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index a4b8799b4..7f38d8c67 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -157,6 +157,21 @@ export const eighteightH12: Map = {
 	]
 };
 
+export const eighteightH16: Map = {
+	name: '8x8 handicap 16',
+	category: '8x8',
+	data: [
+		'bbb---bb',
+		'b------b',
+		'-------b',
+		'---wb---',
+		'---bw---',
+		'b-------',
+		'b------b',
+		'bb---bbb'
+	]
+};
+
 export const eighteightH20: Map = {
 	name: '8x8 handicap 20',
 	category: '8x8',

From 59d3e7aaa99a997c564a92016c57e292a41c8080 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 11 Mar 2018 01:55:46 +0900
Subject: [PATCH 0721/1250] #1232

---
 .../app/common/views/components/othello.game.vue  | 15 +++++++++++++++
 src/web/assets/othello-put-me.mp3                 |  3 +++
 src/web/assets/othello-put-you.mp3                |  3 +++
 3 files changed, 21 insertions(+)
 create mode 100644 src/web/assets/othello-put-me.mp3
 create mode 100644 src/web/assets/othello-put-you.mp3

diff --git a/src/web/app/common/views/components/othello.game.vue b/src/web/app/common/views/components/othello.game.vue
index a84dcedd4..77be45887 100644
--- a/src/web/app/common/views/components/othello.game.vue
+++ b/src/web/app/common/views/components/othello.game.vue
@@ -38,6 +38,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import Othello, { Color } from '../../../../../common/othello/core';
+import { url } from '../../../config';
 
 export default Vue.extend({
 	props: ['game', 'connection'],
@@ -134,6 +135,13 @@ export default Vue.extend({
 
 			this.o.put(this.myColor, pos);
 
+			// サウンドを再生する
+			if ((this as any).os.isEnableSounds) {
+				const sound = new Audio(`${url}/assets/othello-put-me.mp3`);
+				sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 1;
+				sound.play();
+			}
+
 			this.connection.send({
 				type: 'set',
 				pos
@@ -150,6 +158,13 @@ export default Vue.extend({
 			this.o.put(x.color, x.pos);
 			this.checkEnd();
 			this.$forceUpdate();
+
+			// サウンドを再生する
+			if ((this as any).os.isEnableSounds && x.color != this.myColor) {
+				const sound = new Audio(`${url}/assets/othello-put-you.mp3`);
+				sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 1;
+				sound.play();
+			}
 		},
 
 		checkEnd() {
diff --git a/src/web/assets/othello-put-me.mp3 b/src/web/assets/othello-put-me.mp3
new file mode 100644
index 000000000..7c2d02582
--- /dev/null
+++ b/src/web/assets/othello-put-me.mp3
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bf29c3b4d76d2c548ac28fc91772524d33aea65f45fecb9c7d97b6adfb436060
+size 15672
diff --git a/src/web/assets/othello-put-you.mp3 b/src/web/assets/othello-put-you.mp3
new file mode 100644
index 000000000..c1e86f062
--- /dev/null
+++ b/src/web/assets/othello-put-you.mp3
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7802a1f89d72a036c7a3bab4a115dca50302ba47d06e9c8a6379550553484d54
+size 26121

From 70e57ae3234ef49886202225ec8c8006cc922e38 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 11 Mar 2018 01:56:13 +0900
Subject: [PATCH 0722/1250] v4101

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index cb979e442..a1339a064 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4092",
+	"version": "0.0.4101",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 32006e9894ca570eeec22e616de4428bbcc1c44b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 11 Mar 2018 02:17:41 +0900
Subject: [PATCH 0723/1250] Enable wwbww

---
 src/common/othello/core.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/common/othello/core.ts b/src/common/othello/core.ts
index bd04ab768..848cf486e 100644
--- a/src/common/othello/core.ts
+++ b/src/common/othello/core.ts
@@ -240,7 +240,7 @@ export default class Othello {
 						// そこに置いたとするとループして最終的に挟んだことになる」というケースを有効化します。(Test4のマップで違いが分かります)
 						// このケースを有効にした方が良いのか無効にした方が良いのか判断がつかなかったためとりあえず無効としておきます
 						// (あと無効な方がゲームとしておもしろそうだった)
-						//stones = stones.concat(found);
+						stones = stones.concat(found);
 						break;
 					}
 				} else {

From cf37e626ee542151bc75bc7943ef57a32d9e006b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 11 Mar 2018 02:17:48 +0900
Subject: [PATCH 0724/1250] v4103

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index a1339a064..4c72310a7 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4101",
+	"version": "0.0.4103",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 5f49a1690b0c4760ed347c48bf9c415f7d91413b Mon Sep 17 00:00:00 2001
From: Aya Morisawa <AyaMorisawa4869@gmail.com>
Date: Sun, 11 Mar 2018 04:53:00 +0900
Subject: [PATCH 0725/1250] Add Lack of Black map

---
 src/common/othello/maps.ts | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index 7f38d8c67..2d4a55b5d 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -378,6 +378,21 @@ export const parallel: Map = {
 	]
 };
 
+export const lackOfBlack: Map = {
+	name: 'Lack of Black',
+	category: '8x8',
+	data: [
+		'--------',
+		'--------',
+		'--------',
+		'---w----',
+		'---bw---',
+		'--------',
+		'--------',
+		'--------'
+	]
+};
+
 export const squareParty: Map = {
 	name: 'Square Party',
 	category: '8x8',

From e352ff574c10415975b1bbb624c5814c024b02b7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 11 Mar 2018 07:07:17 +0900
Subject: [PATCH 0726/1250] :v:

---
 src/api/models/othello-game.ts                |   2 +
 src/api/stream/othello-game.ts                |  76 ++++++
 .../common/views/components/othello.room.vue  | 230 +++++++++++++-----
 3 files changed, 253 insertions(+), 55 deletions(-)

diff --git a/src/api/models/othello-game.ts b/src/api/models/othello-game.ts
index a8c302510..ab90cffa4 100644
--- a/src/api/models/othello-game.ts
+++ b/src/api/models/othello-game.ts
@@ -33,6 +33,8 @@ export interface IGame {
 		can_put_everywhere: boolean;
 		looped_board: boolean;
 	};
+	form1: any;
+	form2: any;
 }
 
 /**
diff --git a/src/api/stream/othello-game.ts b/src/api/stream/othello-game.ts
index 5f61f0cc2..888c59933 100644
--- a/src/api/stream/othello-game.ts
+++ b/src/api/stream/othello-game.ts
@@ -31,6 +31,21 @@ export default function(request: websocket.request, connection: websocket.connec
 				updateSettings(msg.settings);
 				break;
 
+			case 'init-form':
+				if (msg.body == null) return;
+				initForm(msg.body);
+				break;
+
+			case 'update-form':
+				if (msg.id == null || msg.value === undefined) return;
+				updateForm(msg.id, msg.value);
+				break;
+
+			case 'message':
+				if (msg.body == null) return;
+				message(msg.body);
+				break;
+
 			case 'set':
 				if (msg.pos == null) return;
 				set(msg.pos);
@@ -55,6 +70,67 @@ export default function(request: websocket.request, connection: websocket.connec
 		publishOthelloGameStream(gameId, 'update-settings', settings);
 	}
 
+	async function initForm(form) {
+		const game = await Game.findOne({ _id: gameId });
+
+		if (game.is_started) return;
+		if (!game.user1_id.equals(user._id) && !game.user2_id.equals(user._id)) return;
+
+		const set = game.user1_id.equals(user._id) ? {
+			form1: form
+		} : {
+			form2: form
+		};
+
+		await Game.update({ _id: gameId }, {
+			$set: set
+		});
+
+		publishOthelloGameStream(gameId, 'init-form', {
+			user_id: user._id,
+			form
+		});
+	}
+
+	async function updateForm(id, value) {
+		const game = await Game.findOne({ _id: gameId });
+
+		if (game.is_started) return;
+		if (!game.user1_id.equals(user._id) && !game.user2_id.equals(user._id)) return;
+
+		const form = game.user1_id.equals(user._id) ? game.form2 : game.form1;
+
+		const item = form.find(i => i.id == id);
+
+		if (item == null) return;
+
+		item.value = value;
+
+		const set = game.user1_id.equals(user._id) ? {
+			form2: form
+		} : {
+			form1: form
+		};
+
+		await Game.update({ _id: gameId }, {
+			$set: set
+		});
+
+		publishOthelloGameStream(gameId, 'update-form', {
+			user_id: user._id,
+			id,
+			value
+		});
+	}
+
+	async function message(message) {
+		message.id = Math.random();
+		publishOthelloGameStream(gameId, 'message', {
+			user_id: user._id,
+			message
+		});
+	}
+
 	async function accept(accept: boolean) {
 		const game = await Game.findOne({ _id: gameId });
 
diff --git a/src/web/app/common/views/components/othello.room.vue b/src/web/app/common/views/components/othello.room.vue
index dfdc43ef9..bdefcdc49 100644
--- a/src/web/app/common/views/components/othello.room.vue
+++ b/src/web/app/common/views/components/othello.room.vue
@@ -2,37 +2,77 @@
 <div class="root">
 	<header><b>{{ game.user1.name }}</b> vs <b>{{ game.user2.name }}</b></header>
 
-	<p>ゲームの設定</p>
+	<div>
+		<p>ゲームの設定</p>
 
-	<el-select class="map" v-model="mapName" placeholder="マップを選択" @change="onMapChange">
-		<el-option label="ランダム" :value="null"/>
-		<el-option-group v-for="c in mapCategories" :key="c" :label="c">
-			<el-option v-for="m in maps" v-if="m.category == c" :key="m.name" :label="m.name" :value="m.name">
-				<span style="float: left">{{ m.name }}</span>
-				<span style="float: right; color: #8492a6; font-size: 13px" v-if="m.author">(by <i>{{ m.author }}</i>)</span>
-			</el-option>
-		</el-option-group>
-	</el-select>
+		<el-card class="map">
+			<div slot="header">
+				<el-select :class="$style.mapSelect" v-model="mapName" placeholder="マップを選択" @change="onMapChange">
+					<el-option label="ランダム" :value="null"/>
+					<el-option-group v-for="c in mapCategories" :key="c" :label="c">
+						<el-option v-for="m in maps" v-if="m.category == c" :key="m.name" :label="m.name" :value="m.name">
+							<span style="float: left">{{ m.name }}</span>
+							<span style="float: right; color: #8492a6; font-size: 13px" v-if="m.author">(by <i>{{ m.author }}</i>)</span>
+						</el-option>
+					</el-option-group>
+				</el-select>
+			</div>
+			<div :class="$style.board" v-if="game.settings.map != null" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map[0].length }, 1fr)` }">
+				<div v-for="(x, i) in game.settings.map.join('')"
+					:class="{ none: x == ' ' }"
+					@click="onPixelClick(i, x)"
+				>
+					<template v-if="x == 'b'">%fa:circle%</template>
+					<template v-if="x == 'w'">%fa:circle R%</template>
+				</div>
+			</div>
+		</el-card>
 
-	<div class="board" v-if="game.settings.map != null" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map[0].length }, 1fr)` }">
-		<div v-for="(x, i) in game.settings.map.join('')"
-			:class="{ none: x == ' ' }"
-			@click="onPixelClick(i, x)"
-		>
-			<template v-if="x == 'b'">%fa:circle%</template>
-			<template v-if="x == 'w'">%fa:circle R%</template>
-		</div>
-	</div>
-
-	<div class="rules">
-		<mk-switch v-model="game.settings.is_llotheo" @change="updateSettings" text="石の少ない方が勝ち(ロセオ)"/>
-		<mk-switch v-model="game.settings.looped_board" @change="updateSettings" text="ループマップ"/>
-		<mk-switch v-model="game.settings.can_put_everywhere" @change="updateSettings" text="どこでも置けるモード"/>
-		<div>
+		<el-card class="bw">
+			<div slot="header">
+				<span>先手/後手</span>
+			</div>
 			<el-radio v-model="game.settings.bw" label="random" @change="updateSettings">ランダム</el-radio>
 			<el-radio v-model="game.settings.bw" :label="1" @change="updateSettings">{{ game.user1.name }}が黒</el-radio>
 			<el-radio v-model="game.settings.bw" :label="2" @change="updateSettings">{{ game.user2.name }}が黒</el-radio>
-		</div>
+		</el-card>
+
+		<el-card class="rules">
+			<div slot="header">
+				<span>ルール</span>
+			</div>
+			<mk-switch v-model="game.settings.is_llotheo" @change="updateSettings" text="石の少ない方が勝ち(ロセオ)"/>
+			<mk-switch v-model="game.settings.looped_board" @change="updateSettings" text="ループマップ"/>
+			<mk-switch v-model="game.settings.can_put_everywhere" @change="updateSettings" text="どこでも置けるモード"/>
+		</el-card>
+
+		<el-card class="bot-form" v-if="form">
+			<div slot="header">
+				<span>Botの設定</span>
+			</div>
+			<el-alert v-for="message in messages"
+				:title="message.text"
+				:type="message.type"
+				:key="message.id"
+			/>
+			<template v-for="item in form">
+				<mk-switch v-if="item.type == 'button'" v-model="item.value" :key="item.id" :text="item.label" @change="onChangeForm($event, item)">{{ item.desc || '' }}</mk-switch>
+
+				<el-card v-if="item.type == 'radio'" :key="item.id">
+					<div slot="header">
+						<span>{{ item.label }}</span>
+					</div>
+					<el-radio v-for="(r, i) in item.items" :key="item.id + ':' + i" v-model="item.value" :label="r.value" @change="onChangeForm($event, item)">{{ r.label }}</el-radio>
+				</el-card>
+
+				<el-card v-if="item.type == 'textbox'" :key="item.id">
+					<div slot="header">
+						<span>{{ item.label }}</span>
+					</div>
+					<el-input v-model="item.value" @change="onChangeForm($event, item)"/>
+				</el-card>
+			</template>
+		</el-card>
 	</div>
 
 	<footer>
@@ -64,7 +104,9 @@ export default Vue.extend({
 			o: null,
 			isLlotheo: false,
 			mapName: maps.eighteight.name,
-			maps: maps
+			maps: maps,
+			form: null,
+			messages: []
 		};
 	},
 
@@ -88,11 +130,56 @@ export default Vue.extend({
 	created() {
 		this.connection.on('change-accepts', this.onChangeAccepts);
 		this.connection.on('update-settings', this.onUpdateSettings);
+		this.connection.on('init-form', this.onInitForm);
+		this.connection.on('message', this.onMessage);
+
+		if (this.game.user1_id != (this as any).os.i.id && this.game.settings.form1) this.form = this.game.settings.form1;
+		if (this.game.user2_id != (this as any).os.i.id && this.game.settings.form2) this.form = this.game.settings.form2;
+
+		// for debugging
+		if ((this as any).os.i.username == 'test1') {
+			setTimeout(() => {
+				this.connection.send({
+					type: 'init-form',
+					body: [{
+						id: 'button1',
+						type: 'button',
+						label: 'Enable hoge',
+						value: false
+					}, {
+						id: 'radio1',
+						type: 'radio',
+						label: '強さ',
+						value: 2,
+						items: [{
+							label: '弱',
+							value: 1
+						}, {
+							label: '中',
+							value: 2
+						}, {
+							label: '強',
+							value: 3
+						}]
+					}]
+				});
+
+				this.connection.send({
+					type: 'message',
+					body: {
+						text: 'Hey',
+						type: 'info'
+					}
+				});
+			}, 2000);
+		}
 	},
 
 	beforeDestroy() {
 		this.connection.off('change-accepts', this.onChangeAccepts);
 		this.connection.off('update-settings', this.onUpdateSettings);
+		this.connection.off('init-form', this.onInitForm);
+		this.connection.off('message', this.onMessage);
 	},
 
 	methods: {
@@ -135,6 +222,24 @@ export default Vue.extend({
 			}
 		},
 
+		onInitForm(x) {
+			if (x.user_id == (this as any).os.i.id) return;
+			this.form = x.form;
+		},
+
+		onMessage(x) {
+			if (x.user_id == (this as any).os.i.id) return;
+			this.messages.unshift(x.message);
+		},
+
+		onChangeForm(v, item) {
+			this.connection.send({
+				type: 'update-form',
+				id: item.id,
+				value: v
+			});
+		},
+
 		onMapChange(v) {
 			if (v == null) {
 				this.game.settings.map = null;
@@ -168,40 +273,21 @@ export default Vue.extend({
 
 .root
 	text-align center
+	background #f9f9f9
 
 	> header
 		padding 8px
 		border-bottom dashed 1px #c4cdd4
 
-	> .map
-		width 300px
+	> div
+		padding 0 16px
 
-	> .board
-		display grid
-		grid-gap 4px
-		width 300px
-		height 300px
-		margin 16px auto
-
-		> div
-			background transparent
-			border solid 2px #ddd
-			border-radius 6px
-			overflow hidden
-			cursor pointer
-
-			*
-				pointer-events none
-				user-select none
-				width 100%
-				height 100%
-
-			&.none
-				border-color transparent
-
-	> .rules
-		max-width 300px
-		margin 0 auto 32px auto
+		> .map
+		> .bw
+		> .rules
+		> .bot-form
+			max-width 400px
+			margin 0 auto 16px auto
 
 	> footer
 		position sticky
@@ -213,3 +299,37 @@ export default Vue.extend({
 		> .status
 			margin 0 0 16px 0
 </style>
+
+<style lang="stylus" module>
+.mapSelect
+	width 100%
+
+.board
+	display grid
+	grid-gap 4px
+	width 300px
+	height 300px
+	margin 0 auto
+
+	> div
+		background transparent
+		border solid 2px #ddd
+		border-radius 6px
+		overflow hidden
+		cursor pointer
+
+		*
+			pointer-events none
+			user-select none
+			width 100%
+			height 100%
+
+		&.none
+			border-color transparent
+
+</style>
+
+<style lang="stylus">
+.el-alert__content
+	position initial !important
+</style>

From bd3714e7c96edc29d53b16d31fa4719eda8b7812 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 11 Mar 2018 07:07:43 +0900
Subject: [PATCH 0727/1250] v4107

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 4c72310a7..840fe5a73 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4103",
+	"version": "0.0.4107",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 6d383893dcdc8565eccbc64315908c03485f9c2b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 11 Mar 2018 17:23:59 +0900
Subject: [PATCH 0728/1250] #1230

---
 package.json                                  |  1 +
 src/api/models/othello-game.ts                |  3 ++
 src/api/stream/othello-game.ts                | 28 +++++++++++-
 .../common/views/components/othello.game.vue  | 45 ++++++++++++++++++-
 .../views/components/othello.gameroom.vue     |  2 +-
 5 files changed, 75 insertions(+), 4 deletions(-)

diff --git a/package.json b/package.json
index 840fe5a73..e3ce2f4d2 100644
--- a/package.json
+++ b/package.json
@@ -92,6 +92,7 @@
 		"compression": "1.7.2",
 		"cookie": "0.3.1",
 		"cors": "2.8.4",
+		"crc-32": "^1.2.0",
 		"css-loader": "0.28.10",
 		"debug": "3.1.0",
 		"deep-equal": "1.0.1",
diff --git a/src/api/models/othello-game.ts b/src/api/models/othello-game.ts
index ab90cffa4..788fb5cba 100644
--- a/src/api/models/othello-game.ts
+++ b/src/api/models/othello-game.ts
@@ -35,6 +35,9 @@ export interface IGame {
 	};
 	form1: any;
 	form2: any;
+
+	// ログのposを文字列としてすべて連結したもののCRC32値
+	crc32: string;
 }
 
 /**
diff --git a/src/api/stream/othello-game.ts b/src/api/stream/othello-game.ts
index 888c59933..e2ecce38e 100644
--- a/src/api/stream/othello-game.ts
+++ b/src/api/stream/othello-game.ts
@@ -1,5 +1,6 @@
 import * as websocket from 'websocket';
 import * as redis from 'redis';
+import * as CRC32 from 'crc-32';
 import Game, { pack } from '../models/othello-game';
 import { publishOthelloGameStream } from '../event';
 import Othello from '../../common/othello/core';
@@ -50,6 +51,11 @@ export default function(request: websocket.request, connection: websocket.connec
 				if (msg.pos == null) return;
 				set(msg.pos);
 				break;
+
+			case 'check':
+				if (msg.crc32 == null) return;
+				check(msg.crc32);
+				break;
 		}
 	});
 
@@ -231,11 +237,12 @@ export default function(request: websocket.request, connection: websocket.connec
 				}
 				//#endregion
 
-				publishOthelloGameStream(gameId, 'started', await pack(gameId));
+				publishOthelloGameStream(gameId, 'started', await pack(gameId, user));
 			}, 3000);
 		}
 	}
 
+	// 石を打つ
 	async function set(pos) {
 		const game = await Game.findOne({ _id: gameId });
 
@@ -278,10 +285,13 @@ export default function(request: websocket.request, connection: websocket.connec
 			pos
 		};
 
+		const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString());
+
 		await Game.update({
 			_id: gameId
 		}, {
 			$set: {
+				crc32,
 				is_ended: o.isEnded,
 				winner_id: winner
 			},
@@ -300,4 +310,20 @@ export default function(request: websocket.request, connection: websocket.connec
 			});
 		}
 	}
+
+	async function check(crc32) {
+		const game = await Game.findOne({ _id: gameId });
+
+		if (!game.is_started) return;
+
+		// 互換性のため
+		if (game.crc32 == null) return;
+
+		if (crc32 !== game.crc32) {
+			connection.send(JSON.stringify({
+				type: 'rescue',
+				body: await pack(game, user)
+			}));
+		}
+	}
 }
diff --git a/src/web/app/common/views/components/othello.game.vue b/src/web/app/common/views/components/othello.game.vue
index 77be45887..01148f193 100644
--- a/src/web/app/common/views/components/othello.game.vue
+++ b/src/web/app/common/views/components/othello.game.vue
@@ -37,17 +37,20 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import * as CRC32 from 'crc-32';
 import Othello, { Color } from '../../../../../common/othello/core';
 import { url } from '../../../config';
 
 export default Vue.extend({
-	props: ['game', 'connection'],
+	props: ['initGame', 'connection'],
 
 	data() {
 		return {
+			game: null,
 			o: null as Othello,
 			logs: [],
-			logPos: 0
+			logPos: 0,
+			pollingClock: null
 		};
 	},
 
@@ -104,6 +107,8 @@ export default Vue.extend({
 	},
 
 	created() {
+		this.game = this.initGame;
+
 		this.o = new Othello(this.game.settings.map, {
 			isLlotheo: this.game.settings.is_llotheo,
 			canPutEverywhere: this.game.settings.can_put_everywhere,
@@ -116,14 +121,29 @@ export default Vue.extend({
 
 		this.logs = this.game.logs;
 		this.logPos = this.logs.length;
+
+		// 通信を取りこぼしてもいいように定期的にポーリングさせる
+		if (this.game.is_started && !this.game.is_ended) {
+			this.pollingClock = setInterval(() => {
+				const crc32 = CRC32.str(this.logs.map(x => x.pos.toString()).join(''));
+				this.connection.send({
+					type: 'check',
+					crc32
+				});
+			}, 10000);
+		}
 	},
 
 	mounted() {
 		this.connection.on('set', this.onSet);
+		this.connection.on('rescue', this.onRescue);
 	},
 
 	beforeDestroy() {
 		this.connection.off('set', this.onSet);
+		this.connection.off('rescue', this.onRescue);
+
+		clearInterval(this.pollingClock);
 	},
 
 	methods: {
@@ -181,6 +201,27 @@ export default Vue.extend({
 					this.game.winner = null;
 				}
 			}
+		},
+
+		// 正しいゲーム情報が送られてきたとき
+		onRescue(game) {
+			this.game = game;
+
+			this.o = new Othello(this.game.settings.map, {
+				isLlotheo: this.game.settings.is_llotheo,
+				canPutEverywhere: this.game.settings.can_put_everywhere,
+				loopedBoard: this.game.settings.looped_board
+			});
+
+			this.game.logs.forEach(log => {
+				this.o.put(log.color, log.pos);
+			});
+
+			this.logs = this.game.logs;
+			this.logPos = this.logs.length;
+
+			this.checkEnd();
+			this.$forceUpdate();
 		}
 	}
 });
diff --git a/src/web/app/common/views/components/othello.gameroom.vue b/src/web/app/common/views/components/othello.gameroom.vue
index 9f4037515..9df458f64 100644
--- a/src/web/app/common/views/components/othello.gameroom.vue
+++ b/src/web/app/common/views/components/othello.gameroom.vue
@@ -1,7 +1,7 @@
 <template>
 <div>
 	<x-room v-if="!g.is_started" :game="g" :connection="connection"/>
-	<x-game v-else :game="g" :connection="connection"/>
+	<x-game v-else :init-game="g" :connection="connection"/>
 </div>
 </template>
 

From b5b8cfd3c352f9dde82a664535b285ebe0370540 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 11 Mar 2018 17:26:07 +0900
Subject: [PATCH 0729/1250] #1231

---
 src/api/stream/othello-game.ts | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/api/stream/othello-game.ts b/src/api/stream/othello-game.ts
index e2ecce38e..87d26e241 100644
--- a/src/api/stream/othello-game.ts
+++ b/src/api/stream/othello-game.ts
@@ -232,7 +232,8 @@ export default function(request: websocket.request, connection: websocket.connec
 					});
 
 					publishOthelloGameStream(gameId, 'ended', {
-						winner_id: winner
+						winner_id: winner,
+						game: await pack(gameId, user)
 					});
 				}
 				//#endregion

From ee0485045d6fed60ac2768227981a2d94478a59e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 11 Mar 2018 18:08:26 +0900
Subject: [PATCH 0730/1250] #1204

---
 src/api/endpoints/othello/match.ts            | 16 ++++++++++++++--
 src/api/models/othello-matching.ts            |  2 ++
 src/api/stream/othello.ts                     | 19 +++++++++++++++++++
 .../app/common/views/components/othello.vue   | 17 ++++++++++++++++-
 .../views/components/ui.header.nav.vue        | 16 ++++++++++++++--
 .../app/mobile/views/components/ui.header.vue | 13 ++++++++++++-
 .../app/mobile/views/components/ui.nav.vue    | 13 ++++++++++++-
 7 files changed, 89 insertions(+), 7 deletions(-)

diff --git a/src/api/endpoints/othello/match.ts b/src/api/endpoints/othello/match.ts
index 640be9cb5..b73e105ef 100644
--- a/src/api/endpoints/othello/match.ts
+++ b/src/api/endpoints/othello/match.ts
@@ -2,7 +2,7 @@ import $ from 'cafy';
 import Matching, { pack as packMatching } from '../../models/othello-matching';
 import Game, { pack as packGame } from '../../models/othello-game';
 import User from '../../models/user';
-import { publishOthelloStream } from '../../event';
+import publishUserStream, { publishOthelloStream } from '../../event';
 import { eighteight } from '../../../common/othello/maps';
 
 module.exports = (params, user) => new Promise(async (res, rej) => {
@@ -48,6 +48,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		res(await packGame(game, user));
 
 		publishOthelloStream(exist.parent_id, 'matched', await packGame(game, exist.parent_id));
+
+		const other = await Matching.count({
+			child_id: user._id
+		});
+
+		if (other == 0) {
+			publishUserStream(user._id, 'othello_no_invites');
+		}
 	} else {
 		// Fetch child
 		const child = await User.findOne({
@@ -77,7 +85,11 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		// Reponse
 		res();
 
+		const packed = await packMatching(matching, child);
+
 		// 招待
-		publishOthelloStream(child._id, 'invited', await packMatching(matching, child));
+		publishOthelloStream(child._id, 'invited', packed);
+
+		publishUserStream(child._id, 'othello_invited', packed);
 	}
 });
diff --git a/src/api/models/othello-matching.ts b/src/api/models/othello-matching.ts
index 89fcd6df6..5cc39cae1 100644
--- a/src/api/models/othello-matching.ts
+++ b/src/api/models/othello-matching.ts
@@ -32,6 +32,8 @@ export const pack = (
 
 	const _matching = deepcopy(matching);
 
+	// Rename _id to id
+	_matching.id = _matching._id;
 	delete _matching._id;
 
 	// Populate user
diff --git a/src/api/stream/othello.ts b/src/api/stream/othello.ts
index 5056eb535..bd3b4a763 100644
--- a/src/api/stream/othello.ts
+++ b/src/api/stream/othello.ts
@@ -1,5 +1,8 @@
+import * as mongo from 'mongodb';
 import * as websocket from 'websocket';
 import * as redis from 'redis';
+import Matching, { pack } from '../models/othello-matching';
+import publishUserStream from '../event';
 
 export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void {
 	// Subscribe othello stream
@@ -7,4 +10,20 @@ export default function(request: websocket.request, connection: websocket.connec
 	subscriber.on('message', (_, data) => {
 		connection.send(data);
 	});
+
+	connection.on('message', async (data) => {
+		const msg = JSON.parse(data.utf8Data);
+
+		switch (msg.type) {
+			case 'ping':
+				if (msg.id == null) return;
+				const matching = await Matching.findOne({
+					parent_id: user._id,
+					child_id: new mongo.ObjectID(msg.id)
+				});
+				if (matching == null) return;
+				publishUserStream(matching.child_id, 'othello_invited', await pack(matching, matching.child_id));
+				break;
+		}
+	});
 }
diff --git a/src/web/app/common/views/components/othello.vue b/src/web/app/common/views/components/othello.vue
index 81da02d1c..d65032234 100644
--- a/src/web/app/common/views/components/othello.vue
+++ b/src/web/app/common/views/components/othello.vue
@@ -78,7 +78,8 @@ export default Vue.extend({
 			matching: null,
 			invitations: [],
 			connection: null,
-			connectionId: null
+			connectionId: null,
+			pingClock: null
 		};
 	},
 	watch: {
@@ -112,17 +113,29 @@ export default Vue.extend({
 		(this as any).api('othello/invitations').then(invitations => {
 			this.invitations = this.invitations.concat(invitations);
 		});
+
+		this.pingClock = setInterval(() => {
+			if (this.matching) {
+				this.connection.send({
+					type: 'ping',
+					id: this.matching.id
+				});
+			}
+		}, 3000);
 	},
 	beforeDestroy() {
 		this.connection.off('matched', this.onMatched);
 		this.connection.off('invited', this.onInvited);
 		(this as any).os.streams.othelloStream.dispose(this.connectionId);
+
+		clearInterval(this.pingClock);
 	},
 	methods: {
 		go(game) {
 			(this as any).api('othello/games/show', {
 				game_id: game.id
 			}).then(game => {
+				this.matching = null;
 				this.game = game;
 			});
 		},
@@ -154,11 +167,13 @@ export default Vue.extend({
 				user_id: invitation.parent.id
 			}).then(game => {
 				if (game) {
+					this.matching = null;
 					this.game = game;
 				}
 			});
 		},
 		onMatched(game) {
+			this.matching = null;
 			this.game = game;
 		},
 		onInvited(invite) {
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 54045db8d..7582e8afc 100644
--- a/src/web/app/desktop/views/components/ui.header.nav.vue
+++ b/src/web/app/desktop/views/components/ui.header.nav.vue
@@ -56,6 +56,8 @@ export default Vue.extend({
 
 			this.connection.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
 			this.connection.on('unread_messaging_message', this.onUnreadMessagingMessage);
+			this.connection.on('othello_invited', this.onOthelloInvited);
+			this.connection.on('othello_no_invites', this.onOthelloNoInvites);
 
 			// Fetch count of unread messaging messages
 			(this as any).api('messaging/unread').then(res => {
@@ -69,16 +71,26 @@ export default Vue.extend({
 		if ((this as any).os.isSignedIn) {
 			this.connection.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
 			this.connection.off('unread_messaging_message', this.onUnreadMessagingMessage);
+			this.connection.off('othello_invited', this.onOthelloInvited);
+			this.connection.off('othello_no_invites', this.onOthelloNoInvites);
 			(this as any).os.stream.dispose(this.connectionId);
 		}
 	},
 	methods: {
+		onUnreadMessagingMessage() {
+			this.hasUnreadMessagingMessages = true;
+		},
+
 		onReadAllMessagingMessages() {
 			this.hasUnreadMessagingMessages = false;
 		},
 
-		onUnreadMessagingMessage() {
-			this.hasUnreadMessagingMessages = true;
+		onOthelloInvited() {
+			this.hasGameInvitations = true;
+		},
+
+		onOthelloNoInvites() {
+			this.hasGameInvitations = false;
 		},
 
 		messaging() {
diff --git a/src/web/app/mobile/views/components/ui.header.vue b/src/web/app/mobile/views/components/ui.header.vue
index f06a35fe7..1ccbd5c95 100644
--- a/src/web/app/mobile/views/components/ui.header.vue
+++ b/src/web/app/mobile/views/components/ui.header.vue
@@ -6,7 +6,7 @@
 		<p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ os.i.name }}</b>さん</p>
 		<div class="content" ref="mainContainer">
 			<button class="nav" @click="$parent.isDrawerOpening = true">%fa:bars%</button>
-			<template v-if="hasUnreadNotifications || hasUnreadMessagingMessages">%fa:circle%</template>
+			<template v-if="hasUnreadNotifications || hasUnreadMessagingMessages || hasGameInvitations">%fa:circle%</template>
 			<h1>
 				<slot>Misskey</slot>
 			</h1>
@@ -26,6 +26,7 @@ export default Vue.extend({
 		return {
 			hasUnreadNotifications: false,
 			hasUnreadMessagingMessages: false,
+			hasGameInvitations: false,
 			connection: null,
 			connectionId: null
 		};
@@ -39,6 +40,8 @@ export default Vue.extend({
 			this.connection.on('unread_notification', this.onUnreadNotification);
 			this.connection.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
 			this.connection.on('unread_messaging_message', this.onUnreadMessagingMessage);
+			this.connection.on('othello_invited', this.onOthelloInvited);
+			this.connection.on('othello_no_invites', this.onOthelloNoInvites);
 
 			// Fetch count of unread notifications
 			(this as any).api('notifications/get_unread_count').then(res => {
@@ -107,6 +110,8 @@ export default Vue.extend({
 			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.connection.off('othello_invited', this.onOthelloInvited);
+			this.connection.off('othello_no_invites', this.onOthelloNoInvites);
 			(this as any).os.stream.dispose(this.connectionId);
 		}
 	},
@@ -122,6 +127,12 @@ export default Vue.extend({
 		},
 		onUnreadMessagingMessage() {
 			this.hasUnreadMessagingMessages = true;
+		},
+		onOthelloInvited() {
+			this.hasGameInvitations = true;
+		},
+		onOthelloNoInvites() {
+			this.hasGameInvitations = false;
 		}
 	}
 });
diff --git a/src/web/app/mobile/views/components/ui.nav.vue b/src/web/app/mobile/views/components/ui.nav.vue
index ba35a2783..b8bc2fb04 100644
--- a/src/web/app/mobile/views/components/ui.nav.vue
+++ b/src/web/app/mobile/views/components/ui.nav.vue
@@ -18,7 +18,7 @@
 					<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>
-					<li><router-link to="/othello">%fa:gamepad%ゲーム%fa:angle-right%</router-link></li>
+					<li><router-link to="/othello">%fa:gamepad%ゲーム<template v-if="hasGameInvitations">%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>
@@ -47,6 +47,7 @@ export default Vue.extend({
 		return {
 			hasUnreadNotifications: false,
 			hasUnreadMessagingMessages: false,
+			hasGameInvitations: false,
 			connection: null,
 			connectionId: null,
 			aboutUrl: `${docsUrl}/${lang}/about`,
@@ -62,6 +63,8 @@ export default Vue.extend({
 			this.connection.on('unread_notification', this.onUnreadNotification);
 			this.connection.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
 			this.connection.on('unread_messaging_message', this.onUnreadMessagingMessage);
+			this.connection.on('othello_invited', this.onOthelloInvited);
+			this.connection.on('othello_no_invites', this.onOthelloNoInvites);
 
 			// Fetch count of unread notifications
 			(this as any).api('notifications/get_unread_count').then(res => {
@@ -84,6 +87,8 @@ export default Vue.extend({
 			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.connection.off('othello_invited', this.onOthelloInvited);
+			this.connection.off('othello_no_invites', this.onOthelloNoInvites);
 			(this as any).os.stream.dispose(this.connectionId);
 		}
 	},
@@ -104,6 +109,12 @@ export default Vue.extend({
 		},
 		onUnreadMessagingMessage() {
 			this.hasUnreadMessagingMessages = true;
+		},
+		onOthelloInvited() {
+			this.hasGameInvitations = true;
+		},
+		onOthelloNoInvites() {
+			this.hasGameInvitations = false;
 		}
 	}
 });

From ce57bb1b9b20d0fda2141b83474db2c86fedc768 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 11 Mar 2018 18:10:29 +0900
Subject: [PATCH 0731/1250] v4111

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index e3ce2f4d2..e220d48c0 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4107",
+	"version": "0.0.4111",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From e27acbb99038a535b440436b3d46197a6db1c4bd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 11 Mar 2018 21:48:16 +0900
Subject: [PATCH 0732/1250] :v:

---
 src/common/othello/core.ts | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/common/othello/core.ts b/src/common/othello/core.ts
index 848cf486e..39ceb921a 100644
--- a/src/common/othello/core.ts
+++ b/src/common/othello/core.ts
@@ -166,7 +166,8 @@ export default class Othello {
 	 * @param pos 位置
 	 */
 	public mapDataGet(pos: number): MapPixel {
-		if (pos < 0 || pos >= this.map.length) return 'null';
+		const [x, y] = this.transformPosToXy(pos);
+		if (x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight) return 'null';
 		return this.map[pos];
 	}
 

From 66362fb70f99010f36fb98811f548a45cc1ad063 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 11 Mar 2018 23:50:17 +0900
Subject: [PATCH 0733/1250] Oops

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

diff --git a/src/web/app/common/views/components/othello.room.vue b/src/web/app/common/views/components/othello.room.vue
index bdefcdc49..3b4296d0b 100644
--- a/src/web/app/common/views/components/othello.room.vue
+++ b/src/web/app/common/views/components/othello.room.vue
@@ -19,7 +19,7 @@
 			</div>
 			<div :class="$style.board" v-if="game.settings.map != null" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map[0].length }, 1fr)` }">
 				<div v-for="(x, i) in game.settings.map.join('')"
-					:class="{ none: x == ' ' }"
+					:data-none="x == ' '"
 					@click="onPixelClick(i, x)"
 				>
 					<template v-if="x == 'b'">%fa:circle%</template>
@@ -324,7 +324,7 @@ export default Vue.extend({
 			width 100%
 			height 100%
 
-		&.none
+		&[data-none]
 			border-color transparent
 
 </style>

From c7e56bcd13bea9bc6d8c744a4432dd5e28003cb4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 11 Mar 2018 23:50:50 +0900
Subject: [PATCH 0734/1250] v4114

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index e220d48c0..bcc8faa84 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4111",
+	"version": "0.0.4114",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 56b5328e03071a8d3c3335348bd342ef17c245da Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 12 Mar 2018 07:53:58 +0900
Subject: [PATCH 0735/1250] Update othello.game.vue

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

diff --git a/src/web/app/common/views/components/othello.game.vue b/src/web/app/common/views/components/othello.game.vue
index 01148f193..ecc9e85a8 100644
--- a/src/web/app/common/views/components/othello.game.vue
+++ b/src/web/app/common/views/components/othello.game.vue
@@ -130,7 +130,7 @@ export default Vue.extend({
 					type: 'check',
 					crc32
 				});
-			}, 10000);
+			}, 3000);
 		}
 	},
 

From 0cfcfc989b26a77d4fa6dc7370f8e2cce6b134c6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Mar 2018 01:49:54 +0900
Subject: [PATCH 0736/1250] =?UTF-8?q?=E3=82=AA=E3=82=BB=E3=83=AD=E3=81=A7?=
 =?UTF-8?q?=E9=BB=92=E7=99=BD=E3=82=92=E7=9C=9F=E7=90=86=E5=80=A4=E3=81=A7?=
 =?UTF-8?q?=E8=A1=A8=E7=8F=BE=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/models/othello-game.ts                |   6 +-
 src/api/stream/othello-game.ts                |  12 +-
 src/common/othello/core.ts                    | 115 ++++++++++++------
 .../common/views/components/othello.game.vue  |  22 ++--
 4 files changed, 98 insertions(+), 57 deletions(-)

diff --git a/src/api/models/othello-game.ts b/src/api/models/othello-game.ts
index 788fb5cba..01c6ca6c0 100644
--- a/src/api/models/othello-game.ts
+++ b/src/api/models/othello-game.ts
@@ -25,7 +25,11 @@ export interface IGame {
 	is_started: boolean;
 	is_ended: boolean;
 	winner_id: mongo.ObjectID;
-	logs: any[];
+	logs: Array<{
+		at: Date;
+		color: boolean;
+		pos: number;
+	}>;
 	settings: {
 		map: string[];
 		bw: string | number;
diff --git a/src/api/stream/othello-game.ts b/src/api/stream/othello-game.ts
index 87d26e241..368baa2cb 100644
--- a/src/api/stream/othello-game.ts
+++ b/src/api/stream/othello-game.ts
@@ -214,9 +214,9 @@ export default function(request: websocket.request, connection: websocket.connec
 
 				if (o.isEnded) {
 					let winner;
-					if (o.winner == 'black') {
+					if (o.winner === true) {
 						winner = freshGame.black == 1 ? freshGame.user1_id : freshGame.user2_id;
-					} else if (o.winner == 'white') {
+					} else if (o.winner === false) {
 						winner = freshGame.black == 1 ? freshGame.user2_id : freshGame.user1_id;
 					} else {
 						winner = null;
@@ -263,17 +263,17 @@ export default function(request: websocket.request, connection: websocket.connec
 
 		const myColor =
 			(game.user1_id.equals(user._id) && game.black == 1) || (game.user2_id.equals(user._id) && game.black == 2)
-				? 'black'
-				: 'white';
+				? true
+				: false;
 
 		if (!o.canPut(myColor, pos)) return;
 		o.put(myColor, pos);
 
 		let winner;
 		if (o.isEnded) {
-			if (o.winner == 'black') {
+			if (o.winner === true) {
 				winner = game.black == 1 ? game.user1_id : game.user2_id;
-			} else if (o.winner == 'white') {
+			} else if (o.winner === false) {
 				winner = game.black == 1 ? game.user2_id : game.user1_id;
 			} else {
 				winner = null;
diff --git a/src/common/othello/core.ts b/src/common/othello/core.ts
index 39ceb921a..54201d654 100644
--- a/src/common/othello/core.ts
+++ b/src/common/othello/core.ts
@@ -1,4 +1,11 @@
-export type Color = 'black' | 'white';
+/**
+ * true ... 黒
+ * false ... 白
+ */
+export type Color = boolean;
+const BLACK = true;
+const WHITE = false;
+
 export type MapPixel = 'null' | 'empty';
 
 export type Options = {
@@ -7,6 +14,23 @@ export type Options = {
 	loopedBoard: boolean;
 };
 
+export type Undo = {
+	/**
+	 * 色
+	 */
+	color: Color,
+
+	/**
+	 * どこに打ったか
+	 */
+	pos: number;
+
+	/**
+	 * 反転した石の位置の配列
+	 */
+	effects: number[];
+};
+
 /**
  * オセロエンジン
  */
@@ -15,19 +39,20 @@ export default class Othello {
 	public mapWidth: number;
 	public mapHeight: number;
 	public board: Color[];
-	public turn: Color = 'black';
+	public turn: Color = BLACK;
 	public opts: Options;
 
 	public prevPos = -1;
-	public stats: Array<{
-		b: number;
-		w: number;
-	}>;
+	public prevColor: Color = null;
 
 	/**
 	 * ゲームを初期化します
 	 */
 	constructor(map: string[], opts: Options) {
+		//#region binds
+		this.put = this.put.bind(this);
+		//#endregion
+
 		//#region Options
 		this.opts = opts;
 		if (this.opts.isLlotheo == null) this.opts.isLlotheo = false;
@@ -42,8 +67,8 @@ export default class Othello {
 
 		this.board = mapData.split('').map(d => {
 			if (d == '-') return null;
-			if (d == 'b') return 'black';
-			if (d == 'w') return 'white';
+			if (d == 'b') return BLACK;
+			if (d == 'w') return WHITE;
 			return undefined;
 		});
 
@@ -53,18 +78,12 @@ export default class Othello {
 		});
 		//#endregion
 
-		// Init stats
-		this.stats = [{
-			b: this.blackP,
-			w: this.whiteP
-		}];
-
 		// ゲームが始まった時点で片方の色の石しかないか、始まった時点で勝敗が決定するようなマップの場合がある
-		if (this.canPutSomewhere('black').length == 0) {
-			if (this.canPutSomewhere('white').length == 0) {
+		if (this.canPutSomewhere(BLACK).length == 0) {
+			if (this.canPutSomewhere(WHITE).length == 0) {
 				this.turn = null;
 			} else {
-				this.turn = 'white';
+				this.turn = WHITE;
 			}
 		}
 	}
@@ -73,14 +92,14 @@ export default class Othello {
 	 * 黒石の数
 	 */
 	public get blackCount() {
-		return this.board.filter(x => x == 'black').length;
+		return this.board.filter(x => x === BLACK).length;
 	}
 
 	/**
 	 * 白石の数
 	 */
 	public get whiteCount() {
-		return this.board.filter(x => x == 'white').length;
+		return this.board.filter(x => x === WHITE).length;
 	}
 
 	/**
@@ -123,36 +142,53 @@ export default class Othello {
 	 * @param color 石の色
 	 * @param pos 位置
 	 */
-	public put(color: Color, pos: number) {
-		if (!this.canPut(color, pos)) return;
+	public put(color: Color, pos: number, fast = false): Undo {
+		if (!fast && !this.canPut(color, pos)) return null;
 
 		this.prevPos = pos;
+		this.prevColor = color;
 		this.write(color, pos);
 
 		// 反転させられる石を取得
-		const reverses = this.effects(color, pos);
+		const effects = this.effects(color, pos);
 
 		// 反転させる
-		reverses.forEach(pos => {
+		effects.forEach(pos => {
 			this.write(color, pos);
 		});
 
-		this.stats.push({
-			b: this.blackP,
-			w: this.whiteP
-		});
+		this.calcTurn();
 
+		return {
+			color,
+			pos,
+			effects
+		};
+	}
+
+	private calcTurn() {
 		// ターン計算
-		const opColor = color == 'black' ? 'white' : 'black';
+		const opColor = this.prevColor === BLACK ? WHITE : BLACK;
 		if (this.canPutSomewhere(opColor).length > 0) {
-			this.turn = color == 'black' ? 'white' : 'black';
-		} else if (this.canPutSomewhere(color).length > 0) {
-			this.turn = color == 'black' ? 'black' : 'white';
+			this.turn = this.prevColor === BLACK ? WHITE : BLACK;
+		} else if (this.canPutSomewhere(this.prevColor).length > 0) {
+			this.turn = this.prevColor === BLACK ? BLACK : WHITE;
 		} else {
 			this.turn = null;
 		}
 	}
 
+	public undo(undo: Undo) {
+		this.prevColor = undo.color;
+		this.prevPos = undo.pos;
+		this.write(null, undo.pos);
+		for (const pos of undo.effects) {
+			const color = this.board[pos];
+			this.write(!color, pos);
+		}
+		this.calcTurn();
+	}
+
 	/**
 	 * 指定したマスの状態を取得します
 	 * @param pos 位置
@@ -207,8 +243,8 @@ export default class Othello {
 	 * @param color 自分の色
 	 * @param pos 位置
 	 */
-	private effects(color: Color, pos: number): number[] {
-		const enemyColor = color == 'black' ? 'white' : 'black';
+	public effects(color: Color, pos: number): number[] {
+		const enemyColor = !color;
 
 		// ひっくり返せる石(の位置)リスト
 		let stones = [];
@@ -225,7 +261,7 @@ export default class Othello {
 
 				// 座標が指し示す位置がボード外に出たとき
 				if (this.opts.loopedBoard) {
-					if (x <  0             ) x = this.mapWidth - ((-x) % this.mapWidth);
+					if (x <  0             ) x = this.mapWidth  - ((-x) % this.mapWidth);
 					if (y <  0             ) y = this.mapHeight - ((-y) % this.mapHeight);
 					if (x >= this.mapWidth ) x = x % this.mapWidth;
 					if (y >= this.mapHeight) y = y % this.mapHeight;
@@ -259,16 +295,17 @@ export default class Othello {
 				const stone = this.get(pos);
 
 				// 石が置かれていないマスなら走査終了
-				if (stone == null) break;
+				if (stone === null) break;
 
 				// 相手の石なら「ひっくり返せるかもリスト」に入れておく
-				if (stone == enemyColor) found.push(pos);
+				if (stone === enemyColor) found.push(pos);
 
 				// 自分の石なら「ひっくり返せるかもリスト」を「ひっくり返せるリスト」に入れ、走査終了
-				if (stone == color) {
+				if (stone === color) {
 					stones = stones.concat(found);
 					break;
 				}
+
 				i++;
 			}
 		};
@@ -303,9 +340,9 @@ export default class Othello {
 		if (this.blackCount == this.whiteCount) return null;
 
 		if (this.opts.isLlotheo) {
-			return this.blackCount > this.whiteCount ? 'white' : 'black';
+			return this.blackCount > this.whiteCount ? WHITE : BLACK;
 		} else {
-			return this.blackCount > this.whiteCount ? 'black' : 'white';
+			return this.blackCount > this.whiteCount ? BLACK : WHITE;
 		}
 	}
 }
diff --git a/src/web/app/common/views/components/othello.game.vue b/src/web/app/common/views/components/othello.game.vue
index 01148f193..74139f575 100644
--- a/src/web/app/common/views/components/othello.game.vue
+++ b/src/web/app/common/views/components/othello.game.vue
@@ -60,13 +60,13 @@ export default Vue.extend({
 		},
 		myColor(): Color {
 			if (!this.iAmPlayer) return null;
-			if (this.game.user1_id == (this as any).os.i.id && this.game.black == 1) return 'black';
-			if (this.game.user2_id == (this as any).os.i.id && this.game.black == 2) return 'black';
-			return 'white';
+			if (this.game.user1_id == (this as any).os.i.id && this.game.black == 1) return true;
+			if (this.game.user2_id == (this as any).os.i.id && this.game.black == 2) return true;
+			return false;
 		},
 		opColor(): Color {
 			if (!this.iAmPlayer) return null;
-			return this.myColor == 'black' ? 'white' : 'black';
+			return this.myColor === true ? false : true;
 		},
 		blackUser(): any {
 			return this.game.black == 1 ? this.game.user1 : this.game.user2;
@@ -75,9 +75,9 @@ export default Vue.extend({
 			return this.game.black == 1 ? this.game.user2 : this.game.user1;
 		},
 		turnUser(): any {
-			if (this.o.turn == 'black') {
+			if (this.o.turn === true) {
 				return this.game.black == 1 ? this.game.user1 : this.game.user2;
-			} else if (this.o.turn == 'white') {
+			} else if (this.o.turn === false) {
 				return this.game.black == 1 ? this.game.user2 : this.game.user1;
 			} else {
 				return null;
@@ -99,7 +99,7 @@ export default Vue.extend({
 			});
 			this.logs.forEach((log, i) => {
 				if (i < v) {
-					this.o.put(log.color, log.pos);
+					this.o.put(log.color, log.pos, true);
 				}
 			});
 			this.$forceUpdate();
@@ -116,7 +116,7 @@ export default Vue.extend({
 		});
 
 		this.game.logs.forEach(log => {
-			this.o.put(log.color, log.pos);
+			this.o.put(log.color, log.pos, true);
 		});
 
 		this.logs = this.game.logs;
@@ -190,10 +190,10 @@ export default Vue.extend({
 		checkEnd() {
 			this.game.is_ended = this.o.isEnded;
 			if (this.game.is_ended) {
-				if (this.o.winner == 'black') {
+				if (this.o.winner === true) {
 					this.game.winner_id = this.game.black == 1 ? this.game.user1_id : this.game.user2_id;
 					this.game.winner = this.game.black == 1 ? this.game.user1 : this.game.user2;
-				} else if (this.o.winner == 'white') {
+				} else if (this.o.winner === false) {
 					this.game.winner_id = this.game.black == 1 ? this.game.user2_id : this.game.user1_id;
 					this.game.winner = this.game.black == 1 ? this.game.user2 : this.game.user1;
 				} else {
@@ -214,7 +214,7 @@ export default Vue.extend({
 			});
 
 			this.game.logs.forEach(log => {
-				this.o.put(log.color, log.pos);
+				this.o.put(log.color, log.pos, true);
 			});
 
 			this.logs = this.game.logs;

From 62134e28af02b70cb4271212a55cbec9b774ca6f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Mar 2018 01:51:55 +0900
Subject: [PATCH 0737/1250] v4118

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index bcc8faa84..ac06291c4 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4114",
+	"version": "0.0.4118",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From a3dc7db518c7515500182d063f9e7c0f2e5fa6b0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Mar 2018 01:56:56 +0900
Subject: [PATCH 0738/1250] Add migration script

---
 tools/migration/node.2018-03-13.othello.js | 44 ++++++++++++++++++++++
 1 file changed, 44 insertions(+)
 create mode 100644 tools/migration/node.2018-03-13.othello.js

diff --git a/tools/migration/node.2018-03-13.othello.js b/tools/migration/node.2018-03-13.othello.js
new file mode 100644
index 000000000..12d9e0953
--- /dev/null
+++ b/tools/migration/node.2018-03-13.othello.js
@@ -0,0 +1,44 @@
+// for Node.js interpret
+
+const { default: Othello } = require('../../built/api/models/othello-game')
+const { default: zip } = require('@prezzemolo/zip')
+
+const migrate = async (doc) => {
+	const x = {};
+
+	doc.logs.forEach(log => {
+		log.color = log.color == 'black';
+	});
+
+	const result = await Othello.update(doc._id, {
+		$set: doc.logs
+	});
+
+	return result.ok === 1;
+}
+
+async function main() {
+
+	const count = await Othello.count({});
+
+	const dop = Number.parseInt(process.argv[2]) || 5
+	const idop = ((count - (count % dop)) / dop) + 1
+
+	return zip(
+		1,
+		async (time) => {
+			console.log(`${time} / ${idop}`)
+			const doc = await Othello.find({}, {
+				limit: dop, skip: time * dop
+			})
+			return Promise.all(doc.map(migrate))
+		},
+		idop
+	).then(a => {
+		const rv = []
+		a.forEach(e => rv.push(...e))
+		return rv
+	})
+}
+
+main().then(console.dir).catch(console.error)

From d7d5d12e48f0bb317175f2dd2a3781d9354aa4ec Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Mar 2018 02:03:12 +0900
Subject: [PATCH 0739/1250] oops

---
 tools/migration/node.2018-03-13.othello.js | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/tools/migration/node.2018-03-13.othello.js b/tools/migration/node.2018-03-13.othello.js
index 12d9e0953..4598f8d83 100644
--- a/tools/migration/node.2018-03-13.othello.js
+++ b/tools/migration/node.2018-03-13.othello.js
@@ -11,7 +11,9 @@ const migrate = async (doc) => {
 	});
 
 	const result = await Othello.update(doc._id, {
-		$set: doc.logs
+		$set: {
+			logs: doc.logs
+		}
 	});
 
 	return result.ok === 1;

From b2e95acb48e3f08a3dd37668b16e7a839d38d3ae Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Mar 2018 02:06:18 +0900
Subject: [PATCH 0740/1250] oops

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

diff --git a/src/web/app/common/views/components/othello.game.vue b/src/web/app/common/views/components/othello.game.vue
index 4dbcff247..907e317ce 100644
--- a/src/web/app/common/views/components/othello.game.vue
+++ b/src/web/app/common/views/components/othello.game.vue
@@ -15,11 +15,11 @@
 
 	<div class="board" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map[0].length }, 1fr)` }">
 		<div v-for="(stone, i) in o.board"
-			:class="{ empty: stone == null, none: o.map[i] == 'null', isEnded: game.is_ended, myTurn: !game.is_ended && isMyTurn, can: turnUser ? o.canPut(turnUser.id == blackUser.id ? 'black' : 'white', i) : null, prev: o.prevPos == i }"
+			:class="{ empty: stone == null, none: o.map[i] == 'null', isEnded: game.is_ended, myTurn: !game.is_ended && isMyTurn, can: turnUser ? o.canPut(turnUser.id == blackUser.id, i) : null, prev: o.prevPos == i }"
 			@click="set(i)"
 		>
-			<img v-if="stone == 'black'" :src="`${blackUser.avatar_url}?thumbnail&size=128`" alt="">
-			<img v-if="stone == 'white'" :src="`${whiteUser.avatar_url}?thumbnail&size=128`" alt="">
+			<img v-if="stone === true" :src="`${blackUser.avatar_url}?thumbnail&size=128`" alt="">
+			<img v-if="stone === false" :src="`${whiteUser.avatar_url}?thumbnail&size=128`" alt="">
 		</div>
 	</div>
 

From 94b21fbb4d4d6eeb5e69b880ecb6be8e7c848de5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Mar 2018 02:06:36 +0900
Subject: [PATCH 0741/1250] v4122

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index ac06291c4..37f136f14 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4118",
+	"version": "0.0.4122",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 749dd2d340975ca963c055f0ac731f9770a95ad8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Mar 2018 03:22:40 +0900
Subject: [PATCH 0742/1250] :v:

---
 src/common/othello/core.ts | 59 +++++++++++++++++---------------------
 1 file changed, 27 insertions(+), 32 deletions(-)

diff --git a/src/common/othello/core.ts b/src/common/othello/core.ts
index 54201d654..5e25578ca 100644
--- a/src/common/othello/core.ts
+++ b/src/common/othello/core.ts
@@ -29,6 +29,11 @@ export type Undo = {
 	 * 反転した石の位置の配列
 	 */
 	effects: number[];
+
+	/**
+	 * ターン
+	 */
+	turn: Color;
 };
 
 /**
@@ -128,51 +133,49 @@ export default class Othello {
 		return x + (y * this.mapWidth);
 	}
 
-	/**
-	 * 指定のマスに石を書き込みます
-	 * @param color 石の色
-	 * @param pos 位置
-	 */
-	private write(color: Color, pos: number) {
-		this.board[pos] = color;
-	}
-
 	/**
 	 * 指定のマスに石を打ちます
 	 * @param color 石の色
 	 * @param pos 位置
 	 */
 	public put(color: Color, pos: number, fast = false): Undo {
-		if (!fast && !this.canPut(color, pos)) return null;
+		if (!fast && !this.canPut(color, pos)) {
+			console.warn('can not put this position:', pos, color);
+			console.warn(this.board);
+			return null;
+		}
 
 		this.prevPos = pos;
 		this.prevColor = color;
-		this.write(color, pos);
+
+		this.board[pos] = color;
 
 		// 反転させられる石を取得
 		const effects = this.effects(color, pos);
 
 		// 反転させる
-		effects.forEach(pos => {
-			this.write(color, pos);
-		});
+		for (const pos of effects) {
+			this.board[pos] = color;
+		}
+
+		const turn = this.turn;
 
 		this.calcTurn();
 
 		return {
 			color,
 			pos,
-			effects
+			effects,
+			turn
 		};
 	}
 
 	private calcTurn() {
 		// ターン計算
-		const opColor = this.prevColor === BLACK ? WHITE : BLACK;
-		if (this.canPutSomewhere(opColor).length > 0) {
-			this.turn = this.prevColor === BLACK ? WHITE : BLACK;
+		if (this.canPutSomewhere(!this.prevColor).length > 0) {
+			this.turn = !this.prevColor;
 		} else if (this.canPutSomewhere(this.prevColor).length > 0) {
-			this.turn = this.prevColor === BLACK ? BLACK : WHITE;
+			this.turn = this.prevColor;
 		} else {
 			this.turn = null;
 		}
@@ -181,20 +184,12 @@ export default class Othello {
 	public undo(undo: Undo) {
 		this.prevColor = undo.color;
 		this.prevPos = undo.pos;
-		this.write(null, undo.pos);
+		this.board[undo.pos] = null;
 		for (const pos of undo.effects) {
 			const color = this.board[pos];
-			this.write(!color, pos);
+			this.board[pos] = !color;
 		}
-		this.calcTurn();
-	}
-
-	/**
-	 * 指定したマスの状態を取得します
-	 * @param pos 位置
-	 */
-	public get(pos: number) {
-		return this.board[pos];
+		this.turn = undo.turn;
 	}
 
 	/**
@@ -227,7 +222,7 @@ export default class Othello {
 	 */
 	public canPut(color: Color, pos: number): boolean {
 		// 既に石が置いてある場所には打てない
-		if (this.get(pos) !== null) return false;
+		if (this.board[pos] !== null) return false;
 
 		if (this.opts.canPutEverywhere) {
 			// 挟んでなくても置けるモード
@@ -292,7 +287,7 @@ export default class Othello {
 				//#endregion
 
 				// 石取得
-				const stone = this.get(pos);
+				const stone = this.board[pos];
 
 				// 石が置かれていないマスなら走査終了
 				if (stone === null) break;

From 72d7ab0176196e41339887f211c9a5ccbfe6ab76 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Mar 2018 04:06:14 +0900
Subject: [PATCH 0743/1250] oops

---
 src/api/stream/othello-game.ts | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/api/stream/othello-game.ts b/src/api/stream/othello-game.ts
index 368baa2cb..ba0f11252 100644
--- a/src/api/stream/othello-game.ts
+++ b/src/api/stream/othello-game.ts
@@ -307,7 +307,8 @@ export default function(request: websocket.request, connection: websocket.connec
 
 		if (o.isEnded) {
 			publishOthelloGameStream(gameId, 'ended', {
-				winner_id: winner
+				winner_id: winner,
+				game: await pack(gameId, user)
 			});
 		}
 	}

From da7b076292097185a4fce2403ccd91149484dd08 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 14 Mar 2018 04:20:15 +0900
Subject: [PATCH 0744/1250] Improve othello ai

---
 package.json                   |   4 +
 src/common/othello/ai.ts       |  42 -----
 src/common/othello/ai/back.ts  | 287 +++++++++++++++++++++++++++++++++
 src/common/othello/ai/front.ts | 231 ++++++++++++++++++++++++++
 src/config.ts                  |   4 +
 5 files changed, 526 insertions(+), 42 deletions(-)
 delete mode 100644 src/common/othello/ai.ts
 create mode 100644 src/common/othello/ai/back.ts
 create mode 100644 src/common/othello/ai/front.ts

diff --git a/package.json b/package.json
index 37f136f14..d3bf3fce5 100644
--- a/package.json
+++ b/package.json
@@ -69,6 +69,7 @@
 		"@types/ratelimiter": "2.1.28",
 		"@types/redis": "2.8.5",
 		"@types/request": "2.47.0",
+		"@types/request-promise-native": "^1.0.14",
 		"@types/rimraf": "2.0.2",
 		"@types/seedrandom": "2.4.27",
 		"@types/serve-favicon": "2.2.30",
@@ -78,6 +79,7 @@
 		"@types/webpack": "3.8.8",
 		"@types/webpack-stream": "3.2.9",
 		"@types/websocket": "0.0.37",
+		"@types/ws": "^4.0.1",
 		"accesses": "2.5.0",
 		"animejs": "2.2.0",
 		"autosize": "4.0.0",
@@ -158,6 +160,7 @@
 		"reconnecting-websocket": "3.2.2",
 		"redis": "2.8.0",
 		"request": "2.83.0",
+		"request-promise-native": "^1.0.5",
 		"rimraf": "2.6.2",
 		"rndstr": "1.0.0",
 		"s-age": "1.1.2",
@@ -198,6 +201,7 @@
 		"webpack-cli": "^2.0.8",
 		"webpack-replace-loader": "1.3.0",
 		"websocket": "1.0.25",
+		"ws": "^5.0.0",
 		"xev": "2.0.0"
 	}
 }
diff --git a/src/common/othello/ai.ts b/src/common/othello/ai.ts
deleted file mode 100644
index 3943d04bf..000000000
--- a/src/common/othello/ai.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import Othello, { Color } from './core';
-
-export function ai(color: Color, othello: Othello) {
-	//const opponentColor = color == 'black' ? 'white' : 'black';
-/* wip
-
-	function think() {
-		// 打てる場所を取得
-		const ps = othello.canPutSomewhere(color);
-
-		if (ps.length > 0) { // 打てる場所がある場合
-			// 角を取得
-			const corners = ps.filter(p =>
-				// 左上
-				(p[0] == 0 && p[1] == 0) ||
-				// 右上
-				(p[0] == (BOARD_SIZE - 1) && p[1] == 0) ||
-				// 右下
-				(p[0] == (BOARD_SIZE - 1) && p[1] == (BOARD_SIZE - 1)) ||
-				// 左下
-				(p[0] == 0 && p[1] == (BOARD_SIZE - 1))
-			);
-
-			if (corners.length > 0) { // どこかしらの角に打てる場合
-				// 打てる角からランダムに選択して打つ
-				const p = corners[Math.floor(Math.random() * corners.length)];
-				othello.set(color, p[0], p[1]);
-			} else { // 打てる角がない場合
-				// 打てる場所からランダムに選択して打つ
-				const p = ps[Math.floor(Math.random() * ps.length)];
-				othello.set(color, p[0], p[1]);
-			}
-
-			// 相手の打つ場所がない場合続けてAIのターン
-			if (othello.getPattern(opponentColor).length === 0) {
-				think();
-			}
-		}
-	}
-
-	think();*/
-}
diff --git a/src/common/othello/ai/back.ts b/src/common/othello/ai/back.ts
new file mode 100644
index 000000000..9765b7d4e
--- /dev/null
+++ b/src/common/othello/ai/back.ts
@@ -0,0 +1,287 @@
+/**
+ * -AI-
+ * Botのバックエンド(思考を担当)
+ *
+ * 対話と思考を同じプロセスで行うと、思考時間が長引いたときにストリームから
+ * 切断されてしまうので、別々のプロセスで行うようにします
+ */
+
+import Othello, { Color } from '../core';
+
+let game;
+let form;
+
+/**
+ * このBotのユーザーID
+ */
+let id;
+
+process.on('message', msg => {
+	console.log(msg);
+
+	// 親プロセスからデータをもらう
+	if (msg.type == '_init_') {
+		game = msg.game;
+		form = msg.form;
+		id = msg.id;
+	}
+
+	// フォームが更新されたとき
+	if (msg.type == 'update-form') {
+		form.find(i => i.id == msg.body.id).value = msg.body.value;
+	}
+
+	// ゲームが始まったとき
+	if (msg.type == 'started') {
+		onGameStarted(msg.body);
+
+		//#region TLに投稿する
+		const game = msg.body;
+		const url = `https://misskey.xyz/othello/${game.id}`;
+		const user = game.user1_id == id ? game.user2 : game.user1;
+		const isSettai = form[0].value === 0;
+		const text = isSettai
+			? `?[${user.name}](https://misskey.xyz/${user.username})さんの接待を始めました!`
+			: `対局を?[${user.name}](https://misskey.xyz/${user.username})さんと始めました! (強さ${form[0].value})`;
+		process.send({
+			type: 'tl',
+			text: `${text}\n→[観戦する](${url})`
+		});
+		//#endregion
+	}
+
+	// ゲームが終了したとき
+	if (msg.type == 'ended') {
+		// ストリームから切断
+		process.send({
+			type: 'close'
+		});
+
+		//#region TLに投稿する
+		const url = `https://misskey.xyz/othello/${msg.body.game.id}`;
+		const user = game.user1_id == id ? game.user2 : game.user1;
+		const isSettai = form[0].value === 0;
+		const text = isSettai
+			? msg.body.winner_id === null
+				? `?[${user.name}](https://misskey.xyz/${user.username})さんに接待で引き分けました...`
+				: msg.body.winner_id == id
+					? `?[${user.name}](https://misskey.xyz/${user.username})さんに接待で勝ってしまいました...`
+					: `?[${user.name}](https://misskey.xyz/${user.username})さんに接待で負けてあげました♪`
+			: msg.body.winner_id === null
+				? `?[${user.name}](https://misskey.xyz/${user.username})さんと引き分けました~ (強さ${form[0].value})`
+				: msg.body.winner_id == id
+					? `?[${user.name}](https://misskey.xyz/${user.username})さんに勝ちました♪ (強さ${form[0].value})`
+					: `?[${user.name}](https://misskey.xyz/${user.username})さんに負けました... (強さ${form[0].value})`;
+		process.send({
+			type: 'tl',
+			text: `${text}\n→[結果を見る](${url})`
+		});
+		//#endregion
+	}
+
+	// 打たれたとき
+	if (msg.type == 'set') {
+		onSet(msg.body);
+	}
+});
+
+let o: Othello;
+let botColor: Color;
+
+// 各マスの強さ
+let cellStrongs;
+
+/**
+ * ゲーム開始時
+ * @param g ゲーム情報
+ */
+function onGameStarted(g) {
+	game = g;
+
+	// オセロエンジン初期化
+	o = new Othello(game.settings.map, {
+		isLlotheo: game.settings.is_llotheo,
+		canPutEverywhere: game.settings.can_put_everywhere,
+		loopedBoard: game.settings.looped_board
+	});
+
+	// 各マスの価値を計算しておく
+	cellStrongs = o.map.map((pix, i) => {
+		if (pix == 'null') return 0;
+		const [x, y] = o.transformPosToXy(i);
+		let count = 0;
+		const get = (x, y) => {
+			if (x < 0 || y < 0 || x >= o.mapWidth || y >= o.mapHeight) return 'null';
+			return o.mapDataGet(o.transformXyToPos(x, y));
+		};
+
+		if (get(x    , y - 1) == 'null') count++;
+		if (get(x + 1, y - 1) == 'null') count++;
+		if (get(x + 1, y    ) == 'null') count++;
+		if (get(x + 1, y + 1) == 'null') count++;
+		if (get(x    , y + 1) == 'null') count++;
+		if (get(x - 1, y + 1) == 'null') count++;
+		if (get(x - 1, y    ) == 'null') count++;
+		if (get(x - 1, y - 1) == 'null') count++;
+		//return Math.pow(count, 3);
+		return count >= 5 ? 1 : 0;
+	});
+
+	botColor = game.user1_id == id && game.black == 1 || game.user2_id == id && game.black == 2;
+
+	if (botColor) {
+		think();
+	}
+}
+
+function onSet(x) {
+	o.put(x.color, x.pos, true);
+
+	if (x.next === botColor) {
+		think();
+	}
+}
+
+function think() {
+	console.log('Thinking...');
+
+	const isSettai = form[0].value === 0;
+
+	// 接待モードのときは、全力(5手先読みくらい)で負けるようにする
+	const maxDepth = isSettai ? 5 : form[0].value;
+
+	const db = {};
+
+	/**
+	 * αβ法での探索
+	 */
+	const dive = (o: Othello, pos: number, alpha = -Infinity, beta = Infinity, depth = 0): number => {
+		// 試し打ち
+		const undo = o.put(o.turn, pos, true);
+
+		const key = o.board.toString();
+		let cache = db[key];
+		if (cache) {
+			if (alpha >= cache.upper) {
+				o.undo(undo);
+				return cache.upper;
+			}
+			if (beta <= cache.lower) {
+				o.undo(undo);
+				return cache.lower;
+			}
+			alpha = Math.max(alpha, cache.lower);
+			beta = Math.min(beta, cache.upper);
+		} else {
+			cache = {
+				upper: Infinity,
+				lower: -Infinity
+			};
+		}
+
+		const isBotTurn = o.turn === botColor;
+
+		// 勝った
+		if (o.turn === null) {
+			const winner = o.winner;
+
+			// 勝つことによる基本スコア
+			const base = 10000;
+
+			let score;
+
+			if (game.settings.is_llotheo) {
+				// 勝ちは勝ちでも、より自分の石を少なくした方が美しい勝ちだと判定する
+				score = o.winner ? base - (o.blackCount * 100) : base - (o.whiteCount * 100);
+			} else {
+				// 勝ちは勝ちでも、より相手の石を少なくした方が美しい勝ちだと判定する
+				score = o.winner ? base + (o.blackCount * 100) : base + (o.whiteCount * 100);
+			}
+
+			// 巻き戻し
+			o.undo(undo);
+
+			// 接待なら自分が負けた方が高スコア
+			return isSettai
+				? winner !== botColor ? score : -score
+				: winner === botColor ? score : -score;
+		}
+
+		if (depth === maxDepth) {
+			let score = o.canPutSomewhere(botColor).length;
+
+			cellStrongs.forEach((s, i) => {
+				// 係数
+				const coefficient = 30;
+				s = s * coefficient;
+
+				const stone = o.board[i];
+				if (stone === botColor) {
+					// TODO: 価値のあるマスに設置されている自分の石に縦か横に接するマスは価値があると判断する
+					score += s;
+				} else if (stone !== null) {
+					score -= s;
+				}
+			});
+
+			// 巻き戻し
+			o.undo(undo);
+
+			// ロセオならスコアを反転
+			if (game.settings.is_llotheo) score = -score;
+
+			// 接待ならスコアを反転
+			if (isSettai) score = -score;
+
+			return score;
+		} else {
+			const cans = o.canPutSomewhere(o.turn);
+
+			let value = isBotTurn ? -Infinity : Infinity;
+			let a = alpha;
+			let b = beta;
+
+			// 次のターンのプレイヤーにとって最も良い手を取得
+			for (const p of cans) {
+				if (isBotTurn) {
+					const score = dive(o, p, a, beta, depth + 1);
+					value = Math.max(value, score);
+					a = Math.max(a, value);
+					if (value >= beta) break;
+				} else {
+					const score = dive(o, p, alpha, b, depth + 1);
+					value = Math.min(value, score);
+					b = Math.min(b, value);
+					if (value <= alpha) break;
+				}
+			}
+
+			// 巻き戻し
+			o.undo(undo);
+
+			if (value <= alpha) {
+				cache.upper = value;
+			} else if (value >= beta) {
+				cache.lower = value;
+			} else {
+				cache.upper = value;
+				cache.lower = value;
+			}
+
+			db[key] = cache;
+
+			return value;
+		}
+	};
+
+	const cans = o.canPutSomewhere(botColor);
+	const scores = cans.map(p => dive(o, p));
+	const pos = cans[scores.indexOf(Math.max(...scores))];
+
+	console.log('Thinked:', pos);
+
+	process.send({
+		type: 'put',
+		pos
+	});
+}
diff --git a/src/common/othello/ai/front.ts b/src/common/othello/ai/front.ts
new file mode 100644
index 000000000..99cf1a712
--- /dev/null
+++ b/src/common/othello/ai/front.ts
@@ -0,0 +1,231 @@
+/**
+ * -AI-
+ * Botのフロントエンド(ストリームとの対話を担当)
+ *
+ * 対話と思考を同じプロセスで行うと、思考時間が長引いたときにストリームから
+ * 切断されてしまうので、別々のプロセスで行うようにします
+ */
+
+import * as childProcess from 'child_process';
+import * as WebSocket from 'ws';
+import * as request from 'request-promise-native';
+import conf from '../../../conf';
+
+// 設定 ////////////////////////////////////////////////////////
+
+/**
+ * BotアカウントのAPIキー
+ */
+const i = conf.othello_ai.i;
+
+/**
+ * BotアカウントのユーザーID
+ */
+const id = conf.othello_ai.id;
+
+////////////////////////////////////////////////////////////////
+
+/**
+ * ホームストリーム
+ */
+const homeStream = new WebSocket(`wss://api.misskey.xyz/?i=${i}`);
+
+homeStream.on('open', () => {
+	console.log('home stream opened');
+});
+
+homeStream.on('close', () => {
+	console.log('home stream closed');
+});
+
+homeStream.on('message', message => {
+	const msg = JSON.parse(message.toString());
+
+	// タイムライン上でなんか言われたまたは返信されたとき
+	if (msg.type == 'mention' || msg.type == 'reply') {
+		const post = msg.body;
+
+		// リアクションする
+		request.post('https://api.misskey.xyz/posts/reactions/create', {
+			json: { i,
+				post_id: post.id,
+				reaction: 'love'
+			}
+		});
+
+		if (post.text) {
+			if (post.text.indexOf('オセロ') > -1) {
+				request.post('https://api.misskey.xyz/posts/create', {
+					json: { i,
+						reply_id: post.id,
+						text: '良いですよ~'
+					}
+				});
+
+				invite(post.user_id);
+			}
+		}
+	}
+
+	// メッセージでなんか言われたとき
+	if (msg.type == 'messaging_message') {
+		const message = msg.body;
+		if (message.text) {
+			if (message.text.indexOf('オセロ') > -1) {
+				request.post('https://api.misskey.xyz/messaging/messages/create', {
+					json: { i,
+						user_id: message.user_id,
+						text: '良いですよ~'
+					}
+				});
+
+				invite(message.user_id);
+			}
+		}
+	}
+});
+
+// ユーザーを対局に誘う
+function invite(userId) {
+	request.post('https://api.misskey.xyz/othello/match', {
+		json: { i,
+			user_id: userId
+		}
+	});
+}
+
+/**
+ * オセロストリーム
+ */
+const othelloStream = new WebSocket(`wss://api.misskey.xyz/othello?i=${i}`);
+
+othelloStream.on('open', () => {
+	console.log('othello stream opened');
+});
+
+othelloStream.on('close', () => {
+	console.log('othello stream closed');
+});
+
+othelloStream.on('message', message => {
+	const msg = JSON.parse(message.toString());
+
+	// 招待されたとき
+	if (msg.type == 'invited') {
+		onInviteMe(msg.body.parent);
+	}
+
+	// マッチしたとき
+	if (msg.type == 'matched') {
+		gameStart(msg.body);
+	}
+});
+
+/**
+ * ゲーム開始
+ * @param game ゲーム情報
+ */
+function gameStart(game) {
+	// ゲームストリームに接続
+	const gw = new WebSocket(`wss://api.misskey.xyz/othello-game?i=${i}&game=${game.id}`);
+
+	gw.on('open', () => {
+		console.log('othello game stream opened');
+
+		// フォーム
+		const form = [{
+			id: 'strength',
+			type: 'radio',
+			label: '強さ',
+			value: 2,
+			items: [{
+				label: '接待',
+				value: 0
+			}, {
+				label: '弱',
+				value: 1
+			}, {
+				label: '中',
+				value: 2
+			}, {
+				label: '強',
+				value: 3
+			}, {
+				label: '最強',
+				value: 5
+			}]
+		}];
+
+		//#region バックエンドプロセス開始
+		const ai = childProcess.fork(__dirname + '/back.js');
+
+		// バックエンドプロセスに情報を渡す
+		ai.send({
+			type: '_init_',
+			game,
+			form,
+			id
+		});
+
+		ai.on('message', msg => {
+			if (msg.type == 'put') {
+				gw.send(JSON.stringify({
+					type: 'set',
+					pos: msg.pos
+				}));
+			} else if (msg.type == 'tl') {
+				request.post('https://api.misskey.xyz/posts/create', {
+					json: { i,
+						text: msg.text
+					}
+				});
+			} else if (msg.type == 'close') {
+				gw.close();
+			}
+		});
+
+		// ゲームストリームから情報が流れてきたらそのままバックエンドプロセスに伝える
+		gw.on('message', message => {
+			const msg = JSON.parse(message.toString());
+			ai.send(msg);
+		});
+		//#endregion
+
+		// フォーム初期化
+		setTimeout(() => {
+			gw.send(JSON.stringify({
+				type: 'init-form',
+				body: form
+			}));
+		}, 1000);
+
+		// どんな設定内容の対局でも受け入れる
+		setTimeout(() => {
+			gw.send(JSON.stringify({
+				type: 'accept'
+			}));
+		}, 2000);
+	});
+
+	gw.on('close', () => {
+		console.log('othello game stream closed');
+	});
+}
+
+/**
+ * オセロの対局に招待されたとき
+ * @param inviter 誘ってきたユーザー
+ */
+async function onInviteMe(inviter) {
+	console.log(`Anybody invited me: @${inviter.username}`);
+
+	// 承認
+	const game = await request.post('https://api.misskey.xyz/othello/match', {
+		json: {
+			i,
+			user_id: inviter.id
+		}
+	});
+
+	gameStart(game);
+}
diff --git a/src/config.ts b/src/config.ts
index e327cb0ba..82488cef8 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -74,6 +74,10 @@ type Source = {
 		hook_secret: string;
 		username: string;
 	};
+	othello_ai?: {
+		id: string;
+		i: string;
+	};
 	line_bot?: {
 		channel_secret: string;
 		channel_access_token: string;

From a63adf0419f913e31eeceadfa315746c7a5b9820 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 14 Mar 2018 04:30:24 +0900
Subject: [PATCH 0745/1250] :v:

---
 src/api/endpoints.ts | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts
index 1fc9465cd..c7100bd03 100644
--- a/src/api/endpoints.ts
+++ b/src/api/endpoints.ts
@@ -254,8 +254,7 @@ const endpoints: Endpoint[] = [
 	},
 
 	{
-		name: 'othello/games/show',
-		withCredential: true
+		name: 'othello/games/show'
 	},
 
 	{

From 05e27e15344d98c98a1cb3659376f0bef18c0add Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 14 Mar 2018 04:37:20 +0900
Subject: [PATCH 0746/1250] :v:

---
 src/api/stream/othello-game.ts                       | 2 +-
 src/api/streaming.ts                                 | 6 +++++-
 src/web/app/common/scripts/streaming/othello-game.ts | 2 +-
 src/web/app/common/views/components/othello.game.vue | 1 +
 4 files changed, 8 insertions(+), 3 deletions(-)

diff --git a/src/api/stream/othello-game.ts b/src/api/stream/othello-game.ts
index ba0f11252..1c846f27a 100644
--- a/src/api/stream/othello-game.ts
+++ b/src/api/stream/othello-game.ts
@@ -6,7 +6,7 @@ import { publishOthelloGameStream } from '../event';
 import Othello from '../../common/othello/core';
 import * as maps from '../../common/othello/maps';
 
-export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void {
+export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user?: any): void {
 	const gameId = request.resourceURL.query.game;
 
 	// Subscribe game stream
diff --git a/src/api/streaming.ts b/src/api/streaming.ts
index 7d67ba957..f56c08092 100644
--- a/src/api/streaming.ts
+++ b/src/api/streaming.ts
@@ -53,6 +53,11 @@ module.exports = (server: http.Server) => {
 
 		const user = await authenticate(request.resourceURL.query.i);
 
+		if (request.resourceURL.pathname === '/othello-game') {
+			othelloGameStream(request, connection, subscriber, user);
+			return;
+		}
+
 		if (user == null) {
 			connection.send('authentication-failed');
 			connection.close();
@@ -64,7 +69,6 @@ module.exports = (server: http.Server) => {
 			request.resourceURL.pathname === '/drive' ? driveStream :
 			request.resourceURL.pathname === '/messaging' ? messagingStream :
 			request.resourceURL.pathname === '/messaging-index' ? messagingIndexStream :
-			request.resourceURL.pathname === '/othello-game' ? othelloGameStream :
 			request.resourceURL.pathname === '/othello' ? othelloStream :
 			null;
 
diff --git a/src/web/app/common/scripts/streaming/othello-game.ts b/src/web/app/common/scripts/streaming/othello-game.ts
index 51a435541..cdf46d5d8 100644
--- a/src/web/app/common/scripts/streaming/othello-game.ts
+++ b/src/web/app/common/scripts/streaming/othello-game.ts
@@ -3,7 +3,7 @@ import Stream from './stream';
 export class OthelloGameStream extends Stream {
 	constructor(me, game) {
 		super('othello-game', {
-			i: me.token,
+			i: me ? me.token : null,
 			game: game.id
 		});
 	}
diff --git a/src/web/app/common/views/components/othello.game.vue b/src/web/app/common/views/components/othello.game.vue
index 907e317ce..69b212776 100644
--- a/src/web/app/common/views/components/othello.game.vue
+++ b/src/web/app/common/views/components/othello.game.vue
@@ -56,6 +56,7 @@ export default Vue.extend({
 
 	computed: {
 		iAmPlayer(): boolean {
+			if (!(this as any).os.isSignedIn) return false;
 			return this.game.user1_id == (this as any).os.i.id || this.game.user2_id == (this as any).os.i.id;
 		},
 		myColor(): Color {

From f766d4fbd0677ad35ae9aa858eeb3111dccf92e2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 14 Mar 2018 04:37:45 +0900
Subject: [PATCH 0747/1250] v4128

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index d3bf3fce5..6dfd3b195 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4122",
+	"version": "0.0.4128",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From ef700e4fff07226b832bc9148ed0203fce0247e6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 14 Mar 2018 06:58:40 +0900
Subject: [PATCH 0748/1250] Use ReconnectingWebSocket

---
 src/common/othello/ai/front.ts | 15 +++++++++++----
 1 file changed, 11 insertions(+), 4 deletions(-)

diff --git a/src/common/othello/ai/front.ts b/src/common/othello/ai/front.ts
index 99cf1a712..838e5a143 100644
--- a/src/common/othello/ai/front.ts
+++ b/src/common/othello/ai/front.ts
@@ -7,7 +7,8 @@
  */
 
 import * as childProcess from 'child_process';
-import * as WebSocket from 'ws';
+const WebSocket = require('ws');
+import * as ReconnectingWebSocket from 'reconnecting-websocket';
 import * as request from 'request-promise-native';
 import conf from '../../../conf';
 
@@ -28,7 +29,9 @@ const id = conf.othello_ai.id;
 /**
  * ホームストリーム
  */
-const homeStream = new WebSocket(`wss://api.misskey.xyz/?i=${i}`);
+const homeStream = new ReconnectingWebSocket(`wss://api.misskey.xyz/?i=${i}`, undefined, {
+	constructor: WebSocket
+});
 
 homeStream.on('open', () => {
 	console.log('home stream opened');
@@ -97,7 +100,9 @@ function invite(userId) {
 /**
  * オセロストリーム
  */
-const othelloStream = new WebSocket(`wss://api.misskey.xyz/othello?i=${i}`);
+const othelloStream = new ReconnectingWebSocket(`wss://api.misskey.xyz/othello?i=${i}`, undefined, {
+	constructor: WebSocket
+});
 
 othelloStream.on('open', () => {
 	console.log('othello stream opened');
@@ -127,7 +132,9 @@ othelloStream.on('message', message => {
  */
 function gameStart(game) {
 	// ゲームストリームに接続
-	const gw = new WebSocket(`wss://api.misskey.xyz/othello-game?i=${i}&game=${game.id}`);
+	const gw = new ReconnectingWebSocket(`wss://api.misskey.xyz/othello-game?i=${i}&game=${game.id}`, undefined, {
+		constructor: WebSocket
+	});
 
 	gw.on('open', () => {
 		console.log('othello game stream opened');

From 25c7bd9915ef27cb1c746f4b2e6da928d5ba04b5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 14 Mar 2018 16:12:36 +0900
Subject: [PATCH 0749/1250] :v:

---
 .../desktop/views/components/drive-window.vue |  2 +-
 .../app/desktop/views/components/settings.vue |  7 +++
 .../app/desktop/views/components/window.vue   | 51 ++++++++++++++-----
 3 files changed, 45 insertions(+), 15 deletions(-)

diff --git a/src/web/app/desktop/views/components/drive-window.vue b/src/web/app/desktop/views/components/drive-window.vue
index 8ae48cf39..3a072f479 100644
--- a/src/web/app/desktop/views/components/drive-window.vue
+++ b/src/web/app/desktop/views/components/drive-window.vue
@@ -26,7 +26,7 @@ export default Vue.extend({
 	},
 	methods: {
 		popout() {
-			const folder = (this.$refs.browser as any).folder;
+			const folder = (this.$refs.browser as any) ? (this.$refs.browser as any).folder : null;
 			if (folder) {
 				return `${url}/i/drive/folder/${folder.id}`;
 			} else {
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index 01c41194e..39d9be01d 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -23,6 +23,9 @@
 			<mk-switch v-model="os.i.client_settings.fetchOnScroll" @change="onChangeFetchOnScroll" text="スクロールで自動読み込み">
 				<span>ページを下までスクロールしたときに自動で追加のコンテンツを読み込みます。</span>
 			</mk-switch>
+			<mk-switch v-model="autoPopout" text="ウィンドウの自動ポップアウト">
+				<span>ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトします。この設定はブラウザに記憶されます。</span>
+			</mk-switch>
 		</section>
 
 		<section class="web" v-show="page == 'web'">
@@ -206,6 +209,7 @@ export default Vue.extend({
 			latestVersion: undefined,
 			checkingForUpdate: false,
 			enableSounds: localStorage.getItem('enableSounds') == 'true',
+			autoPopout: localStorage.getItem('autoPopout') == 'true',
 			soundVolume: localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) : 100,
 			lang: localStorage.getItem('lang') || '',
 			preventUpdate: localStorage.getItem('preventUpdate') == 'true',
@@ -214,6 +218,9 @@ export default Vue.extend({
 		};
 	},
 	watch: {
+		autoPopout() {
+			localStorage.setItem('autoPopout', this.autoPopout ? 'true' : 'false');
+		},
 		enableSounds() {
 			localStorage.setItem('enableSounds', this.enableSounds ? 'true' : 'false');
 		},
diff --git a/src/web/app/desktop/views/components/window.vue b/src/web/app/desktop/views/components/window.vue
index f525d6962..42b2600dc 100644
--- a/src/web/app/desktop/views/components/window.vue
+++ b/src/web/app/desktop/views/components/window.vue
@@ -72,6 +72,12 @@ export default Vue.extend({
 		}
 	},
 
+	data() {
+		return {
+			preventMount: false
+		};
+	},
+
 	computed: {
 		isFlexible(): boolean {
 			return this.height == null;
@@ -89,11 +95,21 @@ export default Vue.extend({
 	},
 
 	created() {
-		// ウィンドウをウィンドウシステムに登録
-		(this as any).os.windows.add(this);
+		if (localStorage.getItem('autoPopout') == 'true' && this.popoutUrl) {
+			this.popout();
+			this.preventMount = true;
+		} else {
+			// ウィンドウをウィンドウシステムに登録
+			(this as any).os.windows.add(this);
+		}
 	},
 
 	mounted() {
+		if (this.preventMount) {
+			this.$destroy();
+			return;
+		}
+
 		this.$nextTick(() => {
 			const main = this.$refs.main as any;
 			main.style.top = '15%';
@@ -180,21 +196,28 @@ export default Vue.extend({
 		},
 
 		popout() {
-			const main = this.$refs.main as any;
-
-			const position = main.getBoundingClientRect();
-
-			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;
-
 			const url = typeof this.popoutUrl == 'function' ? this.popoutUrl() : this.popoutUrl;
 
-			window.open(url, url,
-				`height=${height}, width=${width}, left=${x}, top=${y}`);
+			const main = this.$refs.main as any;
 
-			this.close();
+			if (main) {
+				const position = main.getBoundingClientRect();
+
+				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;
+
+				window.open(url, url,
+					`width=${width}, height=${height}, top=${y}, left=${x}`);
+
+				this.close();
+			} else {
+				const x = window.top.outerHeight / 2 + window.top.screenY - (parseInt(this.height, 10) / 2);
+				const y = window.top.outerWidth / 2 + window.top.screenX - (parseInt(this.width, 10) / 2);
+				window.open(url, url,
+					`width=${this.width}, height=${this.height}, top=${x}, left=${y}`);
+			}
 		},
 
 		// 最前面へ移動

From bcca079f8a24ee985839c341d646acfa3f4e3a38 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 14 Mar 2018 16:19:19 +0900
Subject: [PATCH 0750/1250] v4131

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 6dfd3b195..34870a6eb 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4128",
+	"version": "0.0.4131",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 8ddbbf292a0b7caf784e071a2642a3a4c285f634 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Mar 2018 04:41:05 +0900
Subject: [PATCH 0751/1250] Enable typescript source map

---
 gulpfile.ts   | 3 +++
 package.json  | 1 +
 tsconfig.json | 2 +-
 3 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/gulpfile.ts b/gulpfile.ts
index aa6712914..df7f7e329 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -8,6 +8,7 @@ import * as Path from 'path';
 import * as gulp from 'gulp';
 import * as gutil from 'gulp-util';
 import * as ts from 'gulp-typescript';
+const sourcemaps = require('gulp-sourcemaps');
 import tslint from 'gulp-tslint';
 import cssnano = require('gulp-cssnano');
 import * as uglifyComposer from 'gulp-uglify/composer';
@@ -60,7 +61,9 @@ gulp.task('build:ts', () => {
 
 	return tsProject
 		.src()
+		.pipe(sourcemaps.init())
 		.pipe(tsProject())
+		.pipe(sourcemaps.write('.', { includeContent: false, sourceRoot: '../built' }))
 		.pipe(gulp.dest('./built/'));
 });
 
diff --git a/package.json b/package.json
index 34870a6eb..b4674c246 100644
--- a/package.json
+++ b/package.json
@@ -121,6 +121,7 @@
 		"gulp-pug": "3.3.0",
 		"gulp-rename": "1.2.2",
 		"gulp-replace": "0.6.1",
+		"gulp-sourcemaps": "^2.6.4",
 		"gulp-stylus": "2.7.0",
 		"gulp-tslint": "8.1.3",
 		"gulp-typescript": "3.2.4",
diff --git a/tsconfig.json b/tsconfig.json
index 9d26429c5..47aa521bf 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -8,7 +8,7 @@
     "noUnusedLocals": true,
     "noFallthroughCasesInSwitch": true,
     "declaration": false,
-    "sourceMap": false,
+    "sourceMap": true,
     "target": "es2017",
     "module": "commonjs",
     "removeComments": false,

From 72a70a66616c4f1e94b8896aa6f78dade19567c2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Mar 2018 04:58:54 +0900
Subject: [PATCH 0752/1250] Update dependencies :rocket:

---
 package.json | 102 +++++++++++++++++++++++++--------------------------
 1 file changed, 51 insertions(+), 51 deletions(-)

diff --git a/package.json b/package.json
index b4674c246..ca6db98ca 100644
--- a/package.json
+++ b/package.json
@@ -39,7 +39,7 @@
 		"@types/cors": "2.8.3",
 		"@types/debug": "0.0.30",
 		"@types/deep-equal": "1.0.1",
-		"@types/elasticsearch": "5.0.19",
+		"@types/elasticsearch": "5.0.22",
 		"@types/eventemitter3": "2.0.2",
 		"@types/express": "4.11.1",
 		"@types/gm": "1.17.33",
@@ -48,68 +48,68 @@
 		"@types/gulp-mocha": "0.0.31",
 		"@types/gulp-rename": "0.0.33",
 		"@types/gulp-replace": "0.0.31",
-		"@types/gulp-uglify": "3.0.3",
+		"@types/gulp-uglify": "3.0.4",
 		"@types/gulp-util": "3.0.34",
-		"@types/inquirer": "0.0.36",
+		"@types/inquirer": "0.0.37",
 		"@types/is-root": "1.0.0",
 		"@types/is-url": "1.2.28",
 		"@types/js-yaml": "3.10.1",
 		"@types/license-checker": "15.0.0",
 		"@types/mkdirp": "0.5.2",
 		"@types/mocha": "2.2.48",
-		"@types/mongodb": "3.0.5",
+		"@types/mongodb": "3.0.7",
 		"@types/monk": "6.0.0",
 		"@types/morgan": "1.7.35",
 		"@types/ms": "0.7.30",
 		"@types/multer": "1.3.6",
-		"@types/node": "9.4.6",
+		"@types/node": "9.4.7",
 		"@types/proxy-addr": "2.0.0",
 		"@types/pug": "2.0.4",
 		"@types/qrcode": "0.8.1",
 		"@types/ratelimiter": "2.1.28",
-		"@types/redis": "2.8.5",
+		"@types/redis": "2.8.6",
 		"@types/request": "2.47.0",
-		"@types/request-promise-native": "^1.0.14",
+		"@types/request-promise-native": "1.0.14",
 		"@types/rimraf": "2.0.2",
 		"@types/seedrandom": "2.4.27",
 		"@types/serve-favicon": "2.2.30",
 		"@types/speakeasy": "2.0.2",
 		"@types/tmp": "0.0.33",
 		"@types/uuid": "3.4.3",
-		"@types/webpack": "3.8.8",
-		"@types/webpack-stream": "3.2.9",
-		"@types/websocket": "0.0.37",
-		"@types/ws": "^4.0.1",
+		"@types/webpack": "4.1.0",
+		"@types/webpack-stream": "3.2.10",
+		"@types/websocket": "0.0.38",
+		"@types/ws": "4.0.1",
 		"accesses": "2.5.0",
 		"animejs": "2.2.0",
 		"autosize": "4.0.0",
 		"autwh": "0.0.1",
 		"bcryptjs": "2.4.3",
 		"body-parser": "1.18.2",
-		"bootstrap-vue": "^2.0.0-rc.1",
+		"bootstrap-vue": "2.0.0-rc.1",
 		"cafy": "3.2.1",
 		"chai": "4.1.2",
 		"chai-http": "3.0.0",
-		"chalk": "2.3.1",
+		"chalk": "2.3.2",
 		"compression": "1.7.2",
 		"cookie": "0.3.1",
 		"cors": "2.8.4",
-		"crc-32": "^1.2.0",
+		"crc-32": "1.2.0",
 		"css-loader": "0.28.10",
 		"debug": "3.1.0",
 		"deep-equal": "1.0.1",
 		"deepcopy": "0.6.3",
 		"diskusage": "0.2.4",
-		"elasticsearch": "14.1.0",
-		"element-ui": "^2.2.0",
-		"emojilib": "^2.2.12",
+		"elasticsearch": "14.2.0",
+		"element-ui": "2.2.2",
+		"emojilib": "2.2.12",
 		"escape-regexp": "0.0.1",
-		"eslint": "4.18.0",
-		"eslint-plugin-vue": "4.2.2",
+		"eslint": "4.18.2",
+		"eslint-plugin-vue": "4.3.0",
 		"eventemitter3": "3.0.1",
 		"exif-js": "2.3.0",
-		"express": "4.16.2",
-		"file-loader": "^1.1.10",
+		"express": "4.16.3",
+		"file-loader": "1.1.11",
 		"file-type": "7.6.0",
 		"fuckadblock": "3.2.1",
 		"gm": "1.23.1",
@@ -121,88 +121,88 @@
 		"gulp-pug": "3.3.0",
 		"gulp-rename": "1.2.2",
 		"gulp-replace": "0.6.1",
-		"gulp-sourcemaps": "^2.6.4",
+		"gulp-sourcemaps": "2.6.4",
 		"gulp-stylus": "2.7.0",
 		"gulp-tslint": "8.1.3",
-		"gulp-typescript": "3.2.4",
+		"gulp-typescript": "4.0.1",
 		"gulp-uglify": "3.0.0",
 		"gulp-util": "3.0.8",
-		"hard-source-webpack-plugin": "^0.6.1",
+		"hard-source-webpack-plugin": "0.6.4",
 		"highlight.js": "9.12.0",
-		"html-minifier": "3.5.9",
+		"html-minifier": "3.5.11",
 		"inquirer": "5.1.0",
 		"is-root": "1.0.0",
 		"is-url": "1.2.2",
-		"js-yaml": "3.10.0",
+		"js-yaml": "3.11.0",
 		"license-checker": "16.0.0",
 		"loader-utils": "1.1.0",
 		"mecab-async": "0.1.2",
 		"mkdirp": "0.5.1",
-		"mocha": "5.0.1",
+		"mocha": "5.0.4",
 		"moji": "0.5.1",
-		"mongodb": "3.0.3",
+		"mongodb": "3.0.4",
 		"monk": "6.0.5",
 		"morgan": "1.9.0",
 		"ms": "2.1.1",
 		"multer": "1.3.0",
-		"node-sass": "^4.7.2",
-		"node-sass-json-importer": "^3.1.3",
+		"node-sass": "4.7.2",
+		"node-sass-json-importer": "3.1.5",
 		"nprogress": "0.2.0",
-		"object-assign-deep": "^0.3.1",
-		"on-build-webpack": "^0.1.0",
+		"object-assign-deep": "0.3.1",
+		"on-build-webpack": "0.1.0",
 		"os-utils": "0.0.14",
-		"progress-bar-webpack-plugin": "^1.11.0",
+		"progress-bar-webpack-plugin": "1.11.0",
 		"prominence": "0.2.0",
 		"proxy-addr": "2.0.3",
-		"pug": "2.0.0-rc.4",
+		"pug": "2.0.1",
 		"qrcode": "1.2.0",
 		"ratelimiter": "3.0.3",
 		"recaptcha-promise": "0.1.3",
 		"reconnecting-websocket": "3.2.2",
 		"redis": "2.8.0",
-		"request": "2.83.0",
-		"request-promise-native": "^1.0.5",
+		"request": "2.85.0",
+		"request-promise-native": "1.0.5",
 		"rimraf": "2.6.2",
 		"rndstr": "1.0.0",
 		"s-age": "1.1.2",
-		"sass-loader": "^6.0.6",
+		"sass-loader": "6.0.7",
 		"seedrandom": "2.4.3",
 		"serve-favicon": "2.4.5",
 		"speakeasy": "2.0.0",
-		"style-loader": "0.20.2",
+		"style-loader": "0.20.3",
 		"stylus": "0.54.5",
-		"stylus-loader": "3.0.1",
+		"stylus-loader": "3.0.2",
 		"summaly": "2.0.3",
 		"swagger-jsdoc": "1.9.7",
 		"syuilo-password-strength": "0.0.1",
 		"tcp-port-used": "0.1.2",
 		"textarea-caret": "3.1.0",
 		"tmp": "0.0.33",
-		"ts-loader": "3.5.0",
-		"ts-node": "4.1.0",
+		"ts-loader": "4.0.1",
+		"ts-node": "5.0.1",
 		"tslint": "5.9.1",
 		"typescript": "2.7.2",
-		"typescript-eslint-parser": "13.0.0",
+		"typescript-eslint-parser": "14.0.0",
 		"uglify-es": "3.3.9",
-		"uglifyjs-webpack-plugin": "1.2.0",
-		"url-loader": "^0.6.2",
+		"uglifyjs-webpack-plugin": "1.2.3",
+		"url-loader": "1.0.1",
 		"uuid": "3.2.1",
 		"v-animate-css": "0.0.2",
 		"vhost": "3.0.2",
-		"vue": "2.5.13",
+		"vue": "2.5.16",
 		"vue-cropperjs": "2.2.0",
 		"vue-js-modal": "1.3.12",
-		"vue-json-tree-view": "^2.1.3",
-		"vue-loader": "14.1.1",
+		"vue-json-tree-view": "2.1.3",
+		"vue-loader": "14.2.1",
 		"vue-router": "3.0.1",
-		"vue-template-compiler": "2.5.13",
+		"vue-template-compiler": "2.5.16",
 		"vuedraggable": "2.16.0",
 		"web-push": "3.3.0",
-		"webpack": "3.11.0",
-		"webpack-cli": "^2.0.8",
+		"webpack": "4.1.1",
+		"webpack-cli": "2.0.12",
 		"webpack-replace-loader": "1.3.0",
 		"websocket": "1.0.25",
-		"ws": "^5.0.0",
+		"ws": "5.0.0",
 		"xev": "2.0.0"
 	}
 }

From 3ad068e159cd15d9655175a1ab0a877c1bfe0551 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Mar 2018 05:26:24 +0900
Subject: [PATCH 0753/1250] :v:

---
 package.json                                  |  4 +-
 .../webpack.config.ts => webpack.config.ts    | 84 ++++++++++++++++---
 webpack/module/rules/collapse-spaces.ts       | 19 -----
 webpack/plugins/consts.ts                     | 41 ---------
 webpack/plugins/hoist.ts                      |  3 -
 webpack/plugins/index.ts                      | 41 ---------
 webpack/plugins/minify.ts                     |  3 -
 7 files changed, 74 insertions(+), 121 deletions(-)
 rename webpack/webpack.config.ts => webpack.config.ts (60%)
 delete mode 100644 webpack/module/rules/collapse-spaces.ts
 delete mode 100644 webpack/plugins/consts.ts
 delete mode 100644 webpack/plugins/hoist.ts
 delete mode 100644 webpack/plugins/index.ts
 delete mode 100644 webpack/plugins/minify.ts

diff --git a/package.json b/package.json
index ca6db98ca..27af69a4c 100644
--- a/package.json
+++ b/package.json
@@ -13,8 +13,8 @@
 		"start": "node ./built",
 		"debug": "DEBUG=misskey:* node ./built",
 		"swagger": "node ./swagger.js",
-		"build": "./node_modules/.bin/webpack --config ./webpack/webpack.config.ts && gulp build",
-		"webpack": "./node_modules/.bin/webpack --config ./webpack/webpack.config.ts",
+		"build": "./node_modules/.bin/webpack && gulp build",
+		"webpack": "./node_modules/.bin/webpack",
 		"gulp": "gulp build",
 		"rebuild": "gulp rebuild",
 		"clean": "gulp clean",
diff --git a/webpack/webpack.config.ts b/webpack.config.ts
similarity index 60%
rename from webpack/webpack.config.ts
rename to webpack.config.ts
index c4ef4b90f..f24160c55 100644
--- a/webpack/webpack.config.ts
+++ b/webpack.config.ts
@@ -3,18 +3,26 @@
  */
 
 import * as fs from 'fs';
+import * as webpack from 'webpack';
+import chalk from 'chalk';
 import jsonImporter from 'node-sass-json-importer';
 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');
+const WebpackOnBuildPlugin = require('on-build-webpack');
+const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
+const ProgressBarPlugin = require('progress-bar-webpack-plugin');
+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 config from './src/conf';
+import { licenseHtml } from './src/common/build/license';
 
-import plugins from './plugins';
-
-import langs from '../locales';
-const meta = require('../package.json');
+import langs from './locales';
+const meta = require('./package.json');
 const version = meta.version;
 
+const env = process.env.NODE_ENV;
+const isProduction = env === 'production';
+
 global['faReplacement'] = faReplacement;
 
 global['collapseSpacesReplacement'] = html => {
@@ -26,7 +34,7 @@ global['collapseSpacesReplacement'] = html => {
 };
 
 global['base64replacement'] = (_, key) => {
-	return fs.readFileSync(__dirname + '/../src/web/' + key, 'base64');
+	return fs.readFileSync(__dirname + '/src/web/' + key, 'base64');
 };
 
 module.exports = Object.keys(langs).map(lang => {
@@ -46,13 +54,64 @@ module.exports = Object.keys(langs).map(lang => {
 	};
 
 	const output = {
-		path: __dirname + '/../built/web/assets',
+		path: __dirname + '/built/web/assets',
 		filename: `[name].${version}.${lang}.js`
 	};
 
 	const i18nReplacer = new I18nReplacer(lang);
 	global['i18nReplacement'] = i18nReplacer.replacement;
 
+	//#region Define consts
+	const consts = {
+		_RECAPTCHA_SITEKEY_: config.recaptcha.site_key,
+		_SW_PUBLICKEY_: config.sw ? config.sw.public_key : null,
+		_THEME_COLOR_: constants.themeColor,
+		_COPYRIGHT_: constants.copyright,
+		_VERSION_: version,
+		_STATUS_URL_: config.status_url,
+		_STATS_URL_: config.stats_url,
+		_DOCS_URL_: config.docs_url,
+		_API_URL_: config.api_url,
+		_DEV_URL_: config.dev_url,
+		_CH_URL_: config.ch_url,
+		_LANG_: lang,
+		_HOST_: config.host,
+		_URL_: config.url,
+		_LICENSE_: licenseHtml,
+		_GOOGLE_MAPS_API_KEY_: config.google_maps_api_key
+	};
+
+	const _consts = {};
+
+	Object.keys(consts).forEach(key => {
+		_consts[key] = JSON.stringify(consts[key]);
+	});
+	//#endregion
+
+	const plugins = [
+		new HardSourceWebpackPlugin(),
+		new ProgressBarPlugin({
+			format: chalk`  {cyan.bold yes we can} {bold [}:bar{bold ]} {green.bold :percent} {gray (:current/:total)} :elapseds`,
+			clear: false
+		}),
+		new webpack.DefinePlugin(_consts),
+		new webpack.DefinePlugin({
+			'process.env': {
+				NODE_ENV: JSON.stringify(process.env.NODE_ENV)
+			}
+		}),
+		new WebpackOnBuildPlugin(stats => {
+			fs.writeFileSync('./version.json', JSON.stringify({
+				version
+			}), 'utf-8');
+		})
+	];
+
+	if (isProduction) {
+		plugins.push(new webpack.optimize.ModuleConcatenationPlugin());
+		plugins.push(minify());
+	}
+
 	return {
 		name,
 		entry,
@@ -159,19 +218,20 @@ module.exports = Object.keys(langs).map(lang => {
 				}]
 			}]
 		},
-		plugins: plugins(version, lang),
+		plugins,
 		output,
 		resolve: {
 			extensions: [
 				'.js', '.ts', '.json'
 			],
 			alias: {
-				'const.styl': __dirname + '/../src/web/const.styl'
+				'const.styl': __dirname + '/src/web/const.styl'
 			}
 		},
 		resolveLoader: {
 			modules: ['node_modules', './webpack/loaders']
 		},
-		cache: true
+		cache: true,
+		devtool: 'source-map'
 	};
 });
diff --git a/webpack/module/rules/collapse-spaces.ts b/webpack/module/rules/collapse-spaces.ts
deleted file mode 100644
index 734c73592..000000000
--- a/webpack/module/rules/collapse-spaces.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import * as fs from 'fs';
-const minify = require('html-minifier').minify;
-
-export default () => ({
-	enforce: 'pre',
-	test: /\.vue$/,
-	exclude: /node_modules/,
-	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/plugins/consts.ts b/webpack/plugins/consts.ts
deleted file mode 100644
index 643589323..000000000
--- a/webpack/plugins/consts.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * Constant Replacer
- */
-
-import * as webpack from 'webpack';
-
-const meta = require('../../package.json');
-const version = meta.version;
-
-const constants = require('../../src/const.json');
-import config from '../../src/conf';
-import { licenseHtml } from '../../src/common/build/license';
-
-export default lang => {
-	const consts = {
-		_RECAPTCHA_SITEKEY_: config.recaptcha.site_key,
-		_SW_PUBLICKEY_: config.sw ? config.sw.public_key : null,
-		_THEME_COLOR_: constants.themeColor,
-		_COPYRIGHT_: constants.copyright,
-		_VERSION_: version,
-		_STATUS_URL_: config.status_url,
-		_STATS_URL_: config.stats_url,
-		_DOCS_URL_: config.docs_url,
-		_API_URL_: config.api_url,
-		_DEV_URL_: config.dev_url,
-		_CH_URL_: config.ch_url,
-		_LANG_: lang,
-		_HOST_: config.host,
-		_URL_: config.url,
-		_LICENSE_: licenseHtml,
-		_GOOGLE_MAPS_API_KEY_: config.google_maps_api_key
-	};
-
-	const _consts = {};
-
-	Object.keys(consts).forEach(key => {
-		_consts[key] = JSON.stringify(consts[key]);
-	});
-
-	return new webpack.DefinePlugin(_consts);
-};
diff --git a/webpack/plugins/hoist.ts b/webpack/plugins/hoist.ts
deleted file mode 100644
index f61133f8d..000000000
--- a/webpack/plugins/hoist.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import * as webpack from 'webpack';
-
-export default () => new webpack.optimize.ModuleConcatenationPlugin();
diff --git a/webpack/plugins/index.ts b/webpack/plugins/index.ts
deleted file mode 100644
index 4023cd6cb..000000000
--- a/webpack/plugins/index.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import * as fs from 'fs';
-import * as webpack from 'webpack';
-const WebpackOnBuildPlugin = require('on-build-webpack');
-const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
-const ProgressBarPlugin = require('progress-bar-webpack-plugin');
-import chalk from 'chalk';
-
-import consts from './consts';
-import hoist from './hoist';
-import minify from './minify';
-
-const env = process.env.NODE_ENV;
-const isProduction = env === 'production';
-
-export default (version, lang) => {
-	const plugins = [
-		new HardSourceWebpackPlugin(),
-		new ProgressBarPlugin({
-			format: chalk`  {cyan.bold yes we can} {bold [}:bar{bold ]} {green.bold :percent} {gray (:current/:total)} :elapseds`,
-			clear: false
-		}),
-		consts(lang),
-		new webpack.DefinePlugin({
-			'process.env': {
-				NODE_ENV: JSON.stringify(process.env.NODE_ENV)
-			}
-		}),
-		new WebpackOnBuildPlugin(stats => {
-			fs.writeFileSync('./version.json', JSON.stringify({
-				version
-			}), 'utf-8');
-		})
-	];
-
-	if (isProduction) {
-		plugins.push(hoist());
-		plugins.push(minify());
-	}
-
-	return plugins;
-};
diff --git a/webpack/plugins/minify.ts b/webpack/plugins/minify.ts
deleted file mode 100644
index e46d4c5a1..000000000
--- a/webpack/plugins/minify.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
-
-export default () => new UglifyJsPlugin();

From fde2477cfc8675d06e5f757aebba8404d83eb116 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Mar 2018 05:27:53 +0900
Subject: [PATCH 0754/1250] oops

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

diff --git a/webpack.config.ts b/webpack.config.ts
index f24160c55..e3684f9d8 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -10,6 +10,7 @@ const minify = require('html-minifier').minify;
 const WebpackOnBuildPlugin = require('on-build-webpack');
 const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
 const ProgressBarPlugin = require('progress-bar-webpack-plugin');
+
 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');
@@ -23,6 +24,7 @@ const version = meta.version;
 const env = process.env.NODE_ENV;
 const isProduction = env === 'production';
 
+//#region Replacer definitions
 global['faReplacement'] = faReplacement;
 
 global['collapseSpacesReplacement'] = html => {
@@ -36,6 +38,7 @@ global['collapseSpacesReplacement'] = html => {
 global['base64replacement'] = (_, key) => {
 	return fs.readFileSync(__dirname + '/src/web/' + key, 'base64');
 };
+//#endregion
 
 module.exports = Object.keys(langs).map(lang => {
 	// Chunk name

From cb00a239002e3fe44592fff8653c0d2d4eb3b5cc Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Mar 2018 12:56:50 +0900
Subject: [PATCH 0755/1250] #1239

---
 package.json                                  |  1 -
 src/web/app/boot.js                           |  6 ++++-
 .../app/desktop/views/components/settings.vue | 11 ++++++--
 src/web/assets/404.js                         |  2 ++
 webpack.config.ts                             | 27 ++++++++++++-------
 5 files changed, 34 insertions(+), 13 deletions(-)

diff --git a/package.json b/package.json
index 27af69a4c..c75e0e111 100644
--- a/package.json
+++ b/package.json
@@ -184,7 +184,6 @@
 		"typescript": "2.7.2",
 		"typescript-eslint-parser": "14.0.0",
 		"uglify-es": "3.3.9",
-		"uglifyjs-webpack-plugin": "1.2.3",
 		"url-loader": "1.0.1",
 		"uuid": "3.2.1",
 		"v-animate-css": "0.0.2",
diff --git a/src/web/app/boot.js b/src/web/app/boot.js
index 2d2e27df3..41685aadc 100644
--- a/src/web/app/boot.js
+++ b/src/web/app/boot.js
@@ -62,13 +62,17 @@
 		app = isMobile ? 'mobile' : 'desktop';
 	}
 
+	// Script version
 	const ver = localStorage.getItem('v') || VERSION;
 
+	// Whether use raw version script
+	const raw = localStorage.getItem('useRawScript') == 'true';
+
 	// Load an app script
 	// Note: 'async' make it possible to load the script asyncly.
 	//       'defer' make it possible to run the script when the dom loaded.
 	const script = document.createElement('script');
-	script.setAttribute('src', `/assets/${app}.${ver}.${lang}.js`);
+	script.setAttribute('src', `/assets/${app}.${ver}.${lang}.${raw ? 'raw' : 'min'}.js`);
 	script.setAttribute('async', 'true');
 	script.setAttribute('defer', 'true');
 	head.appendChild(script);
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index 39d9be01d..5627da1cc 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -160,10 +160,13 @@
 		<section class="other" v-show="page == 'other'">
 			<h1>高度な設定</h1>
 			<mk-switch v-model="debug" text="デバッグモードを有効にする">
-				<span>この設定はアカウントに保存されません。</span>
+				<span>この設定はブラウザに記憶されます。</span>
 			</mk-switch>
 			<mk-switch v-model="enableExperimental" text="実験的機能を有効にする">
-				<span>この設定はアカウントに保存されません。実験的機能を有効にするとMisskeyの動作が不安定になる可能性があります。</span>
+				<span>実験的機能を有効にするとMisskeyの動作が不安定になる可能性があります。この設定はブラウザに記憶されます。</span>
+			</mk-switch>
+			<mk-switch v-model="useRawScript" text="生のスクリプトを読み込む">
+				<span>圧縮されていない「生の」スクリプトを使用します。サイズが大きいため、読み込みに時間がかかる場合があります。この設定はブラウザに記憶されます。</span>
 			</mk-switch>
 		</section>
 
@@ -214,6 +217,7 @@ export default Vue.extend({
 			lang: localStorage.getItem('lang') || '',
 			preventUpdate: localStorage.getItem('preventUpdate') == 'true',
 			debug: localStorage.getItem('debug') == 'true',
+			useRawScript: localStorage.getItem('useRawScript') == 'true',
 			enableExperimental: localStorage.getItem('enableExperimental') == 'true'
 		};
 	},
@@ -236,6 +240,9 @@ export default Vue.extend({
 		debug() {
 			localStorage.setItem('debug', this.debug ? 'true' : 'false');
 		},
+		useRawScript() {
+			localStorage.setItem('useRawScript', this.useRawScript ? 'true' : 'false');
+		},
 		enableExperimental() {
 			localStorage.setItem('enableExperimental', this.enableExperimental ? 'true' : 'false');
 		}
diff --git a/src/web/assets/404.js b/src/web/assets/404.js
index 285704d11..f897f0db6 100644
--- a/src/web/assets/404.js
+++ b/src/web/assets/404.js
@@ -13,6 +13,8 @@ if (yn) {
 		console.error(e);
 	}
 
+	localStorage.removeItem('v');
+
 	location.reload(true);
 } else {
 	alert('問題が解決しない場合はサーバー管理者までお問い合せください。');
diff --git a/webpack.config.ts b/webpack.config.ts
index e3684f9d8..0fb63499c 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -6,7 +6,7 @@ import * as fs from 'fs';
 import * as webpack from 'webpack';
 import chalk from 'chalk';
 import jsonImporter from 'node-sass-json-importer';
-const minify = require('html-minifier').minify;
+const minifyHtml = require('html-minifier').minify;
 const WebpackOnBuildPlugin = require('on-build-webpack');
 const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
 const ProgressBarPlugin = require('progress-bar-webpack-plugin');
@@ -17,7 +17,7 @@ const constants = require('./src/const.json');
 import config from './src/conf';
 import { licenseHtml } from './src/common/build/license';
 
-import langs from './locales';
+import locales from './locales';
 const meta = require('./package.json');
 const version = meta.version;
 
@@ -28,7 +28,7 @@ const isProduction = env === 'production';
 global['faReplacement'] = faReplacement;
 
 global['collapseSpacesReplacement'] = html => {
-	return minify(html, {
+	return minifyHtml(html, {
 		collapseWhitespace: true,
 		collapseInlineTagWhitespace: true,
 		keepClosingSlash: true
@@ -40,7 +40,14 @@ global['base64replacement'] = (_, key) => {
 };
 //#endregion
 
-module.exports = Object.keys(langs).map(lang => {
+const langs = Object.keys(locales);
+
+let entries = langs.map(l => [l, false]);
+entries = entries.concat(langs.map(l => [l, true]));
+
+module.exports = entries.map(x => {
+	const [lang, doMinify] = x;
+
 	// Chunk name
 	const name = lang;
 
@@ -58,10 +65,10 @@ module.exports = Object.keys(langs).map(lang => {
 
 	const output = {
 		path: __dirname + '/built/web/assets',
-		filename: `[name].${version}.${lang}.js`
+		filename: `[name].${version}.${lang}.${doMinify ? 'min' : 'raw'}.js`
 	};
 
-	const i18nReplacer = new I18nReplacer(lang);
+	const i18nReplacer = new I18nReplacer(lang as string);
 	global['i18nReplacement'] = i18nReplacer.replacement;
 
 	//#region Define consts
@@ -110,9 +117,8 @@ module.exports = Object.keys(langs).map(lang => {
 		})
 	];
 
-	if (isProduction) {
+	if (doMinify) {
 		plugins.push(new webpack.optimize.ModuleConcatenationPlugin());
-		plugins.push(minify());
 	}
 
 	return {
@@ -235,6 +241,9 @@ module.exports = Object.keys(langs).map(lang => {
 			modules: ['node_modules', './webpack/loaders']
 		},
 		cache: true,
-		devtool: 'source-map'
+		devtool: 'source-map',
+		optimization: {
+			minimize: doMinify
+		}
 	};
 });

From 508e2773177883832024e0213c7fb2d474f850af Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Mar 2018 13:11:31 +0900
Subject: [PATCH 0756/1250] :v:

---
 src/web/app/boot.js                               |  5 ++++-
 src/web/app/desktop/views/components/settings.vue | 11 ++++++++---
 webpack.config.ts                                 |  3 ++-
 3 files changed, 14 insertions(+), 5 deletions(-)

diff --git a/src/web/app/boot.js b/src/web/app/boot.js
index 41685aadc..4b7d67942 100644
--- a/src/web/app/boot.js
+++ b/src/web/app/boot.js
@@ -65,8 +65,11 @@
 	// Script version
 	const ver = localStorage.getItem('v') || VERSION;
 
+	// Whether in debug mode
+	const isDebug = localStorage.getItem('debug') == 'true';
+
 	// Whether use raw version script
-	const raw = localStorage.getItem('useRawScript') == 'true';
+	const raw = localStorage.getItem('useRawScript') == 'true' && isDebug;
 
 	// Load an app script
 	// Note: 'async' make it possible to load the script asyncly.
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index 5627da1cc..f0cd69f37 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -162,12 +162,17 @@
 			<mk-switch v-model="debug" text="デバッグモードを有効にする">
 				<span>この設定はブラウザに記憶されます。</span>
 			</mk-switch>
+			<template v-if="debug">
+				<mk-switch v-model="useRawScript" text="生のスクリプトを読み込む">
+					<span>圧縮されていない「生の」スクリプトを使用します。サイズが大きいため、読み込みに時間がかかる場合があります。この設定はブラウザに記憶されます。</span>
+				</mk-switch>
+				<div class="none ui info">
+				<p>%fa:info-circle%Misskeyはソースマップも提供しています。</p>
+			</div>
+			</template>
 			<mk-switch v-model="enableExperimental" text="実験的機能を有効にする">
 				<span>実験的機能を有効にするとMisskeyの動作が不安定になる可能性があります。この設定はブラウザに記憶されます。</span>
 			</mk-switch>
-			<mk-switch v-model="useRawScript" text="生のスクリプトを読み込む">
-				<span>圧縮されていない「生の」スクリプトを使用します。サイズが大きいため、読み込みに時間がかかる場合があります。この設定はブラウザに記憶されます。</span>
-			</mk-switch>
 		</section>
 
 		<section class="other" v-show="page == 'other'">
diff --git a/webpack.config.ts b/webpack.config.ts
index 0fb63499c..878908673 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -244,6 +244,7 @@ module.exports = entries.map(x => {
 		devtool: 'source-map',
 		optimization: {
 			minimize: doMinify
-		}
+		},
+		mode: doMinify ? 'production' : 'development'
 	};
 });

From f5f8d923dcc0687a503013b81ccdba7f1e8ea422 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Mar 2018 13:50:43 +0900
Subject: [PATCH 0757/1250] :v:

---
 src/common/othello/ai/index.ts | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 src/common/othello/ai/index.ts

diff --git a/src/common/othello/ai/index.ts b/src/common/othello/ai/index.ts
new file mode 100644
index 000000000..5cd1db82d
--- /dev/null
+++ b/src/common/othello/ai/index.ts
@@ -0,0 +1 @@
+require('./front');

From faf86c08055941a10350ee5765c46a884a406628 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Mar 2018 14:09:38 +0900
Subject: [PATCH 0758/1250] Refactor

---
 src/common/othello/ai/back.ts | 14 +++++++-------
 src/common/othello/core.ts    | 21 +++++++++------------
 2 files changed, 16 insertions(+), 19 deletions(-)

diff --git a/src/common/othello/ai/back.ts b/src/common/othello/ai/back.ts
index 9765b7d4e..38a564f55 100644
--- a/src/common/othello/ai/back.ts
+++ b/src/common/othello/ai/back.ts
@@ -135,7 +135,7 @@ function onGameStarted(g) {
 }
 
 function onSet(x) {
-	o.put(x.color, x.pos, true);
+	o.put(x.color, x.pos);
 
 	if (x.next === botColor) {
 		think();
@@ -157,17 +157,17 @@ function think() {
 	 */
 	const dive = (o: Othello, pos: number, alpha = -Infinity, beta = Infinity, depth = 0): number => {
 		// 試し打ち
-		const undo = o.put(o.turn, pos, true);
+		o.put(o.turn, pos);
 
 		const key = o.board.toString();
 		let cache = db[key];
 		if (cache) {
 			if (alpha >= cache.upper) {
-				o.undo(undo);
+				o.undo();
 				return cache.upper;
 			}
 			if (beta <= cache.lower) {
-				o.undo(undo);
+				o.undo();
 				return cache.lower;
 			}
 			alpha = Math.max(alpha, cache.lower);
@@ -199,7 +199,7 @@ function think() {
 			}
 
 			// 巻き戻し
-			o.undo(undo);
+			o.undo();
 
 			// 接待なら自分が負けた方が高スコア
 			return isSettai
@@ -225,7 +225,7 @@ function think() {
 			});
 
 			// 巻き戻し
-			o.undo(undo);
+			o.undo();
 
 			// ロセオならスコアを反転
 			if (game.settings.is_llotheo) score = -score;
@@ -257,7 +257,7 @@ function think() {
 			}
 
 			// 巻き戻し
-			o.undo(undo);
+			o.undo();
 
 			if (value <= alpha) {
 				cache.upper = value;
diff --git a/src/common/othello/core.ts b/src/common/othello/core.ts
index 5e25578ca..217066d37 100644
--- a/src/common/othello/core.ts
+++ b/src/common/othello/core.ts
@@ -50,6 +50,8 @@ export default class Othello {
 	public prevPos = -1;
 	public prevColor: Color = null;
 
+	private logs: Undo[] = [];
+
 	/**
 	 * ゲームを初期化します
 	 */
@@ -138,13 +140,7 @@ export default class Othello {
 	 * @param color 石の色
 	 * @param pos 位置
 	 */
-	public put(color: Color, pos: number, fast = false): Undo {
-		if (!fast && !this.canPut(color, pos)) {
-			console.warn('can not put this position:', pos, color);
-			console.warn(this.board);
-			return null;
-		}
-
+	public put(color: Color, pos: number) {
 		this.prevPos = pos;
 		this.prevColor = color;
 
@@ -160,14 +156,14 @@ export default class Othello {
 
 		const turn = this.turn;
 
-		this.calcTurn();
-
-		return {
+		this.logs.push({
 			color,
 			pos,
 			effects,
 			turn
-		};
+		});
+
+		this.calcTurn();
 	}
 
 	private calcTurn() {
@@ -181,7 +177,8 @@ export default class Othello {
 		}
 	}
 
-	public undo(undo: Undo) {
+	public undo() {
+		const undo = this.logs.pop();
 		this.prevColor = undo.color;
 		this.prevPos = undo.pos;
 		this.board[undo.pos] = null;

From 63ffb219d7ca0e3a2a63149e59cdc72c8c53a71f Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 15 Mar 2018 14:36:31 +0900
Subject: [PATCH 0759/1250] Update .travis.yml

---
 .travis.yml | 13 ++-----------
 1 file changed, 2 insertions(+), 11 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 6e33a2d12..aa1410c6e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,14 +4,10 @@
 notifications:
   email: false
 
-branches:
-  except:
-    - release
-
 language: node_js
 
 node_js:
-  - 8.4.0
+  - 9.8.0
 
 env:
   - CXX=g++-4.8 NODE_ENV=production
@@ -45,9 +41,4 @@ before_script:
   - cp ./.travis/default.yml ./.config
   - cp ./.travis/test.yml ./.config
 
-  - npm run build
-
-after_success:
-  # リリース
-  - chmod u+x ./.travis/release.sh
-  - if [ $TRAVIS_BRANCH = "master" ] && [ $TRAVIS_PULL_REQUEST = "false" ]; then ./.travis/release.sh; else echo "Skipping releasing task"; fi
+  - travis_wait npm run build

From 8420953f151a12ed8f3fc695411e1a98a64abb6a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Mar 2018 14:37:56 +0900
Subject: [PATCH 0760/1250] :v:

---
 src/api/bot/core.ts | 65 ---------------------------------------------
 1 file changed, 65 deletions(-)

diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
index 75564b81e..1f07d4eee 100644
--- a/src/api/bot/core.ts
+++ b/src/api/bot/core.ts
@@ -7,8 +7,6 @@ import getPostSummary from '../../common/get-post-summary';
 import getUserSummary from '../../common/get-user-summary';
 import getNotificationSummary from '../../common/get-notification-summary';
 
-import Othello, { ai as othelloAi } from '../../common/othello';
-
 const hmm = [
 	'?',
 	'ふぅ~む...?',
@@ -133,11 +131,6 @@ export default class BotCore extends EventEmitter {
 				this.setContext(new GuessingGameContext(this));
 				return await this.context.greet();
 
-			case 'othello':
-			case 'オセロ':
-				this.setContext(new OthelloContext(this));
-				return await this.context.greet();
-
 			default:
 				return hmm[Math.floor(Math.random() * hmm.length)];
 		}
@@ -197,7 +190,6 @@ abstract class Context extends EventEmitter {
 
 	public static import(bot: BotCore, data: any) {
 		if (data.type == 'guessing-game') return GuessingGameContext.import(bot, data.content);
-		if (data.type == 'othello') return OthelloContext.import(bot, data.content);
 		if (data.type == 'post') return PostContext.import(bot, data.content);
 		if (data.type == 'tl') return TlContext.import(bot, data.content);
 		if (data.type == 'notifications') return NotificationsContext.import(bot, data.content);
@@ -444,60 +436,3 @@ class GuessingGameContext extends Context {
 		return context;
 	}
 }
-
-class OthelloContext extends Context {
-	private othello: Othello = null;
-
-	constructor(bot: BotCore) {
-		super(bot);
-
-		this.othello = new Othello();
-	}
-
-	public async greet(): Promise<string> {
-		return this.othello.toPatternString('black');
-	}
-
-	public async q(query: string): Promise<string> {
-		if (query == 'やめる') {
-			this.bot.clearContext();
-			return 'オセロをやめました。';
-		}
-
-		const n = parseInt(query, 10);
-
-		if (isNaN(n)) {
-			return '番号で指定してください。「やめる」と言うとゲームをやめます。';
-		}
-
-		this.othello.setByNumber('black', n);
-		const s = this.othello.toString() + '\n\n...(AI)...\n\n';
-		othelloAi('white', this.othello);
-		if (this.othello.getPattern('black').length === 0) {
-			this.bot.clearContext();
-			const blackCount = this.othello.board.filter(s => s == 'black').length;
-			const whiteCount = this.othello.board.filter(s => s == 'white').length;
-			const winner = blackCount == whiteCount ? '引き分け' : blackCount > whiteCount ? '黒の勝ち' : '白の勝ち';
-			return this.othello.toString() + `\n\n~終了~\n\n黒${blackCount}、白${whiteCount}で${winner}です。`;
-		} else {
-			this.emit('updated');
-			return s + this.othello.toPatternString('black');
-		}
-	}
-
-	public export() {
-		return {
-			type: 'othello',
-			content: {
-				board: this.othello.board
-			}
-		};
-	}
-
-	public static import(bot: BotCore, data: any) {
-		const context = new OthelloContext(bot);
-		context.othello = new Othello();
-		context.othello.board = data.board;
-		return context;
-	}
-}

From a8fed0dff64ba4d526fc5b1c170f496f6a52d775 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Mar 2018 15:02:15 +0900
Subject: [PATCH 0761/1250] #1203

---
 .eslintrc                                     |  2 ++
 .../common/views/components/othello.game.vue  | 20 +++++++++++--------
 2 files changed, 14 insertions(+), 8 deletions(-)

diff --git a/.eslintrc b/.eslintrc
index 679d4f12d..0d854fc2c 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -12,6 +12,8 @@
 		"vue/html-indent": false,
 		"vue/html-self-closing": false,
 		"vue/no-unused-vars": false,
+		"vue/attributes-order": false,
+		"vue/require-prop-types": false,
 		"no-console": 0,
 		"no-unused-vars": 0,
 		"no-empty": 0
diff --git a/src/web/app/common/views/components/othello.game.vue b/src/web/app/common/views/components/othello.game.vue
index 69b212776..1c88c5d44 100644
--- a/src/web/app/common/views/components/othello.game.vue
+++ b/src/web/app/common/views/components/othello.game.vue
@@ -23,14 +23,18 @@
 		</div>
 	</div>
 
-	<p>黒:{{ o.blackCount }} 白:{{ o.whiteCount }} 合計:{{ o.blackCount + o.whiteCount }}</p>
+	<p><b>{{ logPos }}ターン目</b> 黒:{{ o.blackCount }} 白:{{ o.whiteCount }} 合計:{{ o.blackCount + o.whiteCount }}</p>
 
 	<div class="player" v-if="game.is_ended">
-		<el-button type="primary" @click="logPos = 0" :disabled="logPos == 0">%fa:fast-backward%</el-button>
-		<el-button type="primary" @click="logPos--" :disabled="logPos == 0">%fa:backward%</el-button>
+		<el-button-group>
+			<el-button type="primary" @click="logPos = 0" :disabled="logPos == 0">%fa:angle-double-left%</el-button>
+			<el-button type="primary" @click="logPos--" :disabled="logPos == 0">%fa:angle-left%</el-button>
+		</el-button-group>
 		<span>{{ logPos }} / {{ logs.length }}</span>
-		<el-button type="primary" @click="logPos++" :disabled="logPos == logs.length">%fa:forward%</el-button>
-		<el-button type="primary" @click="logPos = logs.length" :disabled="logPos == logs.length">%fa:fast-forward%</el-button>
+		<el-button-group>
+			<el-button type="primary" @click="logPos++" :disabled="logPos == logs.length">%fa:angle-right%</el-button>
+			<el-button type="primary" @click="logPos = logs.length" :disabled="logPos == logs.length">%fa:angle-double-right%</el-button>
+		</el-button-group>
 	</div>
 </div>
 </template>
@@ -100,7 +104,7 @@ export default Vue.extend({
 			});
 			this.logs.forEach((log, i) => {
 				if (i < v) {
-					this.o.put(log.color, log.pos, true);
+					this.o.put(log.color, log.pos);
 				}
 			});
 			this.$forceUpdate();
@@ -117,7 +121,7 @@ export default Vue.extend({
 		});
 
 		this.game.logs.forEach(log => {
-			this.o.put(log.color, log.pos, true);
+			this.o.put(log.color, log.pos);
 		});
 
 		this.logs = this.game.logs;
@@ -306,7 +310,7 @@ export default Vue.extend({
 				background #ccc
 
 	> .player
-		margin-bottom 16px
+		padding-bottom 32px
 
 		> span
 			display inline-block

From 366a7eafcfaa654392deeb21f53c7dda07e6d87b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Mar 2018 15:11:05 +0900
Subject: [PATCH 0762/1250] :v:

---
 webpack.config.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/webpack.config.ts b/webpack.config.ts
index 878908673..a0e03043f 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -8,7 +8,7 @@ import chalk from 'chalk';
 import jsonImporter from 'node-sass-json-importer';
 const minifyHtml = require('html-minifier').minify;
 const WebpackOnBuildPlugin = require('on-build-webpack');
-const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
+//const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
 const ProgressBarPlugin = require('progress-bar-webpack-plugin');
 
 import I18nReplacer from './src/common/build/i18n';
@@ -99,7 +99,7 @@ module.exports = entries.map(x => {
 	//#endregion
 
 	const plugins = [
-		new HardSourceWebpackPlugin(),
+		//new HardSourceWebpackPlugin(),
 		new ProgressBarPlugin({
 			format: chalk`  {cyan.bold yes we can} {bold [}:bar{bold ]} {green.bold :percent} {gray (:current/:total)} :elapseds`,
 			clear: false

From 8d19c1ae1fc7d95a40ebb885f71400640d103a3a Mon Sep 17 00:00:00 2001
From: Aya Morisawa <AyaMorisawa4869@gmail.com>
Date: Thu, 15 Mar 2018 15:16:54 +0900
Subject: [PATCH 0763/1250] Fix #500

---
 docs/setup.en.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/docs/setup.en.md b/docs/setup.en.md
index c9556e8b5..59637804c 100644
--- a/docs/setup.en.md
+++ b/docs/setup.en.md
@@ -14,8 +14,8 @@ If you can use Docker, please see [Setup with Docker](./docker.en.md).
 ----------------------------------------------------------------
 Misskey requires two domains called the primary domain and the secondary domain.
 
-* The primary domain is used to provide main service of Misskey.
-* The secondary domain is used to avoid vulnerabilities such as XSS.
+* The primary-domain is used to provide the main service of Misskey.
+* The secondary-domain is used to avoid vulnerabilities such as XSS.
 
 **Ensure that the secondary domain is not a subdomain of the primary domain.**
 

From 02f8070d950cc5e1c49d894090f63263bc9ce54a Mon Sep 17 00:00:00 2001
From: Aya Morisawa <AyaMorisawa4869@gmail.com>
Date: Thu, 15 Mar 2018 15:55:13 +0900
Subject: [PATCH 0764/1250] Update front.ts

---
 src/common/othello/ai/front.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/common/othello/ai/front.ts b/src/common/othello/ai/front.ts
index 838e5a143..f9e6bca1b 100644
--- a/src/common/othello/ai/front.ts
+++ b/src/common/othello/ai/front.ts
@@ -224,7 +224,7 @@ function gameStart(game) {
  * @param inviter 誘ってきたユーザー
  */
 async function onInviteMe(inviter) {
-	console.log(`Anybody invited me: @${inviter.username}`);
+	console.log(`Someone invited me: @${inviter.username}`);
 
 	// 承認
 	const game = await request.post('https://api.misskey.xyz/othello/match', {

From 0c481a3f9e674aadf7ff848cf63dde3e3d9073a0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Mar 2018 19:53:46 +0900
Subject: [PATCH 0765/1250] :v:

---
 src/web/app/common/mios.ts                    |  77 +++++--
 .../app/common/scripts/streaming/channel.ts   |   5 +-
 src/web/app/common/scripts/streaming/drive.ts |  11 +-
 src/web/app/common/scripts/streaming/home.ts  |   2 +-
 .../scripts/streaming/messaging-index.ts      |  11 +-
 .../app/common/scripts/streaming/messaging.ts |   5 +-
 .../common/scripts/streaming/othello-game.ts  |   5 +-
 .../app/common/scripts/streaming/othello.ts   |  11 +-
 .../app/common/scripts/streaming/requests.ts  |  15 +-
 .../app/common/scripts/streaming/server.ts    |  15 +-
 .../scripts/streaming/stream-manager.ts       |   6 +
 .../app/common/scripts/streaming/stream.ts    |  52 ++++-
 src/web/app/common/views/components/index.ts  |   2 +
 .../views/components/messaging-room.vue       |   2 +-
 .../views/components/othello.gameroom.vue     |   2 +-
 .../common/views/components/othello.room.vue  |  38 ----
 src/web/app/common/views/components/timer.vue |  49 +++++
 .../app/desktop/views/components/settings.vue |  18 +-
 .../desktop/views/components/taskmanager.vue  | 204 ++++++++++++++++++
 .../app/desktop/views/components/window.vue   |   7 +-
 .../desktop/views/widgets/channel.channel.vue |   2 +-
 webpack.config.ts                             |   8 +-
 22 files changed, 446 insertions(+), 101 deletions(-)
 create mode 100644 src/web/app/common/views/components/timer.vue
 create mode 100644 src/web/app/desktop/views/components/taskmanager.vue

diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
index 3690d3171..aa07cb022 100644
--- a/src/web/app/common/mios.ts
+++ b/src/web/app/common/mios.ts
@@ -1,9 +1,11 @@
 import Vue from 'vue';
 import { EventEmitter } from 'eventemitter3';
 import * as merge from 'object-assign-deep';
+import * as uuid from 'uuid';
 
 import { host, apiUrl, swPublickey, version, lang, googleMapsApiKey } from '../config';
 import Progress from './scripts/loading';
+import Connection from './scripts/streaming/stream';
 import { HomeStreamManager } from './scripts/streaming/home';
 import { DriveStreamManager } from './scripts/streaming/drive';
 import { ServerStreamManager } from './scripts/streaming/server';
@@ -151,9 +153,6 @@ 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);
@@ -165,16 +164,6 @@ export default class MiOS extends EventEmitter {
 		this.registerSw = this.registerSw.bind(this);
 		//#endregion
 
-		this.once('signedin', () => {
-			// Init home stream manager
-			this.stream = new HomeStreamManager(this, this.i);
-
-			// Init other stream manager
-			this.streams.driveStream = new DriveStreamManager(this.i);
-			this.streams.messagingIndexStream = new MessagingIndexStreamManager(this.i);
-			this.streams.othelloStream = new OthelloStreamManager(this.i);
-		});
-
 		if (this.debug) {
 			(window as any).os = this;
 		}
@@ -240,6 +229,21 @@ export default class MiOS extends EventEmitter {
 	 * @param callback A function that call when initialized
 	 */
 	public async init(callback) {
+		//#region Init stream managers
+		this.streams.serverStream = new ServerStreamManager(this);
+		this.streams.requestsStream = new RequestsStreamManager(this);
+
+		this.once('signedin', () => {
+			// Init home stream manager
+			this.stream = new HomeStreamManager(this, this.i);
+
+			// Init other stream manager
+			this.streams.driveStream = new DriveStreamManager(this, this.i);
+			this.streams.messagingIndexStream = new MessagingIndexStreamManager(this, this.i);
+			this.streams.othelloStream = new OthelloStreamManager(this, this.i);
+		});
+		//#endregion
+
 		// ユーザーをフェッチしてコールバックする
 		const fetchme = (token, cb) => {
 			let me = null;
@@ -414,6 +418,8 @@ export default class MiOS extends EventEmitter {
 		});
 	}
 
+	public requests = [];
+
 	/**
 	 * Misskey APIにリクエストします
 	 * @param endpoint エンドポイント名
@@ -446,22 +452,41 @@ export default class MiOS extends EventEmitter {
 					data
 				});
 			} else {*/
+				const req = {
+					id: uuid(),
+					date: new Date(),
+					name: endpoint,
+					data,
+					res: null,
+					status: null
+				};
+
+				if (this.debug) {
+					this.requests.push(req);
+				}
+
 				// Send request
 				fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, {
 					method: 'POST',
 					body: JSON.stringify(data),
 					credentials: endpoint === 'signin' ? 'include' : 'omit',
 					cache: 'no-cache'
-				}).then(res => {
+				}).then(async (res) => {
 					if (--pending === 0) spinner.parentNode.removeChild(spinner);
+
+					const body = await res.json();
+
+					if (this.debug) {
+						req.status = res.status;
+						req.res = body;
+					}
+
 					if (res.status === 200) {
-						res.json().then(resolve);
+						resolve(body);
 					} else if (res.status === 204) {
 						resolve();
 					} else {
-						res.json().then(err => {
-							reject(err.error);
-						}, reject);
+						reject(body.error);
 					}
 				}).catch(reject);
 			/*}*/
@@ -499,17 +524,29 @@ export default class MiOS extends EventEmitter {
 			}
 		});
 	}
+
+	public connections: Connection[] = [];
+
+	public registerStreamConnection(connection: Connection) {
+		this.connections.push(connection);
+	}
+
+	public unregisterStreamConnection(connection: Connection) {
+		this.connections = this.connections.filter(c => c != connection);
+	}
 }
 
-class WindowSystem {
-	private windows = new Set();
+class WindowSystem extends EventEmitter {
+	public windows = new Set();
 
 	public add(window) {
 		this.windows.add(window);
+		this.emit('added', window);
 	}
 
 	public remove(window) {
 		this.windows.delete(window);
+		this.emit('removed', window);
 	}
 
 	public getAll() {
diff --git a/src/web/app/common/scripts/streaming/channel.ts b/src/web/app/common/scripts/streaming/channel.ts
index 434b108b9..cab5f4edb 100644
--- a/src/web/app/common/scripts/streaming/channel.ts
+++ b/src/web/app/common/scripts/streaming/channel.ts
@@ -1,11 +1,12 @@
 import Stream from './stream';
+import MiOS from '../../mios';
 
 /**
  * Channel stream connection
  */
 export default class Connection extends Stream {
-	constructor(channelId) {
-		super('channel', {
+	constructor(os: MiOS, channelId) {
+		super(os, 'channel', {
 			channel: channelId
 		});
 	}
diff --git a/src/web/app/common/scripts/streaming/drive.ts b/src/web/app/common/scripts/streaming/drive.ts
index 5805e5803..7ff85b594 100644
--- a/src/web/app/common/scripts/streaming/drive.ts
+++ b/src/web/app/common/scripts/streaming/drive.ts
@@ -1,12 +1,13 @@
 import Stream from './stream';
 import StreamManager from './stream-manager';
+import MiOS from '../../mios';
 
 /**
  * Drive stream connection
  */
 export class DriveStream extends Stream {
-	constructor(me) {
-		super('drive', {
+	constructor(os: MiOS, me) {
+		super(os, 'drive', {
 			i: me.token
 		});
 	}
@@ -14,16 +15,18 @@ export class DriveStream extends Stream {
 
 export class DriveStreamManager extends StreamManager<DriveStream> {
 	private me;
+	private os: MiOS;
 
-	constructor(me) {
+	constructor(os: MiOS, me) {
 		super();
 
 		this.me = me;
+		this.os = os;
 	}
 
 	public getConnection() {
 		if (this.connection == null) {
-			this.connection = new DriveStream(this.me);
+			this.connection = new DriveStream(this.os, this.me);
 		}
 
 		return this.connection;
diff --git a/src/web/app/common/scripts/streaming/home.ts b/src/web/app/common/scripts/streaming/home.ts
index 1f110bfd3..533c23244 100644
--- a/src/web/app/common/scripts/streaming/home.ts
+++ b/src/web/app/common/scripts/streaming/home.ts
@@ -9,7 +9,7 @@ import MiOS from '../../mios';
  */
 export class HomeStream extends Stream {
 	constructor(os: MiOS, me) {
-		super('', {
+		super(os, '', {
 			i: me.token
 		});
 
diff --git a/src/web/app/common/scripts/streaming/messaging-index.ts b/src/web/app/common/scripts/streaming/messaging-index.ts
index 69758416d..84e2174ec 100644
--- a/src/web/app/common/scripts/streaming/messaging-index.ts
+++ b/src/web/app/common/scripts/streaming/messaging-index.ts
@@ -1,12 +1,13 @@
 import Stream from './stream';
 import StreamManager from './stream-manager';
+import MiOS from '../../mios';
 
 /**
  * Messaging index stream connection
  */
 export class MessagingIndexStream extends Stream {
-	constructor(me) {
-		super('messaging-index', {
+	constructor(os: MiOS, me) {
+		super(os, 'messaging-index', {
 			i: me.token
 		});
 	}
@@ -14,16 +15,18 @@ export class MessagingIndexStream extends Stream {
 
 export class MessagingIndexStreamManager extends StreamManager<MessagingIndexStream> {
 	private me;
+	private os: MiOS;
 
-	constructor(me) {
+	constructor(os: MiOS, me) {
 		super();
 
 		this.me = me;
+		this.os = os;
 	}
 
 	public getConnection() {
 		if (this.connection == null) {
-			this.connection = new MessagingIndexStream(this.me);
+			this.connection = new MessagingIndexStream(this.os, this.me);
 		}
 
 		return this.connection;
diff --git a/src/web/app/common/scripts/streaming/messaging.ts b/src/web/app/common/scripts/streaming/messaging.ts
index 1fff2286b..c1b5875cf 100644
--- a/src/web/app/common/scripts/streaming/messaging.ts
+++ b/src/web/app/common/scripts/streaming/messaging.ts
@@ -1,11 +1,12 @@
 import Stream from './stream';
+import MiOS from '../../mios';
 
 /**
  * Messaging stream connection
  */
 export class MessagingStream extends Stream {
-	constructor(me, otherparty) {
-		super('messaging', {
+	constructor(os: MiOS, me, otherparty) {
+		super(os, 'messaging', {
 			i: me.token,
 			otherparty
 		});
diff --git a/src/web/app/common/scripts/streaming/othello-game.ts b/src/web/app/common/scripts/streaming/othello-game.ts
index cdf46d5d8..b85af8f72 100644
--- a/src/web/app/common/scripts/streaming/othello-game.ts
+++ b/src/web/app/common/scripts/streaming/othello-game.ts
@@ -1,8 +1,9 @@
 import Stream from './stream';
+import MiOS from '../../mios';
 
 export class OthelloGameStream extends Stream {
-	constructor(me, game) {
-		super('othello-game', {
+	constructor(os: MiOS, me, game) {
+		super(os, 'othello-game', {
 			i: me ? me.token : null,
 			game: game.id
 		});
diff --git a/src/web/app/common/scripts/streaming/othello.ts b/src/web/app/common/scripts/streaming/othello.ts
index febc5d498..f5d47431c 100644
--- a/src/web/app/common/scripts/streaming/othello.ts
+++ b/src/web/app/common/scripts/streaming/othello.ts
@@ -1,9 +1,10 @@
 import StreamManager from './stream-manager';
 import Stream from './stream';
+import MiOS from '../../mios';
 
 export class OthelloStream extends Stream {
-	constructor(me) {
-		super('othello', {
+	constructor(os: MiOS, me) {
+		super(os, 'othello', {
 			i: me.token
 		});
 	}
@@ -11,16 +12,18 @@ export class OthelloStream extends Stream {
 
 export class OthelloStreamManager extends StreamManager<OthelloStream> {
 	private me;
+	private os: MiOS;
 
-	constructor(me) {
+	constructor(os: MiOS, me) {
 		super();
 
 		this.me = me;
+		this.os = os;
 	}
 
 	public getConnection() {
 		if (this.connection == null) {
-			this.connection = new OthelloStream(this.me);
+			this.connection = new OthelloStream(this.os, this.me);
 		}
 
 		return this.connection;
diff --git a/src/web/app/common/scripts/streaming/requests.ts b/src/web/app/common/scripts/streaming/requests.ts
index 5d199a074..5bec30143 100644
--- a/src/web/app/common/scripts/streaming/requests.ts
+++ b/src/web/app/common/scripts/streaming/requests.ts
@@ -1,19 +1,28 @@
 import Stream from './stream';
 import StreamManager from './stream-manager';
+import MiOS from '../../mios';
 
 /**
  * Requests stream connection
  */
 export class RequestsStream extends Stream {
-	constructor() {
-		super('requests');
+	constructor(os: MiOS) {
+		super(os, 'requests');
 	}
 }
 
 export class RequestsStreamManager extends StreamManager<RequestsStream> {
+	private os: MiOS;
+
+	constructor(os: MiOS) {
+		super();
+
+		this.os = os;
+	}
+
 	public getConnection() {
 		if (this.connection == null) {
-			this.connection = new RequestsStream();
+			this.connection = new RequestsStream(this.os);
 		}
 
 		return this.connection;
diff --git a/src/web/app/common/scripts/streaming/server.ts b/src/web/app/common/scripts/streaming/server.ts
index b12198d2f..3d35ef4d9 100644
--- a/src/web/app/common/scripts/streaming/server.ts
+++ b/src/web/app/common/scripts/streaming/server.ts
@@ -1,19 +1,28 @@
 import Stream from './stream';
 import StreamManager from './stream-manager';
+import MiOS from '../../mios';
 
 /**
  * Server stream connection
  */
 export class ServerStream extends Stream {
-	constructor() {
-		super('server');
+	constructor(os: MiOS) {
+		super(os, 'server');
 	}
 }
 
 export class ServerStreamManager extends StreamManager<ServerStream> {
+	private os: MiOS;
+
+	constructor(os: MiOS) {
+		super();
+
+		this.os = os;
+	}
+
 	public getConnection() {
 		if (this.connection == null) {
-			this.connection = new ServerStream();
+			this.connection = new ServerStream(this.os);
 		}
 
 		return this.connection;
diff --git a/src/web/app/common/scripts/streaming/stream-manager.ts b/src/web/app/common/scripts/streaming/stream-manager.ts
index a4a73c561..568b8b037 100644
--- a/src/web/app/common/scripts/streaming/stream-manager.ts
+++ b/src/web/app/common/scripts/streaming/stream-manager.ts
@@ -31,6 +31,8 @@ export default abstract class StreamManager<T extends Connection> extends EventE
 			this._connection.on('_disconnected_', () => {
 				this.emit('_disconnected_');
 			});
+
+			this._connection.user = 'Managed';
 		}
 	}
 
@@ -77,6 +79,8 @@ export default abstract class StreamManager<T extends Connection> extends EventE
 
 		this.users.push(userId);
 
+		this._connection.user = `Managed (${ this.users.length })`;
+
 		return userId;
 	}
 
@@ -87,6 +91,8 @@ export default abstract class StreamManager<T extends Connection> extends EventE
 	public dispose(userId) {
 		this.users = this.users.filter(id => id != userId);
 
+		this._connection.user = `Managed (${ this.users.length })`;
+
 		// 誰もコネクションの利用者がいなくなったら
 		if (this.users.length == 0) {
 			// また直ぐに再利用される可能性があるので、一定時間待ち、
diff --git a/src/web/app/common/scripts/streaming/stream.ts b/src/web/app/common/scripts/streaming/stream.ts
index 8799f6fe6..189af0ab3 100644
--- a/src/web/app/common/scripts/streaming/stream.ts
+++ b/src/web/app/common/scripts/streaming/stream.ts
@@ -1,6 +1,8 @@
 import { EventEmitter } from 'eventemitter3';
+import * as uuid from 'uuid';
 import * as ReconnectingWebsocket from 'reconnecting-websocket';
 import { apiUrl } from '../../../config';
+import MiOS from '../../mios';
 
 /**
  * Misskey stream connection
@@ -8,9 +10,21 @@ import { apiUrl } from '../../../config';
 export default class Connection extends EventEmitter {
 	public state: string;
 	private buffer: any[];
-	private socket: ReconnectingWebsocket;
+	public socket: ReconnectingWebsocket;
+	public name: string;
+	public connectedAt: Date;
+	public user: string = null;
+	public in: number = 0;
+	public out: number = 0;
+	public inout: Array<{
+		type: 'in' | 'out',
+		at: Date,
+		data: string
+	}> = [];
+	public id: string;
+	private os: MiOS;
 
-	constructor(endpoint, params?) {
+	constructor(os: MiOS, endpoint, params?) {
 		super();
 
 		//#region BIND
@@ -21,6 +35,9 @@ export default class Connection extends EventEmitter {
 		this.close =     this.close.bind(this);
 		//#endregion
 
+		this.id = uuid();
+		this.os = os;
+		this.name = endpoint;
 		this.state = 'initializing';
 		this.buffer = [];
 
@@ -35,6 +52,9 @@ export default class Connection extends EventEmitter {
 		this.socket.addEventListener('open', this.onOpen);
 		this.socket.addEventListener('close', this.onClose);
 		this.socket.addEventListener('message', this.onMessage);
+
+		// Register this connection for debugging
+		this.os.registerStreamConnection(this);
 	}
 
 	/**
@@ -44,11 +64,18 @@ export default class Connection extends EventEmitter {
 		this.state = 'connected';
 		this.emit('_connected_');
 
+		this.connectedAt = new Date();
+
 		// バッファーを処理
 		const _buffer = [].concat(this.buffer); // Shallow copy
 		this.buffer = []; // Clear buffer
-		_buffer.forEach(message => {
-			this.send(message); // Resend each buffered messages
+		_buffer.forEach(data => {
+			this.send(data); // Resend each buffered messages
+
+			if (this.os.debug) {
+				this.out++;
+				this.inout.push({ type: 'out', at: new Date(), data });
+			}
 		});
 	}
 
@@ -64,6 +91,11 @@ export default class Connection extends EventEmitter {
 	 * Callback of when received a message from connection
 	 */
 	private onMessage(message) {
+		if (this.os.debug) {
+			this.in++;
+			this.inout.push({ type: 'in', at: new Date(), data: message.data });
+		}
+
 		try {
 			const msg = JSON.parse(message.data);
 			if (msg.type) this.emit(msg.type, msg.body);
@@ -75,20 +107,26 @@ export default class Connection extends EventEmitter {
 	/**
 	 * Send a message to connection
 	 */
-	public send(message) {
+	public send(data) {
 		// まだ接続が確立されていなかったらバッファリングして次に接続した時に送信する
 		if (this.state != 'connected') {
-			this.buffer.push(message);
+			this.buffer.push(data);
 			return;
 		}
 
-		this.socket.send(JSON.stringify(message));
+		if (this.os.debug) {
+			this.out++;
+			this.inout.push({ type: 'out', at: new Date(), data });
+		}
+
+		this.socket.send(JSON.stringify(data));
 	}
 
 	/**
 	 * Close this connection
 	 */
 	public close() {
+		this.os.unregisterStreamConnection(this);
 		this.socket.removeEventListener('open', this.onOpen);
 		this.socket.removeEventListener('message', this.onMessage);
 	}
diff --git a/src/web/app/common/views/components/index.ts b/src/web/app/common/views/components/index.ts
index 98fc2352f..25f4e461d 100644
--- a/src/web/app/common/views/components/index.ts
+++ b/src/web/app/common/views/components/index.ts
@@ -10,6 +10,7 @@ import pollEditor from './poll-editor.vue';
 import reactionIcon from './reaction-icon.vue';
 import reactionsViewer from './reactions-viewer.vue';
 import time from './time.vue';
+import timer from './timer.vue';
 import images from './images.vue';
 import uploader from './uploader.vue';
 import specialMessage from './special-message.vue';
@@ -33,6 +34,7 @@ Vue.component('mk-poll-editor', pollEditor);
 Vue.component('mk-reaction-icon', reactionIcon);
 Vue.component('mk-reactions-viewer', reactionsViewer);
 Vue.component('mk-time', time);
+Vue.component('mk-timer', timer);
 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/messaging-room.vue b/src/web/app/common/views/components/messaging-room.vue
index 547e9494e..6ff808b61 100644
--- a/src/web/app/common/views/components/messaging-room.vue
+++ b/src/web/app/common/views/components/messaging-room.vue
@@ -66,7 +66,7 @@ export default Vue.extend({
 	},
 
 	mounted() {
-		this.connection = new MessagingStream((this as any).os.i, this.user.id);
+		this.connection = new MessagingStream((this as any).os, (this as any).os.i, this.user.id);
 
 		this.connection.on('message', this.onMessage);
 		this.connection.on('read', this.onRead);
diff --git a/src/web/app/common/views/components/othello.gameroom.vue b/src/web/app/common/views/components/othello.gameroom.vue
index 9df458f64..38a25f668 100644
--- a/src/web/app/common/views/components/othello.gameroom.vue
+++ b/src/web/app/common/views/components/othello.gameroom.vue
@@ -25,7 +25,7 @@ export default Vue.extend({
 	},
 	created() {
 		this.g = this.game;
-		this.connection = new OthelloGameStream((this as any).os.i, this.game);
+		this.connection = new OthelloGameStream((this as any).os, (this as any).os.i, this.game);
 		this.connection.on('started', this.onStarted);
 	},
 	beforeDestroy() {
diff --git a/src/web/app/common/views/components/othello.room.vue b/src/web/app/common/views/components/othello.room.vue
index 3b4296d0b..396541483 100644
--- a/src/web/app/common/views/components/othello.room.vue
+++ b/src/web/app/common/views/components/othello.room.vue
@@ -135,44 +135,6 @@ export default Vue.extend({
 
 		if (this.game.user1_id != (this as any).os.i.id && this.game.settings.form1) this.form = this.game.settings.form1;
 		if (this.game.user2_id != (this as any).os.i.id && this.game.settings.form2) this.form = this.game.settings.form2;
-
-		// for debugging
-		if ((this as any).os.i.username == 'test1') {
-			setTimeout(() => {
-				this.connection.send({
-					type: 'init-form',
-					body: [{
-						id: 'button1',
-						type: 'button',
-						label: 'Enable hoge',
-						value: false
-					}, {
-						id: 'radio1',
-						type: 'radio',
-						label: '強さ',
-						value: 2,
-						items: [{
-							label: '弱',
-							value: 1
-						}, {
-							label: '中',
-							value: 2
-						}, {
-							label: '強',
-							value: 3
-						}]
-					}]
-				});
-
-				this.connection.send({
-					type: 'message',
-					body: {
-						text: 'Hey',
-						type: 'info'
-					}
-				});
-			}, 2000);
-		}
 	},
 
 	beforeDestroy() {
diff --git a/src/web/app/common/views/components/timer.vue b/src/web/app/common/views/components/timer.vue
new file mode 100644
index 000000000..a3c4f01b7
--- /dev/null
+++ b/src/web/app/common/views/components/timer.vue
@@ -0,0 +1,49 @@
+<template>
+<time class="mk-time">
+	{{ hh }}:{{ mm }}:{{ ss }}
+</time>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: {
+		time: {
+			type: [Date, String],
+			required: true
+		}
+	},
+	data() {
+		return {
+			tickId: null,
+			hh: null,
+			mm: null,
+			ss: null
+		};
+	},
+	computed: {
+		_time(): Date {
+			return typeof this.time == 'string' ? new Date(this.time) : this.time;
+		}
+	},
+	created() {
+		this.tick();
+		this.tickId = setInterval(this.tick, 1000);
+	},
+	destroyed() {
+		clearInterval(this.tickId);
+	},
+	methods: {
+		tick() {
+			const now = new Date().getTime();
+			const start = this._time.getTime();
+			const ago = Math.floor((now - start) / 1000);
+
+			this.hh = Math.floor(ago / (60 * 60)).toString().padStart(2, '0');
+			this.mm = Math.floor(ago / 60).toString().padStart(2, '0');
+			this.ss = (ago % 60).toString().padStart(2, '0');
+		}
+	}
+});
+</script>
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index f0cd69f37..950e60fb3 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -173,6 +173,10 @@
 			<mk-switch v-model="enableExperimental" text="実験的機能を有効にする">
 				<span>実験的機能を有効にするとMisskeyの動作が不安定になる可能性があります。この設定はブラウザに記憶されます。</span>
 			</mk-switch>
+			<details v-if="debug">
+				<summary>ツール</summary>
+				<button class="ui button block" @click="taskmngr">タスクマネージャ</button>
+			</details>
 		</section>
 
 		<section class="other" v-show="page == 'other'">
@@ -196,6 +200,7 @@ import XSignins from './settings.signins.vue';
 import XDrive from './settings.drive.vue';
 import { url, docsUrl, license, lang, version } from '../../../config';
 import checkForUpdate from '../../../common/scripts/check-for-update';
+import MkTaskManager from './taskmanager.vue';
 
 export default Vue.extend({
 	components: {
@@ -226,6 +231,11 @@ export default Vue.extend({
 			enableExperimental: localStorage.getItem('enableExperimental') == 'true'
 		};
 	},
+	computed: {
+		licenseUrl(): string {
+			return `${docsUrl}/${lang}/license`;
+		}
+	},
 	watch: {
 		autoPopout() {
 			localStorage.setItem('autoPopout', this.autoPopout ? 'true' : 'false');
@@ -252,17 +262,15 @@ export default Vue.extend({
 			localStorage.setItem('enableExperimental', this.enableExperimental ? 'true' : 'false');
 		}
 	},
-	computed: {
-		licenseUrl(): string {
-			return `${docsUrl}/${lang}/license`;
-		}
-	},
 	created() {
 		(this as any).os.getMeta().then(meta => {
 			this.meta = meta;
 		});
 	},
 	methods: {
+		taskmngr() {
+			(this as any).os.new(MkTaskManager);
+		},
 		customizeHome() {
 			this.$router.push('/i/customize-home');
 			this.$emit('done');
diff --git a/src/web/app/desktop/views/components/taskmanager.vue b/src/web/app/desktop/views/components/taskmanager.vue
new file mode 100644
index 000000000..c0a8b2e9a
--- /dev/null
+++ b/src/web/app/desktop/views/components/taskmanager.vue
@@ -0,0 +1,204 @@
+<template>
+<mk-window ref="window" width="750px" height="500px" @closed="$destroy" name="TaskManager">
+	<span slot="header" :class="$style.header">%fa:stethoscope%タスクマネージャ</span>
+	<el-tabs :class="$style.content">
+		<el-tab-pane label="Requests">
+			<el-table
+				:data="os.requests"
+				style="width: 100%"
+				:default-sort="{prop: 'date', order: 'descending'}"
+			>
+				<el-table-column type="expand">
+					<template slot-scope="props">
+						<pre>{{ props.row.data }}</pre>
+						<pre>{{ props.row.res }}</pre>
+					</template>
+				</el-table-column>
+
+				<el-table-column
+					label="Requested at"
+					prop="date"
+					sortable
+				>
+					<template slot-scope="scope">
+						<b style="margin-right: 8px">{{ scope.row.date.getTime() }}</b>
+						<span>(<mk-time :time="scope.row.date"/>)</span>
+					</template>
+				</el-table-column>
+
+				<el-table-column
+					label="Name"
+				>
+					<template slot-scope="scope">
+						<b>{{ scope.row.name }}</b>
+					</template>
+				</el-table-column>
+
+				<el-table-column
+					label="Status"
+				>
+					<template slot-scope="scope">
+						<span>{{ scope.row.status || '(pending)' }}</span>
+					</template>
+				</el-table-column>
+			</el-table>
+		</el-tab-pane>
+
+		<el-tab-pane label="Streams">
+			<el-table
+				:data="os.connections"
+				style="width: 100%"
+			>
+				<el-table-column
+					label="Uptime"
+				>
+					<template slot-scope="scope">
+						<mk-timer v-if="scope.row.connectedAt" :time="scope.row.connectedAt"/>
+						<span v-else>-</span>
+					</template>
+				</el-table-column>
+
+				<el-table-column
+					label="Name"
+				>
+					<template slot-scope="scope">
+						<b>{{ scope.row.name == '' ? '[Home]' : scope.row.name }}</b>
+					</template>
+				</el-table-column>
+
+				<el-table-column
+					label="User"
+				>
+					<template slot-scope="scope">
+						<span>{{ scope.row.user || '(anonymous)' }}</span>
+					</template>
+				</el-table-column>
+
+				<el-table-column
+					prop="state"
+					label="State"
+				/>
+
+				<el-table-column
+					prop="in"
+					label="In"
+				/>
+
+				<el-table-column
+					prop="out"
+					label="Out"
+				/>
+			</el-table>
+		</el-tab-pane>
+
+		<el-tab-pane label="Streams (Inspect)">
+			<el-tabs type="card" style="height:50%">
+				<el-tab-pane v-for="c in os.connections" :label="c.name == '' ? '[Home]' : c.name" :key="c.id" :name="c.id" ref="connectionsTab">
+					<el-table
+						:data="c.inout"
+						style="width: 100%"
+						:default-sort="{prop: 'at', order: 'descending'}"
+					>
+						<el-table-column type="expand">
+							<template slot-scope="props">
+								<pre>{{ props.row.data }}</pre>
+							</template>
+						</el-table-column>
+
+						<el-table-column
+							label="Date"
+							prop="at"
+							sortable
+						>
+							<template slot-scope="scope">
+								<b style="margin-right: 8px">{{ scope.row.at.getTime() }}</b>
+								<span>(<mk-time :time="scope.row.at"/>)</span>
+							</template>
+						</el-table-column>
+
+						<el-table-column
+							label="Type"
+						>
+							<template slot-scope="scope">
+								<span>{{ getMessageType(scope.row.data) }}</span>
+							</template>
+						</el-table-column>
+
+						<el-table-column
+							label="Incoming / Outgoing"
+							prop="type"
+						/>
+					</el-table>
+				</el-tab-pane>
+			</el-tabs>
+		</el-tab-pane>
+
+		<el-tab-pane label="Windows">
+			<el-table
+				:data="Array.from(os.windows.windows)"
+				style="width: 100%"
+			>
+				<el-table-column
+					label="Name"
+				>
+					<template slot-scope="scope">
+						<b>{{ scope.row.name || '(unknown)' }}</b>
+					</template>
+				</el-table-column>
+
+				<el-table-column
+					label="Operations"
+				>
+					<template slot-scope="scope">
+						<el-button size="mini" type="danger" @click="scope.row.close">Close</el-button>
+					</template>
+				</el-table-column>
+			</el-table>
+		</el-tab-pane>
+	</el-tabs>
+</mk-window>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	mounted() {
+		(this as any).os.windows.on('added', this.onWindowsChanged);
+		(this as any).os.windows.on('removed', this.onWindowsChanged);
+	},
+	beforeDestroy() {
+		(this as any).os.windows.off('added', this.onWindowsChanged);
+		(this as any).os.windows.off('removed', this.onWindowsChanged);
+	},
+	methods: {
+		getMessageType(data): string {
+			return data.type ? data.type : '-';
+		},
+		onWindowsChanged() {
+			this.$forceUpdate();
+		}
+	}
+});
+</script>
+
+<style lang="stylus" module>
+.header
+	> [data-fa]
+		margin-right 4px
+
+.content
+	height 100%
+	overflow auto
+
+</style>
+
+<style>
+.el-tabs__header {
+	margin-bottom: 0 !important;
+}
+
+.el-tabs__item {
+	padding: 0 20px !important;
+}
+</style>
diff --git a/src/web/app/desktop/views/components/window.vue b/src/web/app/desktop/views/components/window.vue
index 42b2600dc..0f89aa3e4 100644
--- a/src/web/app/desktop/views/components/window.vue
+++ b/src/web/app/desktop/views/components/window.vue
@@ -68,7 +68,12 @@ export default Vue.extend({
 			default: 'auto'
 		},
 		popoutUrl: {
-			type: [String, Function]
+			type: [String, Function],
+			default: null
+		},
+		name: {
+			type: String,
+			default: null
 		}
 	},
 
diff --git a/src/web/app/desktop/views/widgets/channel.channel.vue b/src/web/app/desktop/views/widgets/channel.channel.vue
index 02cdf6de1..de5885bfc 100644
--- a/src/web/app/desktop/views/widgets/channel.channel.vue
+++ b/src/web/app/desktop/views/widgets/channel.channel.vue
@@ -54,7 +54,7 @@ export default Vue.extend({
 				});
 
 				this.disconnect();
-				this.connection = new ChannelStream(this.channel.id);
+				this.connection = new ChannelStream((this as any).os, this.channel.id);
 				this.connection.on('post', this.onPost);
 			});
 		},
diff --git a/webpack.config.ts b/webpack.config.ts
index a0e03043f..da8b37564 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -243,8 +243,12 @@ module.exports = entries.map(x => {
 		cache: true,
 		devtool: 'source-map',
 		optimization: {
-			minimize: doMinify
+			minimize: isProduction && doMinify
 		},
-		mode: doMinify ? 'production' : 'development'
+		mode: isProduction
+			? doMinify
+				? 'production'
+				: 'development'
+			: 'development'
 	};
 });

From a222ba73d2377ecb16685ad695cac74207a11eb3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Mar 2018 20:19:26 +0900
Subject: [PATCH 0766/1250] #1241

---
 src/common/othello/ai/back.ts  | 51 +++++++++++++++++++++-------------
 src/common/othello/ai/front.ts | 23 ++++++---------
 2 files changed, 40 insertions(+), 34 deletions(-)

diff --git a/src/common/othello/ai/back.ts b/src/common/othello/ai/back.ts
index 38a564f55..61dde52d1 100644
--- a/src/common/othello/ai/back.ts
+++ b/src/common/othello/ai/back.ts
@@ -6,24 +6,32 @@
  * 切断されてしまうので、別々のプロセスで行うようにします
  */
 
+import * as request from 'request-promise-native';
 import Othello, { Color } from '../core';
+import conf from '../../../conf';
 
 let game;
 let form;
 
 /**
- * このBotのユーザーID
+ * BotアカウントのユーザーID
  */
-let id;
+const id = conf.othello_ai.id;
 
-process.on('message', msg => {
+/**
+ * BotアカウントのAPIキー
+ */
+const i = conf.othello_ai.i;
+
+let post;
+
+process.on('message', async msg => {
 	console.log(msg);
 
 	// 親プロセスからデータをもらう
 	if (msg.type == '_init_') {
 		game = msg.game;
 		form = msg.form;
-		id = msg.id;
 	}
 
 	// フォームが更新されたとき
@@ -37,16 +45,18 @@ process.on('message', msg => {
 
 		//#region TLに投稿する
 		const game = msg.body;
-		const url = `https://misskey.xyz/othello/${game.id}`;
+		const url = `https://${conf.host}/othello/${game.id}`;
 		const user = game.user1_id == id ? game.user2 : game.user1;
 		const isSettai = form[0].value === 0;
 		const text = isSettai
-			? `?[${user.name}](https://misskey.xyz/${user.username})さんの接待を始めました!`
-			: `対局を?[${user.name}](https://misskey.xyz/${user.username})さんと始めました! (強さ${form[0].value})`;
-		process.send({
-			type: 'tl',
-			text: `${text}\n→[観戦する](${url})`
+			? `?[${user.name}](https://${conf.host}/${user.username})さんの接待を始めました!`
+			: `対局を?[${user.name}](https://${conf.host}/${user.username})さんと始めました! (強さ${form[0].value})`;
+		const res = await request.post(`https://api.${conf.host}/posts/create`, {
+			json: { i,
+				text: `${text}\n→[観戦する](${url})`
+			}
 		});
+		post = res.created_post;
 		//#endregion
 	}
 
@@ -58,23 +68,24 @@ process.on('message', msg => {
 		});
 
 		//#region TLに投稿する
-		const url = `https://misskey.xyz/othello/${msg.body.game.id}`;
 		const user = game.user1_id == id ? game.user2 : game.user1;
 		const isSettai = form[0].value === 0;
 		const text = isSettai
 			? msg.body.winner_id === null
-				? `?[${user.name}](https://misskey.xyz/${user.username})さんに接待で引き分けました...`
+				? `?[${user.name}](https://${conf.host}/${user.username})さんに接待で引き分けました...`
 				: msg.body.winner_id == id
-					? `?[${user.name}](https://misskey.xyz/${user.username})さんに接待で勝ってしまいました...`
-					: `?[${user.name}](https://misskey.xyz/${user.username})さんに接待で負けてあげました♪`
+					? `?[${user.name}](https://${conf.host}/${user.username})さんに接待で勝ってしまいました...`
+					: `?[${user.name}](https://${conf.host}/${user.username})さんに接待で負けてあげました♪`
 			: msg.body.winner_id === null
-				? `?[${user.name}](https://misskey.xyz/${user.username})さんと引き分けました~ (強さ${form[0].value})`
+				? `?[${user.name}](https://${conf.host}/${user.username})さんと引き分けました~ (強さ${form[0].value})`
 				: msg.body.winner_id == id
-					? `?[${user.name}](https://misskey.xyz/${user.username})さんに勝ちました♪ (強さ${form[0].value})`
-					: `?[${user.name}](https://misskey.xyz/${user.username})さんに負けました... (強さ${form[0].value})`;
-		process.send({
-			type: 'tl',
-			text: `${text}\n→[結果を見る](${url})`
+					? `?[${user.name}](https://${conf.host}/${user.username})さんに勝ちました♪ (強さ${form[0].value})`
+					: `?[${user.name}](https://${conf.host}/${user.username})さんに負けました... (強さ${form[0].value})`;
+		request.post(`https://api.${conf.host}/posts/create`, {
+			json: { i,
+				reply_id: post.id,
+				text: text
+			}
 		});
 		//#endregion
 	}
diff --git a/src/common/othello/ai/front.ts b/src/common/othello/ai/front.ts
index f9e6bca1b..4d5f6e53e 100644
--- a/src/common/othello/ai/front.ts
+++ b/src/common/othello/ai/front.ts
@@ -29,7 +29,7 @@ const id = conf.othello_ai.id;
 /**
  * ホームストリーム
  */
-const homeStream = new ReconnectingWebSocket(`wss://api.misskey.xyz/?i=${i}`, undefined, {
+const homeStream = new ReconnectingWebSocket(`wss://api.${conf.host}/?i=${i}`, undefined, {
 	constructor: WebSocket
 });
 
@@ -48,6 +48,8 @@ homeStream.on('message', message => {
 	if (msg.type == 'mention' || msg.type == 'reply') {
 		const post = msg.body;
 
+		if (post.user_id == id) return;
+
 		// リアクションする
 		request.post('https://api.misskey.xyz/posts/reactions/create', {
 			json: { i,
@@ -75,7 +77,7 @@ homeStream.on('message', message => {
 		const message = msg.body;
 		if (message.text) {
 			if (message.text.indexOf('オセロ') > -1) {
-				request.post('https://api.misskey.xyz/messaging/messages/create', {
+				request.post(`https://api.${conf.host}/messaging/messages/create`, {
 					json: { i,
 						user_id: message.user_id,
 						text: '良いですよ~'
@@ -90,7 +92,7 @@ homeStream.on('message', message => {
 
 // ユーザーを対局に誘う
 function invite(userId) {
-	request.post('https://api.misskey.xyz/othello/match', {
+	request.post(`https://api.${conf.host}/othello/match`, {
 		json: { i,
 			user_id: userId
 		}
@@ -100,7 +102,7 @@ function invite(userId) {
 /**
  * オセロストリーム
  */
-const othelloStream = new ReconnectingWebSocket(`wss://api.misskey.xyz/othello?i=${i}`, undefined, {
+const othelloStream = new ReconnectingWebSocket(`wss://api.${conf.host}/othello?i=${i}`, undefined, {
 	constructor: WebSocket
 });
 
@@ -132,7 +134,7 @@ othelloStream.on('message', message => {
  */
 function gameStart(game) {
 	// ゲームストリームに接続
-	const gw = new ReconnectingWebSocket(`wss://api.misskey.xyz/othello-game?i=${i}&game=${game.id}`, undefined, {
+	const gw = new ReconnectingWebSocket(`wss://api.${conf.host}/othello-game?i=${i}&game=${game.id}`, undefined, {
 		constructor: WebSocket
 	});
 
@@ -170,8 +172,7 @@ function gameStart(game) {
 		ai.send({
 			type: '_init_',
 			game,
-			form,
-			id
+			form
 		});
 
 		ai.on('message', msg => {
@@ -180,12 +181,6 @@ function gameStart(game) {
 					type: 'set',
 					pos: msg.pos
 				}));
-			} else if (msg.type == 'tl') {
-				request.post('https://api.misskey.xyz/posts/create', {
-					json: { i,
-						text: msg.text
-					}
-				});
 			} else if (msg.type == 'close') {
 				gw.close();
 			}
@@ -227,7 +222,7 @@ async function onInviteMe(inviter) {
 	console.log(`Someone invited me: @${inviter.username}`);
 
 	// 承認
-	const game = await request.post('https://api.misskey.xyz/othello/match', {
+	const game = await request.post(`https://api.${conf.host}/othello/match`, {
 		json: {
 			i,
 			user_id: inviter.id

From 1ab8df6a0f4603f74e0e44d1a045f95b9e14fa26 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Mar 2018 20:20:25 +0900
Subject: [PATCH 0767/1250] v4151

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index c75e0e111..b933a6a87 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4131",
+	"version": "0.0.4151",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 69396470034bbbe19a6ff685c5fe3bea3cb5312e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Mar 2018 20:30:31 +0900
Subject: [PATCH 0768/1250] :v:

---
 package.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index b933a6a87..e5d881fe0 100644
--- a/package.json
+++ b/package.json
@@ -13,8 +13,8 @@
 		"start": "node ./built",
 		"debug": "DEBUG=misskey:* node ./built",
 		"swagger": "node ./swagger.js",
-		"build": "./node_modules/.bin/webpack && gulp build",
-		"webpack": "./node_modules/.bin/webpack",
+		"build": "./node_modules/.bin/webpack --max_old_space_size 512 && gulp build",
+		"webpack": "./node_modules/.bin/webpack --max_old_space_size 512",
 		"gulp": "gulp build",
 		"rebuild": "gulp rebuild",
 		"clean": "gulp clean",

From 7fdf53675489eb3db07a5f55e21204bec28ee6ca Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Mar 2018 20:33:28 +0900
Subject: [PATCH 0769/1250] Disable source map

---
 webpack.config.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/webpack.config.ts b/webpack.config.ts
index da8b37564..873251087 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -241,7 +241,7 @@ module.exports = entries.map(x => {
 			modules: ['node_modules', './webpack/loaders']
 		},
 		cache: true,
-		devtool: 'source-map',
+		devtool: false, //'source-map',
 		optimization: {
 			minimize: isProduction && doMinify
 		},

From 91d05ab7f052d1f4c3bff5ba8610271f124e320c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Mar 2018 20:34:36 +0900
Subject: [PATCH 0770/1250] Revert ":v:"

This reverts commit fea996d623724a21ef22edacfd990d78c889cd1a.
---
 package.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index e5d881fe0..b933a6a87 100644
--- a/package.json
+++ b/package.json
@@ -13,8 +13,8 @@
 		"start": "node ./built",
 		"debug": "DEBUG=misskey:* node ./built",
 		"swagger": "node ./swagger.js",
-		"build": "./node_modules/.bin/webpack --max_old_space_size 512 && gulp build",
-		"webpack": "./node_modules/.bin/webpack --max_old_space_size 512",
+		"build": "./node_modules/.bin/webpack && gulp build",
+		"webpack": "./node_modules/.bin/webpack",
 		"gulp": "gulp build",
 		"rebuild": "gulp rebuild",
 		"clean": "gulp clean",

From c4b8b79c9748163d47b86ea5a445cd1eca1f0b88 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Mar 2018 20:36:43 +0900
Subject: [PATCH 0771/1250] :v:

---
 package.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index b933a6a87..0831a27c6 100644
--- a/package.json
+++ b/package.json
@@ -13,8 +13,8 @@
 		"start": "node ./built",
 		"debug": "DEBUG=misskey:* node ./built",
 		"swagger": "node ./swagger.js",
-		"build": "./node_modules/.bin/webpack && gulp build",
-		"webpack": "./node_modules/.bin/webpack",
+		"build": "node --max_old_space_size=4096 ./node_modules/webpack/bin/webpack.js && gulp build",
+		"webpack": "node --max_old_space_size=4096 ./node_modules/webpack/bin/webpack.js",
 		"gulp": "gulp build",
 		"rebuild": "gulp rebuild",
 		"clean": "gulp clean",

From 539f7699192d59473a67a31016627debfe5301bc Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 16 Mar 2018 05:45:28 +0900
Subject: [PATCH 0772/1250] :v:

---
 .eslintrc           |  5 +++++
 gulpfile.ts         |  3 ++-
 src/web/app/boot.js |  4 +++-
 webpack.config.ts   | 21 ++++++++++-----------
 4 files changed, 20 insertions(+), 13 deletions(-)

diff --git a/.eslintrc b/.eslintrc
index 0d854fc2c..7a74d6ef9 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -17,5 +17,10 @@
 		"no-console": 0,
 		"no-unused-vars": 0,
 		"no-empty": 0
+	},
+	"globals": {
+		"ENV": true,
+		"VERSION": true,
+		"API": true
 	}
 }
diff --git a/gulpfile.ts b/gulpfile.ts
index df7f7e329..b70e5d8bc 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -28,7 +28,7 @@ import config from './src/conf';
 
 const uglify = uglifyComposer(uglifyes, console);
 
-const env = process.env.NODE_ENV;
+const env = process.env.NODE_ENV || 'development';
 const isProduction = env === 'production';
 const isDebug = !isProduction;
 
@@ -123,6 +123,7 @@ gulp.task('build:client:script', () =>
 	gulp.src(['./src/web/app/boot.js', './src/web/app/safe.js'])
 		.pipe(replace('VERSION', JSON.stringify(version)))
 		.pipe(replace('API', JSON.stringify(config.api_url)))
+		.pipe(replace('ENV', JSON.stringify(env)))
 		.pipe(isProduction ? uglify({
 			toplevel: true
 		} as any) : gutil.noop())
diff --git a/src/web/app/boot.js b/src/web/app/boot.js
index 4b7d67942..2ee61745b 100644
--- a/src/web/app/boot.js
+++ b/src/web/app/boot.js
@@ -36,6 +36,7 @@
 	let lang = navigator.language.split('-')[0];
 	if (!/^(en|ja)$/.test(lang)) lang = 'en';
 	if (localStorage.getItem('lang')) lang = localStorage.getItem('lang');
+	if (ENV != 'production') lang = 'ja';
 
 	// Detect the user agent
 	const ua = navigator.userAgent.toLowerCase();
@@ -69,7 +70,8 @@
 	const isDebug = localStorage.getItem('debug') == 'true';
 
 	// Whether use raw version script
-	const raw = localStorage.getItem('useRawScript') == 'true' && isDebug;
+	const raw = (localStorage.getItem('useRawScript') == 'true' && isDebug)
+		|| ENV != 'production';
 
 	// Load an app script
 	// Note: 'async' make it possible to load the script asyncly.
diff --git a/webpack.config.ts b/webpack.config.ts
index 873251087..05ecd9677 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -21,7 +21,7 @@ import locales from './locales';
 const meta = require('./package.json');
 const version = meta.version;
 
-const env = process.env.NODE_ENV;
+const env = process.env.NODE_ENV || 'development';
 const isProduction = env === 'production';
 
 //#region Replacer definitions
@@ -42,11 +42,12 @@ global['base64replacement'] = (_, key) => {
 
 const langs = Object.keys(locales);
 
-let entries = langs.map(l => [l, false]);
-entries = entries.concat(langs.map(l => [l, true]));
+const entries = isProduction
+	? langs.map(l => [l, false]).concat(langs.map(l => [l, true]))
+	: [['ja', false]];
 
 module.exports = entries.map(x => {
-	const [lang, doMinify] = x;
+	const [lang, shouldOptimize] = x;
 
 	// Chunk name
 	const name = lang;
@@ -65,7 +66,7 @@ module.exports = entries.map(x => {
 
 	const output = {
 		path: __dirname + '/built/web/assets',
-		filename: `[name].${version}.${lang}.${doMinify ? 'min' : 'raw'}.js`
+		filename: `[name].${version}.${lang}.${shouldOptimize ? 'min' : 'raw'}.js`
 	};
 
 	const i18nReplacer = new I18nReplacer(lang as string);
@@ -106,9 +107,7 @@ module.exports = entries.map(x => {
 		}),
 		new webpack.DefinePlugin(_consts),
 		new webpack.DefinePlugin({
-			'process.env': {
-				NODE_ENV: JSON.stringify(process.env.NODE_ENV)
-			}
+			'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
 		}),
 		new WebpackOnBuildPlugin(stats => {
 			fs.writeFileSync('./version.json', JSON.stringify({
@@ -117,7 +116,7 @@ module.exports = entries.map(x => {
 		})
 	];
 
-	if (doMinify) {
+	if (shouldOptimize) {
 		plugins.push(new webpack.optimize.ModuleConcatenationPlugin());
 	}
 
@@ -243,10 +242,10 @@ module.exports = entries.map(x => {
 		cache: true,
 		devtool: false, //'source-map',
 		optimization: {
-			minimize: isProduction && doMinify
+			minimize: isProduction && shouldOptimize
 		},
 		mode: isProduction
-			? doMinify
+			? shouldOptimize
 				? 'production'
 				: 'development'
 			: 'development'

From 95f2f08f39784bdf7dcc96c9f26daeff94f3ab9e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 16 Mar 2018 06:02:18 +0900
Subject: [PATCH 0773/1250] Fix bug

---
 src/web/app/common/mios.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
index aa07cb022..1669504af 100644
--- a/src/web/app/common/mios.ts
+++ b/src/web/app/common/mios.ts
@@ -474,7 +474,7 @@ export default class MiOS extends EventEmitter {
 				}).then(async (res) => {
 					if (--pending === 0) spinner.parentNode.removeChild(spinner);
 
-					const body = await res.json();
+					const body = res.status === 204 ? null : await res.json();
 
 					if (this.debug) {
 						req.status = res.status;

From 38735943ac25a08fffe092ae200a91f93e4b23c7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 16 Mar 2018 06:05:39 +0900
Subject: [PATCH 0774/1250] :v:

---
 src/web/app/common/scripts/streaming/stream.ts    |  5 +++++
 .../app/desktop/views/components/taskmanager.vue  | 15 +++++++++++++++
 2 files changed, 20 insertions(+)

diff --git a/src/web/app/common/scripts/streaming/stream.ts b/src/web/app/common/scripts/streaming/stream.ts
index 189af0ab3..cb4041fd8 100644
--- a/src/web/app/common/scripts/streaming/stream.ts
+++ b/src/web/app/common/scripts/streaming/stream.ts
@@ -22,6 +22,7 @@ export default class Connection extends EventEmitter {
 		data: string
 	}> = [];
 	public id: string;
+	public isSuspended = false;
 	private os: MiOS;
 
 	constructor(os: MiOS, endpoint, params?) {
@@ -91,6 +92,8 @@ export default class Connection extends EventEmitter {
 	 * Callback of when received a message from connection
 	 */
 	private onMessage(message) {
+		if (this.isSuspended) return;
+
 		if (this.os.debug) {
 			this.in++;
 			this.inout.push({ type: 'in', at: new Date(), data: message.data });
@@ -108,6 +111,8 @@ export default class Connection extends EventEmitter {
 	 * Send a message to connection
 	 */
 	public send(data) {
+		if (this.isSuspended) return;
+
 		// まだ接続が確立されていなかったらバッファリングして次に接続した時に送信する
 		if (this.state != 'connected') {
 			this.buffer.push(data);
diff --git a/src/web/app/desktop/views/components/taskmanager.vue b/src/web/app/desktop/views/components/taskmanager.vue
index c0a8b2e9a..a00fabb04 100644
--- a/src/web/app/desktop/views/components/taskmanager.vue
+++ b/src/web/app/desktop/views/components/taskmanager.vue
@@ -94,6 +94,13 @@
 		<el-tab-pane label="Streams (Inspect)">
 			<el-tabs type="card" style="height:50%">
 				<el-tab-pane v-for="c in os.connections" :label="c.name == '' ? '[Home]' : c.name" :key="c.id" :name="c.id" ref="connectionsTab">
+					<div style="padding: 12px 0 0 12px">
+					<el-button size="mini" @click="send(c)">Send</el-button>
+					<el-button size="mini" type="warning" @click="c.isSuspended = true" v-if="!c.isSuspended">Suspend</el-button>
+					<el-button size="mini" type="success" @click="c.isSuspended = false" v-else>Resume</el-button>
+					<el-button size="mini" type="danger" @click="c.close">Disconnect</el-button>
+				</div>
+
 					<el-table
 						:data="c.inout"
 						style="width: 100%"
@@ -177,6 +184,14 @@ export default Vue.extend({
 		},
 		onWindowsChanged() {
 			this.$forceUpdate();
+		},
+		send(c) {
+			(this as any).apis.input({
+				title: 'Send a JSON message',
+				allowEmpty: false
+			}).then(json => {
+				c.send(JSON.parse(json));
+			});
 		}
 	}
 });

From cac7230b06118da850f001670b5bc00136ffcf1e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 16 Mar 2018 06:09:23 +0900
Subject: [PATCH 0775/1250] oops

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

diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
index 1669504af..986630da2 100644
--- a/src/web/app/common/mios.ts
+++ b/src/web/app/common/mios.ts
@@ -405,8 +405,12 @@ export default class MiOS extends EventEmitter {
 			});
 		});
 
+		// Whether use raw version script
+		const raw = (localStorage.getItem('useRawScript') == 'true' && this.debug)
+			|| process.env.NODE_ENV != 'production';
+
 		// The path of service worker script
-		const sw = `/sw.${version}.${lang}.js`;
+		const sw = `/sw.${version}.${lang}.${raw ? 'raw' : 'min'}.js`;
 
 		// Register service worker
 		navigator.serviceWorker.register(sw).then(registration => {

From e1f654e08dc9846a16a7b9f98ee42b7deeed5f79 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 16 Mar 2018 06:09:55 +0900
Subject: [PATCH 0776/1250] v4160

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 0831a27c6..39a963b41 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4151",
+	"version": "0.0.4160",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From a72aae878338901b43e731ae2a49f236780254ac Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 16 Mar 2018 06:17:04 +0900
Subject: [PATCH 0777/1250] :v:

---
 package.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index 39a963b41..29366c655 100644
--- a/package.json
+++ b/package.json
@@ -13,8 +13,8 @@
 		"start": "node ./built",
 		"debug": "DEBUG=misskey:* node ./built",
 		"swagger": "node ./swagger.js",
-		"build": "node --max_old_space_size=4096 ./node_modules/webpack/bin/webpack.js && gulp build",
-		"webpack": "node --max_old_space_size=4096 ./node_modules/webpack/bin/webpack.js",
+		"build": "node --max_old_space_size=8192 ./node_modules/webpack/bin/webpack.js && gulp build",
+		"webpack": "node --max_old_space_size=8192 ./node_modules/webpack/bin/webpack.js",
 		"gulp": "gulp build",
 		"rebuild": "gulp rebuild",
 		"clean": "gulp clean",

From 8bbe1096d90e1f37335603b892a2cb4cd4144836 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 16 Mar 2018 06:22:03 +0900
Subject: [PATCH 0778/1250] :v:

---
 package.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index 29366c655..e05fa66cd 100644
--- a/package.json
+++ b/package.json
@@ -13,8 +13,8 @@
 		"start": "node ./built",
 		"debug": "DEBUG=misskey:* node ./built",
 		"swagger": "node ./swagger.js",
-		"build": "node --max_old_space_size=8192 ./node_modules/webpack/bin/webpack.js && gulp build",
-		"webpack": "node --max_old_space_size=8192 ./node_modules/webpack/bin/webpack.js",
+		"build": "node --max_old_space_size=16384 ./node_modules/webpack/bin/webpack.js && gulp build",
+		"webpack": "node --max_old_space_size=16384 ./node_modules/webpack/bin/webpack.js",
 		"gulp": "gulp build",
 		"rebuild": "gulp rebuild",
 		"clean": "gulp clean",

From 8f98f23f8f4869830c05c6d92f7ba4bec6af2372 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 15 Mar 2018 23:19:11 +0000
Subject: [PATCH 0779/1250] fix(package): update @types/compression to version
 0.0.36

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index e05fa66cd..e27a95714 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,7 @@
 		"@types/body-parser": "1.16.8",
 		"@types/chai": "4.1.2",
 		"@types/chai-http": "3.0.4",
-		"@types/compression": "0.0.35",
+		"@types/compression": "0.0.36",
 		"@types/cookie": "0.3.1",
 		"@types/cors": "2.8.3",
 		"@types/debug": "0.0.30",

From eef5b561d5cc03e04b2e1cef3ad577b0d5b305dd Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 15 Mar 2018 23:28:04 +0000
Subject: [PATCH 0780/1250] fix(package): update @types/inquirer to version
 0.0.38

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index e05fa66cd..b3344435d 100644
--- a/package.json
+++ b/package.json
@@ -50,7 +50,7 @@
 		"@types/gulp-replace": "0.0.31",
 		"@types/gulp-uglify": "3.0.4",
 		"@types/gulp-util": "3.0.34",
-		"@types/inquirer": "0.0.37",
+		"@types/inquirer": "0.0.38",
 		"@types/is-root": "1.0.0",
 		"@types/is-url": "1.2.28",
 		"@types/js-yaml": "3.10.1",

From c9031c6c101c9abe08447622fa8296869df5beab Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 15 Mar 2018 23:35:44 +0000
Subject: [PATCH 0781/1250] fix(package): update @types/mongodb to version
 3.0.8

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index e05fa66cd..22c0ac9db 100644
--- a/package.json
+++ b/package.json
@@ -57,7 +57,7 @@
 		"@types/license-checker": "15.0.0",
 		"@types/mkdirp": "0.5.2",
 		"@types/mocha": "2.2.48",
-		"@types/mongodb": "3.0.7",
+		"@types/mongodb": "3.0.8",
 		"@types/monk": "6.0.0",
 		"@types/morgan": "1.7.35",
 		"@types/ms": "0.7.30",

From 5fd6429b05ed37f35f2e327cf4b9df2034c60a7e Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 15 Mar 2018 23:42:43 +0000
Subject: [PATCH 0782/1250] fix(package): update @types/webpack to version
 4.1.1

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index e05fa66cd..ec97b2890 100644
--- a/package.json
+++ b/package.json
@@ -76,7 +76,7 @@
 		"@types/speakeasy": "2.0.2",
 		"@types/tmp": "0.0.33",
 		"@types/uuid": "3.4.3",
-		"@types/webpack": "4.1.0",
+		"@types/webpack": "4.1.1",
 		"@types/webpack-stream": "3.2.10",
 		"@types/websocket": "0.0.38",
 		"@types/ws": "4.0.1",

From 9cf10b46a71e996161ec957e89033816111a306f Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Fri, 16 Mar 2018 10:38:35 +0000
Subject: [PATCH 0783/1250] fix(package): update css-loader to version 0.28.11

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 52c8b33d0..9ffcad6e5 100644
--- a/package.json
+++ b/package.json
@@ -95,7 +95,7 @@
 		"cookie": "0.3.1",
 		"cors": "2.8.4",
 		"crc-32": "1.2.0",
-		"css-loader": "0.28.10",
+		"css-loader": "0.28.11",
 		"debug": "3.1.0",
 		"deep-equal": "1.0.1",
 		"deepcopy": "0.6.3",

From c8c06da2bee8973c7359a9d74f9ec26eb51c5ca2 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 16 Mar 2018 22:23:53 +0900
Subject: [PATCH 0784/1250] =?UTF-8?q?=E3=82=88=E3=82=8A=E8=89=AF=E3=81=84?=
 =?UTF-8?q?=E9=87=8D=E3=81=BF=E4=BB=98=E3=81=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/common/othello/ai/back.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/common/othello/ai/back.ts b/src/common/othello/ai/back.ts
index 61dde52d1..55c77e6b3 100644
--- a/src/common/othello/ai/back.ts
+++ b/src/common/othello/ai/back.ts
@@ -135,7 +135,7 @@ function onGameStarted(g) {
 		if (get(x - 1, y    ) == 'null') count++;
 		if (get(x - 1, y - 1) == 'null') count++;
 		//return Math.pow(count, 3);
-		return count >= 5 ? 1 : 0;
+		return count >= 4 ? 1 : 0;
 	});
 
 	botColor = game.user1_id == id && game.black == 1 || game.user2_id == id && game.black == 2;

From d0b542dc5bfbf02a3f0797ce97641cb223767af3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 16 Mar 2018 22:30:08 +0900
Subject: [PATCH 0785/1250] Destroy background process when game ended

---
 src/common/othello/ai/back.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/common/othello/ai/back.ts b/src/common/othello/ai/back.ts
index 55c77e6b3..c3a74a834 100644
--- a/src/common/othello/ai/back.ts
+++ b/src/common/othello/ai/back.ts
@@ -88,6 +88,8 @@ process.on('message', async msg => {
 			}
 		});
 		//#endregion
+
+		process.exit();
 	}
 
 	// 打たれたとき

From 9eac90db6eebce3fede9c43c5d1f0513c4a018b2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 16 Mar 2018 22:38:28 +0900
Subject: [PATCH 0786/1250] :v:

---
 src/common/othello/ai/back.ts  | 25 ++++++++++++++-----------
 src/common/othello/ai/front.ts | 16 ++++++++--------
 src/config.ts                  |  4 ++++
 3 files changed, 26 insertions(+), 19 deletions(-)

diff --git a/src/common/othello/ai/back.ts b/src/common/othello/ai/back.ts
index c3a74a834..047ecaf4e 100644
--- a/src/common/othello/ai/back.ts
+++ b/src/common/othello/ai/back.ts
@@ -45,17 +45,19 @@ process.on('message', async msg => {
 
 		//#region TLに投稿する
 		const game = msg.body;
-		const url = `https://${conf.host}/othello/${game.id}`;
+		const url = `${conf.url}/othello/${game.id}`;
 		const user = game.user1_id == id ? game.user2 : game.user1;
 		const isSettai = form[0].value === 0;
 		const text = isSettai
-			? `?[${user.name}](https://${conf.host}/${user.username})さんの接待を始めました!`
-			: `対局を?[${user.name}](https://${conf.host}/${user.username})さんと始めました! (強さ${form[0].value})`;
-		const res = await request.post(`https://api.${conf.host}/posts/create`, {
+			? `?[${user.name}](${conf.url}/${user.username})さんの接待を始めました!`
+			: `対局を?[${user.name}](${conf.url}/${user.username})さんと始めました! (強さ${form[0].value})`;
+
+		const res = await request.post(`${conf.api_url}/posts/create`, {
 			json: { i,
 				text: `${text}\n→[観戦する](${url})`
 			}
 		});
+
 		post = res.created_post;
 		//#endregion
 	}
@@ -72,16 +74,17 @@ process.on('message', async msg => {
 		const isSettai = form[0].value === 0;
 		const text = isSettai
 			? msg.body.winner_id === null
-				? `?[${user.name}](https://${conf.host}/${user.username})さんに接待で引き分けました...`
+				? `?[${user.name}](${conf.url}/${user.username})さんに接待で引き分けました...`
 				: msg.body.winner_id == id
-					? `?[${user.name}](https://${conf.host}/${user.username})さんに接待で勝ってしまいました...`
-					: `?[${user.name}](https://${conf.host}/${user.username})さんに接待で負けてあげました♪`
+					? `?[${user.name}](${conf.url}/${user.username})さんに接待で勝ってしまいました...`
+					: `?[${user.name}](${conf.url}/${user.username})さんに接待で負けてあげました♪`
 			: msg.body.winner_id === null
-				? `?[${user.name}](https://${conf.host}/${user.username})さんと引き分けました~ (強さ${form[0].value})`
+				? `?[${user.name}](${conf.url}/${user.username})さんと引き分けました~ (強さ${form[0].value})`
 				: msg.body.winner_id == id
-					? `?[${user.name}](https://${conf.host}/${user.username})さんに勝ちました♪ (強さ${form[0].value})`
-					: `?[${user.name}](https://${conf.host}/${user.username})さんに負けました... (強さ${form[0].value})`;
-		request.post(`https://api.${conf.host}/posts/create`, {
+					? `?[${user.name}](${conf.url}/${user.username})さんに勝ちました♪ (強さ${form[0].value})`
+					: `?[${user.name}](${conf.url}/${user.username})さんに負けました... (強さ${form[0].value})`;
+
+		request.post(`${conf.api_url}/posts/create`, {
 			json: { i,
 				reply_id: post.id,
 				text: text
diff --git a/src/common/othello/ai/front.ts b/src/common/othello/ai/front.ts
index 4d5f6e53e..d892afbed 100644
--- a/src/common/othello/ai/front.ts
+++ b/src/common/othello/ai/front.ts
@@ -29,7 +29,7 @@ const id = conf.othello_ai.id;
 /**
  * ホームストリーム
  */
-const homeStream = new ReconnectingWebSocket(`wss://api.${conf.host}/?i=${i}`, undefined, {
+const homeStream = new ReconnectingWebSocket(`${conf.ws_url}/?i=${i}`, undefined, {
 	constructor: WebSocket
 });
 
@@ -51,7 +51,7 @@ homeStream.on('message', message => {
 		if (post.user_id == id) return;
 
 		// リアクションする
-		request.post('https://api.misskey.xyz/posts/reactions/create', {
+		request.post(`${conf.api_url}/posts/reactions/create`, {
 			json: { i,
 				post_id: post.id,
 				reaction: 'love'
@@ -60,7 +60,7 @@ homeStream.on('message', message => {
 
 		if (post.text) {
 			if (post.text.indexOf('オセロ') > -1) {
-				request.post('https://api.misskey.xyz/posts/create', {
+				request.post(`${conf.api_url}/posts/create`, {
 					json: { i,
 						reply_id: post.id,
 						text: '良いですよ~'
@@ -77,7 +77,7 @@ homeStream.on('message', message => {
 		const message = msg.body;
 		if (message.text) {
 			if (message.text.indexOf('オセロ') > -1) {
-				request.post(`https://api.${conf.host}/messaging/messages/create`, {
+				request.post(`${conf.api_url}/messaging/messages/create`, {
 					json: { i,
 						user_id: message.user_id,
 						text: '良いですよ~'
@@ -92,7 +92,7 @@ homeStream.on('message', message => {
 
 // ユーザーを対局に誘う
 function invite(userId) {
-	request.post(`https://api.${conf.host}/othello/match`, {
+	request.post(`${conf.api_url}/othello/match`, {
 		json: { i,
 			user_id: userId
 		}
@@ -102,7 +102,7 @@ function invite(userId) {
 /**
  * オセロストリーム
  */
-const othelloStream = new ReconnectingWebSocket(`wss://api.${conf.host}/othello?i=${i}`, undefined, {
+const othelloStream = new ReconnectingWebSocket(`${conf.ws_url}/othello?i=${i}`, undefined, {
 	constructor: WebSocket
 });
 
@@ -134,7 +134,7 @@ othelloStream.on('message', message => {
  */
 function gameStart(game) {
 	// ゲームストリームに接続
-	const gw = new ReconnectingWebSocket(`wss://api.${conf.host}/othello-game?i=${i}&game=${game.id}`, undefined, {
+	const gw = new ReconnectingWebSocket(`${conf.ws_url}/othello-game?i=${i}&game=${game.id}`, undefined, {
 		constructor: WebSocket
 	});
 
@@ -222,7 +222,7 @@ async function onInviteMe(inviter) {
 	console.log(`Someone invited me: @${inviter.username}`);
 
 	// 承認
-	const game = await request.post(`https://api.${conf.host}/othello/match`, {
+	const game = await request.post(`${conf.api_url}/othello/match`, {
 		json: {
 			i,
 			user_id: inviter.id
diff --git a/src/config.ts b/src/config.ts
index 82488cef8..09e06f331 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -103,9 +103,11 @@ type Source = {
 type Mixin = {
 	host: string;
 	scheme: string;
+	ws_scheme: string;
 	secondary_host: string;
 	secondary_scheme: string;
 	api_url: string;
+	ws_url: string;
 	auth_url: string;
 	docs_url: string;
 	ch_url: string;
@@ -131,6 +133,8 @@ export default function load() {
 
 	mixin.host = config.url.substr(config.url.indexOf('://') + 3);
 	mixin.scheme = config.url.substr(0, config.url.indexOf('://'));
+	mixin.ws_scheme = mixin.scheme.replace('http', 'ws');
+	mixin.ws_url = `${mixin.ws_scheme}://api.${mixin.host}`;
 	mixin.secondary_host = config.secondary_url.substr(config.secondary_url.indexOf('://') + 3);
 	mixin.secondary_scheme = config.secondary_url.substr(0, config.secondary_url.indexOf('://'));
 	mixin.api_url = `${mixin.scheme}://api.${mixin.host}`;

From b06a0f98e3924b024698348d28612c940245a822 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 16 Mar 2018 23:38:25 +0900
Subject: [PATCH 0787/1250] :v:

---
 src/web/app/common/views/components/othello.game.vue | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/web/app/common/views/components/othello.game.vue b/src/web/app/common/views/components/othello.game.vue
index 1c88c5d44..414d819a5 100644
--- a/src/web/app/common/views/components/othello.game.vue
+++ b/src/web/app/common/views/components/othello.game.vue
@@ -17,13 +17,14 @@
 		<div v-for="(stone, i) in o.board"
 			:class="{ empty: stone == null, none: o.map[i] == 'null', isEnded: game.is_ended, myTurn: !game.is_ended && isMyTurn, can: turnUser ? o.canPut(turnUser.id == blackUser.id, i) : null, prev: o.prevPos == i }"
 			@click="set(i)"
+			:title="'[' + (o.transformPosToXy(i)[0] + 1) + ', ' + (o.transformPosToXy(i)[1] + 1) + '] (' + i + ')'"
 		>
 			<img v-if="stone === true" :src="`${blackUser.avatar_url}?thumbnail&size=128`" alt="">
 			<img v-if="stone === false" :src="`${whiteUser.avatar_url}?thumbnail&size=128`" alt="">
 		</div>
 	</div>
 
-	<p><b>{{ logPos }}ターン目</b> 黒:{{ o.blackCount }} 白:{{ o.whiteCount }} 合計:{{ o.blackCount + o.whiteCount }}</p>
+	<p class="status"><b>{{ logPos }}ターン目</b> 黒:{{ o.blackCount }} 白:{{ o.whiteCount }} 合計:{{ o.blackCount + o.whiteCount }}</p>
 
 	<div class="player" v-if="game.is_ended">
 		<el-button-group>
@@ -309,6 +310,10 @@ export default Vue.extend({
 			> div:last-child
 				background #ccc
 
+	> .status
+		margin 0
+		padding 16px 0
+
 	> .player
 		padding-bottom 32px
 

From 224b275e34a3c4865b43dad1bcd96c59d3cbaf7d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 16 Mar 2018 23:38:34 +0900
Subject: [PATCH 0788/1250] Add test map

---
 src/common/othello/maps.ts | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts
index 2d4a55b5d..68e5a446f 100644
--- a/src/common/othello/maps.ts
+++ b/src/common/othello/maps.ts
@@ -891,3 +891,21 @@ export const test4: Map = {
 		'-w--b-'
 	]
 };
+
+// https://misskey.xyz/othello/5aaabf7fe126e10b5216ea09 64
+export const test5: Map = {
+	name: 'Test5',
+	category: 'Test',
+	data: [
+		'--wwwwww--',
+		'--wwwbwwww',
+		'-bwwbwbwww',
+		'-bwwwbwbww',
+		'-bwwbwbwbw',
+		'-bwbwbwb-w',
+		'bwbwwbbb-w',
+		'w-wbbbbb--',
+		'--w-b-w---',
+		'----------'
+	]
+};

From a3dcdf82c524b6efde85ddbe20c84e59fffe0526 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 16 Mar 2018 23:38:53 +0900
Subject: [PATCH 0789/1250] Refactor

---
 src/common/othello/ai/back.ts | 134 ++++++++++++++++++++++++++--------
 1 file changed, 104 insertions(+), 30 deletions(-)

diff --git a/src/common/othello/ai/back.ts b/src/common/othello/ai/back.ts
index 047ecaf4e..54e99da38 100644
--- a/src/common/othello/ai/back.ts
+++ b/src/common/othello/ai/back.ts
@@ -26,8 +26,6 @@ const i = conf.othello_ai.i;
 let post;
 
 process.on('message', async msg => {
-	console.log(msg);
-
 	// 親プロセスからデータをもらう
 	if (msg.type == '_init_') {
 		game = msg.game;
@@ -105,7 +103,7 @@ let o: Othello;
 let botColor: Color;
 
 // 各マスの強さ
-let cellStrongs;
+let cellWeights;
 
 /**
  * ゲーム開始時
@@ -122,7 +120,7 @@ function onGameStarted(g) {
 	});
 
 	// 各マスの価値を計算しておく
-	cellStrongs = o.map.map((pix, i) => {
+	cellWeights = o.map.map((pix, i) => {
 		if (pix == 'null') return 0;
 		const [x, y] = o.transformPosToXy(i);
 		let count = 0;
@@ -158,20 +156,50 @@ function onSet(x) {
 	}
 }
 
+const db = {};
+
 function think() {
 	console.log('Thinking...');
+	console.time('think');
 
 	const isSettai = form[0].value === 0;
 
 	// 接待モードのときは、全力(5手先読みくらい)で負けるようにする
 	const maxDepth = isSettai ? 5 : form[0].value;
 
-	const db = {};
+	/**
+	 * Botにとってある局面がどれだけ有利か取得する
+	 */
+	function staticEval() {
+		let score = o.canPutSomewhere(botColor).length;
+
+		cellWeights.forEach((weight, i) => {
+			// 係数
+			const coefficient = 30;
+			weight = weight * coefficient;
+
+			const stone = o.board[i];
+			if (stone === botColor) {
+				// TODO: 価値のあるマスに設置されている自分の石に縦か横に接するマスは価値があると判断する
+				score += weight;
+			} else if (stone !== null) {
+				score -= weight;
+			}
+		});
+
+		// ロセオならスコアを反転
+		if (game.settings.is_llotheo) score = -score;
+
+		// 接待ならスコアを反転
+		if (isSettai) score = -score;
+
+		return score;
+	}
 
 	/**
 	 * αβ法での探索
 	 */
-	const dive = (o: Othello, pos: number, alpha = -Infinity, beta = Infinity, depth = 0): number => {
+	const dive = (pos: number, alpha = -Infinity, beta = Infinity, depth = 0): number => {
 		// 試し打ち
 		o.put(o.turn, pos);
 
@@ -224,31 +252,12 @@ function think() {
 		}
 
 		if (depth === maxDepth) {
-			let score = o.canPutSomewhere(botColor).length;
-
-			cellStrongs.forEach((s, i) => {
-				// 係数
-				const coefficient = 30;
-				s = s * coefficient;
-
-				const stone = o.board[i];
-				if (stone === botColor) {
-					// TODO: 価値のあるマスに設置されている自分の石に縦か横に接するマスは価値があると判断する
-					score += s;
-				} else if (stone !== null) {
-					score -= s;
-				}
-			});
+			// 静的に評価
+			const score = staticEval();
 
 			// 巻き戻し
 			o.undo();
 
-			// ロセオならスコアを反転
-			if (game.settings.is_llotheo) score = -score;
-
-			// 接待ならスコアを反転
-			if (isSettai) score = -score;
-
 			return score;
 		} else {
 			const cans = o.canPutSomewhere(o.turn);
@@ -260,12 +269,12 @@ function think() {
 			// 次のターンのプレイヤーにとって最も良い手を取得
 			for (const p of cans) {
 				if (isBotTurn) {
-					const score = dive(o, p, a, beta, depth + 1);
+					const score = dive(p, a, beta, depth + 1);
 					value = Math.max(value, score);
 					a = Math.max(a, value);
 					if (value >= beta) break;
 				} else {
-					const score = dive(o, p, alpha, b, depth + 1);
+					const score = dive(p, alpha, b, depth + 1);
 					value = Math.min(value, score);
 					b = Math.min(b, value);
 					if (value <= alpha) break;
@@ -290,11 +299,76 @@ function think() {
 		}
 	};
 
+	/**
+	 * αβ法での探索(キャッシュ無し)(デバッグ用)
+	 */
+	const dive2 = (pos: number, alpha = -Infinity, beta = Infinity, depth = 0): number => {
+		// 試し打ち
+		o.put(o.turn, pos);
+
+		const isBotTurn = o.turn === botColor;
+
+		// 勝った
+		if (o.turn === null) {
+			const winner = o.winner;
+
+			// 勝つことによる基本スコア
+			const base = 10000;
+
+			let score;
+
+			if (game.settings.is_llotheo) {
+				// 勝ちは勝ちでも、より自分の石を少なくした方が美しい勝ちだと判定する
+				score = o.winner ? base - (o.blackCount * 100) : base - (o.whiteCount * 100);
+			} else {
+				// 勝ちは勝ちでも、より相手の石を少なくした方が美しい勝ちだと判定する
+				score = o.winner ? base + (o.blackCount * 100) : base + (o.whiteCount * 100);
+			}
+
+			// 巻き戻し
+			o.undo();
+
+			// 接待なら自分が負けた方が高スコア
+			return isSettai
+				? winner !== botColor ? score : -score
+				: winner === botColor ? score : -score;
+		}
+
+		if (depth === maxDepth) {
+			// 静的に評価
+			const score = staticEval();
+
+			// 巻き戻し
+			o.undo();
+
+			return score;
+		} else {
+			const cans = o.canPutSomewhere(o.turn);
+
+			// 次のターンのプレイヤーにとって最も良い手を取得
+			for (const p of cans) {
+				if (isBotTurn) {
+					alpha = Math.max(alpha, dive2(p, alpha, beta, depth + 1));
+					if (alpha >= beta) break;
+				} else {
+					beta = Math.min(beta, dive2(p, alpha, beta, depth + 1));
+					if (alpha >= beta) break;
+				}
+			}
+
+			// 巻き戻し
+			o.undo();
+
+			return isBotTurn ? alpha : beta;
+		}
+	};
+
 	const cans = o.canPutSomewhere(botColor);
-	const scores = cans.map(p => dive(o, p));
+	const scores = cans.map(p => dive(p));
 	const pos = cans[scores.indexOf(Math.max(...scores))];
 
 	console.log('Thinked:', pos);
+	console.timeEnd('think');
 
 	process.send({
 		type: 'put',

From dbcc12a6fe535b2b0188c4d79225aff60f378a88 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 16 Mar 2018 23:56:20 +0900
Subject: [PATCH 0790/1250] Fix bug

---
 src/common/othello/ai/back.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/common/othello/ai/back.ts b/src/common/othello/ai/back.ts
index 54e99da38..b6813bcb8 100644
--- a/src/common/othello/ai/back.ts
+++ b/src/common/othello/ai/back.ts
@@ -82,7 +82,7 @@ process.on('message', async msg => {
 					? `?[${user.name}](${conf.url}/${user.username})さんに勝ちました♪ (強さ${form[0].value})`
 					: `?[${user.name}](${conf.url}/${user.username})さんに負けました... (強さ${form[0].value})`;
 
-		request.post(`${conf.api_url}/posts/create`, {
+		await request.post(`${conf.api_url}/posts/create`, {
 			json: { i,
 				reply_id: post.id,
 				text: text

From 629bda733dac8db09f55f5a74be92e769f675618 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 17 Mar 2018 00:04:33 +0900
Subject: [PATCH 0791/1250] Repost instead of reply

---
 src/common/othello/ai/back.ts | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/common/othello/ai/back.ts b/src/common/othello/ai/back.ts
index b6813bcb8..b87ab9985 100644
--- a/src/common/othello/ai/back.ts
+++ b/src/common/othello/ai/back.ts
@@ -77,14 +77,14 @@ process.on('message', async msg => {
 					? `?[${user.name}](${conf.url}/${user.username})さんに接待で勝ってしまいました...`
 					: `?[${user.name}](${conf.url}/${user.username})さんに接待で負けてあげました♪`
 			: msg.body.winner_id === null
-				? `?[${user.name}](${conf.url}/${user.username})さんと引き分けました~ (強さ${form[0].value})`
+				? `?[${user.name}](${conf.url}/${user.username})さんと引き分けました~`
 				: msg.body.winner_id == id
-					? `?[${user.name}](${conf.url}/${user.username})さんに勝ちました♪ (強さ${form[0].value})`
-					: `?[${user.name}](${conf.url}/${user.username})さんに負けました... (強さ${form[0].value})`;
+					? `?[${user.name}](${conf.url}/${user.username})さんに勝ちました♪`
+					: `?[${user.name}](${conf.url}/${user.username})さんに負けました...`;
 
 		await request.post(`${conf.api_url}/posts/create`, {
 			json: { i,
-				reply_id: post.id,
+				repost_id: post.id,
 				text: text
 			}
 		});

From 9cdee52fcc14965c0000d9319206f2be9332b5ab Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 17 Mar 2018 00:22:22 +0900
Subject: [PATCH 0792/1250] Refactor

---
 src/common/othello/ai/back.ts | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/common/othello/ai/back.ts b/src/common/othello/ai/back.ts
index b87ab9985..42a256c0b 100644
--- a/src/common/othello/ai/back.ts
+++ b/src/common/othello/ai/back.ts
@@ -349,11 +349,10 @@ function think() {
 			for (const p of cans) {
 				if (isBotTurn) {
 					alpha = Math.max(alpha, dive2(p, alpha, beta, depth + 1));
-					if (alpha >= beta) break;
 				} else {
 					beta = Math.min(beta, dive2(p, alpha, beta, depth + 1));
-					if (alpha >= beta) break;
 				}
+				if (alpha >= beta) break;
 			}
 
 			// 巻き戻し

From a3db0fb2066711b736ee3c6c883ed1396e46fbea Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 17 Mar 2018 00:43:00 +0900
Subject: [PATCH 0793/1250] v4182

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 9ffcad6e5..33279ce69 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4160",
+	"version": "0.0.4182",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From a081f7dbca2c53d4af37ae7757246816c7052fa3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 17 Mar 2018 03:33:36 +0900
Subject: [PATCH 0794/1250] :v:

---
 src/api/endpoints/posts.ts                    |   9 ++
 src/web/app/common/views/components/index.ts  |   2 +
 .../views/components/welcome-timeline.vue     | 115 ++++++++++++++++
 src/web/app/desktop/views/pages/welcome.vue   | 127 +++++++++++-------
 src/web/app/mobile/views/pages/welcome.vue    |  48 +++++--
 src/web/assets/welcome.svg                    |   3 +
 6 files changed, 246 insertions(+), 58 deletions(-)
 create mode 100644 src/web/app/common/views/components/welcome-timeline.vue
 create mode 100644 src/web/assets/welcome.svg

diff --git a/src/api/endpoints/posts.ts b/src/api/endpoints/posts.ts
index 3b2942592..7df744d2a 100644
--- a/src/api/endpoints/posts.ts
+++ b/src/api/endpoints/posts.ts
@@ -27,6 +27,10 @@ module.exports = (params) => new Promise(async (res, rej) => {
 	const [poll, pollErr] = $(params.poll).optional.boolean().$;
 	if (pollErr) return rej('invalid poll param');
 
+	// Get 'bot' parameter
+	//const [bot, botErr] = $(params.bot).optional.boolean().$;
+	//if (botErr) return rej('invalid bot param');
+
 	// Get 'limit' parameter
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) return rej('invalid limit param');
@@ -76,6 +80,11 @@ module.exports = (params) => new Promise(async (res, rej) => {
 		query.poll = poll ? { $exists: true, $ne: null } : null;
 	}
 
+	// TODO
+	//if (bot != undefined) {
+	//	query.is_bot = bot;
+	//}
+
 	// Issue query
 	const posts = await Post
 		.find(query, {
diff --git a/src/web/app/common/views/components/index.ts b/src/web/app/common/views/components/index.ts
index 25f4e461d..fbf9d22a0 100644
--- a/src/web/app/common/views/components/index.ts
+++ b/src/web/app/common/views/components/index.ts
@@ -23,6 +23,7 @@ import twitterSetting from './twitter-setting.vue';
 import fileTypeIcon from './file-type-icon.vue';
 import Switch from './switch.vue';
 import Othello from './othello.vue';
+import welcomeTimeline from './welcome-timeline.vue';
 
 Vue.component('mk-signin', signin);
 Vue.component('mk-signup', signup);
@@ -47,3 +48,4 @@ Vue.component('mk-twitter-setting', twitterSetting);
 Vue.component('mk-file-type-icon', fileTypeIcon);
 Vue.component('mk-switch', Switch);
 Vue.component('mk-othello', Othello);
+Vue.component('mk-welcome-timeline', welcomeTimeline);
diff --git a/src/web/app/common/views/components/welcome-timeline.vue b/src/web/app/common/views/components/welcome-timeline.vue
new file mode 100644
index 000000000..ab402f126
--- /dev/null
+++ b/src/web/app/common/views/components/welcome-timeline.vue
@@ -0,0 +1,115 @@
+<template>
+<div class="mk-welcome-timeline">
+	<div v-for="post in posts">
+		<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="body">
+			<header>
+				<router-link class="name" :to="`/${post.user.username}`">{{ post.user.name }}</router-link>
+				<span class="username">@{{ post.user.username }}</span>
+				<div class="info">
+					<router-link class="created-at" :to="`/${post.user.username}/${post.id}`">
+						<mk-time :time="post.created_at"/>
+					</router-link>
+				</div>
+			</header>
+			<div class="text">
+				<mk-post-html :ast="post.ast"/>
+			</div>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	data() {
+		return {
+			fetching: true,
+			posts: []
+		};
+	},
+	mounted() {
+		this.fetch();
+	},
+	methods: {
+		fetch(cb?) {
+			this.fetching = true;
+			(this as any).api('posts', {
+				reply: false,
+				repost: false,
+				media: false,
+				poll: false,
+				bot: false
+			}).then(posts => {
+				this.posts = posts;
+				this.fetching = false;
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-welcome-timeline
+	background #fff
+
+	> div
+		padding 16px
+		overflow-wrap break-word
+		font-size .9em
+		color #4C4C4C
+		border-bottom 1px solid rgba(0, 0, 0, 0.05)
+
+		&:after
+			content ""
+			display block
+			clear both
+
+		> .avatar-anchor
+			display block
+			float left
+			position -webkit-sticky
+			position sticky
+			top 16px
+
+			> img
+				display block
+				width 36px
+				height 36px
+				border-radius 6px
+
+		> .body
+			float right
+			width calc(100% - 36px)
+			padding-left 8px
+
+			> header
+				display flex
+				align-items center
+				margin-bottom 4px
+				white-space nowrap
+
+				> .name
+					display block
+					margin 0 .5em 0 0
+					padding 0
+					overflow hidden
+					font-weight bold
+					text-overflow ellipsis
+
+				> .username
+					margin 0 .5em 0 0
+					color #ccc
+
+				> .info
+					margin-left auto
+					font-size 0.9em
+
+					> .created-at
+						color #c0c0c0
+
+</style>
diff --git a/src/web/app/desktop/views/pages/welcome.vue b/src/web/app/desktop/views/pages/welcome.vue
index 9f6930520..53ea99598 100644
--- a/src/web/app/desktop/views/pages/welcome.vue
+++ b/src/web/app/desktop/views/pages/welcome.vue
@@ -1,13 +1,20 @@
 <template>
 <div class="mk-welcome">
 	<main>
-		<div>
-			<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>
-		<div>
-
+		<div class="top">
+			<div>
+				<div>
+					<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>
+				<div>
+					<div>
+						<header>%fa:comments R% タイムライン</header>
+						<mk-welcome-timeline/>
+					</div>
+				</div>
+			</div>
 		</div>
 	</main>
 	<mk-forkit/>
@@ -69,61 +76,85 @@ export default Vue.extend({
 	> 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
+		> .top
+			display flex
+			width 100%
+			background-image url('/assets/welcome.svg')
+			background-size cover
+			background-position top center
 
-			> h1
-				margin 0
-				font-weight normal
-				font-variant small-caps
-				letter-spacing 12px
+			> div
+				display flex
+				max-width $width
+				margin 0 auto
+				padding 80px 0 0 0
 
-			> p
-				margin 0.5em 0
-				line-height 2em
+				> div:first-child
+					margin 0 48px 0 0
+					color #777
 
-			button
-				padding 8px 16px
-				font-size inherit
+					> h1
+						margin 0
+						font-weight normal
+						font-variant small-caps
+						letter-spacing 12px
 
-			.signup
-				color $theme-color
-				border solid 2px $theme-color
-				border-radius 4px
+					> p
+						margin 0.5em 0
+						line-height 2em
 
-				&:focus
-					box-shadow 0 0 0 3px rgba($theme-color, 0.2)
+					button
+						padding 8px 16px
+						font-size inherit
 
-				&:hover
-					color $theme-color-foreground
-					background $theme-color
+					.signup
+						color $theme-color
+						border solid 2px $theme-color
+						border-radius 4px
 
-				&:active
-					color $theme-color-foreground
-					background darken($theme-color, 10%)
-					border-color darken($theme-color, 10%)
+						&:focus
+							box-shadow 0 0 0 3px rgba($theme-color, 0.2)
 
-			.signin
-				&:focus
-					color #444
+						&:hover
+							color $theme-color-foreground
+							background $theme-color
 
-				&:hover
-					color #444
+						&:active
+							color $theme-color-foreground
+							background darken($theme-color, 10%)
+							border-color darken($theme-color, 10%)
 
-				&:active
-					color #333
+					.signin
+						&:focus
+							color #444
 
-		> div:last-child
-			margin 0 0 0 auto
+						&:hover
+							color #444
+
+						&:active
+							color #333
+
+				> div:last-child
+
+					> div
+						width 410px
+						background #fff
+						border-radius 8px
+						overflow hidden
+
+						> header
+							z-index 1
+							padding 12px 16px
+							color #888d94
+							box-shadow 0 1px 0px rgba(0, 0, 0, 0.1)
+
+						> .mk-welcome-timeline
+							max-height 350px
+							overflow auto
 
 	> footer
-		color #666
+		color #949ea5
 		background #fff
 
 		> div
diff --git a/src/web/app/mobile/views/pages/welcome.vue b/src/web/app/mobile/views/pages/welcome.vue
index 84e5ae550..cb8756f3b 100644
--- a/src/web/app/mobile/views/pages/welcome.vue
+++ b/src/web/app/mobile/views/pages/welcome.vue
@@ -1,9 +1,9 @@
 <template>
 <div class="welcome">
 	<h1><b>Misskey</b>へようこそ</h1>
-	<p>Twitter風ミニブログSNS、Misskeyへようこそ。思ったことを投稿したり、タイムラインでみんなの投稿を読むこともできます。</p>
+	<p>Twitter風ミニブログSNS、Misskeyへようこそ。思ったことを投稿したり、タイムラインでみんなの投稿を読むこともできます。<a href="/signup">アカウントを作成する</a></p>
 	<div class="form">
-		<p>ログイン</p>
+		<p>%fa:lock% ログイン</p>
 		<div>
 			<form @submit.prevent="onSubmit">
 				<input v-model="username" type="text" pattern="^[a-zA-Z0-9-]+$" placeholder="ユーザー名" autofocus required @change="onUsernameChange"/>
@@ -16,13 +16,19 @@
 			</div>
 		</div>
 	</div>
-	<a href="/signup">アカウントを作成する</a>
+	<div class="tl">
+		<p>%fa:comments R% タイムラインを見てみる</p>
+		<mk-welcome-timeline/>
+	</div>
+	<footer>
+		<small>{{ copyright }}</small>
+	</footer>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-import { apiUrl } from '../../../config';
+import { apiUrl, copyright } from '../../../config';
 
 export default Vue.extend({
 	data() {
@@ -32,7 +38,8 @@ export default Vue.extend({
 			username: '',
 			password: '',
 			token: '',
-			apiUrl
+			apiUrl,
+			copyright
 		};
 	},
 	mounted() {
@@ -83,16 +90,12 @@ export default Vue.extend({
 			color #949fa9
 
 	.form
+		margin-bottom 16px
 		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
@@ -143,4 +146,29 @@ export default Vue.extend({
 				padding 16px
 				text-align center
 
+	.tl
+		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
+
+		> .mk-welcome-timeline
+			max-height 300px
+			overflow auto
+
+	> footer
+		text-align center
+		color #949fa9
+
+		> small
+			display block
+			margin 16px 0 0 0
+
 </style>
diff --git a/src/web/assets/welcome.svg b/src/web/assets/welcome.svg
new file mode 100644
index 000000000..b969b0150
--- /dev/null
+++ b/src/web/assets/welcome.svg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7ac334d3aa7b2c6bc1d73a3ed767745d1eea851d0688ce40c7e48d5e70dfcd63
+size 15476

From cdcc394dfdcb571e47ce14e9e44d837ab3bac864 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 17 Mar 2018 03:33:47 +0900
Subject: [PATCH 0795/1250] v4184

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 33279ce69..f566db513 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4182",
+	"version": "0.0.4184",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 1ab872287443b4b2bd4bbf8056792f926527e56f Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Fri, 16 Mar 2018 20:28:40 +0000
Subject: [PATCH 0796/1250] fix(package): update elasticsearch to version
 14.2.1

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index f566db513..5cbd75dbe 100644
--- a/package.json
+++ b/package.json
@@ -100,7 +100,7 @@
 		"deep-equal": "1.0.1",
 		"deepcopy": "0.6.3",
 		"diskusage": "0.2.4",
-		"elasticsearch": "14.2.0",
+		"elasticsearch": "14.2.1",
 		"element-ui": "2.2.2",
 		"emojilib": "2.2.12",
 		"escape-regexp": "0.0.1",

From 36d0bce8d35b3702d97ecece48477cd7d157914b Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Fri, 16 Mar 2018 23:16:15 +0000
Subject: [PATCH 0797/1250] fix(package): update eslint to version 4.19.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 5cbd75dbe..a2b852c57 100644
--- a/package.json
+++ b/package.json
@@ -104,7 +104,7 @@
 		"element-ui": "2.2.2",
 		"emojilib": "2.2.12",
 		"escape-regexp": "0.0.1",
-		"eslint": "4.18.2",
+		"eslint": "4.19.0",
 		"eslint-plugin-vue": "4.3.0",
 		"eventemitter3": "3.0.1",
 		"exif-js": "2.3.0",

From 5a0df1d632a52a858f671f5e90929bfd8c508696 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 17 Mar 2018 17:47:46 +0900
Subject: [PATCH 0798/1250] :art:

---
 .../views/components/welcome-timeline.vue     |  8 ++---
 src/web/app/desktop/views/pages/welcome.vue   | 34 +++++++++++++++----
 src/web/assets/welcome-bg.svg                 |  3 ++
 src/web/assets/welcome-fg.svg                 |  3 ++
 src/web/assets/welcome.svg                    |  3 --
 5 files changed, 37 insertions(+), 14 deletions(-)
 create mode 100644 src/web/assets/welcome-bg.svg
 create mode 100644 src/web/assets/welcome-fg.svg
 delete mode 100644 src/web/assets/welcome.svg

diff --git a/src/web/app/common/views/components/welcome-timeline.vue b/src/web/app/common/views/components/welcome-timeline.vue
index ab402f126..2ff06e2cc 100644
--- a/src/web/app/common/views/components/welcome-timeline.vue
+++ b/src/web/app/common/views/components/welcome-timeline.vue
@@ -78,14 +78,14 @@ export default Vue.extend({
 
 			> img
 				display block
-				width 36px
-				height 36px
+				width 42px
+				height 42px
 				border-radius 6px
 
 		> .body
 			float right
-			width calc(100% - 36px)
-			padding-left 8px
+			width calc(100% - 42px)
+			padding-left 12px
 
 			> header
 				display flex
diff --git a/src/web/app/desktop/views/pages/welcome.vue b/src/web/app/desktop/views/pages/welcome.vue
index 53ea99598..5d1d4ad01 100644
--- a/src/web/app/desktop/views/pages/welcome.vue
+++ b/src/web/app/desktop/views/pages/welcome.vue
@@ -70,9 +70,24 @@ export default Vue.extend({
 	display flex
 	flex-direction column
 	flex 1
-	background #eee
 	$width = 1000px
 
+	background-image url('/assets/welcome-bg.svg')
+	background-size cover
+	background-position top center
+
+	&:before
+		content ""
+		display block
+		position fixed
+		bottom 0
+		left 0
+		width 100%
+		height 100%
+		background-image url('/assets/welcome-fg.svg')
+		background-size cover
+		background-position bottom center
+
 	> main
 		display flex
 		flex 1
@@ -80,9 +95,6 @@ export default Vue.extend({
 		> .top
 			display flex
 			width 100%
-			background-image url('/assets/welcome.svg')
-			background-size cover
-			background-position top center
 
 			> div
 				display flex
@@ -92,7 +104,8 @@ export default Vue.extend({
 
 				> div:first-child
 					margin 0 48px 0 0
-					color #777
+					color #d1e6bf
+					text-shadow 0 0 12px #172062
 
 					> h1
 						margin 0
@@ -154,18 +167,19 @@ export default Vue.extend({
 							overflow auto
 
 	> footer
+		font-size 12px
 		color #949ea5
-		background #fff
 
 		> div
 			max-width $width
 			margin 0 auto
-			padding 42px 0
+			padding 0 0 42px 0
 			text-align center
 
 			> .c
 				margin 16px 0 0 0
 				font-size 10px
+				opacity 0.7
 
 </style>
 
@@ -194,3 +208,9 @@ export default Vue.extend({
 	a
 		color #666
 </style>
+
+<style lang="stylus">
+html
+body
+	background linear-gradient(to bottom, #1e1d65, #bd6659)
+</style>
diff --git a/src/web/assets/welcome-bg.svg b/src/web/assets/welcome-bg.svg
new file mode 100644
index 000000000..ba9d4bf34
--- /dev/null
+++ b/src/web/assets/welcome-bg.svg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:992dba610bbc82bffb0cb8e867c3cd86cde088941084ce205ccbe1f803cff0f4
+size 18113
diff --git a/src/web/assets/welcome-fg.svg b/src/web/assets/welcome-fg.svg
new file mode 100644
index 000000000..112dd7f5c
--- /dev/null
+++ b/src/web/assets/welcome-fg.svg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7e3884fd93b9fd16953a9b57ce607bc250af14c28304f486f254968b68479c1d
+size 12542
diff --git a/src/web/assets/welcome.svg b/src/web/assets/welcome.svg
deleted file mode 100644
index b969b0150..000000000
--- a/src/web/assets/welcome.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:7ac334d3aa7b2c6bc1d73a3ed767745d1eea851d0688ce40c7e48d5e70dfcd63
-size 15476

From a99fedb71c217779ce4a91de25ddc04decc48433 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 17 Mar 2018 17:53:35 +0900
Subject: [PATCH 0799/1250] #1252

---
 src/web/app/common/scripts/compose-notification.ts | 7 +++++++
 src/web/app/desktop/script.ts                      | 8 ++++++++
 2 files changed, 15 insertions(+)

diff --git a/src/web/app/common/scripts/compose-notification.ts b/src/web/app/common/scripts/compose-notification.ts
index d0e0c2098..e1dbd3bc1 100644
--- a/src/web/app/common/scripts/compose-notification.ts
+++ b/src/web/app/common/scripts/compose-notification.ts
@@ -54,6 +54,13 @@ export default function(type, data): Notification {
 				icon: data.user.avatar_url + '?thumbnail&size=64'
 			};
 
+		case 'othello_invited':
+			return {
+				title: '対局への招待があります',
+				body: `${data.parent.name}さんから`,
+				icon: data.parent.avatar_url + '?thumbnail&size=64'
+			};
+
 		default:
 			return null;
 	}
diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
index 25a60d7ec..2362613cd 100644
--- a/src/web/app/desktop/script.ts
+++ b/src/web/app/desktop/script.ts
@@ -150,5 +150,13 @@ function registerNotifications(stream: HomeStreamManager) {
 			};
 			setTimeout(n.close.bind(n), 7000);
 		});
+
+		connection.on('othello_invited', matching => {
+			const _n = composeNotification('othello_invited', matching);
+			const n = new Notification(_n.title, {
+				body: _n.body,
+				icon: _n.icon
+			});
+		});
 	}
 }

From 2e4372d266cca30f7abf1ae13dfbecdfcc7f58cc Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 17 Mar 2018 17:54:08 +0900
Subject: [PATCH 0800/1250] v4192

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index a2b852c57..2271ff51d 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4184",
+	"version": "0.0.4192",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From f114ab02a89e243229eb6048d5c09d59756d89c5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 17 Mar 2018 22:16:35 +0900
Subject: [PATCH 0801/1250] :art:

---
 src/web/assets/welcome-fg.svg | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/web/assets/welcome-fg.svg b/src/web/assets/welcome-fg.svg
index 112dd7f5c..8c8e18b8c 100644
--- a/src/web/assets/welcome-fg.svg
+++ b/src/web/assets/welcome-fg.svg
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:7e3884fd93b9fd16953a9b57ce607bc250af14c28304f486f254968b68479c1d
-size 12542
+oid sha256:1b15baeb2d10b4e42c6577f2a782016b25c7d681e494d1849c621023a28f3dbe
+size 15964

From 540ee5e3643a490b71ba1f57fe71e005c1951ec2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 17 Mar 2018 22:29:25 +0900
Subject: [PATCH 0802/1250] :art:

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

diff --git a/src/web/app/desktop/views/pages/welcome.vue b/src/web/app/desktop/views/pages/welcome.vue
index 5d1d4ad01..47068bd93 100644
--- a/src/web/app/desktop/views/pages/welcome.vue
+++ b/src/web/app/desktop/views/pages/welcome.vue
@@ -10,7 +10,7 @@
 				</div>
 				<div>
 					<div>
-						<header>%fa:comments R% タイムライン</header>
+						<header>%fa:comments R% タイムライン<div><span></span><span></span><span></span></div></header>
 						<mk-welcome-timeline/>
 					</div>
 				</div>
@@ -162,6 +162,30 @@ export default Vue.extend({
 							color #888d94
 							box-shadow 0 1px 0px rgba(0, 0, 0, 0.1)
 
+							> div
+								position absolute
+								top 0
+								right 0
+								padding inherit
+
+								> span
+									display inline-block
+									height 11px
+									width 11px
+									margin-left 5px
+									background #ccc
+									border-radius 100%
+									vertical-align middle
+
+									&:nth-child(1)
+										background #5BCC8B
+
+									&:nth-child(2)
+										background #E6BB46
+
+									&:nth-child(3)
+										background #DF7065
+
 						> .mk-welcome-timeline
 							max-height 350px
 							overflow auto

From 5cab83d6a93dc62134e3ede125629bae1e933072 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 17 Mar 2018 23:01:17 +0900
Subject: [PATCH 0803/1250] :v:

---
 src/api/endpoints/users.ts                    | 48 +++++++++----------
 .../views/components/welcome-timeline.vue     |  5 +-
 src/web/app/desktop/views/pages/welcome.vue   | 41 ++++++++++++----
 src/web/app/mobile/views/pages/welcome.vue    | 44 ++++++++++++++---
 4 files changed, 96 insertions(+), 42 deletions(-)

diff --git a/src/api/endpoints/users.ts b/src/api/endpoints/users.ts
index 095b9fe40..249faed36 100644
--- a/src/api/endpoints/users.ts
+++ b/src/api/endpoints/users.ts
@@ -16,40 +16,38 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) return rej('invalid limit param');
 
-	// Get 'since_id' parameter
-	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) return rej('invalid since_id param');
+	// Get 'offset' parameter
+	const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$;
+	if (offsetErr) return rej('invalid offset param');
 
-	// Get 'until_id' parameter
-	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
-	if (untilIdErr) return rej('invalid until_id param');
-
-	// Check if both of since_id and until_id is specified
-	if (sinceId && untilId) {
-		return rej('cannot set since_id and until_id');
-	}
+	// Get 'sort' parameter
+	const [sort, sortError] = $(params.sort).optional.string().or('+follower|-follower').$;
+	if (sortError) return rej('invalid sort param');
 
 	// Construct query
-	const sort = {
-		_id: -1
-	};
-	const query = {} as any;
-	if (sinceId) {
-		sort._id = 1;
-		query._id = {
-			$gt: sinceId
-		};
-	} else if (untilId) {
-		query._id = {
-			$lt: untilId
+	let _sort;
+	if (sort) {
+		if (sort == '+follower') {
+			_sort = {
+				followers_count: 1
+			};
+		} else if (sort == '-follower') {
+			_sort = {
+				followers_count: -1
+			};
+		}
+	} else {
+		_sort = {
+			_id: -1
 		};
 	}
 
 	// Issue query
 	const users = await User
-		.find(query, {
+		.find({}, {
 			limit: limit,
-			sort: sort
+			sort: _sort,
+			skip: offset
 		});
 
 	// Serialize
diff --git a/src/web/app/common/views/components/welcome-timeline.vue b/src/web/app/common/views/components/welcome-timeline.vue
index 2ff06e2cc..7e35e1f71 100644
--- a/src/web/app/common/views/components/welcome-timeline.vue
+++ b/src/web/app/common/views/components/welcome-timeline.vue
@@ -1,12 +1,12 @@
 <template>
 <div class="mk-welcome-timeline">
 	<div v-for="post in posts">
-		<router-link class="avatar-anchor" :to="`/${post.user.username}`">
+		<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=96`" alt="avatar"/>
 		</router-link>
 		<div class="body">
 			<header>
-				<router-link class="name" :to="`/${post.user.username}`">{{ post.user.name }}</router-link>
+				<router-link class="name" :to="`/${post.user.username}`" v-user-preview="post.user.id">{{ post.user.name }}</router-link>
 				<span class="username">@{{ post.user.username }}</span>
 				<div class="info">
 					<router-link class="created-at" :to="`/${post.user.username}/${post.id}`">
@@ -100,6 +100,7 @@ export default Vue.extend({
 					overflow hidden
 					font-weight bold
 					text-overflow ellipsis
+					color #627079
 
 				> .username
 					margin 0 .5em 0 0
diff --git a/src/web/app/desktop/views/pages/welcome.vue b/src/web/app/desktop/views/pages/welcome.vue
index 47068bd93..2ec7f9209 100644
--- a/src/web/app/desktop/views/pages/welcome.vue
+++ b/src/web/app/desktop/views/pages/welcome.vue
@@ -5,8 +5,13 @@
 			<div>
 				<div>
 					<h1>Share<br>Everything!</h1>
-					<p>ようこそ! <b>Misskey</b>はTwitter風ミニブログSNSです。思ったことや皆と共有したいことを投稿しましょう。タイムラインを見れば、皆の関心事をすぐにチェックすることもできます。<a>詳しく...</a></p>
+					<p>ようこそ! <b>Misskey</b>はTwitter風ミニブログSNSです。思ったことや皆と共有したいことを投稿しましょう。タイムラインを見れば、皆の関心事をすぐにチェックすることもできます。<a :href="aboutUrl">詳しく...</a></p>
 					<p><button class="signup" @click="signup">はじめる</button><button class="signin" @click="signin">ログイン</button></p>
+					<div class="users">
+						<router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="`/${user.username}`" v-user-preview="user.id">
+							<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+						</router-link>
+					</div>
 				</div>
 				<div>
 					<div>
@@ -37,14 +42,24 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import { copyright } from '../../../config';
+import { docsUrl, copyright, lang } from '../../../config';
 
 export default Vue.extend({
 	data() {
 		return {
-			copyright
+			aboutUrl: `${docsUrl}/${lang}/about`,
+			copyright,
+			users: []
 		};
 	},
+	mounted() {
+		(this as any).api('users', {
+			sort: '+follower',
+			limit: 20
+		}).then(users => {
+			this.users = users;
+		});
+	},
 	methods: {
 		signup() {
 			this.$modal.show('signup');
@@ -139,14 +154,22 @@ export default Vue.extend({
 							border-color darken($theme-color, 10%)
 
 					.signin
-						&:focus
-							color #444
-
 						&:hover
-							color #444
+							color #fff
 
-						&:active
-							color #333
+					> .users
+						margin 16px 0 0 0
+
+						> *
+							display inline-block
+							margin 4px
+
+							> *
+								display inline-block
+								width 38px
+								height 38px
+								vertical-align top
+								border-radius 6px
 
 				> div:last-child
 
diff --git a/src/web/app/mobile/views/pages/welcome.vue b/src/web/app/mobile/views/pages/welcome.vue
index cb8756f3b..e212d6c78 100644
--- a/src/web/app/mobile/views/pages/welcome.vue
+++ b/src/web/app/mobile/views/pages/welcome.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="welcome">
 	<h1><b>Misskey</b>へようこそ</h1>
-	<p>Twitter風ミニブログSNS、Misskeyへようこそ。思ったことを投稿したり、タイムラインでみんなの投稿を読むこともできます。<a href="/signup">アカウントを作成する</a></p>
+	<p>Twitter風ミニブログSNS、Misskeyへようこそ。思ったことを投稿したり、タイムラインでみんなの投稿を読むこともできます。<br><a href="/signup">アカウントを作成する</a></p>
 	<div class="form">
 		<p>%fa:lock% ログイン</p>
 		<div>
@@ -20,6 +20,11 @@
 		<p>%fa:comments R% タイムラインを見てみる</p>
 		<mk-welcome-timeline/>
 	</div>
+	<div class="users">
+		<router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="`/${user.username}`">
+			<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		</router-link>
+	</div>
 	<footer>
 		<small>{{ copyright }}</small>
 	</footer>
@@ -39,11 +44,17 @@ export default Vue.extend({
 			password: '',
 			token: '',
 			apiUrl,
-			copyright
+			copyright,
+			users: []
 		};
 	},
 	mounted() {
-		document.documentElement.style.background = '#293946';
+		(this as any).api('users', {
+			sort: '+follower',
+			limit: 20
+		}).then(users => {
+			this.users = users;
+		});
 	},
 	methods: {
 		onUsernameChange() {
@@ -82,7 +93,7 @@ export default Vue.extend({
 		padding 8px
 		font-size 1.5em
 		font-weight normal
-		color #c3c6ca
+		color #cacac3
 
 		& + p
 			margin 0 0 16px 0
@@ -146,7 +157,7 @@ export default Vue.extend({
 				padding 16px
 				text-align center
 
-	.tl
+	> .tl
 		background #fff
 		border solid 1px rgba(0, 0, 0, 0.2)
 		border-radius 8px
@@ -163,12 +174,33 @@ export default Vue.extend({
 			max-height 300px
 			overflow auto
 
+	> .users
+		margin 12px 0 0 0
+
+		> *
+			display inline-block
+			margin 4px
+
+			> *
+				display inline-block
+				width 38px
+				height 38px
+				vertical-align top
+				border-radius 6px
+
 	> footer
 		text-align center
-		color #949fa9
+		color #fff
 
 		> small
 			display block
 			margin 16px 0 0 0
+			opacity 0.7
 
 </style>
+
+<style lang="stylus">
+html
+body
+	background linear-gradient(to bottom, #1e1d65, #bd6659)
+</style>

From d4e38c2c2b99a1827c7933cfffa8f523f88932d1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 17 Mar 2018 23:01:53 +0900
Subject: [PATCH 0804/1250] v4196

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 2271ff51d..d9264b006 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4192",
+	"version": "0.0.4196",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From f9c024e2726b52061009d2804cf8627052ffe1d4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 17 Mar 2018 23:16:01 +0900
Subject: [PATCH 0805/1250] oops

---
 src/api/endpoints/users.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/api/endpoints/users.ts b/src/api/endpoints/users.ts
index 249faed36..4acc13c28 100644
--- a/src/api/endpoints/users.ts
+++ b/src/api/endpoints/users.ts
@@ -29,11 +29,11 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	if (sort) {
 		if (sort == '+follower') {
 			_sort = {
-				followers_count: 1
+				followers_count: -1
 			};
 		} else if (sort == '-follower') {
 			_sort = {
-				followers_count: -1
+				followers_count: 1
 			};
 		}
 	} else {

From 077e363fd32fdb0ada9acf54aba9b8084b1d35e6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Mar 2018 03:40:06 +0900
Subject: [PATCH 0806/1250] :art:

---
 src/web/assets/welcome-bg.svg | 4 ++--
 src/web/assets/welcome-fg.svg | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/web/assets/welcome-bg.svg b/src/web/assets/welcome-bg.svg
index ba9d4bf34..accd2d790 100644
--- a/src/web/assets/welcome-bg.svg
+++ b/src/web/assets/welcome-bg.svg
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:992dba610bbc82bffb0cb8e867c3cd86cde088941084ce205ccbe1f803cff0f4
-size 18113
+oid sha256:815b59836e1c6d3698c7cba1e937bd19a2a00f926805c328c504b1238389f456
+size 20184
diff --git a/src/web/assets/welcome-fg.svg b/src/web/assets/welcome-fg.svg
index 8c8e18b8c..56c88062b 100644
--- a/src/web/assets/welcome-fg.svg
+++ b/src/web/assets/welcome-fg.svg
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:1b15baeb2d10b4e42c6577f2a782016b25c7d681e494d1849c621023a28f3dbe
-size 15964
+oid sha256:32b1b0f50dc582638edf866297ebbcf24c3ed1fcbe73bf34008d5f860adf8d5d
+size 18599

From 8cbf717aabb382eb61eebf9863e7e0c69dc2dfdd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Mar 2018 03:40:51 +0900
Subject: [PATCH 0807/1250] :art:

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

diff --git a/src/web/app/desktop/views/pages/welcome.vue b/src/web/app/desktop/views/pages/welcome.vue
index 2ec7f9209..f092271e9 100644
--- a/src/web/app/desktop/views/pages/welcome.vue
+++ b/src/web/app/desktop/views/pages/welcome.vue
@@ -195,7 +195,7 @@ export default Vue.extend({
 									display inline-block
 									height 11px
 									width 11px
-									margin-left 5px
+									margin-left 6px
 									background #ccc
 									border-radius 100%
 									vertical-align middle

From f29aed5100fc59d4d25ebc104b8b8d8e3e515017 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Mar 2018 03:47:10 +0900
Subject: [PATCH 0808/1250] :art:

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

diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index 1a5f7c3b0..ddc338e37 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -397,7 +397,7 @@ export default Vue.extend({
 					margin 0 .5em 0 0
 					padding 0
 					overflow hidden
-					color #777
+					color #627079
 					font-size 1em
 					font-weight bold
 					text-decoration none
diff --git a/src/web/app/mobile/views/components/post.vue b/src/web/app/mobile/views/components/post.vue
index 3b31b827f..d464f6460 100644
--- a/src/web/app/mobile/views/components/post.vue
+++ b/src/web/app/mobile/views/components/post.vue
@@ -340,7 +340,7 @@ export default Vue.extend({
 					margin 0 0.5em 0 0
 					padding 0
 					overflow hidden
-					color #777
+					color #627079
 					font-size 1em
 					font-weight bold
 					text-decoration none

From 09a20e891bf96a49fdf424cdf65c0e803632cf2a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Mar 2018 03:55:43 +0900
Subject: [PATCH 0809/1250] :art:

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

diff --git a/src/web/app/desktop/views/pages/welcome.vue b/src/web/app/desktop/views/pages/welcome.vue
index f092271e9..8ffa06337 100644
--- a/src/web/app/desktop/views/pages/welcome.vue
+++ b/src/web/app/desktop/views/pages/welcome.vue
@@ -113,13 +113,16 @@ export default Vue.extend({
 
 			> div
 				display flex
-				max-width $width
+				max-width $width + 64px
 				margin 0 auto
-				padding 80px 0 0 0
+				padding 80px 32px 0 32px
+
+				> *
+					margin-bottom 48px
 
 				> div:first-child
-					margin 0 48px 0 0
-					color #d1e6bf
+					margin-right 48px
+					color #fff
 					text-shadow 0 0 12px #172062
 
 					> h1

From 4e812c9ef19293a5c7b5c38f208d6c7821336831 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Mar 2018 04:00:14 +0900
Subject: [PATCH 0810/1250] :art:

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

diff --git a/src/web/app/desktop/views/pages/welcome.vue b/src/web/app/desktop/views/pages/welcome.vue
index 8ffa06337..3940594f7 100644
--- a/src/web/app/desktop/views/pages/welcome.vue
+++ b/src/web/app/desktop/views/pages/welcome.vue
@@ -127,7 +127,7 @@ export default Vue.extend({
 
 					> h1
 						margin 0
-						font-weight normal
+						font-weight bold
 						font-variant small-caps
 						letter-spacing 12px
 

From ba2fe82dbb054a2be018412cf4403c4c2b612aec Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Mar 2018 04:03:30 +0900
Subject: [PATCH 0811/1250] :coffee:

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

diff --git a/src/web/app/mobile/views/pages/welcome.vue b/src/web/app/mobile/views/pages/welcome.vue
index e212d6c78..aa50b572e 100644
--- a/src/web/app/mobile/views/pages/welcome.vue
+++ b/src/web/app/mobile/views/pages/welcome.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="welcome">
 	<h1><b>Misskey</b>へようこそ</h1>
-	<p>Twitter風ミニブログSNS、Misskeyへようこそ。思ったことを投稿したり、タイムラインでみんなの投稿を読むこともできます。<br><a href="/signup">アカウントを作成する</a></p>
+	<p>Twitter風ミニブログSNS、Misskeyへようこそ。共有したいことを投稿したり、タイムラインでみんなの投稿を読むこともできます。<br><a href="/signup">アカウントを作成する</a></p>
 	<div class="form">
 		<p>%fa:lock% ログイン</p>
 		<div>

From 3f63e7d26383e0235360b9a19be216b7ec0ae9b5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Mar 2018 04:05:17 +0900
Subject: [PATCH 0812/1250] v4204

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index d9264b006..1db6fa883 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4196",
+	"version": "0.0.4204",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From c19e346fef7422f7fcc7af96391cb8baf0d8ccb5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Mar 2018 15:52:09 +0900
Subject: [PATCH 0813/1250] Update setup doc

---
 docs/setup.en.md | 8 +++++++-
 docs/setup.ja.md | 8 +++++++-
 2 files changed, 14 insertions(+), 2 deletions(-)

diff --git a/docs/setup.en.md b/docs/setup.en.md
index 59637804c..3ba3c16ad 100644
--- a/docs/setup.en.md
+++ b/docs/setup.en.md
@@ -71,7 +71,13 @@ Please install and setup these softwares:
 2. `npm install`
 3. `npm run build`
 
-*5.* That is it.
+*5.* Prepare configuration
+----------------------------------------------------------------
+First, you need to create a `.config` directory in the directory that
+Misskey installed. Then you need to create a `default.yml` file in
+the directory. The template of configuration is available [here](./config.md).
+
+*6.* That is it.
 ----------------------------------------------------------------
 Well done! Now, you have an environment that run to Misskey.
 
diff --git a/docs/setup.ja.md b/docs/setup.ja.md
index 13dda8f1f..55febca2d 100644
--- a/docs/setup.ja.md
+++ b/docs/setup.ja.md
@@ -72,7 +72,13 @@ web-push generate-vapid-keys
 2. `npm install`
 3. `npm run build`
 
-*5.* 以上です!
+*5.* 設定ファイルを用意する
+----------------------------------------------------------------
+Misskeyをインストールしたディレクトリに、`.config`というディレクトリを作成し、
+その中に`default.yml`という名前で設定ファイルを作ってください。
+設定ファイルの下書きは[ここ](./config.md)にありますので、コピペしてご利用ください。
+
+*6.* 以上です!
 ----------------------------------------------------------------
 お疲れ様でした。これでMisskeyを動かす準備は整いました。
 

From d2312959f13969ef6944eee3fdc1bd5465f1fb12 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Mar 2018 16:47:33 +0900
Subject: [PATCH 0814/1250] :art:

---
 src/web/app/desktop/views/pages/welcome.vue | 1 +
 src/web/assets/welcome-bg.svg               | 4 ++--
 src/web/assets/welcome-fg.svg               | 4 ++--
 3 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/src/web/app/desktop/views/pages/welcome.vue b/src/web/app/desktop/views/pages/welcome.vue
index 3940594f7..59d2f6338 100644
--- a/src/web/app/desktop/views/pages/welcome.vue
+++ b/src/web/app/desktop/views/pages/welcome.vue
@@ -180,6 +180,7 @@ export default Vue.extend({
 						width 410px
 						background #fff
 						border-radius 8px
+						box-shadow 0 0 0 12px rgba(0, 0, 0, 0.1)
 						overflow hidden
 
 						> header
diff --git a/src/web/assets/welcome-bg.svg b/src/web/assets/welcome-bg.svg
index accd2d790..de275c2b4 100644
--- a/src/web/assets/welcome-bg.svg
+++ b/src/web/assets/welcome-bg.svg
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:815b59836e1c6d3698c7cba1e937bd19a2a00f926805c328c504b1238389f456
-size 20184
+oid sha256:322a337a3af8f234c942919d66514d6e645f74969f3837d06dfdcbb164dbb85b
+size 27200
diff --git a/src/web/assets/welcome-fg.svg b/src/web/assets/welcome-fg.svg
index 56c88062b..cba4835de 100644
--- a/src/web/assets/welcome-fg.svg
+++ b/src/web/assets/welcome-fg.svg
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:32b1b0f50dc582638edf866297ebbcf24c3ed1fcbe73bf34008d5f860adf8d5d
-size 18599
+oid sha256:72ebf860e5c37cd889a0518d2cea49a700bef392ac2135c4aaae5f8bb9a6160a
+size 19560

From 0ca58ed6d857614c887d61f9342fb4aec7757d97 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Mar 2018 16:52:28 +0900
Subject: [PATCH 0815/1250] :art:

---
 src/web/assets/welcome-bg.svg | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/web/assets/welcome-bg.svg b/src/web/assets/welcome-bg.svg
index de275c2b4..af72e6a26 100644
--- a/src/web/assets/welcome-bg.svg
+++ b/src/web/assets/welcome-bg.svg
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:322a337a3af8f234c942919d66514d6e645f74969f3837d06dfdcbb164dbb85b
-size 27200
+oid sha256:c86490238df8da0ea9bbe15904500de2b738f1ec1c46aceca370a4cd1df70bf7
+size 27340

From 8735f5407491fc144c1f29f082188fef37e7bb47 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Mar 2018 16:56:26 +0900
Subject: [PATCH 0816/1250] v4208

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 1db6fa883..56a59b3e9 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4204",
+	"version": "0.0.4208",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From da3719620705e18ad96167a2f7f6122664ccc5bb Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Mar 2018 17:21:58 +0900
Subject: [PATCH 0817/1250] :v:

---
 webpack.config.ts | 22 ++++++----------------
 1 file changed, 6 insertions(+), 16 deletions(-)

diff --git a/webpack.config.ts b/webpack.config.ts
index 05ecd9677..c80293133 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -21,9 +21,6 @@ import locales from './locales';
 const meta = require('./package.json');
 const version = meta.version;
 
-const env = process.env.NODE_ENV || 'development';
-const isProduction = env === 'production';
-
 //#region Replacer definitions
 global['faReplacement'] = faReplacement;
 
@@ -42,12 +39,12 @@ global['base64replacement'] = (_, key) => {
 
 const langs = Object.keys(locales);
 
-const entries = isProduction
+const entries = process.env.NODE_ENV == 'production'
 	? langs.map(l => [l, false]).concat(langs.map(l => [l, true]))
 	: [['ja', false]];
 
 module.exports = entries.map(x => {
-	const [lang, shouldOptimize] = x;
+	const [lang, isProduction] = x;
 
 	// Chunk name
 	const name = lang;
@@ -66,7 +63,7 @@ module.exports = entries.map(x => {
 
 	const output = {
 		path: __dirname + '/built/web/assets',
-		filename: `[name].${version}.${lang}.${shouldOptimize ? 'min' : 'raw'}.js`
+		filename: `[name].${version}.${lang}.${isProduction ? 'min' : 'raw'}.js`
 	};
 
 	const i18nReplacer = new I18nReplacer(lang as string);
@@ -107,7 +104,7 @@ module.exports = entries.map(x => {
 		}),
 		new webpack.DefinePlugin(_consts),
 		new webpack.DefinePlugin({
-			'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
+			'process.env.NODE_ENV': JSON.stringify(isProduction ? 'production' : 'development')
 		}),
 		new WebpackOnBuildPlugin(stats => {
 			fs.writeFileSync('./version.json', JSON.stringify({
@@ -116,7 +113,7 @@ module.exports = entries.map(x => {
 		})
 	];
 
-	if (shouldOptimize) {
+	if (isProduction) {
 		plugins.push(new webpack.optimize.ModuleConcatenationPlugin());
 	}
 
@@ -241,13 +238,6 @@ module.exports = entries.map(x => {
 		},
 		cache: true,
 		devtool: false, //'source-map',
-		optimization: {
-			minimize: isProduction && shouldOptimize
-		},
-		mode: isProduction
-			? shouldOptimize
-				? 'production'
-				: 'development'
-			: 'development'
+		mode: isProduction ? 'production' : 'development'
 	};
 });

From 4347b934910bf0327614b8831ca78a8f356f24a7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Mar 2018 01:20:49 +0900
Subject: [PATCH 0818/1250] Add animation

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

diff --git a/src/web/app/desktop/views/pages/welcome.vue b/src/web/app/desktop/views/pages/welcome.vue
index 59d2f6338..4055efaf3 100644
--- a/src/web/app/desktop/views/pages/welcome.vue
+++ b/src/web/app/desktop/views/pages/welcome.vue
@@ -4,7 +4,7 @@
 		<div class="top">
 			<div>
 				<div>
-					<h1>Share<br>Everything!</h1>
+					<h1>Share<br><span ref="share">Everything!</span><span class="cursor">_</span></h1>
 					<p>ようこそ! <b>Misskey</b>はTwitter風ミニブログSNSです。思ったことや皆と共有したいことを投稿しましょう。タイムラインを見れば、皆の関心事をすぐにチェックすることもできます。<a :href="aboutUrl">詳しく...</a></p>
 					<p><button class="signup" @click="signup">はじめる</button><button class="signin" @click="signin">ログイン</button></p>
 					<div class="users">
@@ -44,12 +44,22 @@
 import Vue from 'vue';
 import { docsUrl, copyright, lang } from '../../../config';
 
+const shares = [
+	'Everything!',
+	'Webpages',
+	'Photos',
+	'Interests',
+	'Favorites'
+];
+
 export default Vue.extend({
 	data() {
 		return {
 			aboutUrl: `${docsUrl}/${lang}/about`,
 			copyright,
-			users: []
+			users: [],
+			clock: null,
+			i: 0
 		};
 	},
 	mounted() {
@@ -59,6 +69,32 @@ export default Vue.extend({
 		}).then(users => {
 			this.users = users;
 		});
+
+		this.clock = setInterval(() => {
+			if (++this.i == shares.length) this.i = 0;
+			const speed = 70;
+			const text = (this.$refs.share as any).innerText;
+			for (let i = 0; i < text.length; i++) {
+				setTimeout(() => {
+					if (this.$refs.share) {
+						(this.$refs.share as any).innerText = text.substr(0, text.length - i);
+					}
+				}, i * speed)
+			}
+			setTimeout(() => {
+				const newText = shares[this.i];
+				for (let i = 0; i <= newText.length; i++) {
+					setTimeout(() => {
+						if (this.$refs.share) {
+							(this.$refs.share as any).innerText = newText.substr(0, i);
+						}
+					}, i * speed)
+				}
+			}, text.length * speed);
+		}, 4000);
+	},
+	beforeDestroy() {
+		clearInterval(this.clock);
 	},
 	methods: {
 		signup() {
@@ -131,6 +167,15 @@ export default Vue.extend({
 						font-variant small-caps
 						letter-spacing 12px
 
+						> .cursor
+							animation cursor 1s infinite linear both
+
+							@keyframes cursor
+								0%
+									opacity 1
+								50%
+									opacity 0
+
 					> p
 						margin 0.5em 0
 						line-height 2em

From f005c6e9af2e0b59be8b9fd6af1263f2c01bb5a6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Mar 2018 01:26:52 +0900
Subject: [PATCH 0819/1250] v4211

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 56a59b3e9..d31283c5c 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4208",
+	"version": "0.0.4211",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From f3e6e91a695a965f7531f323cac188070e9a3559 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Mar 2018 02:56:35 +0900
Subject: [PATCH 0820/1250] Embed YouTube

---
 .../common/views/components/url-preview.vue   | 63 ++++++++++++-------
 1 file changed, 40 insertions(+), 23 deletions(-)

diff --git a/src/web/app/common/views/components/url-preview.vue b/src/web/app/common/views/components/url-preview.vue
index b84634617..8de7bbfcf 100644
--- a/src/web/app/common/views/components/url-preview.vue
+++ b/src/web/app/common/views/components/url-preview.vue
@@ -1,17 +1,22 @@
 <template>
-<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>
+<iframe v-if="youtubeId" type="text/html" height="250"
+	:src="`http://www.youtube.com/embed/${youtubeId}`"
+	frameborder="0"/>
+<div v-else>
+	<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>
+</div>
 </template>
 
 <script lang="ts">
@@ -26,26 +31,38 @@ export default Vue.extend({
 			description: null,
 			thumbnail: null,
 			icon: null,
-			sitename: null
+			sitename: null,
+			youtubeId: 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;
+		const url = new URL(this.url);
 
-				this.fetching = false;
+		if (url.hostname == 'www.youtube.com') {
+			this.youtubeId = url.searchParams.get('v');
+		} else if (url.hostname == 'youtu.be') {
+			this.youtubeId = url.pathname;
+		} else {
+			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>
+iframe
+	width 100%
+
 .mk-url-preview
 	display block
 	font-size 16px

From af21a434f3c5e6ed13a2deacd617f7b2c7fd14b2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Mar 2018 02:57:27 +0900
Subject: [PATCH 0821/1250] v4213

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index d31283c5c..0a82d9c74 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4211",
+	"version": "0.0.4213",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 2cf311a980ffe04f2cc4cd2b18f7d353b43492d4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Mar 2018 03:20:10 +0900
Subject: [PATCH 0822/1250] Fix bug

---
 src/web/app/common/views/components/url-preview.vue | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/web/app/common/views/components/url-preview.vue b/src/web/app/common/views/components/url-preview.vue
index 8de7bbfcf..91f0a85fb 100644
--- a/src/web/app/common/views/components/url-preview.vue
+++ b/src/web/app/common/views/components/url-preview.vue
@@ -1,6 +1,6 @@
 <template>
 <iframe v-if="youtubeId" type="text/html" height="250"
-	:src="`http://www.youtube.com/embed/${youtubeId}`"
+	:src="`http://www.youtube.com/embed/${youtubeId}?origin=${misskeyUrl}`"
 	frameborder="0"/>
 <div v-else>
 	<a class="mk-url-preview" :href="url" target="_blank" :title="url" v-if="!fetching">
@@ -21,6 +21,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import { url as misskeyUrl } from '../../../config';
 
 export default Vue.extend({
 	props: ['url'],
@@ -32,7 +33,8 @@ export default Vue.extend({
 			thumbnail: null,
 			icon: null,
 			sitename: null,
-			youtubeId: null
+			youtubeId: null,
+			misskeyUrl
 		};
 	},
 	created() {

From a269504eed4def2b32864c24d7c3cab51832e089 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Mar 2018 03:20:16 +0900
Subject: [PATCH 0823/1250] v4215

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 0a82d9c74..20c2383c5 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4213",
+	"version": "0.0.4215",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 316fdd8d77cb9a6cc0da089e5bdfeeb568a0bac6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Mar 2018 03:46:13 +0900
Subject: [PATCH 0824/1250] Fix bug

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

diff --git a/src/web/app/common/views/components/url-preview.vue b/src/web/app/common/views/components/url-preview.vue
index 91f0a85fb..e91e51055 100644
--- a/src/web/app/common/views/components/url-preview.vue
+++ b/src/web/app/common/views/components/url-preview.vue
@@ -1,6 +1,6 @@
 <template>
 <iframe v-if="youtubeId" type="text/html" height="250"
-	:src="`http://www.youtube.com/embed/${youtubeId}?origin=${misskeyUrl}`"
+	:src="`https://www.youtube.com/embed/${youtubeId}?origin=${misskeyUrl}`"
 	frameborder="0"/>
 <div v-else>
 	<a class="mk-url-preview" :href="url" target="_blank" :title="url" v-if="!fetching">

From 1ae10bd5494674b59889becfec0d35fdc6cb2501 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Mar 2018 03:46:20 +0900
Subject: [PATCH 0825/1250] v4217

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 20c2383c5..9513a6ec6 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4215",
+	"version": "0.0.4217",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 747d2e543ac52aa30a08ede4c40e4b91bb140c9b Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 19 Mar 2018 21:43:27 +0900
Subject: [PATCH 0826/1250] Update setup.en.md

---
 docs/setup.en.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/setup.en.md b/docs/setup.en.md
index 3ba3c16ad..e94e229d1 100644
--- a/docs/setup.en.md
+++ b/docs/setup.en.md
@@ -74,7 +74,7 @@ Please install and setup these softwares:
 *5.* Prepare configuration
 ----------------------------------------------------------------
 First, you need to create a `.config` directory in the directory that
-Misskey installed. Then you need to create a `default.yml` file in
+Misskey installed. And then you need to create a `default.yml` file in
 the directory. The template of configuration is available [here](./config.md).
 
 *6.* That is it.

From af66e2f61c7232b9d72a4a3f65196d140059c6af Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 19 Mar 2018 13:27:25 +0000
Subject: [PATCH 0827/1250] fix(package): update license-checker to version
 17.0.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 9513a6ec6..1228e24cd 100644
--- a/package.json
+++ b/package.json
@@ -134,7 +134,7 @@
 		"is-root": "1.0.0",
 		"is-url": "1.2.2",
 		"js-yaml": "3.11.0",
-		"license-checker": "16.0.0",
+		"license-checker": "17.0.0",
 		"loader-utils": "1.1.0",
 		"mecab-async": "0.1.2",
 		"mkdirp": "0.5.1",

From aaa38b34e41cec4acc77d7a5fcdb3fce4148f554 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Mar 2018 22:48:00 +0900
Subject: [PATCH 0828/1250] oops

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

diff --git a/webpack.config.ts b/webpack.config.ts
index c80293133..1fa18d287 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -167,8 +167,7 @@ module.exports = entries.map(x => {
 					}
 				}, {
 					loader: 'stylus-loader'
-				}
-				]
+				}]
 			}, {
 				test: /\.scss$/,
 				exclude: /node_modules/,

From 07b37bb3675a6f781ce269deef8facfbbee22ef8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Mar 2018 23:27:34 +0900
Subject: [PATCH 0829/1250] :art:

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

diff --git a/src/web/app/desktop/views/pages/welcome.vue b/src/web/app/desktop/views/pages/welcome.vue
index 4055efaf3..ad1dccae9 100644
--- a/src/web/app/desktop/views/pages/welcome.vue
+++ b/src/web/app/desktop/views/pages/welcome.vue
@@ -117,6 +117,8 @@ export default Vue.extend({
 <style lang="stylus" scoped>
 @import '~const.styl'
 
+@import url('https://fonts.googleapis.com/css?family=Sarpanch:700')
+
 .mk-welcome
 	display flex
 	flex-direction column
@@ -164,8 +166,11 @@ export default Vue.extend({
 					> h1
 						margin 0
 						font-weight bold
-						font-variant small-caps
+						//font-variant small-caps
 						letter-spacing 12px
+						font-family 'Sarpanch', sans-serif
+						font-size 42px
+						line-height 48px
 
 						> .cursor
 							animation cursor 1s infinite linear both
@@ -177,7 +182,7 @@ export default Vue.extend({
 									opacity 0
 
 					> p
-						margin 0.5em 0
+						margin 1em 0
 						line-height 2em
 
 					button

From 637cc140d73a0b97320eee79754e922d25d3a98d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Mar 2018 23:31:27 +0900
Subject: [PATCH 0830/1250] v4224

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 1228e24cd..d167fd739 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4217",
+	"version": "0.0.4224",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",

From 7750ec1b72c8e973e627924dc4d89e06e389a7b6 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 19 Mar 2018 17:53:05 +0000
Subject: [PATCH 0831/1250] fix(package): update ws to version 5.1.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index d167fd739..b3af49407 100644
--- a/package.json
+++ b/package.json
@@ -201,7 +201,7 @@
 		"webpack-cli": "2.0.12",
 		"webpack-replace-loader": "1.3.0",
 		"websocket": "1.0.25",
-		"ws": "5.0.0",
+		"ws": "5.1.0",
 		"xev": "2.0.0"
 	}
 }

From a5f03249f565729278a8f4605bdb73f5dc4b546c Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 19 Mar 2018 19:05:50 +0000
Subject: [PATCH 0832/1250] fix(package): update html-minifier to version
 3.5.12

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index b3af49407..9da02d0c7 100644
--- a/package.json
+++ b/package.json
@@ -129,7 +129,7 @@
 		"gulp-util": "3.0.8",
 		"hard-source-webpack-plugin": "0.6.4",
 		"highlight.js": "9.12.0",
-		"html-minifier": "3.5.11",
+		"html-minifier": "3.5.12",
 		"inquirer": "5.1.0",
 		"is-root": "1.0.0",
 		"is-url": "1.2.2",

From cdc0f46a2256bd1a61086677c23deb44ceb39750 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 20 Mar 2018 12:30:25 +0000
Subject: [PATCH 0833/1250] fix(package): update pug to version 2.0.2

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 9da02d0c7..4df174b6c 100644
--- a/package.json
+++ b/package.json
@@ -154,7 +154,7 @@
 		"progress-bar-webpack-plugin": "1.11.0",
 		"prominence": "0.2.0",
 		"proxy-addr": "2.0.3",
-		"pug": "2.0.1",
+		"pug": "2.0.2",
 		"qrcode": "1.2.0",
 		"ratelimiter": "3.0.3",
 		"recaptcha-promise": "0.1.3",

From 12589f62e1da3f2595d8b6e7fbdbe14f27082c7f Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 20 Mar 2018 12:36:30 +0000
Subject: [PATCH 0834/1250] fix(package): update license-checker to version
 18.0.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 9da02d0c7..7aa707961 100644
--- a/package.json
+++ b/package.json
@@ -134,7 +134,7 @@
 		"is-root": "1.0.0",
 		"is-url": "1.2.2",
 		"js-yaml": "3.11.0",
-		"license-checker": "17.0.0",
+		"license-checker": "18.0.0",
 		"loader-utils": "1.1.0",
 		"mecab-async": "0.1.2",
 		"mkdirp": "0.5.1",

From 5b07f11d6e866eef3c46e4a1e6f8de43806e0173 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 20 Mar 2018 23:22:05 +0000
Subject: [PATCH 0835/1250] fix(package): update is-url to version 1.2.3

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index d820f9269..fe9f0876e 100644
--- a/package.json
+++ b/package.json
@@ -132,7 +132,7 @@
 		"html-minifier": "3.5.12",
 		"inquirer": "5.1.0",
 		"is-root": "1.0.0",
-		"is-url": "1.2.2",
+		"is-url": "1.2.3",
 		"js-yaml": "3.11.0",
 		"license-checker": "18.0.0",
 		"loader-utils": "1.1.0",

From ad1c7b38d991068699494c279c865eb66f0b44f5 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 21 Mar 2018 09:43:05 +0000
Subject: [PATCH 0836/1250] fix(package): update webpack to version 4.2.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index fe9f0876e..8b5579955 100644
--- a/package.json
+++ b/package.json
@@ -197,7 +197,7 @@
 		"vue-template-compiler": "2.5.16",
 		"vuedraggable": "2.16.0",
 		"web-push": "3.3.0",
-		"webpack": "4.1.1",
+		"webpack": "4.2.0",
 		"webpack-cli": "2.0.12",
 		"webpack-replace-loader": "1.3.0",
 		"websocket": "1.0.25",

From 6bc1b31eeb22b8809314fc46de3d1ce4221d23c3 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 21 Mar 2018 11:46:57 +0000
Subject: [PATCH 0837/1250] fix(package): update pug to version 2.0.3

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index fe9f0876e..9d7701272 100644
--- a/package.json
+++ b/package.json
@@ -154,7 +154,7 @@
 		"progress-bar-webpack-plugin": "1.11.0",
 		"prominence": "0.2.0",
 		"proxy-addr": "2.0.3",
-		"pug": "2.0.2",
+		"pug": "2.0.3",
 		"qrcode": "1.2.0",
 		"ratelimiter": "3.0.3",
 		"recaptcha-promise": "0.1.3",

From a4e19c8d06611f1d28cc2461378d5442f8bdfe63 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 22 Mar 2018 02:05:11 +0000
Subject: [PATCH 0838/1250] fix(package): update eslint to version 4.19.1

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index f72340f06..edc4ac8c1 100644
--- a/package.json
+++ b/package.json
@@ -104,7 +104,7 @@
 		"element-ui": "2.2.2",
 		"emojilib": "2.2.12",
 		"escape-regexp": "0.0.1",
-		"eslint": "4.19.0",
+		"eslint": "4.19.1",
 		"eslint-plugin-vue": "4.3.0",
 		"eventemitter3": "3.0.1",
 		"exif-js": "2.3.0",

From 5bd340a0f57f3e9232f9ff31bbe73a0fe7c04b58 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 22 Mar 2018 05:51:21 +0000
Subject: [PATCH 0839/1250] fix(package): update eslint-plugin-vue to version
 4.4.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index edc4ac8c1..e6e98a81e 100644
--- a/package.json
+++ b/package.json
@@ -105,7 +105,7 @@
 		"emojilib": "2.2.12",
 		"escape-regexp": "0.0.1",
 		"eslint": "4.19.1",
-		"eslint-plugin-vue": "4.3.0",
+		"eslint-plugin-vue": "4.4.0",
 		"eventemitter3": "3.0.1",
 		"exif-js": "2.3.0",
 		"express": "4.16.3",

From be9d2c0e75390410c191e9b209197329b6dda5e7 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 22 Mar 2018 13:58:05 +0000
Subject: [PATCH 0840/1250] fix(package): update chai-http to version 4.0.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index edc4ac8c1..1f72d610e 100644
--- a/package.json
+++ b/package.json
@@ -89,7 +89,7 @@
 		"bootstrap-vue": "2.0.0-rc.1",
 		"cafy": "3.2.1",
 		"chai": "4.1.2",
-		"chai-http": "3.0.0",
+		"chai-http": "4.0.0",
 		"chalk": "2.3.2",
 		"compression": "1.7.2",
 		"cookie": "0.3.1",

From 2b53044dafb606302073f8dc93b39042a0f3f3a8 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 22 Mar 2018 15:26:46 +0000
Subject: [PATCH 0841/1250] fix(package): update is-root to version 2.0.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index edc4ac8c1..e1926e608 100644
--- a/package.json
+++ b/package.json
@@ -131,7 +131,7 @@
 		"highlight.js": "9.12.0",
 		"html-minifier": "3.5.12",
 		"inquirer": "5.1.0",
-		"is-root": "1.0.0",
+		"is-root": "2.0.0",
 		"is-url": "1.2.3",
 		"js-yaml": "3.11.0",
 		"license-checker": "18.0.0",

From b4e7df47b2191018221fb44fd8c24d3034aebc53 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 22 Mar 2018 16:26:20 +0000
Subject: [PATCH 0842/1250] fix(package): update webpack-cli to version 2.0.13

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index edc4ac8c1..6d2d78532 100644
--- a/package.json
+++ b/package.json
@@ -198,7 +198,7 @@
 		"vuedraggable": "2.16.0",
 		"web-push": "3.3.0",
 		"webpack": "4.2.0",
-		"webpack-cli": "2.0.12",
+		"webpack-cli": "2.0.13",
 		"webpack-replace-loader": "1.3.0",
 		"websocket": "1.0.25",
 		"ws": "5.1.0",

From 8ee957a0324b3b046713bd76ed9c0492d6c80ecf Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 24 Mar 2018 05:06:13 +0900
Subject: [PATCH 0843/1250] Create donate.ja.md

---
 docs/donate.ja.md | 24 ++++++++++++++++++++++++
 1 file changed, 24 insertions(+)
 create mode 100644 docs/donate.ja.md

diff --git a/docs/donate.ja.md b/docs/donate.ja.md
new file mode 100644
index 000000000..ec620b596
--- /dev/null
+++ b/docs/donate.ja.md
@@ -0,0 +1,24 @@
+# Misskeyにカンパする方法
+Misskeyのサポートにご興味をお持ちいただきありがとうございます!
+Misskeyにカンパして開発・運営をサポートするには、次のいくつかの方法があります:
+
+## ConoHaカードを購入する
+(本家)Misskeyは、ConoHaというVPSサービスを利用しています。ConoHaカードを購入して、
+カードに記載されているクーポンコードを syuilotan@yahoo.co.jp までお送りいただければ、
+そのクーポンをチャージしてサーバーの運営費に充てることができます。
+
+ConoHaカードについてはこちらをご覧ください: https://www.conoha.jp/conohacard/
+
+Amazonでも買えます: https://www.amazon.co.jp/dp/B01N9E3416
+
+## Amazonギフトカード
+これは間接的な方法です。
+
+## 銀行振込
+syuilotan@yahoo.co.jp までお問い合わせください。
+
+## 手渡し
+オフ会を行ったときなどに行使できる方法です。
+
+## その他
+なにかいいアイデアがあればお教えください。

From d294924d851ecacc53655495b59f61244f5a7fa8 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 24 Mar 2018 05:09:30 +0900
Subject: [PATCH 0844/1250] Update donate.ja.md

---
 docs/donate.ja.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/docs/donate.ja.md b/docs/donate.ja.md
index ec620b596..b19d7bc37 100644
--- a/docs/donate.ja.md
+++ b/docs/donate.ja.md
@@ -1,5 +1,7 @@
 # Misskeyにカンパする方法
 Misskeyのサポートにご興味をお持ちいただきありがとうございます!
+Misskeyにカンパをしていただくと、貴方のお名前と好きなURLなどをMisskeyのリポジトリに刻む権利がもらえます。
+
 Misskeyにカンパして開発・運営をサポートするには、次のいくつかの方法があります:
 
 ## ConoHaカードを購入する

From 9b9668311b8f97cba8e308985393d2b500bdc3ca Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 24 Mar 2018 15:35:14 +0900
Subject: [PATCH 0845/1250] Update DONORS.md

---
 DONORS.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/DONORS.md b/DONORS.md
index 9752068b3..8c764a5e0 100644
--- a/DONORS.md
+++ b/DONORS.md
@@ -10,6 +10,7 @@ The list of people who have sent donation for Misskey.
 * スルメ https://surume.tk/
 * 藍
 * 音船 https://otofune.me/
+* aqz https://misskey.xyz/aqz
 
 :heart: Thanks for donating, guys!
 

From 397792cd5393522e7e9fff139f73dfbc48c111a6 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Sun, 25 Mar 2018 20:19:32 +0900
Subject: [PATCH 0846/1250] Fix the order of setup procedure

---
 LICENSE_AGPL-3.0 | 661 +++++++++++++++++++++++++++++++++++++++++++++++
 README.md        |  10 +-
 docs/setup.en.md |  14 +-
 docs/setup.ja.md |  14 +-
 4 files changed, 682 insertions(+), 17 deletions(-)
 create mode 100644 LICENSE_AGPL-3.0

diff --git a/LICENSE_AGPL-3.0 b/LICENSE_AGPL-3.0
new file mode 100644
index 000000000..dba13ed2d
--- /dev/null
+++ b/LICENSE_AGPL-3.0
@@ -0,0 +1,661 @@
+                    GNU AFFERO GENERAL PUBLIC LICENSE
+                       Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+  A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate.  Many developers of free software are heartened and
+encouraged by the resulting cooperation.  However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+  The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community.  It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server.  Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+  An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals.  This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU Affero General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Remote Network Interaction; Use with the GNU General Public License.
+
+  Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software.  This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time.  Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source.  For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code.  There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<http://www.gnu.org/licenses/>.
diff --git a/README.md b/README.md
index 68a8f4b38..c50566cc9 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
 [![][dependencies-badge]][dependencies-link]
 [![][himawari-badge]][himasaku]
 [![][sakurako-badge]][himasaku]
-[![][mit-badge]][mit]
+[![][agpl-3.0-badge]][AGPL-3.0]
 
 [Misskey](https://misskey.xyz) is a completely open source,
 ultimately sophisticated new type of mini-blog based SNS.
@@ -55,8 +55,12 @@ Copyright
 ----------------------------------------------------------------
 Misskey is an open-source software licensed under [The MIT License](LICENSE).
 
-[mit]:                http://opensource.org/licenses/MIT
-[mit-badge]:          https://img.shields.io/badge/license-MIT-444444.svg?style=flat-square
+The portions of Misskey contributed by Akihiko Odaki <nekomanma@pixiv.co.jp> is
+licensed under GNU Affero General Public License (only version 3.0 of the
+license is applied.) See Git log to identify them.
+
+[agpl-3.0]:           https://www.gnu.org/licenses/agpl-3.0.en.html
+[agpl-3.0-badge]:     https://img.shields.io/badge/license-AGPL--3.0-444444.svg?style=flat-square
 [travis-link]:        https://travis-ci.org/syuilo/misskey
 [travis-badge]:       http://img.shields.io/travis/syuilo/misskey/master.svg?style=flat-square
 [dependencies-link]:  https://david-dm.org/syuilo/misskey
diff --git a/docs/setup.en.md b/docs/setup.en.md
index e94e229d1..e7de83689 100644
--- a/docs/setup.en.md
+++ b/docs/setup.en.md
@@ -58,7 +58,13 @@ Please install and setup these softwares:
 ##### Optional
 * [Elasticsearch](https://www.elastic.co/) - used to provide searching feature instead of MongoDB
 
-*4.* Install Misskey
+*4.* Prepare configuration
+----------------------------------------------------------------
+First, you need to create a `.config` directory in the directory that
+Misskey installed. And then you need to create a `default.yml` file in
+the directory. The template of configuration is available [here](./config.md).
+
+*5.* Install Misskey
 ----------------------------------------------------------------
 
 1. `git clone -b master git://github.com/syuilo/misskey.git`
@@ -71,12 +77,6 @@ Please install and setup these softwares:
 2. `npm install`
 3. `npm run build`
 
-*5.* Prepare configuration
-----------------------------------------------------------------
-First, you need to create a `.config` directory in the directory that
-Misskey installed. And then you need to create a `default.yml` file in
-the directory. The template of configuration is available [here](./config.md).
-
 *6.* That is it.
 ----------------------------------------------------------------
 Well done! Now, you have an environment that run to Misskey.
diff --git a/docs/setup.ja.md b/docs/setup.ja.md
index 55febca2d..9528d1aae 100644
--- a/docs/setup.ja.md
+++ b/docs/setup.ja.md
@@ -59,7 +59,13 @@ web-push generate-vapid-keys
 ##### オプション
 * [Elasticsearch](https://www.elastic.co/) - 検索機能を向上させるために用います。
 
-*4.* Misskeyのインストール
+*4.* 設定ファイルを用意する
+----------------------------------------------------------------
+Misskeyをインストールしたディレクトリに、`.config`というディレクトリを作成し、
+その中に`default.yml`という名前で設定ファイルを作ってください。
+設定ファイルの下書きは[ここ](./config.md)にありますので、コピペしてご利用ください。
+
+*5.* Misskeyのインストール
 ----------------------------------------------------------------
 
 1. `git clone -b master git://github.com/syuilo/misskey.git`
@@ -72,12 +78,6 @@ web-push generate-vapid-keys
 2. `npm install`
 3. `npm run build`
 
-*5.* 設定ファイルを用意する
-----------------------------------------------------------------
-Misskeyをインストールしたディレクトリに、`.config`というディレクトリを作成し、
-その中に`default.yml`という名前で設定ファイルを作ってください。
-設定ファイルの下書きは[ここ](./config.md)にありますので、コピペしてご利用ください。
-
 *6.* 以上です!
 ----------------------------------------------------------------
 お疲れ様でした。これでMisskeyを動かす準備は整いました。

From a0c778f5c91bab0c78f79c2ff212ee91a030e066 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Sun, 25 Mar 2018 22:49:30 +0900
Subject: [PATCH 0847/1250] Pass hostname instead of host to vhost module

---
 src/server.ts | 13 ++++++++++---
 1 file changed, 10 insertions(+), 3 deletions(-)

diff --git a/src/server.ts b/src/server.ts
index a2165d672..84e8c4148 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -14,6 +14,11 @@ import vhost = require('vhost');
 import log from './log-request';
 import config from './conf';
 
+function extractHostname(host) {
+	const index = host.indexOf(':');
+	return index < 0 ? host : host.substr(0, index);
+}
+
 /**
  * Init app
  */
@@ -53,9 +58,11 @@ app.use((req, res, next) => {
 /**
  * Register modules
  */
-app.use(vhost(`api.${config.host}`, require('./api/server')));
-app.use(vhost(config.secondary_host, require('./himasaku/server')));
-app.use(vhost(`file.${config.secondary_host}`, require('./file/server')));
+const hostname = extractHostname(config.host);
+const secondaryHostname = extractHostname(config.secondary_host);
+app.use(vhost(`api.${hostname}`, require('./api/server')));
+app.use(vhost(secondaryHostname, require('./himasaku/server')));
+app.use(vhost(`file.${secondaryHostname}`, require('./file/server')));
 app.use(require('./web/server'));
 
 /**

From d8e754acbe0be59d800a5507769c64398b1f429c Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Sun, 25 Mar 2018 23:10:31 +0900
Subject: [PATCH 0848/1250] Allow to use domain whose prefix is not misskey

---
 gulpfile.ts         | 1 +
 src/web/app/boot.js | 4 +---
 2 files changed, 2 insertions(+), 3 deletions(-)

diff --git a/gulpfile.ts b/gulpfile.ts
index b70e5d8bc..c10d0a98d 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -124,6 +124,7 @@ gulp.task('build:client:script', () =>
 		.pipe(replace('VERSION', JSON.stringify(version)))
 		.pipe(replace('API', JSON.stringify(config.api_url)))
 		.pipe(replace('ENV', JSON.stringify(env)))
+		.pipe(replace('HOST', JSON.stringify(config.host)))
 		.pipe(isProduction ? uglify({
 			toplevel: true
 		} as any) : gutil.noop())
diff --git a/src/web/app/boot.js b/src/web/app/boot.js
index 2ee61745b..00ac9daad 100644
--- a/src/web/app/boot.js
+++ b/src/web/app/boot.js
@@ -27,9 +27,7 @@
 	//   misskey.alice               => misskey
 	//   misskey.strawberry.pasta    => misskey
 	//   dev.misskey.arisu.tachibana => dev
-	let app = url.host == 'localhost'
-		? 'misskey'
-		: url.host.split('.')[0];
+	let app = url.host === HOST ? 'misskey' : url.host.substr(0, -HOST.length);
 
 	// Detect the user language
 	// Note: The default language is English

From b397fe1c2ea27872b0c991726d78f81fa81f8f49 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Mon, 26 Mar 2018 13:21:41 +0900
Subject: [PATCH 0849/1250] Specify Cookie domain with hostname

---
 src/api/common/signin.ts   | 2 +-
 src/web/app/common/mios.ts | 4 ++--
 src/web/app/config.ts      | 2 ++
 webpack.config.ts          | 1 +
 4 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/src/api/common/signin.ts b/src/api/common/signin.ts
index 693e62f39..ec3dd8030 100644
--- a/src/api/common/signin.ts
+++ b/src/api/common/signin.ts
@@ -4,7 +4,7 @@ export default function(res, user, redirect: boolean) {
 	const expires = 1000 * 60 * 60 * 24 * 365; // One Year
 	res.cookie('i', user.token, {
 		path: '/',
-		domain: `.${config.host}`,
+		domain: `.${config.hostname}`,
 		secure: config.url.substr(0, 5) === 'https',
 		httpOnly: false,
 		expires: new Date(Date.now() + expires),
diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
index 986630da2..582a46c4c 100644
--- a/src/web/app/common/mios.ts
+++ b/src/web/app/common/mios.ts
@@ -3,7 +3,7 @@ import { EventEmitter } from 'eventemitter3';
 import * as merge from 'object-assign-deep';
 import * as uuid from 'uuid';
 
-import { host, apiUrl, swPublickey, version, lang, googleMapsApiKey } from '../config';
+import { hostname, apiUrl, swPublickey, version, lang, googleMapsApiKey } from '../config';
 import Progress from './scripts/loading';
 import Connection from './scripts/streaming/stream';
 import { HomeStreamManager } from './scripts/streaming/home';
@@ -220,7 +220,7 @@ export default class MiOS extends EventEmitter {
 
 	public signout() {
 		localStorage.removeItem('me');
-		document.cookie = `i=; domain=.${host}; expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
+		document.cookie = `i=; domain=.${hostname}; expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
 		location.href = '/';
 	}
 
diff --git a/src/web/app/config.ts b/src/web/app/config.ts
index 24158c3ae..32710dd9c 100644
--- a/src/web/app/config.ts
+++ b/src/web/app/config.ts
@@ -1,4 +1,5 @@
 declare const _HOST_: string;
+declare const _HOSTNAME_: string;
 declare const _URL_: string;
 declare const _API_URL_: string;
 declare const _DOCS_URL_: string;
@@ -16,6 +17,7 @@ declare const _LICENSE_: string;
 declare const _GOOGLE_MAPS_API_KEY_: string;
 
 export const host = _HOST_;
+export const hostname = _HOSTNAME_;
 export const url = _URL_;
 export const apiUrl = _API_URL_;
 export const docsUrl = _DOCS_URL_;
diff --git a/webpack.config.ts b/webpack.config.ts
index 1fa18d287..f7e7ae39c 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -84,6 +84,7 @@ module.exports = entries.map(x => {
 		_CH_URL_: config.ch_url,
 		_LANG_: lang,
 		_HOST_: config.host,
+		_HOSTNAME_: config.hostname,
 		_URL_: config.url,
 		_LICENSE_: licenseHtml,
 		_GOOGLE_MAPS_API_KEY_: config.google_maps_api_key

From d2a85aebe9b72a8b0fa1ffef22bd5111fa4261a3 Mon Sep 17 00:00:00 2001
From: rinsuki <428rinsuki+git@gmail.com>
Date: Mon, 26 Mar 2018 13:57:28 +0900
Subject: [PATCH 0850/1250] using WHATWG URL API

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

diff --git a/src/config.ts b/src/config.ts
index 09e06f331..23feadc73 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -3,6 +3,7 @@
  */
 
 import * as fs from 'fs';
+import { URL } from 'url';
 import * as yaml from 'js-yaml';
 import isUrl = require('is-url');
 
@@ -128,11 +129,12 @@ export default function load() {
 	if (!isUrl(config.url)) urlError(config.url);
 	if (!isUrl(config.secondary_url)) urlError(config.secondary_url);
 
+	const url = new URL(config.url);
 	config.url = normalizeUrl(config.url);
 	config.secondary_url = normalizeUrl(config.secondary_url);
 
-	mixin.host = config.url.substr(config.url.indexOf('://') + 3);
-	mixin.scheme = config.url.substr(0, config.url.indexOf('://'));
+	mixin.host = url.host;
+	mixin.scheme = url.protocol.replace(/:$/, '');
 	mixin.ws_scheme = mixin.scheme.replace('http', 'ws');
 	mixin.ws_url = `${mixin.ws_scheme}://api.${mixin.host}`;
 	mixin.secondary_host = config.secondary_url.substr(config.secondary_url.indexOf('://') + 3);

From 0b12d87896a4c26b7f1f6dae4006ad491bc669b9 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Mon, 26 Mar 2018 00:19:07 +0900
Subject: [PATCH 0851/1250] Introduce account document to user document

An account document is attached to a user document if an account of the
user is on the server. It may be missing if the user is on a remote server.
---
 src/api/authenticate.ts                       |  2 +-
 src/api/bot/core.ts                           |  2 +-
 src/api/bot/interfaces/line.ts                | 10 +-
 src/api/common/signin.ts                      |  2 +-
 src/api/endpoints/auth/accept.ts              |  2 +-
 src/api/endpoints/following/create.ts         |  2 +-
 src/api/endpoints/following/delete.ts         |  2 +-
 src/api/endpoints/i.ts                        |  2 +-
 src/api/endpoints/i/2fa/done.ts               |  4 +-
 src/api/endpoints/i/2fa/register.ts           |  2 +-
 src/api/endpoints/i/2fa/unregister.ts         |  6 +-
 src/api/endpoints/i/change_password.ts        |  4 +-
 src/api/endpoints/i/regenerate_token.ts       |  4 +-
 src/api/endpoints/i/update.ts                 | 14 +--
 src/api/endpoints/i/update_client_setting.ts  |  4 +-
 src/api/endpoints/i/update_home.ts            |  6 +-
 src/api/endpoints/i/update_mobile_home.ts     |  6 +-
 src/api/endpoints/mute/create.ts              |  2 +-
 src/api/endpoints/mute/delete.ts              |  2 +-
 src/api/endpoints/posts/categorize.ts         |  2 +-
 src/api/endpoints/posts/create.ts             |  2 +-
 src/api/endpoints/posts/polls/vote.ts         |  2 +-
 src/api/endpoints/posts/reactions/create.ts   |  2 +-
 src/api/endpoints/users/recommendation.ts     |  2 +-
 src/api/models/user.ts                        | 88 ++++++++---------
 src/api/private/signin.ts                     |  8 +-
 src/api/private/signup.ts                     | 40 ++++----
 src/api/service/twitter.ts                    | 10 +-
 src/api/stream/home.ts                        |  2 +-
 src/api/streaming.ts                          |  2 +-
 src/common/get-user-summary.ts                |  2 +-
 src/web/app/common/define-widget.ts           |  4 +-
 src/web/app/common/mios.ts                    | 10 +-
 src/web/app/common/scripts/streaming/drive.ts |  2 +-
 src/web/app/common/scripts/streaming/home.ts  |  4 +-
 .../scripts/streaming/messaging-index.ts      |  2 +-
 .../app/common/scripts/streaming/messaging.ts |  4 +-
 .../common/scripts/streaming/othello-game.ts  |  2 +-
 .../app/common/scripts/streaming/othello.ts   |  2 +-
 .../app/common/views/components/signin.vue    |  4 +-
 .../views/components/twitter-setting.vue      | 12 +--
 .../app/common/views/components/uploader.vue  |  2 +-
 src/web/app/desktop/api/update-avatar.ts      |  2 +-
 src/web/app/desktop/api/update-banner.ts      |  2 +-
 src/web/app/desktop/views/components/home.vue | 20 ++--
 .../desktop/views/components/post-detail.vue  |  2 +-
 .../desktop/views/components/posts.post.vue   |  4 +-
 .../desktop/views/components/settings.2fa.vue |  8 +-
 .../desktop/views/components/settings.api.vue |  2 +-
 .../views/components/settings.profile.vue     |  8 +-
 .../app/desktop/views/components/settings.vue | 12 +--
 .../app/desktop/views/components/timeline.vue |  2 +-
 .../desktop/views/components/ui.header.vue    |  4 +-
 .../views/components/widget-container.vue     |  4 +-
 .../app/desktop/views/components/window.vue   |  4 +-
 .../desktop/views/pages/user/user.header.vue  |  2 +-
 .../desktop/views/pages/user/user.home.vue    |  2 +-
 .../desktop/views/pages/user/user.profile.vue | 10 +-
 .../mobile/views/components/post-detail.vue   |  2 +-
 .../app/mobile/views/components/post-form.vue |  2 +-
 src/web/app/mobile/views/components/post.vue  |  4 +-
 .../app/mobile/views/components/ui.header.vue |  4 +-
 src/web/app/mobile/views/pages/home.vue       | 20 ++--
 .../mobile/views/pages/profile-setting.vue    |  4 +-
 src/web/app/mobile/views/pages/user.vue       | 10 +-
 src/web/app/mobile/views/pages/user/home.vue  |  2 +-
 src/web/app/mobile/views/pages/welcome.vue    |  4 +-
 src/web/docs/api/entities/user.yaml           | 96 ++++++++++---------
 test/api.js                                   | 70 +++++++++-----
 .../shell.1522038492.user-account.js          | 41 ++++++++
 70 files changed, 355 insertions(+), 280 deletions(-)
 create mode 100644 tools/migration/shell.1522038492.user-account.js

diff --git a/src/api/authenticate.ts b/src/api/authenticate.ts
index b289959ac..537c3d1e1 100644
--- a/src/api/authenticate.ts
+++ b/src/api/authenticate.ts
@@ -34,7 +34,7 @@ export default (req: express.Request) => new Promise<IAuthContext>(async (resolv
 
 	if (isNativeToken(token)) {
 		const user: IUser = await User
-			.findOne({ token: token });
+			.findOne({ 'account.token': token });
 
 		if (user === null) {
 			return reject('user not found');
diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
index 1f07d4eee..ad29f1003 100644
--- a/src/api/bot/core.ts
+++ b/src/api/bot/core.ts
@@ -225,7 +225,7 @@ class SigninContext extends Context {
 			}
 		} else {
 			// Compare password
-			const same = await bcrypt.compare(query, this.temporaryUser.password);
+			const same = await bcrypt.compare(query, this.temporaryUser.account.password);
 
 			if (same) {
 				this.bot.signin(this.temporaryUser);
diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts
index 43c25f803..bf0882159 100644
--- a/src/api/bot/interfaces/line.ts
+++ b/src/api/bot/interfaces/line.ts
@@ -110,11 +110,11 @@ class LineBot extends BotCore {
 			data: `showtl|${user.id}`
 		});
 
-		if (user.twitter) {
+		if (user.account.twitter) {
 			actions.push({
 				type: 'uri',
 				label: 'Twitterアカウントを見る',
-				uri: `https://twitter.com/${user.twitter.screen_name}`
+				uri: `https://twitter.com/${user.account.twitter.screen_name}`
 			});
 		}
 
@@ -171,7 +171,7 @@ module.exports = async (app: express.Application) => {
 
 		if (session == null) {
 			const user = await User.findOne({
-				line: {
+				'account.line': {
 					user_id: sourceId
 				}
 			});
@@ -181,7 +181,7 @@ module.exports = async (app: express.Application) => {
 			bot.on('signin', user => {
 				User.update(user._id, {
 					$set: {
-						line: {
+						'account.line': {
 							user_id: sourceId
 						}
 					}
@@ -191,7 +191,7 @@ module.exports = async (app: express.Application) => {
 			bot.on('signout', user => {
 				User.update(user._id, {
 					$set: {
-						line: {
+						'account.line': {
 							user_id: null
 						}
 					}
diff --git a/src/api/common/signin.ts b/src/api/common/signin.ts
index 693e62f39..c069fa4ec 100644
--- a/src/api/common/signin.ts
+++ b/src/api/common/signin.ts
@@ -2,7 +2,7 @@ import config from '../../conf';
 
 export default function(res, user, redirect: boolean) {
 	const expires = 1000 * 60 * 60 * 24 * 365; // One Year
-	res.cookie('i', user.token, {
+	res.cookie('i', user.account.token, {
 		path: '/',
 		domain: `.${config.host}`,
 		secure: config.url.substr(0, 5) === 'https',
diff --git a/src/api/endpoints/auth/accept.ts b/src/api/endpoints/auth/accept.ts
index 4ee20a6d2..8955738eb 100644
--- a/src/api/endpoints/auth/accept.ts
+++ b/src/api/endpoints/auth/accept.ts
@@ -45,7 +45,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Fetch token
 	const session = await AuthSess
-		.findOne({ token: token });
+		.findOne({ 'account.token': token });
 
 	if (session === null) {
 		return rej('session not found');
diff --git a/src/api/endpoints/following/create.ts b/src/api/endpoints/following/create.ts
index 8e1aa3471..767b837b3 100644
--- a/src/api/endpoints/following/create.ts
+++ b/src/api/endpoints/following/create.ts
@@ -32,7 +32,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}, {
 		fields: {
 			data: false,
-			profile: false
+			'account.profile': false
 		}
 	});
 
diff --git a/src/api/endpoints/following/delete.ts b/src/api/endpoints/following/delete.ts
index b68cec09d..64b9a8cec 100644
--- a/src/api/endpoints/following/delete.ts
+++ b/src/api/endpoints/following/delete.ts
@@ -31,7 +31,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}, {
 		fields: {
 			data: false,
-			profile: false
+			'account.profile': false
 		}
 	});
 
diff --git a/src/api/endpoints/i.ts b/src/api/endpoints/i.ts
index 7efdbcd7c..32b0382fa 100644
--- a/src/api/endpoints/i.ts
+++ b/src/api/endpoints/i.ts
@@ -22,7 +22,7 @@ module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) =>
 	// Update lastUsedAt
 	User.update({ _id: user._id }, {
 		$set: {
-			last_used_at: new Date()
+			'account.last_used_at': new Date()
 		}
 	});
 });
diff --git a/src/api/endpoints/i/2fa/done.ts b/src/api/endpoints/i/2fa/done.ts
index 0b36033bb..0f1db7382 100644
--- a/src/api/endpoints/i/2fa/done.ts
+++ b/src/api/endpoints/i/2fa/done.ts
@@ -28,8 +28,8 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 	await User.update(user._id, {
 		$set: {
-			two_factor_secret: user.two_factor_temp_secret,
-			two_factor_enabled: true
+			'account.two_factor_secret': user.two_factor_temp_secret,
+			'account.two_factor_enabled': true
 		}
 	});
 
diff --git a/src/api/endpoints/i/2fa/register.ts b/src/api/endpoints/i/2fa/register.ts
index c2b5037a2..24abfcdfc 100644
--- a/src/api/endpoints/i/2fa/register.ts
+++ b/src/api/endpoints/i/2fa/register.ts
@@ -14,7 +14,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	if (passwordErr) return rej('invalid password param');
 
 	// Compare password
-	const same = await bcrypt.compare(password, user.password);
+	const same = await bcrypt.compare(password, user.account.password);
 
 	if (!same) {
 		return rej('incorrect password');
diff --git a/src/api/endpoints/i/2fa/unregister.ts b/src/api/endpoints/i/2fa/unregister.ts
index 6bee6a26f..c43f9ccc4 100644
--- a/src/api/endpoints/i/2fa/unregister.ts
+++ b/src/api/endpoints/i/2fa/unregister.ts
@@ -11,7 +11,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	if (passwordErr) return rej('invalid password param');
 
 	// Compare password
-	const same = await bcrypt.compare(password, user.password);
+	const same = await bcrypt.compare(password, user.account.password);
 
 	if (!same) {
 		return rej('incorrect password');
@@ -19,8 +19,8 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 	await User.update(user._id, {
 		$set: {
-			two_factor_secret: null,
-			two_factor_enabled: false
+			'account.two_factor_secret': null,
+			'account.two_factor_enabled': false
 		}
 	});
 
diff --git a/src/api/endpoints/i/change_password.ts b/src/api/endpoints/i/change_password.ts
index 16f1a2e4e..88fb36b1f 100644
--- a/src/api/endpoints/i/change_password.ts
+++ b/src/api/endpoints/i/change_password.ts
@@ -22,7 +22,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	if (newPasswordErr) return rej('invalid new_password param');
 
 	// Compare password
-	const same = await bcrypt.compare(currentPassword, user.password);
+	const same = await bcrypt.compare(currentPassword, user.account.password);
 
 	if (!same) {
 		return rej('incorrect password');
@@ -34,7 +34,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 	await User.update(user._id, {
 		$set: {
-			password: hash
+			'account.password': hash
 		}
 	});
 
diff --git a/src/api/endpoints/i/regenerate_token.ts b/src/api/endpoints/i/regenerate_token.ts
index 653468330..9ac7b5507 100644
--- a/src/api/endpoints/i/regenerate_token.ts
+++ b/src/api/endpoints/i/regenerate_token.ts
@@ -20,7 +20,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	if (passwordErr) return rej('invalid password param');
 
 	// Compare password
-	const same = await bcrypt.compare(password, user.password);
+	const same = await bcrypt.compare(password, user.account.password);
 
 	if (!same) {
 		return rej('incorrect password');
@@ -31,7 +31,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 	await User.update(user._id, {
 		$set: {
-			token: secret
+			'account.token': secret
 		}
 	});
 
diff --git a/src/api/endpoints/i/update.ts b/src/api/endpoints/i/update.ts
index 76bad2d15..db8a3f25b 100644
--- a/src/api/endpoints/i/update.ts
+++ b/src/api/endpoints/i/update.ts
@@ -29,12 +29,12 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re
 	// Get 'location' parameter
 	const [location, locationErr] = $(params.location).optional.nullable.string().pipe(isValidLocation).$;
 	if (locationErr) return rej('invalid location param');
-	if (location !== undefined) user.profile.location = location;
+	if (location !== undefined) user.account.profile.location = location;
 
 	// Get 'birthday' parameter
 	const [birthday, birthdayErr] = $(params.birthday).optional.nullable.string().pipe(isValidBirthday).$;
 	if (birthdayErr) return rej('invalid birthday param');
-	if (birthday !== undefined) user.profile.birthday = birthday;
+	if (birthday !== undefined) user.account.profile.birthday = birthday;
 
 	// Get 'avatar_id' parameter
 	const [avatarId, avatarIdErr] = $(params.avatar_id).optional.id().$;
@@ -49,12 +49,12 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re
 	// Get 'is_bot' parameter
 	const [isBot, isBotErr] = $(params.is_bot).optional.boolean().$;
 	if (isBotErr) return rej('invalid is_bot param');
-	if (isBot != null) user.is_bot = isBot;
+	if (isBot != null) user.account.is_bot = isBot;
 
 	// Get 'auto_watch' parameter
 	const [autoWatch, autoWatchErr] = $(params.auto_watch).optional.boolean().$;
 	if (autoWatchErr) return rej('invalid auto_watch param');
-	if (autoWatch != null) user.settings.auto_watch = autoWatch;
+	if (autoWatch != null) user.account.settings.auto_watch = autoWatch;
 
 	await User.update(user._id, {
 		$set: {
@@ -62,9 +62,9 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re
 			description: user.description,
 			avatar_id: user.avatar_id,
 			banner_id: user.banner_id,
-			profile: user.profile,
-			is_bot: user.is_bot,
-			settings: user.settings
+			'account.profile': user.account.profile,
+			'account.is_bot': user.account.is_bot,
+			'account.settings': user.account.settings
 		}
 	});
 
diff --git a/src/api/endpoints/i/update_client_setting.ts b/src/api/endpoints/i/update_client_setting.ts
index b817ff354..c772ed5dc 100644
--- a/src/api/endpoints/i/update_client_setting.ts
+++ b/src/api/endpoints/i/update_client_setting.ts
@@ -22,14 +22,14 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	if (valueErr) return rej('invalid value param');
 
 	const x = {};
-	x[`client_settings.${name}`] = value;
+	x[`account.client_settings.${name}`] = value;
 
 	await User.update(user._id, {
 		$set: x
 	});
 
 	// Serialize
-	user.client_settings[name] = value;
+	user.account.client_settings[name] = value;
 	const iObj = await pack(user, user, {
 		detail: true,
 		includeSecrets: true
diff --git a/src/api/endpoints/i/update_home.ts b/src/api/endpoints/i/update_home.ts
index 394686cbd..9ce44e25e 100644
--- a/src/api/endpoints/i/update_home.ts
+++ b/src/api/endpoints/i/update_home.ts
@@ -26,7 +26,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	if (home) {
 		await User.update(user._id, {
 			$set: {
-				'client_settings.home': home
+				'account.client_settings.home': home
 			}
 		});
 
@@ -38,7 +38,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	} else {
 		if (id == null && data == null) return rej('you need to set id and data params if home param unset');
 
-		const _home = user.client_settings.home;
+		const _home = user.account.client_settings.home;
 		const widget = _home.find(w => w.id == id);
 
 		if (widget == null) return rej('widget not found');
@@ -47,7 +47,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 		await User.update(user._id, {
 			$set: {
-				'client_settings.home': _home
+				'account.client_settings.home': _home
 			}
 		});
 
diff --git a/src/api/endpoints/i/update_mobile_home.ts b/src/api/endpoints/i/update_mobile_home.ts
index 70181431a..1daddf42b 100644
--- a/src/api/endpoints/i/update_mobile_home.ts
+++ b/src/api/endpoints/i/update_mobile_home.ts
@@ -25,7 +25,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	if (home) {
 		await User.update(user._id, {
 			$set: {
-				'client_settings.mobile_home': home
+				'account.client_settings.mobile_home': home
 			}
 		});
 
@@ -37,7 +37,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	} else {
 		if (id == null && data == null) return rej('you need to set id and data params if home param unset');
 
-		const _home = user.client_settings.mobile_home || [];
+		const _home = user.account.client_settings.mobile_home || [];
 		const widget = _home.find(w => w.id == id);
 
 		if (widget == null) return rej('widget not found');
@@ -46,7 +46,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 		await User.update(user._id, {
 			$set: {
-				'client_settings.mobile_home': _home
+				'account.client_settings.mobile_home': _home
 			}
 		});
 
diff --git a/src/api/endpoints/mute/create.ts b/src/api/endpoints/mute/create.ts
index f44854ab5..f99b40d32 100644
--- a/src/api/endpoints/mute/create.ts
+++ b/src/api/endpoints/mute/create.ts
@@ -30,7 +30,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}, {
 		fields: {
 			data: false,
-			profile: false
+			'account.profile': false
 		}
 	});
 
diff --git a/src/api/endpoints/mute/delete.ts b/src/api/endpoints/mute/delete.ts
index d6bff3353..36e2fd101 100644
--- a/src/api/endpoints/mute/delete.ts
+++ b/src/api/endpoints/mute/delete.ts
@@ -30,7 +30,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}, {
 		fields: {
 			data: false,
-			profile: false
+			'account.profile': false
 		}
 	});
 
diff --git a/src/api/endpoints/posts/categorize.ts b/src/api/endpoints/posts/categorize.ts
index 3530ba6bc..0c85c2b4e 100644
--- a/src/api/endpoints/posts/categorize.ts
+++ b/src/api/endpoints/posts/categorize.ts
@@ -12,7 +12,7 @@ import Post from '../../models/post';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	if (!user.is_pro) {
+	if (!user.account.is_pro) {
 		return rej('This endpoint is available only from a Pro account');
 	}
 
diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index 1c3ab5345..f46a84e1f 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -390,7 +390,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 			});
 
 		// この投稿をWatchする
-		if (user.settings.auto_watch !== false) {
+		if (user.account.settings.auto_watch !== false) {
 			watch(user._id, reply);
 		}
 
diff --git a/src/api/endpoints/posts/polls/vote.ts b/src/api/endpoints/posts/polls/vote.ts
index 8222fe532..16ce76a6f 100644
--- a/src/api/endpoints/posts/polls/vote.ts
+++ b/src/api/endpoints/posts/polls/vote.ts
@@ -100,7 +100,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		});
 
 	// この投稿をWatchする
-	if (user.settings.auto_watch !== false) {
+	if (user.account.settings.auto_watch !== false) {
 		watch(user._id, post);
 	}
 });
diff --git a/src/api/endpoints/posts/reactions/create.ts b/src/api/endpoints/posts/reactions/create.ts
index 93d9756d0..f77afed40 100644
--- a/src/api/endpoints/posts/reactions/create.ts
+++ b/src/api/endpoints/posts/reactions/create.ts
@@ -116,7 +116,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		});
 
 	// この投稿をWatchする
-	if (user.settings.auto_watch !== false) {
+	if (user.account.settings.auto_watch !== false) {
 		watch(user._id, post);
 	}
 });
diff --git a/src/api/endpoints/users/recommendation.ts b/src/api/endpoints/users/recommendation.ts
index 736233b34..f1f5bcd0a 100644
--- a/src/api/endpoints/users/recommendation.ts
+++ b/src/api/endpoints/users/recommendation.ts
@@ -30,7 +30,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 			_id: {
 				$nin: followingIds
 			},
-			last_used_at: {
+			'account.last_used_at': {
 				$gte: new Date(Date.now() - ms('7days'))
 			}
 		}, {
diff --git a/src/api/models/user.ts b/src/api/models/user.ts
index ba2765c79..372e2c5da 100644
--- a/src/api/models/user.ts
+++ b/src/api/models/user.ts
@@ -11,7 +11,7 @@ import config from '../../conf';
 const User = db.get<IUser>('users');
 
 User.createIndex('username');
-User.createIndex('token');
+User.createIndex('account.token');
 
 export default User;
 
@@ -43,46 +43,48 @@ export type IUser = {
 	_id: mongo.ObjectID;
 	created_at: Date;
 	deleted_at: Date;
-	email: string;
 	followers_count: number;
 	following_count: number;
-	links: string[];
 	name: string;
-	password: string;
 	posts_count: number;
 	drive_capacity: number;
 	username: string;
 	username_lower: string;
-	token: string;
 	avatar_id: mongo.ObjectID;
 	banner_id: mongo.ObjectID;
 	data: any;
-	twitter: {
-		access_token: string;
-		access_token_secret: string;
-		user_id: string;
-		screen_name: string;
-	};
-	line: {
-		user_id: string;
-	};
 	description: string;
-	profile: {
-		location: string;
-		birthday: string; // 'YYYY-MM-DD'
-		tags: string[];
-	};
-	last_used_at: Date;
 	latest_post: IPost;
 	pinned_post_id: mongo.ObjectID;
-	is_bot: boolean;
-	is_pro: boolean;
 	is_suspended: boolean;
 	keywords: string[];
-	two_factor_secret: string;
-	two_factor_enabled: boolean;
-	client_settings: any;
-	settings: any;
+	account: {
+		email: string;
+		links: string[];
+		password: string;
+		token: string;
+		twitter: {
+			access_token: string;
+			access_token_secret: string;
+			user_id: string;
+			screen_name: string;
+		};
+		line: {
+			user_id: string;
+		};
+		profile: {
+			location: string;
+			birthday: string; // 'YYYY-MM-DD'
+			tags: string[];
+		};
+		last_used_at: Date;
+		is_bot: boolean;
+		is_pro: boolean;
+		two_factor_secret: string;
+		two_factor_enabled: boolean;
+		client_settings: any;
+		settings: any;
+	};
 };
 
 export function init(user): IUser {
@@ -119,11 +121,11 @@ export const pack = (
 
 	const fields = opts.detail ? {
 	} : {
-		settings: false,
-		client_settings: false,
-		profile: false,
-		keywords: false,
-		domains: false
+		'account.settings': false,
+		'account.client_settings': false,
+		'account.profile': false,
+		'account.keywords': false,
+		'account.domains': false
 	};
 
 	// Populate the user if 'user' is ID
@@ -158,26 +160,26 @@ export const pack = (
 	delete _user.latest_post;
 
 	// Remove private properties
-	delete _user.password;
-	delete _user.token;
-	delete _user.two_factor_temp_secret;
-	delete _user.two_factor_secret;
+	delete _user.account.password;
+	delete _user.account.token;
+	delete _user.account.two_factor_temp_secret;
+	delete _user.account.two_factor_secret;
 	delete _user.username_lower;
-	if (_user.twitter) {
-		delete _user.twitter.access_token;
-		delete _user.twitter.access_token_secret;
+	if (_user.account.twitter) {
+		delete _user.account.twitter.access_token;
+		delete _user.account.twitter.access_token_secret;
 	}
-	delete _user.line;
+	delete _user.account.line;
 
 	// Visible via only the official client
 	if (!opts.includeSecrets) {
-		delete _user.email;
-		delete _user.settings;
-		delete _user.client_settings;
+		delete _user.account.email;
+		delete _user.account.settings;
+		delete _user.account.client_settings;
 	}
 
 	if (!opts.detail) {
-		delete _user.two_factor_enabled;
+		delete _user.account.two_factor_enabled;
 	}
 
 	_user.avatar_url = _user.avatar_id != null
diff --git a/src/api/private/signin.ts b/src/api/private/signin.ts
index b49d25d99..ae0be03c7 100644
--- a/src/api/private/signin.ts
+++ b/src/api/private/signin.ts
@@ -36,7 +36,7 @@ export default async (req: express.Request, res: express.Response) => {
 	}, {
 		fields: {
 			data: false,
-			profile: false
+			'account.profile': false
 		}
 	});
 
@@ -48,12 +48,12 @@ export default async (req: express.Request, res: express.Response) => {
 	}
 
 	// Compare password
-	const same = await bcrypt.compare(password, user.password);
+	const same = await bcrypt.compare(password, user.account.password);
 
 	if (same) {
-		if (user.two_factor_enabled) {
+		if (user.account.two_factor_enabled) {
 			const verified = (speakeasy as any).totp.verify({
-				secret: user.two_factor_secret,
+				secret: user.account.two_factor_secret,
 				encoding: 'base32',
 				token: token
 			});
diff --git a/src/api/private/signup.ts b/src/api/private/signup.ts
index 3df00ae42..902642425 100644
--- a/src/api/private/signup.ts
+++ b/src/api/private/signup.ts
@@ -105,38 +105,40 @@ export default async (req: express.Request, res: express.Response) => {
 
 	// Create account
 	const account: IUser = await User.insert({
-		token: secret,
 		avatar_id: null,
 		banner_id: null,
 		created_at: new Date(),
 		description: null,
-		email: null,
 		followers_count: 0,
 		following_count: 0,
-		links: null,
 		name: name,
-		password: hash,
 		posts_count: 0,
 		likes_count: 0,
 		liked_count: 0,
 		drive_capacity: 1073741824, // 1GB
 		username: username,
 		username_lower: username.toLowerCase(),
-		profile: {
-			bio: null,
-			birthday: null,
-			blood: null,
-			gender: null,
-			handedness: null,
-			height: null,
-			location: null,
-			weight: null
-		},
-		settings: {
-			auto_watch: true
-		},
-		client_settings: {
-			home: homeData
+		account: {
+			token: secret,
+			email: null,
+			links: null,
+			password: hash,
+			profile: {
+				bio: null,
+				birthday: null,
+				blood: null,
+				gender: null,
+				handedness: null,
+				height: null,
+				location: null,
+				weight: null
+			},
+			settings: {
+				auto_watch: true
+			},
+			client_settings: {
+				home: homeData
+			}
 		}
 	});
 
diff --git a/src/api/service/twitter.ts b/src/api/service/twitter.ts
index adcd5ac49..02b613454 100644
--- a/src/api/service/twitter.ts
+++ b/src/api/service/twitter.ts
@@ -39,10 +39,10 @@ module.exports = (app: express.Application) => {
 		if (userToken == null) return res.send('plz signin');
 
 		const user = await User.findOneAndUpdate({
-			token: userToken
+			'account.token': userToken
 		}, {
 			$set: {
-				twitter: null
+				'account.twitter': null
 			}
 		});
 
@@ -126,7 +126,7 @@ module.exports = (app: express.Application) => {
 				const result = await twAuth.done(JSON.parse(ctx), req.query.oauth_verifier);
 
 				const user = await User.findOne({
-					'twitter.user_id': result.userId
+					'account.twitter.user_id': result.userId
 				});
 
 				if (user == null) {
@@ -148,10 +148,10 @@ module.exports = (app: express.Application) => {
 				const result = await twAuth.done(JSON.parse(ctx), verifier);
 
 				const user = await User.findOneAndUpdate({
-					token: userToken
+					'account.token': userToken
 				}, {
 					$set: {
-						twitter: {
+						'account.twitter': {
 							access_token: result.accessToken,
 							access_token_secret: result.accessTokenSecret,
 							user_id: result.userId,
diff --git a/src/api/stream/home.ts b/src/api/stream/home.ts
index cc3fb885e..1ef0f33b4 100644
--- a/src/api/stream/home.ts
+++ b/src/api/stream/home.ts
@@ -74,7 +74,7 @@ export default async function(request: websocket.request, connection: websocket.
 				// Update lastUsedAt
 				User.update({ _id: user._id }, {
 					$set: {
-						last_used_at: new Date()
+						'account.last_used_at': new Date()
 					}
 				});
 				break;
diff --git a/src/api/streaming.ts b/src/api/streaming.ts
index f56c08092..427e01afd 100644
--- a/src/api/streaming.ts
+++ b/src/api/streaming.ts
@@ -94,7 +94,7 @@ function authenticate(token: string): Promise<IUser> {
 			// Fetch user
 			const user: IUser = await User
 				.findOne({
-					token: token
+					'account.token': token
 				});
 
 			resolve(user);
diff --git a/src/common/get-user-summary.ts b/src/common/get-user-summary.ts
index 1bec2f9a2..619814e8a 100644
--- a/src/common/get-user-summary.ts
+++ b/src/common/get-user-summary.ts
@@ -7,6 +7,6 @@ import { IUser } from '../api/models/user';
 export default function(user: IUser): string {
 	return `${user.name} (@${user.username})\n` +
 		`${user.posts_count}投稿、${user.following_count}フォロー、${user.followers_count}フォロワー\n` +
-		`場所: ${user.profile.location}、誕生日: ${user.profile.birthday}\n` +
+		`場所: ${user.account.profile.location}、誕生日: ${user.account.profile.birthday}\n` +
 		`「${user.description}」`;
 }
diff --git a/src/web/app/common/define-widget.ts b/src/web/app/common/define-widget.ts
index efce7e813..d8d29873a 100644
--- a/src/web/app/common/define-widget.ts
+++ b/src/web/app/common/define-widget.ts
@@ -56,14 +56,14 @@ export default function<T extends object>(data: {
 						id: this.id,
 						data: newProps
 					}).then(() => {
-						(this as any).os.i.client_settings.mobile_home.find(w => w.id == this.id).data = newProps;
+						(this as any).os.i.account.client_settings.mobile_home.find(w => w.id == this.id).data = newProps;
 					});
 				} else {
 					(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;
+						(this as any).os.i.account.client_settings.home.find(w => w.id == this.id).data = newProps;
 					});
 				}
 			}, {
diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
index 986630da2..1c950c3e7 100644
--- a/src/web/app/common/mios.ts
+++ b/src/web/app/common/mios.ts
@@ -270,7 +270,7 @@ export default class MiOS extends EventEmitter {
 				// Parse response
 				res.json().then(i => {
 					me = i;
-					me.token = token;
+					me.account.token = token;
 					done();
 				});
 			})
@@ -294,12 +294,12 @@ export default class MiOS extends EventEmitter {
 		const fetched = me => {
 			if (me) {
 				// デフォルトの設定をマージ
-				me.client_settings = Object.assign({
+				me.account.client_settings = Object.assign({
 					fetchOnScroll: true,
 					showMaps: true,
 					showPostFormOnTopOfTl: false,
 					gradientWindowHeader: false
-				}, me.client_settings);
+				}, me.account.client_settings);
 
 				// ローカルストレージにキャッシュ
 				localStorage.setItem('me', JSON.stringify(me));
@@ -329,7 +329,7 @@ export default class MiOS extends EventEmitter {
 			fetched(cachedMe);
 
 			// 後から新鮮なデータをフェッチ
-			fetchme(cachedMe.token, freshData => {
+			fetchme(cachedMe.account.token, freshData => {
 				merge(cachedMe, freshData);
 			});
 		} else {
@@ -437,7 +437,7 @@ export default class MiOS extends EventEmitter {
 		}
 
 		// Append a credential
-		if (this.isSignedIn) (data as any).i = this.i.token;
+		if (this.isSignedIn) (data as any).i = this.i.account.token;
 
 		// TODO
 		//const viaStream = localStorage.getItem('enableExperimental') == 'true';
diff --git a/src/web/app/common/scripts/streaming/drive.ts b/src/web/app/common/scripts/streaming/drive.ts
index 7ff85b594..f11573685 100644
--- a/src/web/app/common/scripts/streaming/drive.ts
+++ b/src/web/app/common/scripts/streaming/drive.ts
@@ -8,7 +8,7 @@ import MiOS from '../../mios';
 export class DriveStream extends Stream {
 	constructor(os: MiOS, me) {
 		super(os, 'drive', {
-			i: me.token
+			i: me.account.token
 		});
 	}
 }
diff --git a/src/web/app/common/scripts/streaming/home.ts b/src/web/app/common/scripts/streaming/home.ts
index 533c23244..ffcf6e536 100644
--- a/src/web/app/common/scripts/streaming/home.ts
+++ b/src/web/app/common/scripts/streaming/home.ts
@@ -10,13 +10,13 @@ import MiOS from '../../mios';
 export class HomeStream extends Stream {
 	constructor(os: MiOS, me) {
 		super(os, '', {
-			i: me.token
+			i: me.account.token
 		});
 
 		// 最終利用日時を更新するため定期的にaliveメッセージを送信
 		setInterval(() => {
 			this.send({ type: 'alive' });
-			me.last_used_at = new Date();
+			me.account.last_used_at = new Date();
 		}, 1000 * 60);
 
 		// 自分の情報が更新されたとき
diff --git a/src/web/app/common/scripts/streaming/messaging-index.ts b/src/web/app/common/scripts/streaming/messaging-index.ts
index 84e2174ec..24f0ce0c9 100644
--- a/src/web/app/common/scripts/streaming/messaging-index.ts
+++ b/src/web/app/common/scripts/streaming/messaging-index.ts
@@ -8,7 +8,7 @@ import MiOS from '../../mios';
 export class MessagingIndexStream extends Stream {
 	constructor(os: MiOS, me) {
 		super(os, 'messaging-index', {
-			i: me.token
+			i: me.account.token
 		});
 	}
 }
diff --git a/src/web/app/common/scripts/streaming/messaging.ts b/src/web/app/common/scripts/streaming/messaging.ts
index c1b5875cf..4c593deb3 100644
--- a/src/web/app/common/scripts/streaming/messaging.ts
+++ b/src/web/app/common/scripts/streaming/messaging.ts
@@ -7,13 +7,13 @@ import MiOS from '../../mios';
 export class MessagingStream extends Stream {
 	constructor(os: MiOS, me, otherparty) {
 		super(os, 'messaging', {
-			i: me.token,
+			i: me.account.token,
 			otherparty
 		});
 
 		(this as any).on('_connected_', () => {
 			this.send({
-				i: me.token
+				i: me.account.token
 			});
 		});
 	}
diff --git a/src/web/app/common/scripts/streaming/othello-game.ts b/src/web/app/common/scripts/streaming/othello-game.ts
index b85af8f72..f34ef3514 100644
--- a/src/web/app/common/scripts/streaming/othello-game.ts
+++ b/src/web/app/common/scripts/streaming/othello-game.ts
@@ -4,7 +4,7 @@ import MiOS from '../../mios';
 export class OthelloGameStream extends Stream {
 	constructor(os: MiOS, me, game) {
 		super(os, 'othello-game', {
-			i: me ? me.token : null,
+			i: me ? me.account.token : null,
 			game: game.id
 		});
 	}
diff --git a/src/web/app/common/scripts/streaming/othello.ts b/src/web/app/common/scripts/streaming/othello.ts
index f5d47431c..8c6f4b9c3 100644
--- a/src/web/app/common/scripts/streaming/othello.ts
+++ b/src/web/app/common/scripts/streaming/othello.ts
@@ -5,7 +5,7 @@ import MiOS from '../../mios';
 export class OthelloStream extends Stream {
 	constructor(os: MiOS, me) {
 		super(os, 'othello', {
-			i: me.token
+			i: me.account.token
 		});
 	}
 }
diff --git a/src/web/app/common/views/components/signin.vue b/src/web/app/common/views/components/signin.vue
index 1738d0df7..d8b135776 100644
--- a/src/web/app/common/views/components/signin.vue
+++ b/src/web/app/common/views/components/signin.vue
@@ -6,7 +6,7 @@
 	<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">
+	<label class="token" v-if="user && user.account.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>
@@ -40,7 +40,7 @@ export default Vue.extend({
 			(this as any).api('signin', {
 				username: this.username,
 				password: this.password,
-				token: this.user && this.user.two_factor_enabled ? this.token : undefined
+				token: this.user && this.user.account.two_factor_enabled ? this.token : undefined
 			}).then(() => {
 				location.reload();
 			}).catch(() => {
diff --git a/src/web/app/common/views/components/twitter-setting.vue b/src/web/app/common/views/components/twitter-setting.vue
index a0de27085..15968d20a 100644
--- a/src/web/app/common/views/components/twitter-setting.vue
+++ b/src/web/app/common/views/components/twitter-setting.vue
@@ -1,13 +1,13 @@
 <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">@{{ os.i.twitter.screen_name }}</a></p>
+	<p class="account" v-if="os.i.account.twitter" :title="`Twitter ID: ${os.i.account.twitter.user_id}`">%i18n:common.tags.mk-twitter-setting.connected-to%: <a :href="`https://twitter.com/${os.i.account.twitter.screen_name}`" target="_blank">@{{ os.i.account.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>
+		<a :href="`${apiUrl}/connect/twitter`" target="_blank" @click.prevent="connect">{{ os.i.account.twitter ? '%i18n:common.tags.mk-twitter-setting.reconnect%' : '%i18n:common.tags.mk-twitter-setting.connect%' }}</a>
+		<span v-if="os.i.account.twitter"> or </span>
+		<a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="os.i.account.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>
+	<p class="id" v-if="os.i.account.twitter">Twitter ID: {{ os.i.account.twitter.user_id }}</p>
 </div>
 </template>
 
@@ -25,7 +25,7 @@ export default Vue.extend({
 	},
 	mounted() {
 		this.$watch('os.i', () => {
-			if ((this as any).os.i.twitter) {
+			if ((this as any).os.i.account.twitter) {
 				if (this.form) this.form.close();
 			}
 		}, {
diff --git a/src/web/app/common/views/components/uploader.vue b/src/web/app/common/views/components/uploader.vue
index 6465ad35e..73006b16e 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 as any).os.i.token);
+			data.append('i', (this as any).os.i.account.token);
 			data.append('file', file);
 
 			if (folder) data.append('folder_id', folder);
diff --git a/src/web/app/desktop/api/update-avatar.ts b/src/web/app/desktop/api/update-avatar.ts
index c3e0ce14c..8f748d853 100644
--- a/src/web/app/desktop/api/update-avatar.ts
+++ b/src/web/app/desktop/api/update-avatar.ts
@@ -16,7 +16,7 @@ export default (os: OS) => (cb, file = null) => {
 
 		w.$once('cropped', blob => {
 			const data = new FormData();
-			data.append('i', os.i.token);
+			data.append('i', os.i.account.token);
 			data.append('file', blob, file.name + '.cropped.png');
 
 			os.api('drive/folders/find', {
diff --git a/src/web/app/desktop/api/update-banner.ts b/src/web/app/desktop/api/update-banner.ts
index 9e94dc423..9ed48b267 100644
--- a/src/web/app/desktop/api/update-banner.ts
+++ b/src/web/app/desktop/api/update-banner.ts
@@ -16,7 +16,7 @@ export default (os: OS) => (cb, file = null) => {
 
 		w.$once('cropped', blob => {
 			const data = new FormData();
-			data.append('i', os.i.token);
+			data.append('i', os.i.account.token);
 			data.append('file', blob, file.name + '.cropped.png');
 
 			os.api('drive/folders/find', {
diff --git a/src/web/app/desktop/views/components/home.vue b/src/web/app/desktop/views/components/home.vue
index 562b22d11..a4ce1ef94 100644
--- a/src/web/app/desktop/views/components/home.vue
+++ b/src/web/app/desktop/views/components/home.vue
@@ -53,7 +53,7 @@
 			<div class="main">
 				<a @click="hint">カスタマイズのヒント</a>
 				<div>
-					<mk-post-form v-if="os.i.client_settings.showPostFormOnTopOfTl"/>
+					<mk-post-form v-if="os.i.account.client_settings.showPostFormOnTopOfTl"/>
 					<mk-timeline ref="tl" @loaded="onTlLoaded"/>
 				</div>
 			</div>
@@ -63,7 +63,7 @@
 				<component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" @chosen="warp"/>
 			</div>
 			<div class="main">
-				<mk-post-form v-if="os.i.client_settings.showPostFormOnTopOfTl"/>
+				<mk-post-form v-if="os.i.account.client_settings.showPostFormOnTopOfTl"/>
 				<mk-timeline ref="tl" @loaded="onTlLoaded" v-if="mode == 'timeline'"/>
 				<mk-mentions @loaded="onTlLoaded" v-if="mode == 'mentions'"/>
 			</div>
@@ -104,16 +104,16 @@ export default Vue.extend({
 		home: {
 			get(): any[] {
 				//#region 互換性のため
-				(this as any).os.i.client_settings.home.forEach(w => {
+				(this as any).os.i.account.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;
+				return (this as any).os.i.account.client_settings.home;
 			},
 			set(value) {
-				(this as any).os.i.client_settings.home = value;
+				(this as any).os.i.account.client_settings.home = value;
 			}
 		},
 		left(): any[] {
@@ -126,7 +126,7 @@ export default Vue.extend({
 	created() {
 		this.widgets.left = this.left;
 		this.widgets.right = this.right;
-		this.$watch('os.i.client_settings', i => {
+		this.$watch('os.i.account.client_settings', i => {
 			this.widgets.left = this.left;
 			this.widgets.right = this.right;
 		}, {
@@ -161,17 +161,17 @@ export default Vue.extend({
 		},
 		onHomeUpdated(data) {
 			if (data.home) {
-				(this as any).os.i.client_settings.home = data.home;
+				(this as any).os.i.account.client_settings.home = data.home;
 				this.widgets.left = data.home.filter(w => w.place == 'left');
 				this.widgets.right = data.home.filter(w => w.place == 'right');
 			} else {
-				const w = (this as any).os.i.client_settings.home.find(w => w.id == data.id);
+				const w = (this as any).os.i.account.client_settings.home.find(w => w.id == data.id);
 				if (w != null) {
 					w.data = data.data;
 					this.$refs[w.id][0].preventSave = true;
 					this.$refs[w.id][0].props = w.data;
-					this.widgets.left = (this as any).os.i.client_settings.home.filter(w => w.place == 'left');
-					this.widgets.right = (this as any).os.i.client_settings.home.filter(w => w.place == 'right');
+					this.widgets.left = (this as any).os.i.account.client_settings.home.filter(w => w.place == 'left');
+					this.widgets.right = (this as any).os.i.account.client_settings.home.filter(w => w.place == 'right');
 				}
 			}
 		},
diff --git a/src/web/app/desktop/views/components/post-detail.vue b/src/web/app/desktop/views/components/post-detail.vue
index e454c2870..98777e224 100644
--- a/src/web/app/desktop/views/components/post-detail.vue
+++ b/src/web/app/desktop/views/components/post-detail.vue
@@ -148,7 +148,7 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.client_settings.showMaps : true;
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.client_settings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
 					const uluru = new maps.LatLng(this.p.geo.latitude, this.p.geo.longitude);
diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index ddc338e37..073b89957 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -22,7 +22,7 @@
 		<div class="main">
 			<header>
 				<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="is-bot" v-if="p.user.account.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>
@@ -162,7 +162,7 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.client_settings.showMaps : true;
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.client_settings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
 					const uluru = new maps.LatLng(this.p.geo.latitude, this.p.geo.longitude);
diff --git a/src/web/app/desktop/views/components/settings.2fa.vue b/src/web/app/desktop/views/components/settings.2fa.vue
index 87783e799..85f2d6ba5 100644
--- a/src/web/app/desktop/views/components/settings.2fa.vue
+++ b/src/web/app/desktop/views/components/settings.2fa.vue
@@ -2,8 +2,8 @@
 <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 && !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 v-if="!data && !os.i.account.two_factor_enabled"><button @click="register" class="ui primary">%i18n:desktop.tags.mk-2fa-setting.register%</button></p>
+	<template v-if="os.i.account.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>
@@ -54,7 +54,7 @@ export default Vue.extend({
 					password: password
 				}).then(() => {
 					(this as any).apis.notify('%i18n:desktop.tags.mk-2fa-setting.unregistered%');
-					(this as any).os.i.two_factor_enabled = false;
+					(this as any).os.i.account.two_factor_enabled = false;
 				});
 			});
 		},
@@ -64,7 +64,7 @@ export default Vue.extend({
 				token: this.token
 			}).then(() => {
 				(this as any).apis.notify('%i18n:desktop.tags.mk-2fa-setting.success%');
-				(this as any).os.i.two_factor_enabled = true;
+				(this as any).os.i.account.two_factor_enabled = true;
 			}).catch(() => {
 				(this as any).apis.notify('%i18n:desktop.tags.mk-2fa-setting.failed%');
 			});
diff --git a/src/web/app/desktop/views/components/settings.api.vue b/src/web/app/desktop/views/components/settings.api.vue
index 5831f8207..0d5921ab7 100644
--- a/src/web/app/desktop/views/components/settings.api.vue
+++ b/src/web/app/desktop/views/components/settings.api.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="root api">
-	<p>Token: <code>{{ os.i.token }}</code></p>
+	<p>Token: <code>{{ os.i.account.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>
diff --git a/src/web/app/desktop/views/components/settings.profile.vue b/src/web/app/desktop/views/components/settings.profile.vue
index 23a166376..67a211c79 100644
--- a/src/web/app/desktop/views/components/settings.profile.vue
+++ b/src/web/app/desktop/views/components/settings.profile.vue
@@ -24,7 +24,7 @@
 	<button class="ui primary" @click="save">%i18n:desktop.tags.mk-profile-setting.save%</button>
 	<section>
 		<h2>その他</h2>
-		<mk-switch v-model="os.i.is_bot" @change="onChangeIsBot" text="このアカウントはbotです"/>
+		<mk-switch v-model="os.i.account.is_bot" @change="onChangeIsBot" text="このアカウントはbotです"/>
 	</section>
 </div>
 </template>
@@ -43,9 +43,9 @@ export default Vue.extend({
 	},
 	created() {
 		this.name = (this as any).os.i.name;
-		this.location = (this as any).os.i.profile.location;
+		this.location = (this as any).os.i.account.profile.location;
 		this.description = (this as any).os.i.description;
-		this.birthday = (this as any).os.i.profile.birthday;
+		this.birthday = (this as any).os.i.account.profile.birthday;
 	},
 	methods: {
 		updateAvatar() {
@@ -63,7 +63,7 @@ export default Vue.extend({
 		},
 		onChangeIsBot() {
 			(this as any).api('i/update', {
-				is_bot: (this as any).os.i.is_bot
+				is_bot: (this as any).os.i.account.is_bot
 			});
 		}
 	}
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index 950e60fb3..3e6a477ce 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -20,7 +20,7 @@
 
 		<section class="web" v-show="page == 'web'">
 			<h1>動作</h1>
-			<mk-switch v-model="os.i.client_settings.fetchOnScroll" @change="onChangeFetchOnScroll" text="スクロールで自動読み込み">
+			<mk-switch v-model="os.i.account.client_settings.fetchOnScroll" @change="onChangeFetchOnScroll" text="スクロールで自動読み込み">
 				<span>ページを下までスクロールしたときに自動で追加のコンテンツを読み込みます。</span>
 			</mk-switch>
 			<mk-switch v-model="autoPopout" text="ウィンドウの自動ポップアウト">
@@ -33,11 +33,11 @@
 			<div class="div">
 				<button class="ui button" @click="customizeHome">ホームをカスタマイズ</button>
 			</div>
-			<mk-switch v-model="os.i.client_settings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="タイムライン上部に投稿フォームを表示する"/>
-			<mk-switch v-model="os.i.client_settings.showMaps" @change="onChangeShowMaps" text="マップの自動展開">
+			<mk-switch v-model="os.i.account.client_settings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="タイムライン上部に投稿フォームを表示する"/>
+			<mk-switch v-model="os.i.account.client_settings.showMaps" @change="onChangeShowMaps" text="マップの自動展開">
 				<span>位置情報が添付された投稿のマップを自動的に展開します。</span>
 			</mk-switch>
-			<mk-switch v-model="os.i.client_settings.gradientWindowHeader" @change="onChangeGradientWindowHeader" text="ウィンドウのタイトルバーにグラデーションを使用"/>
+			<mk-switch v-model="os.i.account.client_settings.gradientWindowHeader" @change="onChangeGradientWindowHeader" text="ウィンドウのタイトルバーにグラデーションを使用"/>
 		</section>
 
 		<section class="web" v-show="page == 'web'">
@@ -57,7 +57,7 @@
 
 		<section class="web" v-show="page == 'web'">
 			<h1>モバイル</h1>
-			<mk-switch v-model="os.i.client_settings.disableViaMobile" @change="onChangeDisableViaMobile" text="「モバイルからの投稿」フラグを付けない"/>
+			<mk-switch v-model="os.i.account.client_settings.disableViaMobile" @change="onChangeDisableViaMobile" text="「モバイルからの投稿」フラグを付けない"/>
 		</section>
 
 		<section class="web" v-show="page == 'web'">
@@ -86,7 +86,7 @@
 
 		<section class="notification" v-show="page == 'notification'">
 			<h1>通知</h1>
-			<mk-switch v-model="os.i.settings.auto_watch" @change="onChangeAutoWatch" text="投稿の自動ウォッチ">
+			<mk-switch v-model="os.i.account.settings.auto_watch" @change="onChangeAutoWatch" text="投稿の自動ウォッチ">
 				<span>リアクションしたり返信したりした投稿に関する通知を自動的に受け取るようにします。</span>
 			</mk-switch>
 		</section>
diff --git a/src/web/app/desktop/views/components/timeline.vue b/src/web/app/desktop/views/components/timeline.vue
index b6b28c352..47a9688b6 100644
--- a/src/web/app/desktop/views/components/timeline.vue
+++ b/src/web/app/desktop/views/components/timeline.vue
@@ -107,7 +107,7 @@ export default Vue.extend({
 			this.fetch();
 		},
 		onScroll() {
-			if ((this as any).os.i.client_settings.fetchOnScroll !== false) {
+			if ((this as any).os.i.account.client_settings.fetchOnScroll !== false) {
 				const current = window.scrollY + window.innerHeight;
 				if (current > document.body.offsetHeight - 8) this.more();
 			}
diff --git a/src/web/app/desktop/views/components/ui.header.vue b/src/web/app/desktop/views/components/ui.header.vue
index 5425ec876..8af0e2fbe 100644
--- a/src/web/app/desktop/views/components/ui.header.vue
+++ b/src/web/app/desktop/views/components/ui.header.vue
@@ -44,9 +44,9 @@ export default Vue.extend({
 	},
 	mounted() {
 		if ((this as any).os.isSignedIn) {
-			const ago = (new Date().getTime() - new Date((this as any).os.i.last_used_at).getTime()) / 1000
+			const ago = (new Date().getTime() - new Date((this as any).os.i.account.last_used_at).getTime()) / 1000
 			const isHisasiburi = ago >= 3600;
-			(this as any).os.i.last_used_at = new Date();
+			(this as any).os.i.account.last_used_at = new Date();
 			if (isHisasiburi) {
 				(this.$refs.welcomeback as any).style.display = 'block';
 				(this.$refs.main as any).style.overflow = 'hidden';
diff --git a/src/web/app/desktop/views/components/widget-container.vue b/src/web/app/desktop/views/components/widget-container.vue
index c08e58e21..dd42be63b 100644
--- a/src/web/app/desktop/views/components/widget-container.vue
+++ b/src/web/app/desktop/views/components/widget-container.vue
@@ -24,8 +24,8 @@ export default Vue.extend({
 	computed: {
 		withGradient(): boolean {
 			return (this as any).os.isSignedIn
-				? (this as any).os.i.client_settings.gradientWindowHeader != null
-					? (this as any).os.i.client_settings.gradientWindowHeader
+				? (this as any).os.i.account.client_settings.gradientWindowHeader != null
+					? (this as any).os.i.account.client_settings.gradientWindowHeader
 					: false
 				: false;
 		}
diff --git a/src/web/app/desktop/views/components/window.vue b/src/web/app/desktop/views/components/window.vue
index 0f89aa3e4..75f725d4b 100644
--- a/src/web/app/desktop/views/components/window.vue
+++ b/src/web/app/desktop/views/components/window.vue
@@ -92,8 +92,8 @@ export default Vue.extend({
 		},
 		withGradient(): boolean {
 			return (this as any).os.isSignedIn
-				? (this as any).os.i.client_settings.gradientWindowHeader != null
-					? (this as any).os.i.client_settings.gradientWindowHeader
+				? (this as any).os.i.account.client_settings.gradientWindowHeader != null
+					? (this as any).os.i.account.client_settings.gradientWindowHeader
 					: false
 				: false;
 		}
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 b2119e52e..63542055d 100644
--- a/src/web/app/desktop/views/pages/user/user.header.vue
+++ b/src/web/app/desktop/views/pages/user/user.header.vue
@@ -9,7 +9,7 @@
 		<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>
+			<p class="location" v-if="user.account.profile.location">%fa:map-marker%{{ user.account.profile.location }}</p>
 		</div>
 		<footer>
 			<router-link :to="`/${user.username}`" :data-active="$parent.page == 'home'">%fa:home%概要</router-link>
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 17aa83201..592d5cca6 100644
--- a/src/web/app/desktop/views/pages/user/user.home.vue
+++ b/src/web/app/desktop/views/pages/user/user.home.vue
@@ -5,7 +5,7 @@
 			<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>
+			<p>%i18n:desktop.tags.mk-user.last-used-at%: <b><mk-time :time="user.account.last_used_at"/></b></p>
 		</div>
 	</div>
 	<main>
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 ceca829ac..1e7cb455b 100644
--- a/src/web/app/desktop/views/pages/user/user.profile.vue
+++ b/src/web/app/desktop/views/pages/user/user.profile.vue
@@ -7,11 +7,11 @@
 		<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 class="birthday" v-if="user.account.profile.birthday">
+		<p>%fa:birthday-cake%{{ user.account.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 class="twitter" v-if="user.account.twitter">
+		<p>%fa:B twitter%<a :href="`https://twitter.com/${user.account.twitter.screen_name}`" target="_blank">@{{ user.account.twitter.screen_name }}</a></p>
 	</div>
 	<div class="status">
 		<p class="posts-count">%fa:angle-right%<a>{{ user.posts_count }}</a><b>投稿</b></p>
@@ -31,7 +31,7 @@ export default Vue.extend({
 	props: ['user'],
 	computed: {
 		age(): number {
-			return age(this.user.profile.birthday);
+			return age(this.user.account.profile.birthday);
 		}
 	},
 	methods: {
diff --git a/src/web/app/mobile/views/components/post-detail.vue b/src/web/app/mobile/views/components/post-detail.vue
index d51bfbc0e..4b0b59eff 100644
--- a/src/web/app/mobile/views/components/post-detail.vue
+++ b/src/web/app/mobile/views/components/post-detail.vue
@@ -144,7 +144,7 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.client_settings.showMaps : true;
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.client_settings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
 					const uluru = new maps.LatLng(this.p.geo.latitude, this.p.geo.longitude);
diff --git a/src/web/app/mobile/views/components/post-form.vue b/src/web/app/mobile/views/components/post-form.vue
index 5b47caebc..2aa3c6f6c 100644
--- a/src/web/app/mobile/views/components/post-form.vue
+++ b/src/web/app/mobile/views/components/post-form.vue
@@ -111,7 +111,7 @@ export default Vue.extend({
 		},
 		post() {
 			this.posting = true;
-			const viaMobile = (this as any).os.i.client_settings.disableViaMobile !== true;
+			const viaMobile = (this as any).os.i.account.client_settings.disableViaMobile !== 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,
diff --git a/src/web/app/mobile/views/components/post.vue b/src/web/app/mobile/views/components/post.vue
index d464f6460..8df4dbf22 100644
--- a/src/web/app/mobile/views/components/post.vue
+++ b/src/web/app/mobile/views/components/post.vue
@@ -22,7 +22,7 @@
 		<div class="main">
 			<header>
 				<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="is-bot" v-if="p.user.account.is_bot">bot</span>
 				<span class="username">@{{ p.user.username }}</span>
 				<div class="info">
 					<span class="mobile" v-if="p.via_mobile">%fa:mobile-alt%</span>
@@ -137,7 +137,7 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.client_settings.showMaps : true;
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.client_settings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
 					const uluru = new maps.LatLng(this.p.geo.latitude, this.p.geo.longitude);
diff --git a/src/web/app/mobile/views/components/ui.header.vue b/src/web/app/mobile/views/components/ui.header.vue
index 1ccbd5c95..66e10a0f8 100644
--- a/src/web/app/mobile/views/components/ui.header.vue
+++ b/src/web/app/mobile/views/components/ui.header.vue
@@ -57,9 +57,9 @@ export default Vue.extend({
 				}
 			});
 
-			const ago = (new Date().getTime() - new Date((this as any).os.i.last_used_at).getTime()) / 1000
+			const ago = (new Date().getTime() - new Date((this as any).os.i.account.last_used_at).getTime()) / 1000
 			const isHisasiburi = ago >= 3600;
-			(this as any).os.i.last_used_at = new Date();
+			(this as any).os.i.account.last_used_at = new Date();
 			if (isHisasiburi) {
 				(this.$refs.welcomeback as any).style.display = 'block';
 				(this.$refs.main as any).style.overflow = 'hidden';
diff --git a/src/web/app/mobile/views/pages/home.vue b/src/web/app/mobile/views/pages/home.vue
index 44b072491..b110fc409 100644
--- a/src/web/app/mobile/views/pages/home.vue
+++ b/src/web/app/mobile/views/pages/home.vue
@@ -82,8 +82,8 @@ export default Vue.extend({
 		};
 	},
 	created() {
-		if ((this as any).os.i.client_settings.mobile_home == null) {
-			Vue.set((this as any).os.i.client_settings, 'mobile_home', [{
+		if ((this as any).os.i.account.client_settings.mobile_home == null) {
+			Vue.set((this as any).os.i.account.client_settings, 'mobile_home', [{
 				name: 'calendar',
 				id: 'a', data: {}
 			}, {
@@ -105,14 +105,14 @@ export default Vue.extend({
 				name: 'version',
 				id: 'g', data: {}
 			}]);
-			this.widgets = (this as any).os.i.client_settings.mobile_home;
+			this.widgets = (this as any).os.i.account.client_settings.mobile_home;
 			this.saveHome();
 		} else {
-			this.widgets = (this as any).os.i.client_settings.mobile_home;
+			this.widgets = (this as any).os.i.account.client_settings.mobile_home;
 		}
 
-		this.$watch('os.i.client_settings', i => {
-			this.widgets = (this as any).os.i.client_settings.mobile_home;
+		this.$watch('os.i.account.client_settings', i => {
+			this.widgets = (this as any).os.i.account.client_settings.mobile_home;
 		}, {
 			deep: true
 		});
@@ -157,15 +157,15 @@ export default Vue.extend({
 		},
 		onHomeUpdated(data) {
 			if (data.home) {
-				(this as any).os.i.client_settings.mobile_home = data.home;
+				(this as any).os.i.account.client_settings.mobile_home = data.home;
 				this.widgets = data.home;
 			} else {
-				const w = (this as any).os.i.client_settings.mobile_home.find(w => w.id == data.id);
+				const w = (this as any).os.i.account.client_settings.mobile_home.find(w => w.id == data.id);
 				if (w != null) {
 					w.data = data.data;
 					this.$refs[w.id][0].preventSave = true;
 					this.$refs[w.id][0].props = w.data;
-					this.widgets = (this as any).os.i.client_settings.mobile_home;
+					this.widgets = (this as any).os.i.account.client_settings.mobile_home;
 				}
 			}
 		},
@@ -194,7 +194,7 @@ export default Vue.extend({
 			this.saveHome();
 		},
 		saveHome() {
-			(this as any).os.i.client_settings.mobile_home = this.widgets;
+			(this as any).os.i.account.client_settings.mobile_home = this.widgets;
 			(this as any).api('i/update_mobile_home', {
 				home: this.widgets
 			});
diff --git a/src/web/app/mobile/views/pages/profile-setting.vue b/src/web/app/mobile/views/pages/profile-setting.vue
index f25d4bbe8..941165c99 100644
--- a/src/web/app/mobile/views/pages/profile-setting.vue
+++ b/src/web/app/mobile/views/pages/profile-setting.vue
@@ -53,9 +53,9 @@ export default Vue.extend({
 	},
 	created() {
 		this.name = (this as any).os.i.name;
-		this.location = (this as any).os.i.profile.location;
+		this.location = (this as any).os.i.account.profile.location;
 		this.description = (this as any).os.i.description;
-		this.birthday = (this as any).os.i.profile.birthday;
+		this.birthday = (this as any).os.i.account.profile.birthday;
 	},
 	mounted() {
 		document.title = 'Misskey | %i18n:mobile.tags.mk-profile-setting-page.title%';
diff --git a/src/web/app/mobile/views/pages/user.vue b/src/web/app/mobile/views/pages/user.vue
index 90f49a99a..9f677f6ca 100644
--- a/src/web/app/mobile/views/pages/user.vue
+++ b/src/web/app/mobile/views/pages/user.vue
@@ -18,11 +18,11 @@
 				</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 class="location" v-if="user.account.profile.location">
+						%fa:map-marker%{{ user.account.profile.location }}
 					</p>
-					<p class="birthday" v-if="user.profile.birthday">
-						%fa:birthday-cake%{{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳)
+					<p class="birthday" v-if="user.account.profile.birthday">
+						%fa:birthday-cake%{{ user.account.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳)
 					</p>
 				</div>
 				<div class="status">
@@ -74,7 +74,7 @@ export default Vue.extend({
 	},
 	computed: {
 		age(): number {
-			return age(this.user.profile.birthday);
+			return age(this.user.account.profile.birthday);
 		}
 	},
 	watch: {
diff --git a/src/web/app/mobile/views/pages/user/home.vue b/src/web/app/mobile/views/pages/user/home.vue
index fdbfd1bf5..dabb3f60b 100644
--- a/src/web/app/mobile/views/pages/user/home.vue
+++ b/src/web/app/mobile/views/pages/user/home.vue
@@ -31,7 +31,7 @@
 			<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>
+	<p>%i18n:mobile.tags.mk-user-overview.last-used-at%: <b><mk-time :time="user.account.last_used_at"/></b></p>
 </div>
 </template>
 
diff --git a/src/web/app/mobile/views/pages/welcome.vue b/src/web/app/mobile/views/pages/welcome.vue
index aa50b572e..563d2b28c 100644
--- a/src/web/app/mobile/views/pages/welcome.vue
+++ b/src/web/app/mobile/views/pages/welcome.vue
@@ -8,7 +8,7 @@
 			<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/>
+				<input v-if="user && user.account.two_factor_enabled" v-model="token" type="number" placeholder="トークン" required/>
 				<button type="submit" :disabled="signing">{{ signing ? 'ログインしています' : 'ログイン' }}</button>
 			</form>
 			<div>
@@ -70,7 +70,7 @@ export default Vue.extend({
 			(this as any).api('signin', {
 				username: this.username,
 				password: this.password,
-				token: this.user && this.user.two_factor_enabled ? this.token : undefined
+				token: this.user && this.user.account.two_factor_enabled ? this.token : undefined
 			}).then(() => {
 				location.reload();
 			}).catch(() => {
diff --git a/src/web/docs/api/entities/user.yaml b/src/web/docs/api/entities/user.yaml
index 528b9b0e1..f0d5fdbba 100644
--- a/src/web/docs/api/entities/user.yaml
+++ b/src/web/docs/api/entities/user.yaml
@@ -81,12 +81,6 @@ props:
     desc:
       ja: "自分がこのユーザーをミュートしているか"
       en: "Whether you muted this user"
-  - name: "last_used_at"
-    type: "date"
-    optional: false
-    desc:
-      ja: "最終利用日時"
-      en: "The last used date of this user"
   - name: "posts_count"
     type: "number"
     optional: false
@@ -111,49 +105,63 @@ props:
     desc:
       ja: "ドライブの容量(bytes)"
       en: "The capacity of drive of this user (bytes)"
-  - name: "is_bot"
-    type: "boolean"
-    optional: true
-    desc:
-      ja: "botか否か(自己申告であることに留意)"
-      en: "Whether is bot or not"
-  - name: "twitter"
-    type: "object"
-    optional: true
-    desc:
-      ja: "連携されているTwitterアカウント情報"
-      en: "The info of the connected twitter account of this user"
-    defName: "twitter"
-    def:
-      - name: "user_id"
-        type: "string"
-        optional: false
-        desc:
-          ja: "ユーザーID"
-          en: "The user ID"
-      - name: "screen_name"
-        type: "string"
-        optional: false
-        desc:
-          ja: "ユーザー名"
-          en: "The screen name of this user"
-  - name: "profile"
+  - name: "account"
     type: "object"
     optional: false
     desc:
-      ja: "プロフィール"
-      en: "The profile of this user"
-    defName: "profile"
+      ja: "このサーバーにおけるアカウント"
+      en: "The account of this user on this server"
+    defName: "account"
     def:
-      - name: "location"
-        type: "string"
+      - name: "last_used_at"
+        type: "date"
+        optional: false
+        desc:
+          ja: "最終利用日時"
+          en: "The last used date of this user"
+      - name: "is_bot"
+        type: "boolean"
         optional: true
         desc:
-          ja: "場所"
-          en: "The location of this user"
-      - name: "birthday"
-        type: "string"
+          ja: "botか否か(自己申告であることに留意)"
+          en: "Whether is bot or not"
+      - name: "twitter"
+        type: "object"
         optional: true
         desc:
-          ja: "誕生日 (YYYY-MM-DD)"
-          en: "The birthday of this user (YYYY-MM-DD)"
+          ja: "連携されているTwitterアカウント情報"
+          en: "The info of the connected twitter account of this user"
+        defName: "twitter"
+        def:
+          - name: "user_id"
+            type: "string"
+            optional: false
+            desc:
+              ja: "ユーザーID"
+              en: "The user ID"
+          - name: "screen_name"
+            type: "string"
+            optional: false
+            desc:
+              ja: "ユーザー名"
+              en: "The screen name of this user"
+      - name: "profile"
+        type: "object"
+        optional: false
+        desc:
+          ja: "プロフィール"
+          en: "The profile of this user"
+        defName: "profile"
+        def:
+          - name: "location"
+            type: "string"
+            optional: true
+            desc:
+              ja: "場所"
+              en: "The location of this user"
+          - name: "birthday"
+            type: "string"
+            optional: true
+            desc:
+              ja: "誕生日 (YYYY-MM-DD)"
+              en: "The birthday of this user (YYYY-MM-DD)"
diff --git a/test/api.js b/test/api.js
index 500b9adb7..9e55dd991 100644
--- a/test/api.js
+++ b/test/api.js
@@ -30,7 +30,7 @@ const async = fn => (done) => {
 
 const request = (endpoint, params, me) => new Promise((ok, ng) => {
 	const auth = me ? {
-		i: me.token
+		i: me.account.token
 	} : {};
 
 	_chai.request(server)
@@ -136,8 +136,10 @@ describe('API', () => {
 	describe('i/update', () => {
 		it('アカウント設定を更新できる', async(async () => {
 			const me = await insertSakurako({
-				profile: {
-					gender: 'female'
+				account: {
+					profile: {
+						gender: 'female'
+					}
 				}
 			});
 
@@ -153,10 +155,10 @@ describe('API', () => {
 			res.should.have.status(200);
 			res.body.should.be.a('object');
 			res.body.should.have.property('name').eql(myName);
-			res.body.should.have.property('profile').a('object');
-			res.body.should.have.nested.property('profile.location').eql(myLocation);
-			res.body.should.have.nested.property('profile.birthday').eql(myBirthday);
-			res.body.should.have.nested.property('profile.gender').eql('female');
+			res.body.should.have.nested.property('account.profile').a('object');
+			res.body.should.have.nested.property('account.profile.location').eql(myLocation);
+			res.body.should.have.nested.property('account.profile.birthday').eql(myBirthday);
+			res.body.should.have.nested.property('account.profile.gender').eql('female');
 		}));
 
 		it('名前を空白にできない', async(async () => {
@@ -176,8 +178,8 @@ describe('API', () => {
 			}, me);
 			res.should.have.status(200);
 			res.body.should.be.a('object');
-			res.body.should.have.property('profile').a('object');
-			res.body.should.have.nested.property('profile.birthday').eql(null);
+			res.body.should.have.nested.property('account.profile').a('object');
+			res.body.should.have.nested.property('account.profile.birthday').eql(null);
 		}));
 
 		it('不正な誕生日の形式で怒られる', async(async () => {
@@ -764,7 +766,7 @@ describe('API', () => {
 			const me = await insertSakurako();
 			const res = await _chai.request(server)
 				.post('/drive/files/create')
-				.field('i', me.token)
+				.field('i', me.account.token)
 				.attach('file', fs.readFileSync(__dirname + '/resources/Lenna.png'), 'Lenna.png');
 			res.should.have.status(200);
 			res.body.should.be.a('object');
@@ -1138,27 +1140,47 @@ describe('API', () => {
 	});
 });
 
+function deepAssign(destination, ...sources) {
+	for (const source of sources) {
+		for (const key in source) {
+			const destinationChild = destination[key];
+
+			if (typeof destinationChild === 'object' && destinationChild != null) {
+				deepAssign(destinationChild, source[key]);
+			} else {
+				destination[key] = source[key];
+			}
+		}
+	}
+
+	return destination;
+}
+
 function insertSakurako(opts) {
-	return db.get('users').insert(Object.assign({
-		token: '!00000000000000000000000000000000',
+	return db.get('users').insert(deepAssign({
 		username: 'sakurako',
 		username_lower: 'sakurako',
-		password: '$2a$08$FnHXg3tP.M/kINWgQSXNqeoBsiVrkj.ecXX8mW9rfBzMRkibYfjYy', // HimawariDaisuki06160907
-		profile: {},
-		settings: {},
-		client_settings: {}
+		account: {
+			token: '!00000000000000000000000000000000',
+			password: '$2a$08$FnHXg3tP.M/kINWgQSXNqeoBsiVrkj.ecXX8mW9rfBzMRkibYfjYy', // HimawariDaisuki06160907
+			profile: {},
+			settings: {},
+			client_settings: {}
+		}
 	}, opts));
 }
 
 function insertHimawari(opts) {
-	return db.get('users').insert(Object.assign({
-		token: '!00000000000000000000000000000001',
+	return db.get('users').insert(deepAssign({
 		username: 'himawari',
 		username_lower: 'himawari',
-		password: '$2a$08$OPESxR2RE/ZijjGanNKk6ezSqGFitqsbZqTjWUZPLhORMKxHCbc4O', // ilovesakurako
-		profile: {},
-		settings: {},
-		client_settings: {}
+		account: {
+			token: '!00000000000000000000000000000001',
+			password: '$2a$08$OPESxR2RE/ZijjGanNKk6ezSqGFitqsbZqTjWUZPLhORMKxHCbc4O', // ilovesakurako
+			profile: {},
+			settings: {},
+			client_settings: {}
+		}
 	}, opts));
 }
 
@@ -1171,14 +1193,14 @@ function insertDriveFile(opts) {
 }
 
 function insertDriveFolder(opts) {
-	return db.get('drive_folders').insert(Object.assign({
+	return db.get('drive_folders').insert(deepAssign({
 		name: 'my folder',
 		parent_id: null
 	}, opts));
 }
 
 function insertApp(opts) {
-	return db.get('apps').insert(Object.assign({
+	return db.get('apps').insert(deepAssign({
 		name: 'my app',
 		secret: 'mysecret'
 	}, opts));
diff --git a/tools/migration/shell.1522038492.user-account.js b/tools/migration/shell.1522038492.user-account.js
new file mode 100644
index 000000000..056c29e8e
--- /dev/null
+++ b/tools/migration/shell.1522038492.user-account.js
@@ -0,0 +1,41 @@
+db.users.dropIndex({ token: 1 });
+
+db.users.find({}).forEach(function(user) {
+	print(user._id);
+	db.users.update({ _id: user._id }, {
+		$unset: {
+			email: '',
+			links: '',
+			password: '',
+			token: '',
+			twitter: '',
+			line: '',
+			profile: '',
+			last_used_at: '',
+			is_bot: '',
+			is_pro: '',
+			two_factor_secret: '',
+			two_factor_enabled: '',
+			client_settings: '',
+			settings: ''
+		},
+		$set: {
+			account: {
+				email: user.email,
+				links: user.links,
+				password: user.password,
+				token: user.token,
+				twitter: user.twitter,
+				line: user.line,
+				profile: user.profile,
+				last_used_at: user.last_used_at,
+				is_bot: user.is_bot,
+				is_pro: user.is_pro,
+				two_factor_secret: user.two_factor_secret,
+				two_factor_enabled: user.two_factor_enabled,
+				client_settings: user.client_settings,
+				settings: user.settings
+			}
+		}
+	}, false, false);
+});

From dff27ab5b7b2c348936a55fb55a157384aa3fc77 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Mon, 26 Mar 2018 14:11:16 +0900
Subject: [PATCH 0852/1250] Define hostname and secondary_hostname in config

This is missing part of commit 0109e0811c4142153ae3f915295e62630653909e.
---
 src/config.ts |  5 +++++
 src/server.ts | 13 +++----------
 2 files changed, 8 insertions(+), 10 deletions(-)

diff --git a/src/config.ts b/src/config.ts
index 23feadc73..42dfd5f54 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -103,9 +103,11 @@ type Source = {
  */
 type Mixin = {
 	host: string;
+	hostname: string;
 	scheme: string;
 	ws_scheme: string;
 	secondary_host: string;
+	secondary_hostname: string;
 	secondary_scheme: string;
 	api_url: string;
 	ws_url: string;
@@ -130,14 +132,17 @@ export default function load() {
 	if (!isUrl(config.secondary_url)) urlError(config.secondary_url);
 
 	const url = new URL(config.url);
+	const secondaryUrl = new URL(config.secondary_url);
 	config.url = normalizeUrl(config.url);
 	config.secondary_url = normalizeUrl(config.secondary_url);
 
 	mixin.host = url.host;
+	mixin.hostname = url.hostname;
 	mixin.scheme = url.protocol.replace(/:$/, '');
 	mixin.ws_scheme = mixin.scheme.replace('http', 'ws');
 	mixin.ws_url = `${mixin.ws_scheme}://api.${mixin.host}`;
 	mixin.secondary_host = config.secondary_url.substr(config.secondary_url.indexOf('://') + 3);
+	mixin.secondary_hostname = secondaryUrl.hostname;
 	mixin.secondary_scheme = config.secondary_url.substr(0, config.secondary_url.indexOf('://'));
 	mixin.api_url = `${mixin.scheme}://api.${mixin.host}`;
 	mixin.auth_url = `${mixin.scheme}://auth.${mixin.host}`;
diff --git a/src/server.ts b/src/server.ts
index 84e8c4148..7f66c4207 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -14,11 +14,6 @@ import vhost = require('vhost');
 import log from './log-request';
 import config from './conf';
 
-function extractHostname(host) {
-	const index = host.indexOf(':');
-	return index < 0 ? host : host.substr(0, index);
-}
-
 /**
  * Init app
  */
@@ -58,11 +53,9 @@ app.use((req, res, next) => {
 /**
  * Register modules
  */
-const hostname = extractHostname(config.host);
-const secondaryHostname = extractHostname(config.secondary_host);
-app.use(vhost(`api.${hostname}`, require('./api/server')));
-app.use(vhost(secondaryHostname, require('./himasaku/server')));
-app.use(vhost(`file.${secondaryHostname}`, require('./file/server')));
+app.use(vhost(`api.${config.hostname}`, require('./api/server')));
+app.use(vhost(config.secondary_hostname, require('./himasaku/server')));
+app.use(vhost(`file.${config.secondary_hostname}`, require('./file/server')));
 app.use(require('./web/server'));
 
 /**

From ab6fa2fb8b188453214c5af410cdfb30734780d0 Mon Sep 17 00:00:00 2001
From: rinsuki <428rinsuki+git@gmail.com>
Date: Mon, 26 Mar 2018 14:27:42 +0900
Subject: [PATCH 0853/1250] =?UTF-8?q?document.domain=E3=82=92=E3=83=81?=
 =?UTF-8?q?=E3=82=A7=E3=83=83=E3=82=AF=E3=81=99=E3=82=8B=E9=9A=9B=E3=81=AB?=
 =?UTF-8?q?=E3=80=81host=E3=81=AE=E3=81=8B=E3=82=8F=E3=82=8A=E3=81=ABhostn?=
 =?UTF-8?q?ame=E3=82=92=E4=BD=BF=E3=81=86=E3=82=88=E3=81=86=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

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

diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index 28adfd3b0..6cc7ae6ce 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -14,7 +14,7 @@ import ElementLocaleJa from 'element-ui/lib/locale/lang/ja';
 import App from './app.vue';
 import checkForUpdate from './common/scripts/check-for-update';
 import MiOS, { API } from './common/mios';
-import { version, host, lang } from './config';
+import { version, hostname, lang } from './config';
 
 let elementLocale;
 switch (lang) {
@@ -60,8 +60,8 @@ console.info(
 window.clearTimeout((window as any).mkBootTimer);
 delete (window as any).mkBootTimer;
 
-if (host != 'localhost') {
-	document.domain = host;
+if (hostname != 'localhost') {
+	document.domain = hostname;
 }
 
 //#region Set lang attr

From 9648cca8e9354b0bfc9ef3f01213b9ebcca323aa Mon Sep 17 00:00:00 2001
From: rinsuki <428rinsuki+git@gmail.com>
Date: Mon, 26 Mar 2018 15:56:31 +0900
Subject: [PATCH 0854/1250] =?UTF-8?q?=E3=83=A2=E3=83=90=E3=82=A4=E3=83=AB?=
 =?UTF-8?q?=E7=89=88=E3=81=AE=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E3=83=9A?=
 =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=81=AE=E8=A1=A8=E7=A4=BA=E5=88=87=E3=82=8A?=
 =?UTF-8?q?=E5=A4=89=E3=81=88nav=E3=82=92=E4=B8=8A=E3=81=AB=E3=81=A4?=
 =?UTF-8?q?=E3=81=84=E3=81=A6=E3=81=8F=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/web/app/mobile/views/pages/user.vue | 17 ++++++++++++-----
 1 file changed, 12 insertions(+), 5 deletions(-)

diff --git a/src/web/app/mobile/views/pages/user.vue b/src/web/app/mobile/views/pages/user.vue
index 90f49a99a..a9953fdd4 100644
--- a/src/web/app/mobile/views/pages/user.vue
+++ b/src/web/app/mobile/views/pages/user.vue
@@ -40,12 +40,14 @@
 					</a>
 				</div>
 			</div>
-			<nav>
+		</header>
+		<nav>
+			<div class="nav-container">
 				<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>
+		</nav>
 		<div class="body">
 			<x-home v-if="page == 'home'" :user="user"/>
 			<mk-user-timeline v-if="page == 'posts'" :user="user"/>
@@ -109,7 +111,6 @@ export default Vue.extend({
 
 main
 	> header
-		box-shadow 0 4px 4px rgba(0, 0, 0, 0.3)
 
 		> .banner
 			padding-bottom 33.3%
@@ -207,7 +208,13 @@ main
 					> i
 						font-size 14px
 
-		> nav
+	> nav
+		position sticky
+		top 48px
+		box-shadow 0 4px 4px rgba(0, 0, 0, 0.3)
+		background-color #313a42
+		z-index 1
+		> .nav-container
 			display flex
 			justify-content center
 			margin 0 auto

From c0ea6939e3ad40a6a2719f37a5c7efcaa75e7af5 Mon Sep 17 00:00:00 2001
From: rinsuki <428rinsuki+git@gmail.com>
Date: Mon, 26 Mar 2018 16:18:40 +0900
Subject: [PATCH 0855/1250] =?UTF-8?q?=E3=83=87=E3=82=B9=E3=82=AF=E3=83=88?=
 =?UTF-8?q?=E3=83=83=E3=83=97=E7=89=88=E3=81=ABTwitter=E3=81=A7=E3=83=AD?=
 =?UTF-8?q?=E3=82=B0=E3=82=A4=E3=83=B3=E3=81=99=E3=82=8B=E3=83=AA=E3=83=B3?=
 =?UTF-8?q?=E3=82=AF=E3=82=92=E5=BE=A9=E6=B4=BB=E3=81=95=E3=81=9B=E3=81=9F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/web/app/common/views/components/signin.vue | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/src/web/app/common/views/components/signin.vue b/src/web/app/common/views/components/signin.vue
index d8b135776..37ce4de32 100644
--- a/src/web/app/common/views/components/signin.vue
+++ b/src/web/app/common/views/components/signin.vue
@@ -10,11 +10,13 @@
 		<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>
+	もしくは <a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a>
 </form>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import { apiUrl } from '../../../config';
 
 export default Vue.extend({
 	data() {
@@ -23,7 +25,8 @@ export default Vue.extend({
 			user: null,
 			username: '',
 			password: '',
-			token: ''
+			token: '',
+			apiUrl,
 		};
 	},
 	methods: {
@@ -40,7 +43,7 @@ export default Vue.extend({
 			(this as any).api('signin', {
 				username: this.username,
 				password: this.password,
-				token: this.user && this.user.account.two_factor_enabled ? this.token : undefined
+				token: this.user && this.user.two_factor_enabled ? this.token : undefined
 			}).then(() => {
 				location.reload();
 			}).catch(() => {

From 6dd71c2770037cd1646b77fcc823432f6bc86cda Mon Sep 17 00:00:00 2001
From: rinsuki <428rinsuki+git@gmail.com>
Date: Mon, 26 Mar 2018 16:22:00 +0900
Subject: [PATCH 0856/1250] =?UTF-8?q?#1296=20=E3=81=A7=E5=A3=8A=E3=81=97?=
 =?UTF-8?q?=E3=81=A6=E3=81=97=E3=81=BE=E3=81=A3=E3=81=9F=E3=81=AE=E3=82=92?=
 =?UTF-8?q?=E7=9B=B4=E3=81=97=E3=81=9F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

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

diff --git a/src/web/app/common/views/components/signin.vue b/src/web/app/common/views/components/signin.vue
index 37ce4de32..243468408 100644
--- a/src/web/app/common/views/components/signin.vue
+++ b/src/web/app/common/views/components/signin.vue
@@ -43,7 +43,7 @@ export default Vue.extend({
 			(this as any).api('signin', {
 				username: this.username,
 				password: this.password,
-				token: this.user && this.user.two_factor_enabled ? this.token : undefined
+				token: this.user && this.user.account.two_factor_enabled ? this.token : undefined
 			}).then(() => {
 				location.reload();
 			}).catch(() => {

From ec9c693d8c14907f4b71868a07bdbd50947bdf03 Mon Sep 17 00:00:00 2001
From: rinsuki <428rinsuki+git@gmail.com>
Date: Mon, 26 Mar 2018 16:56:46 +0900
Subject: [PATCH 0857/1250] mk-images -> mk-media-list, mk-images-image ->
 mk-media-image

---
 src/web/app/common/views/components/index.ts   |  4 ++--
 .../components/{images.vue => media-list.vue}  | 18 +++++++++---------
 src/web/app/desktop/views/components/index.ts  |  8 ++++----
 ...image-dialog.vue => media-image-dialog.vue} |  4 ++--
 .../{images-image.vue => media-image.vue}      |  8 ++++----
 .../views/components/post-detail.sub.vue       |  2 +-
 .../desktop/views/components/post-detail.vue   |  2 +-
 .../desktop/views/components/posts.post.vue    |  2 +-
 .../views/components/sub-post-content.vue      |  2 +-
 src/web/app/mobile/views/components/index.ts   |  4 ++--
 .../{images-image.vue => media-image.vue}      |  4 ++--
 .../mobile/views/components/post-detail.vue    |  2 +-
 src/web/app/mobile/views/components/post.vue   |  2 +-
 .../views/components/sub-post-content.vue      |  2 +-
 14 files changed, 32 insertions(+), 32 deletions(-)
 rename src/web/app/common/views/components/{images.vue => media-list.vue} (82%)
 rename src/web/app/desktop/views/components/{images-image-dialog.vue => media-image-dialog.vue} (94%)
 rename src/web/app/desktop/views/components/{images-image.vue => media-image.vue} (89%)
 rename src/web/app/mobile/views/components/{images-image.vue => media-image.vue} (82%)

diff --git a/src/web/app/common/views/components/index.ts b/src/web/app/common/views/components/index.ts
index fbf9d22a0..b58ba37ec 100644
--- a/src/web/app/common/views/components/index.ts
+++ b/src/web/app/common/views/components/index.ts
@@ -11,7 +11,7 @@ import reactionIcon from './reaction-icon.vue';
 import reactionsViewer from './reactions-viewer.vue';
 import time from './time.vue';
 import timer from './timer.vue';
-import images from './images.vue';
+import mediaList from './media-list.vue';
 import uploader from './uploader.vue';
 import specialMessage from './special-message.vue';
 import streamIndicator from './stream-indicator.vue';
@@ -36,7 +36,7 @@ Vue.component('mk-reaction-icon', reactionIcon);
 Vue.component('mk-reactions-viewer', reactionsViewer);
 Vue.component('mk-time', time);
 Vue.component('mk-timer', timer);
-Vue.component('mk-images', images);
+Vue.component('mk-media-list', mediaList);
 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/images.vue b/src/web/app/common/views/components/media-list.vue
similarity index 82%
rename from src/web/app/common/views/components/images.vue
rename to src/web/app/common/views/components/media-list.vue
index dc802a018..b0e668508 100644
--- a/src/web/app/common/views/components/images.vue
+++ b/src/web/app/common/views/components/media-list.vue
@@ -1,6 +1,6 @@
 <template>
-<div class="mk-images">
-	<mk-images-image v-for="image in images" ref="image" :image="image" :key="image.id"/>
+<div class="mk-media-list">
+	<mk-media-image v-for="media in mediaList" ref="media" :image="media" :key="media.id"/>
 </div>
 </template>
 
@@ -8,16 +8,16 @@
 import Vue from 'vue';
 
 export default Vue.extend({
-	props: ['images'],
+	props: ['mediaList'],
 	mounted() {
-		const tags = this.$refs.image as Vue[];
+		const tags = this.$refs.media as Vue[];
 
-		if (this.images.length == 1) {
+		if (this.mediaList.length == 1) {
 			(this.$el.style as any).gridTemplateRows = '1fr';
 
 			(tags[0].$el.style as any).gridColumn = '1 / 2';
 			(tags[0].$el.style as any).gridRow = '1 / 2';
-		} else if (this.images.length == 2) {
+		} else if (this.mediaList.length == 2) {
 			(this.$el.style as any).gridTemplateColumns = '1fr 1fr';
 			(this.$el.style as any).gridTemplateRows = '1fr';
 
@@ -25,7 +25,7 @@ export default Vue.extend({
 			(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) {
+		} else if (this.mediaList.length == 3) {
 			(this.$el.style as any).gridTemplateColumns = '1fr 0.5fr';
 			(this.$el.style as any).gridTemplateRows = '1fr 1fr';
 
@@ -35,7 +35,7 @@ export default Vue.extend({
 			(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) {
+		} else if (this.mediaList.length == 4) {
 			(this.$el.style as any).gridTemplateColumns = '1fr 1fr';
 			(this.$el.style as any).gridTemplateRows = '1fr 1fr';
 
@@ -53,7 +53,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-images
+.mk-media-list
 	display grid
 	grid-gap 4px
 	height 256px
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index 52b9680ba..9bca603a5 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -11,8 +11,8 @@ 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 imagesImage from './images-image.vue';
-import imagesImageDialog from './images-image-dialog.vue';
+import mediaImage from './media-image.vue';
+import mediaImageDialog from './media-image-dialog.vue';
 import notifications from './notifications.vue';
 import postForm from './post-form.vue';
 import repostForm from './repost-form.vue';
@@ -40,8 +40,8 @@ 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-image', imagesImage);
-Vue.component('mk-images-image-dialog', imagesImageDialog);
+Vue.component('mk-media-image', mediaImage);
+Vue.component('mk-media-image-dialog', mediaImageDialog);
 Vue.component('mk-notifications', notifications);
 Vue.component('mk-post-form', postForm);
 Vue.component('mk-repost-form', repostForm);
diff --git a/src/web/app/desktop/views/components/images-image-dialog.vue b/src/web/app/desktop/views/components/media-image-dialog.vue
similarity index 94%
rename from src/web/app/desktop/views/components/images-image-dialog.vue
rename to src/web/app/desktop/views/components/media-image-dialog.vue
index 60afa7af8..dec140d1c 100644
--- a/src/web/app/desktop/views/components/images-image-dialog.vue
+++ b/src/web/app/desktop/views/components/media-image-dialog.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-images-image-dialog">
+<div class="mk-media-image-dialog">
 	<div class="bg" @click="close"></div>
 	<img :src="image.url" :alt="image.name" :title="image.name" @click="close"/>
 </div>
@@ -34,7 +34,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-images-image-dialog
+.mk-media-image-dialog
 	display block
 	position fixed
 	z-index 2048
diff --git a/src/web/app/desktop/views/components/images-image.vue b/src/web/app/desktop/views/components/media-image.vue
similarity index 89%
rename from src/web/app/desktop/views/components/images-image.vue
rename to src/web/app/desktop/views/components/media-image.vue
index 5b7dc4173..bc02d0f9b 100644
--- a/src/web/app/desktop/views/components/images-image.vue
+++ b/src/web/app/desktop/views/components/media-image.vue
@@ -1,5 +1,5 @@
 <template>
-<a class="mk-images-image"
+<a class="mk-media-image"
 	:href="image.url"
 	@mousemove="onMousemove"
 	@mouseleave="onMouseleave"
@@ -11,7 +11,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import MkImagesImageDialog from './images-image-dialog.vue';
+import MkMediaImageDialog from './media-image-dialog.vue';
 
 export default Vue.extend({
 	props: ['image'],
@@ -39,7 +39,7 @@ export default Vue.extend({
 		},
 
 		onClick() {
-			(this as any).os.new(MkImagesImageDialog, {
+			(this as any).os.new(MkMediaImageDialog, {
 				image: this.image
 			});
 		}
@@ -48,7 +48,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-images-image
+.mk-media-image
 	display block
 	cursor zoom-in
 	overflow hidden
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 bf6e3ac3b..322e1a173 100644
--- a/src/web/app/desktop/views/components/post-detail.sub.vue
+++ b/src/web/app/desktop/views/components/post-detail.sub.vue
@@ -18,7 +18,7 @@
 		<div class="body">
 			<mk-post-html v-if="post.ast" :ast="post.ast" :i="os.i" :class="$style.text"/>
 			<div class="media" v-if="post.media">
-				<mk-images :images="post.media"/>
+				<mk-media-list :mediaList="post.media"/>
 			</div>
 		</div>
 	</div>
diff --git a/src/web/app/desktop/views/components/post-detail.vue b/src/web/app/desktop/views/components/post-detail.vue
index 98777e224..59cd01fbf 100644
--- a/src/web/app/desktop/views/components/post-detail.vue
+++ b/src/web/app/desktop/views/components/post-detail.vue
@@ -40,7 +40,7 @@
 		<div class="body">
 			<mk-post-html :class="$style.text" v-if="p.ast" :ast="p.ast" :i="os.i"/>
 			<div class="media" v-if="p.media">
-				<mk-images :images="p.media"/>
+				<mk-media-list :mediaList="p.media"/>
 			</div>
 			<mk-poll v-if="p.poll" :post="p"/>
 			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index 073b89957..cde9b6f85 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -42,7 +42,7 @@
 					<a class="rp" v-if="p.repost">RP:</a>
 				</div>
 				<div class="media" v-if="p.media">
-					<mk-images :images="p.media"/>
+					<mk-media-list :mediaList="p.media"/>
 				</div>
 				<mk-poll v-if="p.poll" :post="p" ref="pollViewer"/>
 				<div class="tags" v-if="p.tags && p.tags.length > 0">
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 7f4c3b4f6..662853f65 100644
--- a/src/web/app/desktop/views/components/sub-post-content.vue
+++ b/src/web/app/desktop/views/components/sub-post-content.vue
@@ -8,7 +8,7 @@
 	</div>
 	<details v-if="post.media">
 		<summary>({{ post.media.length }}つのメディア)</summary>
-		<mk-images :images="post.media"/>
+		<mk-media-list :mediaList="post.media"/>
 	</details>
 	<details v-if="post.poll">
 		<summary>投票</summary>
diff --git a/src/web/app/mobile/views/components/index.ts b/src/web/app/mobile/views/components/index.ts
index 19135d08d..4743f50e0 100644
--- a/src/web/app/mobile/views/components/index.ts
+++ b/src/web/app/mobile/views/components/index.ts
@@ -4,7 +4,7 @@ import ui from './ui.vue';
 import timeline from './timeline.vue';
 import post from './post.vue';
 import posts from './posts.vue';
-import imagesImage from './images-image.vue';
+import mediaImage from './media-image.vue';
 import drive from './drive.vue';
 import postPreview from './post-preview.vue';
 import subPostContent from './sub-post-content.vue';
@@ -26,7 +26,7 @@ Vue.component('mk-ui', ui);
 Vue.component('mk-timeline', timeline);
 Vue.component('mk-post', post);
 Vue.component('mk-posts', posts);
-Vue.component('mk-images-image', imagesImage);
+Vue.component('mk-media-image', mediaImage);
 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/components/images-image.vue b/src/web/app/mobile/views/components/media-image.vue
similarity index 82%
rename from src/web/app/mobile/views/components/images-image.vue
rename to src/web/app/mobile/views/components/media-image.vue
index 6bc1dc0ae..faf8bad48 100644
--- a/src/web/app/mobile/views/components/images-image.vue
+++ b/src/web/app/mobile/views/components/media-image.vue
@@ -1,5 +1,5 @@
 <template>
-<a class="mk-images-image" :href="image.url" target="_blank" :style="style" :title="image.name"></a>
+<a class="mk-media-image" :href="image.url" target="_blank" :style="style" :title="image.name"></a>
 </template>
 
 <script lang="ts">
@@ -19,7 +19,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-images-image
+.mk-media-image
 	display block
 	overflow hidden
 	width 100%
diff --git a/src/web/app/mobile/views/components/post-detail.vue b/src/web/app/mobile/views/components/post-detail.vue
index 4b0b59eff..db7509d0f 100644
--- a/src/web/app/mobile/views/components/post-detail.vue
+++ b/src/web/app/mobile/views/components/post-detail.vue
@@ -43,7 +43,7 @@
 				<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
 			</div>
 			<div class="media" v-if="p.media">
-				<mk-images :images="p.media"/>
+				<mk-media-list :mediaList="p.media"/>
 			</div>
 			<mk-poll v-if="p.poll" :post="p"/>
 			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
diff --git a/src/web/app/mobile/views/components/post.vue b/src/web/app/mobile/views/components/post.vue
index 8df4dbf22..cf4c38337 100644
--- a/src/web/app/mobile/views/components/post.vue
+++ b/src/web/app/mobile/views/components/post.vue
@@ -41,7 +41,7 @@
 					<a class="rp" v-if="p.repost != null">RP:</a>
 				</div>
 				<div class="media" v-if="p.media">
-					<mk-images :images="p.media"/>
+					<mk-media-list :mediaList="p.media"/>
 				</div>
 				<mk-poll v-if="p.poll" :post="p" ref="pollViewer"/>
 				<div class="tags" v-if="p.tags && p.tags.length > 0">
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 b97f08255..7a9fcb633 100644
--- a/src/web/app/mobile/views/components/sub-post-content.vue
+++ b/src/web/app/mobile/views/components/sub-post-content.vue
@@ -7,7 +7,7 @@
 	</div>
 	<details v-if="post.media">
 		<summary>({{ post.media.length }}個のメディア)</summary>
-		<mk-images :images="post.media"/>
+		<mk-media-list :mediaList="post.media"/>
 	</details>
 	<details v-if="post.poll">
 		<summary>%i18n:mobile.tags.mk-sub-post-content.poll%</summary>

From 0b91abc8dd3151cd85035cdd7eee5215cca88e1b Mon Sep 17 00:00:00 2001
From: rinsuki <428rinsuki+git@gmail.com>
Date: Mon, 26 Mar 2018 17:03:20 +0900
Subject: [PATCH 0858/1250] :mediaList -> :media-list

---
 src/web/app/desktop/views/components/post-detail.sub.vue  | 2 +-
 src/web/app/desktop/views/components/post-detail.vue      | 2 +-
 src/web/app/desktop/views/components/posts.post.vue       | 2 +-
 src/web/app/desktop/views/components/sub-post-content.vue | 2 +-
 src/web/app/mobile/views/components/post-detail.vue       | 2 +-
 src/web/app/mobile/views/components/post.vue              | 2 +-
 src/web/app/mobile/views/components/sub-post-content.vue  | 2 +-
 7 files changed, 7 insertions(+), 7 deletions(-)

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 322e1a173..c35a07d7c 100644
--- a/src/web/app/desktop/views/components/post-detail.sub.vue
+++ b/src/web/app/desktop/views/components/post-detail.sub.vue
@@ -18,7 +18,7 @@
 		<div class="body">
 			<mk-post-html v-if="post.ast" :ast="post.ast" :i="os.i" :class="$style.text"/>
 			<div class="media" v-if="post.media">
-				<mk-media-list :mediaList="post.media"/>
+				<mk-media-list :media-list="post.media"/>
 			</div>
 		</div>
 	</div>
diff --git a/src/web/app/desktop/views/components/post-detail.vue b/src/web/app/desktop/views/components/post-detail.vue
index 59cd01fbf..5845ab4f8 100644
--- a/src/web/app/desktop/views/components/post-detail.vue
+++ b/src/web/app/desktop/views/components/post-detail.vue
@@ -40,7 +40,7 @@
 		<div class="body">
 			<mk-post-html :class="$style.text" v-if="p.ast" :ast="p.ast" :i="os.i"/>
 			<div class="media" v-if="p.media">
-				<mk-media-list :mediaList="p.media"/>
+				<mk-media-list :media-list="p.media"/>
 			</div>
 			<mk-poll v-if="p.poll" :post="p"/>
 			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index cde9b6f85..71cbbc68d 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -42,7 +42,7 @@
 					<a class="rp" v-if="p.repost">RP:</a>
 				</div>
 				<div class="media" v-if="p.media">
-					<mk-media-list :mediaList="p.media"/>
+					<mk-media-list :media-list="p.media"/>
 				</div>
 				<mk-poll v-if="p.poll" :post="p" ref="pollViewer"/>
 				<div class="tags" v-if="p.tags && p.tags.length > 0">
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 662853f65..8c8f42c80 100644
--- a/src/web/app/desktop/views/components/sub-post-content.vue
+++ b/src/web/app/desktop/views/components/sub-post-content.vue
@@ -8,7 +8,7 @@
 	</div>
 	<details v-if="post.media">
 		<summary>({{ post.media.length }}つのメディア)</summary>
-		<mk-media-list :mediaList="post.media"/>
+		<mk-media-list :media-list="post.media"/>
 	</details>
 	<details v-if="post.poll">
 		<summary>投票</summary>
diff --git a/src/web/app/mobile/views/components/post-detail.vue b/src/web/app/mobile/views/components/post-detail.vue
index db7509d0f..9baa5de6d 100644
--- a/src/web/app/mobile/views/components/post-detail.vue
+++ b/src/web/app/mobile/views/components/post-detail.vue
@@ -43,7 +43,7 @@
 				<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
 			</div>
 			<div class="media" v-if="p.media">
-				<mk-media-list :mediaList="p.media"/>
+				<mk-media-list :media-list="p.media"/>
 			</div>
 			<mk-poll v-if="p.poll" :post="p"/>
 			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
diff --git a/src/web/app/mobile/views/components/post.vue b/src/web/app/mobile/views/components/post.vue
index cf4c38337..d53649b11 100644
--- a/src/web/app/mobile/views/components/post.vue
+++ b/src/web/app/mobile/views/components/post.vue
@@ -41,7 +41,7 @@
 					<a class="rp" v-if="p.repost != null">RP:</a>
 				</div>
 				<div class="media" v-if="p.media">
-					<mk-media-list :mediaList="p.media"/>
+					<mk-media-list :media-list="p.media"/>
 				</div>
 				<mk-poll v-if="p.poll" :post="p" ref="pollViewer"/>
 				<div class="tags" v-if="p.tags && p.tags.length > 0">
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 7a9fcb633..389fc420e 100644
--- a/src/web/app/mobile/views/components/sub-post-content.vue
+++ b/src/web/app/mobile/views/components/sub-post-content.vue
@@ -7,7 +7,7 @@
 	</div>
 	<details v-if="post.media">
 		<summary>({{ post.media.length }}個のメディア)</summary>
-		<mk-media-list :mediaList="post.media"/>
+		<mk-media-list :media-list="post.media"/>
 	</details>
 	<details v-if="post.poll">
 		<summary>%i18n:mobile.tags.mk-sub-post-content.poll%</summary>

From abbb03f1e8b9b533857fc4da36fd685bf223c8c8 Mon Sep 17 00:00:00 2001
From: rinsuki <428rinsuki+git@gmail.com>
Date: Mon, 26 Mar 2018 17:26:21 +0900
Subject: [PATCH 0859/1250] =?UTF-8?q?media-list=E3=81=AEgrid=E3=81=AE?=
 =?UTF-8?q?=E5=87=A6=E7=90=86=E3=82=92JS=E3=81=8B=E3=82=89CSS=E3=81=AB?=
 =?UTF-8?q?=E7=A7=BB=E8=A1=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../common/views/components/media-list.vue    | 77 +++++++++----------
 1 file changed, 35 insertions(+), 42 deletions(-)

diff --git a/src/web/app/common/views/components/media-list.vue b/src/web/app/common/views/components/media-list.vue
index b0e668508..d0da584a4 100644
--- a/src/web/app/common/views/components/media-list.vue
+++ b/src/web/app/common/views/components/media-list.vue
@@ -1,6 +1,8 @@
 <template>
-<div class="mk-media-list">
-	<mk-media-image v-for="media in mediaList" ref="media" :image="media" :key="media.id"/>
+<div class="mk-media-list" :data-count="mediaList.length">
+	<template v-for="media in mediaList">
+		<mk-media-image :image="media" :key="media.id"/>
+	</template>
 </div>
 </template>
 
@@ -9,46 +11,6 @@ import Vue from 'vue';
 
 export default Vue.extend({
 	props: ['mediaList'],
-	mounted() {
-		const tags = this.$refs.media as Vue[];
-
-		if (this.mediaList.length == 1) {
-			(this.$el.style as any).gridTemplateRows = '1fr';
-
-			(tags[0].$el.style as any).gridColumn = '1 / 2';
-			(tags[0].$el.style as any).gridRow = '1 / 2';
-		} else if (this.mediaList.length == 2) {
-			(this.$el.style as any).gridTemplateColumns = '1fr 1fr';
-			(this.$el.style as any).gridTemplateRows = '1fr';
-
-			(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.mediaList.length == 3) {
-			(this.$el.style as any).gridTemplateColumns = '1fr 0.5fr';
-			(this.$el.style as any).gridTemplateRows = '1fr 1fr';
-
-			(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.mediaList.length == 4) {
-			(this.$el.style as any).gridTemplateColumns = '1fr 1fr';
-			(this.$el.style as any).gridTemplateRows = '1fr 1fr';
-
-			(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';
-		}
-	}
 });
 </script>
 
@@ -60,4 +22,35 @@ export default Vue.extend({
 
 	@media (max-width 500px)
 		height 192px
+	
+	&[data-count="1"]
+		grid-template-rows 1fr
+	&[data-count="2"]
+		grid-template-columns 1fr 1fr
+		grid-template-rows 1fr
+	&[data-count="3"]
+		grid-template-columns 1fr 0.5fr
+		grid-template-rows 1fr 1fr
+		:nth-child(1)
+			grid-row 1 / 3
+		:nth-child(3)
+			grid-column 2 / 3
+			grid-row 2/3
+	&[data-count="4"]
+		grid-template-columns 1fr 1fr
+		grid-template-rows 1fr 1fr
+	
+	:nth-child(1)
+		grid-column 1 / 2
+		grid-row 1 / 2
+	:nth-child(2)
+		grid-column 2 / 3
+		grid-row 1 / 2
+	:nth-child(3)
+		grid-column 1 / 2
+		grid-row 2 / 3
+	:nth-child(4)
+		grid-column 2 / 3
+		grid-row 2 / 3
+		
 </style>

From abb386141bdff23c29988a0c2408e30fd16020ab Mon Sep 17 00:00:00 2001
From: rinsuki <428rinsuki+git@gmail.com>
Date: Mon, 26 Mar 2018 17:33:29 +0900
Subject: [PATCH 0860/1250] add mobile.tags.mk-user-timeline.load-more
 translation

---
 locales/en.yml | 1 +
 locales/ja.yml | 1 +
 2 files changed, 2 insertions(+)

diff --git a/locales/en.yml b/locales/en.yml
index e55984677..2cc857f69 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -654,6 +654,7 @@ mobile:
     mk-user-timeline:
       no-posts: "This user seems never post"
       no-posts-with-media: "There is no posts with media"
+      load-more: "More"
 
     mk-user:
       follows-you: "Follows you"
diff --git a/locales/ja.yml b/locales/ja.yml
index b60e9e3f4..86b594990 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -654,6 +654,7 @@ mobile:
     mk-user-timeline:
       no-posts: "このユーザーはまだ投稿していないようです。"
       no-posts-with-media: "メディア付き投稿はありません。"
+      load-more: "もっとみる"
 
     mk-user:
       follows-you: "フォローされています"

From dfdb5647a1e0b9b22fa009ce2cc4f7a6a11affc9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 26 Mar 2018 17:54:10 +0900
Subject: [PATCH 0861/1250] Implement packForAp

---
 src/api/models/user.ts | 51 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 51 insertions(+)

diff --git a/src/api/models/user.ts b/src/api/models/user.ts
index 372e2c5da..545747b50 100644
--- a/src/api/models/user.ts
+++ b/src/api/models/user.ts
@@ -262,6 +262,57 @@ export const pack = (
 	resolve(_user);
 });
 
+/**
+ * Pack a user for ActivityPub
+ *
+ * @param user target
+ * @return Packed user
+ */
+export const packForAp = (
+	user: string | mongo.ObjectID | IUser
+) => new Promise<any>(async (resolve, reject) => {
+
+	let _user: any;
+
+	const fields = {
+		// something
+	};
+
+	// Populate the user if 'user' is ID
+	if (mongo.ObjectID.prototype.isPrototypeOf(user)) {
+		_user = await User.findOne({
+			_id: user
+		}, { fields });
+	} else if (typeof user === 'string') {
+		_user = await User.findOne({
+			_id: new mongo.ObjectID(user)
+		}, { fields });
+	} else {
+		_user = deepcopy(user);
+	}
+
+	if (!_user) return reject('invalid user arg.');
+
+	resolve({
+		"@context": ["https://www.w3.org/ns/activitystreams", {
+			"@language": "ja"
+		}],
+		"type": "Person",
+		"id": `${config.url}/${_user.username}`,
+		"following": `${config.url}/${_user.username}/following.json`,
+		"followers": `${config.url}/${_user.username}/followers.json`,
+		"liked": `${config.url}/${_user.username}/liked.json`,
+		"inbox": `${config.url}/${_user.username}/inbox.json`,
+		"outbox": `${config.url}/${_user.username}/feed.json`,
+		"preferredUsername": _user.username,
+		"name": _user.name,
+		"summary": _user.description,
+		"icon": [
+			`${config.drive_url}/${_user.avatar_id}`
+		]
+	});
+});
+
 /*
 function img(url) {
 	return {

From 61e9bdba52c773b243aebc8abdd1e31206e2c302 Mon Sep 17 00:00:00 2001
From: rinsuki <428rinsuki+git@gmail.com>
Date: Mon, 26 Mar 2018 21:54:38 +0900
Subject: [PATCH 0862/1250] implement mk-media-video

---
 .../common/views/components/media-list.vue    |  3 +-
 src/web/app/desktop/views/components/index.ts |  2 +
 .../views/components/media-video-dialog.vue   | 70 +++++++++++++++++++
 .../desktop/views/components/media-video.vue  | 67 ++++++++++++++++++
 4 files changed, 141 insertions(+), 1 deletion(-)
 create mode 100644 src/web/app/desktop/views/components/media-video-dialog.vue
 create mode 100644 src/web/app/desktop/views/components/media-video.vue

diff --git a/src/web/app/common/views/components/media-list.vue b/src/web/app/common/views/components/media-list.vue
index d0da584a4..64172ad0b 100644
--- a/src/web/app/common/views/components/media-list.vue
+++ b/src/web/app/common/views/components/media-list.vue
@@ -1,7 +1,8 @@
 <template>
 <div class="mk-media-list" :data-count="mediaList.length">
 	<template v-for="media in mediaList">
-		<mk-media-image :image="media" :key="media.id"/>
+		<mk-media-video :video="media" :key="media.id" v-if="media.type.startsWith('video')" :inline-playable="mediaList.length === 1"/>
+		<mk-media-image :image="media" :key="media.id" v-else />
 	</template>
 </div>
 </template>
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index 9bca603a5..3798bf6d2 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -13,6 +13,7 @@ import analogClock from './analog-clock.vue';
 import ellipsisIcon from './ellipsis-icon.vue';
 import mediaImage from './media-image.vue';
 import mediaImageDialog from './media-image-dialog.vue';
+import mediaVideo from './media-video.vue';
 import notifications from './notifications.vue';
 import postForm from './post-form.vue';
 import repostForm from './repost-form.vue';
@@ -42,6 +43,7 @@ Vue.component('mk-analog-clock', analogClock);
 Vue.component('mk-ellipsis-icon', ellipsisIcon);
 Vue.component('mk-media-image', mediaImage);
 Vue.component('mk-media-image-dialog', mediaImageDialog);
+Vue.component('mk-media-video', mediaVideo);
 Vue.component('mk-notifications', notifications);
 Vue.component('mk-post-form', postForm);
 Vue.component('mk-repost-form', repostForm);
diff --git a/src/web/app/desktop/views/components/media-video-dialog.vue b/src/web/app/desktop/views/components/media-video-dialog.vue
new file mode 100644
index 000000000..cbf862cd1
--- /dev/null
+++ b/src/web/app/desktop/views/components/media-video-dialog.vue
@@ -0,0 +1,70 @@
+<template>
+<div class="mk-media-video-dialog">
+	<div class="bg" @click="close"></div>
+	<video :src="video.url" :title="video.name" controls autoplay ref="video"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as anime from 'animejs';
+
+export default Vue.extend({
+	props: ['video', 'start'],
+	mounted() {
+		anime({
+			targets: this.$el,
+			opacity: 1,
+			duration: 100,
+			easing: 'linear'
+		});
+		const videoTag = this.$refs.video as HTMLVideoElement
+		if (this.start) videoTag.currentTime = this.start
+	},
+	methods: {
+		close() {
+			anime({
+				targets: this.$el,
+				opacity: 0,
+				duration: 100,
+				easing: 'linear',
+				complete: () => this.$destroy()
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-media-video-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)
+
+	> video
+		position fixed
+		z-index 2
+		top 0
+		right 0
+		bottom 0
+		left 0
+		max-width 80vw
+		max-height 80vh
+		margin auto
+
+</style>
diff --git a/src/web/app/desktop/views/components/media-video.vue b/src/web/app/desktop/views/components/media-video.vue
new file mode 100644
index 000000000..4fd955a82
--- /dev/null
+++ b/src/web/app/desktop/views/components/media-video.vue
@@ -0,0 +1,67 @@
+<template>
+	<video class="mk-media-video"
+		:src="video.url"
+		:title="video.name"
+		controls
+		@dblclick.prevent="onClick"
+		ref="video"
+		v-if="inlinePlayable" />
+	<a class="mk-media-video-thumbnail"
+		:href="video.url"
+		:style="imageStyle"
+		@click.prevent="onClick"
+		:title="video.name"
+		v-else>
+		%fa:R play-circle%
+	</a>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import MkMediaVideoDialog from './media-video-dialog.vue';
+
+export default Vue.extend({
+	props: ['video', 'inlinePlayable'],
+	computed: {
+		imageStyle(): any {
+			return {
+				'background-image': `url(${this.video.url}?thumbnail&size=512)`
+			};
+		}
+	},
+	methods: {
+		onClick() {
+			const videoTag = this.$refs.video as (HTMLVideoElement | null)
+			var start = 0
+			if (videoTag) {
+				start = videoTag.currentTime
+				videoTag.pause()
+			}
+			(this as any).os.new(MkMediaVideoDialog, {
+				video: this.video,
+				start,
+			})
+		}
+	}
+})
+</script>
+
+<style lang="stylus" scoped>
+.mk-media-video
+	display block
+	width 100%
+	height 100%
+	border-radius 4px
+.mk-media-video-thumbnail
+	display flex
+	justify-content center
+	align-items center
+	font-size 3.5em
+
+	cursor zoom-in
+	overflow hidden
+	background-position center
+	background-size cover
+	width 100%
+	height 100%
+</style>

From 677a21320860bc947d55b309dc3c3afa82e20132 Mon Sep 17 00:00:00 2001
From: rinsuki <428rinsuki+git@gmail.com>
Date: Mon, 26 Mar 2018 22:04:34 +0900
Subject: [PATCH 0863/1250] mobile version of media-video

---
 src/web/app/mobile/views/components/index.ts  |  2 ++
 .../mobile/views/components/media-video.vue   | 36 +++++++++++++++++++
 2 files changed, 38 insertions(+)
 create mode 100644 src/web/app/mobile/views/components/media-video.vue

diff --git a/src/web/app/mobile/views/components/index.ts b/src/web/app/mobile/views/components/index.ts
index 4743f50e0..fb8f65f47 100644
--- a/src/web/app/mobile/views/components/index.ts
+++ b/src/web/app/mobile/views/components/index.ts
@@ -5,6 +5,7 @@ import timeline from './timeline.vue';
 import post from './post.vue';
 import posts from './posts.vue';
 import mediaImage from './media-image.vue';
+import mediaVideo from './media-video.vue';
 import drive from './drive.vue';
 import postPreview from './post-preview.vue';
 import subPostContent from './sub-post-content.vue';
@@ -27,6 +28,7 @@ Vue.component('mk-timeline', timeline);
 Vue.component('mk-post', post);
 Vue.component('mk-posts', posts);
 Vue.component('mk-media-image', mediaImage);
+Vue.component('mk-media-video', mediaVideo);
 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/components/media-video.vue b/src/web/app/mobile/views/components/media-video.vue
new file mode 100644
index 000000000..68cd48587
--- /dev/null
+++ b/src/web/app/mobile/views/components/media-video.vue
@@ -0,0 +1,36 @@
+<template>
+	<a class="mk-media-video"
+		:href="video.url"
+		target="_blank"
+		:style="imageStyle"
+		:title="video.name">
+		%fa:R play-circle%
+	</a>
+</template>
+
+<script lang="ts">
+import Vue from 'vue'
+export default Vue.extend({
+	props: ['video'],
+	computed: {
+		imageStyle(): any {
+			return {
+				'background-image': `url(${this.video.url}?thumbnail&size=512)`
+			};
+		}
+	},})
+</script>
+
+<style lang="stylus" scoped>
+.mk-media-video
+	display flex
+	justify-content center
+	align-items center
+
+	font-size 3.5em
+	overflow hidden
+	background-position center
+	background-size cover
+	width 100%
+	height 100%
+</style>

From bfe6fc9ebb00ab4c6b354c92b7ad3fa17a2c98ef Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Mon, 26 Mar 2018 20:23:55 +0900
Subject: [PATCH 0864/1250] Add keypair to local account

---
 .gitignore                                    |   1 +
 binding.gyp                                   |   9 ++
 gulpfile.ts                                   |   1 +
 package.json                                  |   1 +
 src/api/models/user.ts                        |   2 +
 src/api/private/signup.ts                     |   2 +
 src/crypto_key.cc                             | 111 ++++++++++++++++++
 src/crypto_key.d.ts                           |   1 +
 test/api.js                                   |   2 +
 .../node.1522066477.user-account-keypair.js   |  16 +++
 10 files changed, 146 insertions(+)
 create mode 100644 binding.gyp
 create mode 100644 src/crypto_key.cc
 create mode 100644 src/crypto_key.d.ts
 create mode 100644 tools/migration/node.1522066477.user-account-keypair.js

diff --git a/.gitignore b/.gitignore
index 6c8b99c85..d0ae0b808 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
 /.config
 /.vscode
 /node_modules
+/build
 /built
 /data
 npm-debug.log
diff --git a/binding.gyp b/binding.gyp
new file mode 100644
index 000000000..0349526d5
--- /dev/null
+++ b/binding.gyp
@@ -0,0 +1,9 @@
+{
+	'targets': [
+		{
+			'target_name': 'crypto_key',
+			'sources': ['src/crypto_key.cc'],
+			'include_dirs': ['<!(node -e "require(\'nan\')")']
+		}
+	]
+}
diff --git a/gulpfile.ts b/gulpfile.ts
index c10d0a98d..9c61e3a1c 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -69,6 +69,7 @@ gulp.task('build:ts', () => {
 
 gulp.task('build:copy', () =>
 	gulp.src([
+		'./build/Release/crypto_key.node',
 		'./src/**/assets/**/*',
 		'!./src/web/app/**/assets/**/*'
 	]).pipe(gulp.dest('./built/'))
diff --git a/package.json b/package.json
index 3ec1620dd..eee658fbd 100644
--- a/package.json
+++ b/package.json
@@ -145,6 +145,7 @@
 		"morgan": "1.9.0",
 		"ms": "2.1.1",
 		"multer": "1.3.0",
+		"nan": "^2.10.0",
 		"node-sass": "4.7.2",
 		"node-sass-json-importer": "3.1.5",
 		"nprogress": "0.2.0",
diff --git a/src/api/models/user.ts b/src/api/models/user.ts
index 545747b50..042f13b23 100644
--- a/src/api/models/user.ts
+++ b/src/api/models/user.ts
@@ -59,6 +59,7 @@ export type IUser = {
 	is_suspended: boolean;
 	keywords: string[];
 	account: {
+		keypair: string;
 		email: string;
 		links: string[];
 		password: string;
@@ -160,6 +161,7 @@ export const pack = (
 	delete _user.latest_post;
 
 	// Remove private properties
+	delete _user.account.keypair;
 	delete _user.account.password;
 	delete _user.account.token;
 	delete _user.account.two_factor_temp_secret;
diff --git a/src/api/private/signup.ts b/src/api/private/signup.ts
index 902642425..690f3001c 100644
--- a/src/api/private/signup.ts
+++ b/src/api/private/signup.ts
@@ -1,6 +1,7 @@
 import * as uuid from 'uuid';
 import * as express from 'express';
 import * as bcrypt from 'bcryptjs';
+import { generate as generateKeypair } from '../../crypto_key';
 import recaptcha = require('recaptcha-promise');
 import User, { IUser, validateUsername, validatePassword, pack } from '../models/user';
 import generateUserToken from '../common/generate-native-user-token';
@@ -119,6 +120,7 @@ export default async (req: express.Request, res: express.Response) => {
 		username: username,
 		username_lower: username.toLowerCase(),
 		account: {
+			keypair: generateKeypair(),
 			token: secret,
 			email: null,
 			links: null,
diff --git a/src/crypto_key.cc b/src/crypto_key.cc
new file mode 100644
index 000000000..c8e4d8f7f
--- /dev/null
+++ b/src/crypto_key.cc
@@ -0,0 +1,111 @@
+#include <nan.h>
+#include <openssl/bio.h>
+#include <openssl/buffer.h>
+#include <openssl/crypto.h>
+#include <openssl/pem.h>
+#include <openssl/rsa.h>
+#include <openssl/x509.h>
+
+NAN_METHOD(extractPublic)
+{
+	const auto sourceString = info[0]->ToString();
+	if (!sourceString->IsOneByte()) {
+		Nan::ThrowError("Malformed character found");
+		return;
+	}
+
+	size_t sourceLength = sourceString->Length();
+	const auto sourceBuf = new char[sourceLength];
+
+	Nan::DecodeWrite(sourceBuf, sourceLength, sourceString);
+
+	const auto source = BIO_new_mem_buf(sourceBuf, sourceLength);
+	if (source == nullptr) {
+		Nan::ThrowError("Memory allocation failed");
+		delete sourceBuf;
+		return;
+	}
+
+	const auto rsa = PEM_read_bio_RSAPrivateKey(source, nullptr, nullptr, nullptr);
+
+	BIO_free(source);
+	delete sourceBuf;
+
+	if (rsa == nullptr) {
+		Nan::ThrowError("Decode failed");
+		return;
+	}
+
+	const auto destination = BIO_new(BIO_s_mem());
+	if (destination == nullptr) {
+		Nan::ThrowError("Memory allocation failed");
+		return;
+	}
+
+	const auto result = PEM_write_bio_RSAPublicKey(destination, rsa);
+
+	RSA_free(rsa);
+
+	if (result != 1) {
+		Nan::ThrowError("Public key extraction failed");
+		BIO_free(destination);
+		return;
+	}
+
+	char *pem;
+	const auto pemLength = BIO_get_mem_data(destination, &pem);
+
+	info.GetReturnValue().Set(Nan::Encode(pem, pemLength));
+	BIO_free(destination);
+}
+
+NAN_METHOD(generate)
+{
+	const auto exponent = BN_new();
+	const auto mem = BIO_new(BIO_s_mem());
+	const auto rsa = RSA_new();
+	char *data;
+	long result;
+
+	if (exponent == nullptr || mem == nullptr || rsa == nullptr) {
+		Nan::ThrowError("Memory allocation failed");
+		goto done;
+	}
+
+	result = BN_set_word(exponent, 65537);
+	if (result != 1) {
+		Nan::ThrowError("Exponent setting failed");
+		goto done;
+	}
+
+	result = RSA_generate_key_ex(rsa, 2048, exponent, nullptr);
+	if (result != 1) {
+		Nan::ThrowError("Key generation failed");
+		goto done;
+	}
+
+	result = PEM_write_bio_RSAPrivateKey(mem, rsa, NULL, NULL, 0, NULL, NULL);
+	if (result != 1) {
+		Nan::ThrowError("Key export failed");
+		goto done;
+	}
+
+	result = BIO_get_mem_data(mem, &data);
+	info.GetReturnValue().Set(Nan::Encode(data, result));
+
+done:
+	RSA_free(rsa);
+	BIO_free(mem);
+	BN_free(exponent);
+}
+
+NAN_MODULE_INIT(InitAll)
+{
+	Nan::Set(target, Nan::New<v8::String>("extractPublic").ToLocalChecked(),
+		Nan::GetFunction(Nan::New<v8::FunctionTemplate>(extractPublic)).ToLocalChecked());
+
+	Nan::Set(target, Nan::New<v8::String>("generate").ToLocalChecked(),
+		Nan::GetFunction(Nan::New<v8::FunctionTemplate>(generate)).ToLocalChecked());
+}
+
+NODE_MODULE(crypto_key, InitAll);
diff --git a/src/crypto_key.d.ts b/src/crypto_key.d.ts
new file mode 100644
index 000000000..28ac2f968
--- /dev/null
+++ b/src/crypto_key.d.ts
@@ -0,0 +1 @@
+export function generate(): String;
diff --git a/test/api.js b/test/api.js
index 9e55dd991..b8b2aecc9 100644
--- a/test/api.js
+++ b/test/api.js
@@ -1161,6 +1161,7 @@ function insertSakurako(opts) {
 		username: 'sakurako',
 		username_lower: 'sakurako',
 		account: {
+			keypair: '-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAtdTG9rlFWjNqhgbg2V6X5XF1WpQXZS3KNXykEWl2UAiMyfVV\nBvf3zQP0dDEdNtcqdPJgis03bpiHCzQusc/YLyHYB0m+TJXsxJatb8cqUogOFeE4\ngQ4Dc5kAT6gLh/d4yz03EIg9bizX07EiGWnZqWxb+21ypqsPxST64sAtG9f5O/G4\nXe2m3cSbfAAvEUP1Ig1LUNyJB4jhM60w1cQic/qO8++sk/+GoX9g71X+i4NArGv+\n1c11acDIIPGAAQpFeYVeGaKakNDNp8RtJJp8R8FLwJXZ4/gATBnScCiHUSrGfRly\nYyR0w/BNlQ6/NijAdB9pR5csPvyIPkx1gauZewIDAQABAoIBAQCwWf/mhuY2h6uG\n9eDZsZ7Mj2/sO7k9Dl4R5iMSKCDxmnlB3slqitExa+aJUqEs8R5icjkkJcjfYNuJ\nCEFJf3YCsGZfGyyQBtCuEh2ATcBEb2SJ3/f3YuoCEaB1oVwdsOzc4TAovpol4yQo\nUqHp1/mdElVb01jhQQN4h1c02IJnfzvfU1C8szBni+Etfd+MxqGfv006DY3KOEb3\nlCrCS3GmooJW2Fjj7q1kCcaEQbMB1/aQHLXd1qe3KJOzXh3Voxsp/jEH0hvp2TII\nfY9UK+b7mA+xlvXwKuTkHVaZm0ylg0nbembS8MF4GfFMujinSexvLrVKaQhdMFoF\nvBLxHYHRAoGBANfNVYJYeCDPFNLmak5Xg33Rfvc2II8UmrZOVdhOWs8ZK0pis9e+\nPo2MKtTzrzipXI2QXv5w7kO+LJWNDva+xRlW8Wlj9Dde9QdQ7Y8+dk7SJgf24DzM\n023elgX5DvTeLODjStk6SMPRL0FmGovUqAAA8ZeHtJzkIr1HROWnQiwnAoGBANez\nhFwKnVoQu0RpBz/i4W0RKIxOwltN2zmlN8KjJPhSy00A7nBUfKLRbcwiSHE98Yi/\nUrXwMwR5QeD2ngngRppddJnpiRfjNjnsaqeqNtpO8AxB3XjpCC5zmHUMFHKvPpDj\n1zU/F44li0YjKcMBebZy9PbfAjrIgJfxhPo/oXiNAoGAfx6gaTjOAp2ZaaZ7Jozc\nkyft/5et1DrR6+P3I4T8bxQncRj1UXfqhxzzOiAVrm3tbCKIIp/JarRCtRGzp9u2\nZPfXGzra6CcSdW3Rkli7/jBCYNynOIl7XjQI8ZnFmq6phwu80ntH07mMeZy4tHff\nQqlLpvQ0i1rDr/Wkexdsnm8CgYBgxha9ILoF/Xm3MJPjEsxmnYsen/tM8XpIu5pv\nxbhBfQvfKWrQlOcyOVnUexEbVVo3KvdVz0VkXW60GpE/BxNGEGXO49rxD6x1gl87\nh/+CJGZIaYiOxaY5CP2+jcPizEL6yG32Yq8TxD5fIkmLRu8vbxX+aIFclDY1dVNe\n3wt3xQKBgGEL0EjwRch+P2V+YHAhbETPrEqJjHRWT95pIdF9XtC8fasSOVH81cLX\nXXsX1FTvOJNwG9Nk8rQjYJXGTb2O/2unaazlYUwxKwVpwuGzz/vhH/roHZBAkIVT\njvpykpn9QMezEdpzj5BEv01QzSYBPzIh5myrpoJIoSW7py7zFG3h\n-----END RSA PRIVATE KEY-----\n',
 			token: '!00000000000000000000000000000000',
 			password: '$2a$08$FnHXg3tP.M/kINWgQSXNqeoBsiVrkj.ecXX8mW9rfBzMRkibYfjYy', // HimawariDaisuki06160907
 			profile: {},
@@ -1175,6 +1176,7 @@ function insertHimawari(opts) {
 		username: 'himawari',
 		username_lower: 'himawari',
 		account: {
+			keypair: '-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAtdTG9rlFWjNqhgbg2V6X5XF1WpQXZS3KNXykEWl2UAiMyfVV\nBvf3zQP0dDEdNtcqdPJgis03bpiHCzQusc/YLyHYB0m+TJXsxJatb8cqUogOFeE4\ngQ4Dc5kAT6gLh/d4yz03EIg9bizX07EiGWnZqWxb+21ypqsPxST64sAtG9f5O/G4\nXe2m3cSbfAAvEUP1Ig1LUNyJB4jhM60w1cQic/qO8++sk/+GoX9g71X+i4NArGv+\n1c11acDIIPGAAQpFeYVeGaKakNDNp8RtJJp8R8FLwJXZ4/gATBnScCiHUSrGfRly\nYyR0w/BNlQ6/NijAdB9pR5csPvyIPkx1gauZewIDAQABAoIBAQCwWf/mhuY2h6uG\n9eDZsZ7Mj2/sO7k9Dl4R5iMSKCDxmnlB3slqitExa+aJUqEs8R5icjkkJcjfYNuJ\nCEFJf3YCsGZfGyyQBtCuEh2ATcBEb2SJ3/f3YuoCEaB1oVwdsOzc4TAovpol4yQo\nUqHp1/mdElVb01jhQQN4h1c02IJnfzvfU1C8szBni+Etfd+MxqGfv006DY3KOEb3\nlCrCS3GmooJW2Fjj7q1kCcaEQbMB1/aQHLXd1qe3KJOzXh3Voxsp/jEH0hvp2TII\nfY9UK+b7mA+xlvXwKuTkHVaZm0ylg0nbembS8MF4GfFMujinSexvLrVKaQhdMFoF\nvBLxHYHRAoGBANfNVYJYeCDPFNLmak5Xg33Rfvc2II8UmrZOVdhOWs8ZK0pis9e+\nPo2MKtTzrzipXI2QXv5w7kO+LJWNDva+xRlW8Wlj9Dde9QdQ7Y8+dk7SJgf24DzM\n023elgX5DvTeLODjStk6SMPRL0FmGovUqAAA8ZeHtJzkIr1HROWnQiwnAoGBANez\nhFwKnVoQu0RpBz/i4W0RKIxOwltN2zmlN8KjJPhSy00A7nBUfKLRbcwiSHE98Yi/\nUrXwMwR5QeD2ngngRppddJnpiRfjNjnsaqeqNtpO8AxB3XjpCC5zmHUMFHKvPpDj\n1zU/F44li0YjKcMBebZy9PbfAjrIgJfxhPo/oXiNAoGAfx6gaTjOAp2ZaaZ7Jozc\nkyft/5et1DrR6+P3I4T8bxQncRj1UXfqhxzzOiAVrm3tbCKIIp/JarRCtRGzp9u2\nZPfXGzra6CcSdW3Rkli7/jBCYNynOIl7XjQI8ZnFmq6phwu80ntH07mMeZy4tHff\nQqlLpvQ0i1rDr/Wkexdsnm8CgYBgxha9ILoF/Xm3MJPjEsxmnYsen/tM8XpIu5pv\nxbhBfQvfKWrQlOcyOVnUexEbVVo3KvdVz0VkXW60GpE/BxNGEGXO49rxD6x1gl87\nh/+CJGZIaYiOxaY5CP2+jcPizEL6yG32Yq8TxD5fIkmLRu8vbxX+aIFclDY1dVNe\n3wt3xQKBgGEL0EjwRch+P2V+YHAhbETPrEqJjHRWT95pIdF9XtC8fasSOVH81cLX\nXXsX1FTvOJNwG9Nk8rQjYJXGTb2O/2unaazlYUwxKwVpwuGzz/vhH/roHZBAkIVT\njvpykpn9QMezEdpzj5BEv01QzSYBPzIh5myrpoJIoSW7py7zFG3h\n-----END RSA PRIVATE KEY-----\n',
 			token: '!00000000000000000000000000000001',
 			password: '$2a$08$OPESxR2RE/ZijjGanNKk6ezSqGFitqsbZqTjWUZPLhORMKxHCbc4O', // ilovesakurako
 			profile: {},
diff --git a/tools/migration/node.1522066477.user-account-keypair.js b/tools/migration/node.1522066477.user-account-keypair.js
new file mode 100644
index 000000000..4a968aae2
--- /dev/null
+++ b/tools/migration/node.1522066477.user-account-keypair.js
@@ -0,0 +1,16 @@
+const { default: User } = require('../../built/api/models/user');
+const { generate } = require('../../built/crypto_key');
+
+const updates = [];
+
+User.find({}).each(function(user) {
+	updates.push(User.update({ _id: user._id }, {
+		$set: {
+			account: {
+				keypair: generate(),
+			}
+		}
+	}));
+}).then(function () {
+	Promise.all(updates)
+}).then(process.exit);

From ad515f1cbaea29380a14bfc9a9907f7431b81302 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 27 Mar 2018 11:47:13 +0900
Subject: [PATCH 0865/1250] Refactor

---
 src/api/models/user.ts | 14 ++++++++------
 1 file changed, 8 insertions(+), 6 deletions(-)

diff --git a/src/api/models/user.ts b/src/api/models/user.ts
index 545747b50..3f0b10d4c 100644
--- a/src/api/models/user.ts
+++ b/src/api/models/user.ts
@@ -293,17 +293,19 @@ export const packForAp = (
 
 	if (!_user) return reject('invalid user arg.');
 
+	const userUrl = `${config.url}/${_user.username}`;
+
 	resolve({
 		"@context": ["https://www.w3.org/ns/activitystreams", {
 			"@language": "ja"
 		}],
 		"type": "Person",
-		"id": `${config.url}/${_user.username}`,
-		"following": `${config.url}/${_user.username}/following.json`,
-		"followers": `${config.url}/${_user.username}/followers.json`,
-		"liked": `${config.url}/${_user.username}/liked.json`,
-		"inbox": `${config.url}/${_user.username}/inbox.json`,
-		"outbox": `${config.url}/${_user.username}/feed.json`,
+		"id": userUrl,
+		"following": `${userUrl}/following.json`,
+		"followers": `${userUrl}/followers.json`,
+		"liked": `${userUrl}/liked.json`,
+		"inbox": `${userUrl}/inbox.json`,
+		"outbox": `${userUrl}/outbox.json`,
 		"preferredUsername": _user.username,
 		"name": _user.name,
 		"summary": _user.description,

From 27ca691543acfeb89e3e1c7de257cfa3213c8f68 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Tue, 27 Mar 2018 12:02:43 +0900
Subject: [PATCH 0866/1250] Add host field to User

---
 src/api/models/user.ts                        | 1 +
 src/api/private/signup.ts                     | 1 +
 tools/migration/shell.1522116709.user-host.js | 1 +
 3 files changed, 3 insertions(+)
 create mode 100644 tools/migration/shell.1522116709.user-host.js

diff --git a/src/api/models/user.ts b/src/api/models/user.ts
index 042f13b23..7c4b993e9 100644
--- a/src/api/models/user.ts
+++ b/src/api/models/user.ts
@@ -58,6 +58,7 @@ export type IUser = {
 	pinned_post_id: mongo.ObjectID;
 	is_suspended: boolean;
 	keywords: string[];
+	host: string;
 	account: {
 		keypair: string;
 		email: string;
diff --git a/src/api/private/signup.ts b/src/api/private/signup.ts
index 690f3001c..a4c06b5f5 100644
--- a/src/api/private/signup.ts
+++ b/src/api/private/signup.ts
@@ -119,6 +119,7 @@ export default async (req: express.Request, res: express.Response) => {
 		drive_capacity: 1073741824, // 1GB
 		username: username,
 		username_lower: username.toLowerCase(),
+		host: null,
 		account: {
 			keypair: generateKeypair(),
 			token: secret,
diff --git a/tools/migration/shell.1522116709.user-host.js b/tools/migration/shell.1522116709.user-host.js
new file mode 100644
index 000000000..b354709a6
--- /dev/null
+++ b/tools/migration/shell.1522116709.user-host.js
@@ -0,0 +1 @@
+db.users.update({ }, { $set: { host: null } }, { multi: true });

From 3eb659b649535efb42e9401a7bfaff7526f84327 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Tue, 27 Mar 2018 12:18:22 +0900
Subject: [PATCH 0867/1250] Fix keypair assignment in a migration script

---
 tools/migration/node.1522066477.user-account-keypair.js | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/tools/migration/node.1522066477.user-account-keypair.js b/tools/migration/node.1522066477.user-account-keypair.js
index 4a968aae2..effea137c 100644
--- a/tools/migration/node.1522066477.user-account-keypair.js
+++ b/tools/migration/node.1522066477.user-account-keypair.js
@@ -6,9 +6,7 @@ const updates = [];
 User.find({}).each(function(user) {
 	updates.push(User.update({ _id: user._id }, {
 		$set: {
-			account: {
-				keypair: generate(),
-			}
+			'account.keypair': generate(),
 		}
 	}));
 }).then(function () {

From f06e5d1c61c5e75120e1bd1594324ec094722006 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 27 Mar 2018 12:33:51 +0900
Subject: [PATCH 0868/1250] Fix bug

---
 .../node.1522066477.user-account-keypair.js   | 43 +++++++++++++++----
 1 file changed, 34 insertions(+), 9 deletions(-)

diff --git a/tools/migration/node.1522066477.user-account-keypair.js b/tools/migration/node.1522066477.user-account-keypair.js
index effea137c..c413e3db1 100644
--- a/tools/migration/node.1522066477.user-account-keypair.js
+++ b/tools/migration/node.1522066477.user-account-keypair.js
@@ -1,14 +1,39 @@
+// for Node.js interpret
+
 const { default: User } = require('../../built/api/models/user');
 const { generate } = require('../../built/crypto_key');
+const { default: zip } = require('@prezzemolo/zip')
 
-const updates = [];
-
-User.find({}).each(function(user) {
-	updates.push(User.update({ _id: user._id }, {
+const migrate = async (user) => {
+	const result = await User.update(user._id, {
 		$set: {
-			'account.keypair': generate(),
+			'account.keypair': generate()
 		}
-	}));
-}).then(function () {
-	Promise.all(updates)
-}).then(process.exit);
+	});
+	return result.ok === 1;
+}
+
+async function main() {
+	const count = await User.count({});
+
+	const dop = Number.parseInt(process.argv[2]) || 5
+	const idop = ((count - (count % dop)) / dop) + 1
+
+	return zip(
+		1,
+		async (time) => {
+			console.log(`${time} / ${idop}`)
+			const doc = await User.find({}, {
+				limit: dop, skip: time * dop
+			})
+			return Promise.all(doc.map(migrate))
+		},
+		idop
+	).then(a => {
+		const rv = []
+		a.forEach(e => rv.push(...e))
+		return rv
+	})
+}
+
+main().then(console.dir).catch(console.error)

From 3fe42336b324fc13b50fd3475c887143bf6f6700 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 27 Mar 2018 12:45:18 +0900
Subject: [PATCH 0869/1250] Destory api. subdomain

api.example.com --> example.com/api
---
 src/config.ts                                  | 4 ++--
 src/server.ts                                  | 2 +-
 src/web/app/common/scripts/streaming/stream.ts | 5 ++---
 src/web/app/config.ts                          | 2 ++
 webpack.config.ts                              | 1 +
 5 files changed, 8 insertions(+), 6 deletions(-)

diff --git a/src/config.ts b/src/config.ts
index 42dfd5f54..406183c91 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -140,11 +140,11 @@ export default function load() {
 	mixin.hostname = url.hostname;
 	mixin.scheme = url.protocol.replace(/:$/, '');
 	mixin.ws_scheme = mixin.scheme.replace('http', 'ws');
-	mixin.ws_url = `${mixin.ws_scheme}://api.${mixin.host}`;
+	mixin.ws_url = `${mixin.ws_scheme}://${mixin.host}`;
 	mixin.secondary_host = config.secondary_url.substr(config.secondary_url.indexOf('://') + 3);
 	mixin.secondary_hostname = secondaryUrl.hostname;
 	mixin.secondary_scheme = config.secondary_url.substr(0, config.secondary_url.indexOf('://'));
-	mixin.api_url = `${mixin.scheme}://api.${mixin.host}`;
+	mixin.api_url = `${mixin.scheme}://${mixin.host}/api`;
 	mixin.auth_url = `${mixin.scheme}://auth.${mixin.host}`;
 	mixin.ch_url = `${mixin.scheme}://ch.${mixin.host}`;
 	mixin.dev_url = `${mixin.scheme}://dev.${mixin.host}`;
diff --git a/src/server.ts b/src/server.ts
index 7f66c4207..fb581bae5 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -53,7 +53,7 @@ app.use((req, res, next) => {
 /**
  * Register modules
  */
-app.use(vhost(`api.${config.hostname}`, require('./api/server')));
+app.use('/api', require('./api/server'));
 app.use(vhost(config.secondary_hostname, require('./himasaku/server')));
 app.use(vhost(`file.${config.secondary_hostname}`, require('./file/server')));
 app.use(require('./web/server'));
diff --git a/src/web/app/common/scripts/streaming/stream.ts b/src/web/app/common/scripts/streaming/stream.ts
index cb4041fd8..3912186ad 100644
--- a/src/web/app/common/scripts/streaming/stream.ts
+++ b/src/web/app/common/scripts/streaming/stream.ts
@@ -1,7 +1,7 @@
 import { EventEmitter } from 'eventemitter3';
 import * as uuid from 'uuid';
 import * as ReconnectingWebsocket from 'reconnecting-websocket';
-import { apiUrl } from '../../../config';
+import { wsUrl } from '../../../config';
 import MiOS from '../../mios';
 
 /**
@@ -42,14 +42,13 @@ export default class Connection extends EventEmitter {
 		this.state = 'initializing';
 		this.buffer = [];
 
-		const host = apiUrl.replace('http', 'ws');
 		const query = params
 			? Object.keys(params)
 				.map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
 				.join('&')
 			: null;
 
-		this.socket = new ReconnectingWebsocket(`${host}/${endpoint}${query ? '?' + query : ''}`);
+		this.socket = new ReconnectingWebsocket(`${wsUrl}/${endpoint}${query ? '?' + query : ''}`);
 		this.socket.addEventListener('open', this.onOpen);
 		this.socket.addEventListener('close', this.onClose);
 		this.socket.addEventListener('message', this.onMessage);
diff --git a/src/web/app/config.ts b/src/web/app/config.ts
index 32710dd9c..8ea6f7010 100644
--- a/src/web/app/config.ts
+++ b/src/web/app/config.ts
@@ -2,6 +2,7 @@ declare const _HOST_: string;
 declare const _HOSTNAME_: string;
 declare const _URL_: string;
 declare const _API_URL_: string;
+declare const _WS_URL_: string;
 declare const _DOCS_URL_: string;
 declare const _STATS_URL_: string;
 declare const _STATUS_URL_: string;
@@ -20,6 +21,7 @@ export const host = _HOST_;
 export const hostname = _HOSTNAME_;
 export const url = _URL_;
 export const apiUrl = _API_URL_;
+export const wsUrl = _WS_URL_;
 export const docsUrl = _DOCS_URL_;
 export const statsUrl = _STATS_URL_;
 export const statusUrl = _STATUS_URL_;
diff --git a/webpack.config.ts b/webpack.config.ts
index f7e7ae39c..9a952c8ef 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -80,6 +80,7 @@ module.exports = entries.map(x => {
 		_STATS_URL_: config.stats_url,
 		_DOCS_URL_: config.docs_url,
 		_API_URL_: config.api_url,
+		_WS_URL_: config.ws_url,
 		_DEV_URL_: config.dev_url,
 		_CH_URL_: config.ch_url,
 		_LANG_: lang,

From b76e29ecdd2d18b841c9ae2e97033ea7c7183863 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 27 Mar 2018 12:53:34 +0900
Subject: [PATCH 0870/1250] Bye bye secondary domain

---
 src/config.ts                    | 12 +----------
 src/himasaku/assets/himasaku.png |  3 ---
 src/himasaku/assets/index.html   | 35 --------------------------------
 src/himasaku/server.ts           | 23 ---------------------
 src/server.ts                    |  3 +--
 5 files changed, 2 insertions(+), 74 deletions(-)
 delete mode 100644 src/himasaku/assets/himasaku.png
 delete mode 100644 src/himasaku/assets/index.html
 delete mode 100644 src/himasaku/server.ts

diff --git a/src/config.ts b/src/config.ts
index 406183c91..349b161de 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -37,7 +37,6 @@ type Source = {
 		url: string;
 	};
 	url: string;
-	secondary_url: string;
 	port: number;
 	https?: { [x: string]: string };
 	mongodb: {
@@ -106,9 +105,6 @@ type Mixin = {
 	hostname: string;
 	scheme: string;
 	ws_scheme: string;
-	secondary_host: string;
-	secondary_hostname: string;
-	secondary_scheme: string;
 	api_url: string;
 	ws_url: string;
 	auth_url: string;
@@ -129,21 +125,15 @@ export default function load() {
 
 	// Validate URLs
 	if (!isUrl(config.url)) urlError(config.url);
-	if (!isUrl(config.secondary_url)) urlError(config.secondary_url);
 
 	const url = new URL(config.url);
-	const secondaryUrl = new URL(config.secondary_url);
 	config.url = normalizeUrl(config.url);
-	config.secondary_url = normalizeUrl(config.secondary_url);
 
 	mixin.host = url.host;
 	mixin.hostname = url.hostname;
 	mixin.scheme = url.protocol.replace(/:$/, '');
 	mixin.ws_scheme = mixin.scheme.replace('http', 'ws');
 	mixin.ws_url = `${mixin.ws_scheme}://${mixin.host}`;
-	mixin.secondary_host = config.secondary_url.substr(config.secondary_url.indexOf('://') + 3);
-	mixin.secondary_hostname = secondaryUrl.hostname;
-	mixin.secondary_scheme = config.secondary_url.substr(0, config.secondary_url.indexOf('://'));
 	mixin.api_url = `${mixin.scheme}://${mixin.host}/api`;
 	mixin.auth_url = `${mixin.scheme}://auth.${mixin.host}`;
 	mixin.ch_url = `${mixin.scheme}://ch.${mixin.host}`;
@@ -151,7 +141,7 @@ export default function load() {
 	mixin.docs_url = `${mixin.scheme}://docs.${mixin.host}`;
 	mixin.stats_url = `${mixin.scheme}://stats.${mixin.host}`;
 	mixin.status_url = `${mixin.scheme}://status.${mixin.host}`;
-	mixin.drive_url = `${mixin.secondary_scheme}://file.${mixin.secondary_host}`;
+	mixin.drive_url = `${mixin.scheme}://${mixin.host}/files`;
 
 	return Object.assign(config, mixin);
 }
diff --git a/src/himasaku/assets/himasaku.png b/src/himasaku/assets/himasaku.png
deleted file mode 100644
index 226e8d568..000000000
--- a/src/himasaku/assets/himasaku.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:feceacb833ea782674dd3fe924ec5de6fa97b71301f921a7bef8ebeb02fd276c
-size 144018
diff --git a/src/himasaku/assets/index.html b/src/himasaku/assets/index.html
deleted file mode 100644
index f9e45d7a7..000000000
--- a/src/himasaku/assets/index.html
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<html>
-	<head>
-		<meta charset="utf-8">
-		<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
-		<meta name="description" content="ひまさく">
-		<meta name="keywords" content="ひまさく, さくひま, 向日葵, 櫻子">
-		<title>ひまさく</title>
-		<style>
-			html {
-				height: 100%;
-				font-size: 0;
-			}
-
-			body {
-				margin: 0;
-				height: 100%;
-				overflow: hidden;
-			}
-
-			img {
-				display: block;
-				position: absolute;
-				max-width: 100%;
-				margin: auto;
-				top: 0; right: 0; bottom: 0; left: 0;
-				pointer-events: none;
-				user-select: none;
-			}
-		</style>
-	</head>
-	<body>
-		<img src="/himasaku.png" alt="ひまさく">
-	</body>
-</html>
diff --git a/src/himasaku/server.ts b/src/himasaku/server.ts
deleted file mode 100644
index fb129513d..000000000
--- a/src/himasaku/server.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Himasaku Server
- */
-
-import * as express from 'express';
-
-/**
- * Init app
- */
-const app = express();
-
-app.disable('x-powered-by');
-app.locals.cache = true;
-
-app.get('/himasaku.png', (req, res) => {
-	res.sendFile(`${__dirname}/assets/himasaku.png`);
-});
-
-app.get('*', (req, res) => {
-	res.sendFile(`${__dirname}/assets/index.html`);
-});
-
-module.exports = app;
diff --git a/src/server.ts b/src/server.ts
index fb581bae5..00d09f153 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -54,8 +54,7 @@ app.use((req, res, next) => {
  * Register modules
  */
 app.use('/api', require('./api/server'));
-app.use(vhost(config.secondary_hostname, require('./himasaku/server')));
-app.use(vhost(`file.${config.secondary_hostname}`, require('./file/server')));
+app.use('/files', require('./file/server'));
 app.use(require('./web/server'));
 
 /**

From b3d9fced338904e01d6db29ff7aee13aaf418680 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Tue, 27 Mar 2018 12:53:56 +0900
Subject: [PATCH 0871/1250] Replace /:user endpoints with /@:user

---
 src/api/bot/interfaces/line.ts                |  2 +-
 src/api/models/user.ts                        |  2 +-
 src/common/othello/ai/back.ts                 | 16 ++++----
 src/web/app/ch/tags/channel.tag               |  2 +-
 src/web/app/ch/tags/header.tag                |  2 +-
 .../components/messaging-room.message.vue     |  2 +-
 .../app/common/views/components/post-html.ts  |  2 +-
 .../app/common/views/components/signup.vue    |  2 +-
 .../views/components/welcome-timeline.vue     |  6 +--
 src/web/app/desktop/script.ts                 |  4 +-
 .../views/components/friends-maker.vue        |  4 +-
 .../views/components/notifications.vue        | 40 +++++++++----------
 .../views/components/post-detail.sub.vue      |  6 +--
 .../desktop/views/components/post-detail.vue  | 10 ++---
 .../desktop/views/components/post-preview.vue |  6 +--
 .../views/components/posts.post.sub.vue       |  6 +--
 .../desktop/views/components/posts.post.vue   | 10 ++---
 .../views/components/ui.header.account.vue    |  2 +-
 .../desktop/views/components/user-preview.vue |  4 +-
 .../views/components/users-list.item.vue      |  4 +-
 .../pages/user/user.followers-you-know.vue    |  2 +-
 .../desktop/views/pages/user/user.friends.vue |  4 +-
 .../desktop/views/pages/user/user.header.vue  |  6 +--
 src/web/app/desktop/views/pages/welcome.vue   |  2 +-
 .../views/widgets/channel.channel.post.vue    |  2 +-
 src/web/app/desktop/views/widgets/polls.vue   |  4 +-
 src/web/app/desktop/views/widgets/profile.vue |  2 +-
 src/web/app/desktop/views/widgets/trends.vue  |  4 +-
 src/web/app/desktop/views/widgets/users.vue   |  4 +-
 src/web/app/mobile/script.ts                  |  8 ++--
 .../mobile/views/components/notification.vue  | 22 +++++-----
 .../app/mobile/views/components/post-card.vue |  2 +-
 .../views/components/post-detail.sub.vue      |  6 +--
 .../mobile/views/components/post-detail.vue   | 10 ++---
 .../mobile/views/components/post-preview.vue  |  6 +--
 .../app/mobile/views/components/post.sub.vue  |  6 +--
 src/web/app/mobile/views/components/post.vue  | 10 ++---
 .../app/mobile/views/components/ui.nav.vue    |  2 +-
 .../app/mobile/views/components/user-card.vue |  4 +-
 .../mobile/views/components/user-preview.vue  |  4 +-
 src/web/app/mobile/views/pages/user.vue       |  4 +-
 .../pages/user/home.followers-you-know.vue    |  2 +-
 .../mobile/views/pages/user/home.photos.vue   |  2 +-
 src/web/app/mobile/views/pages/welcome.vue    |  2 +-
 src/web/app/mobile/views/widgets/profile.vue  |  2 +-
 45 files changed, 127 insertions(+), 127 deletions(-)

diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts
index bf0882159..6b2ebdec8 100644
--- a/src/api/bot/interfaces/line.ts
+++ b/src/api/bot/interfaces/line.ts
@@ -121,7 +121,7 @@ class LineBot extends BotCore {
 		actions.push({
 			type: 'uri',
 			label: 'Webで見る',
-			uri: `${config.url}/${user.username}`
+			uri: `${config.url}/@${user.username}`
 		});
 
 		this.reply([{
diff --git a/src/api/models/user.ts b/src/api/models/user.ts
index 08d7fbb8c..63f79908e 100644
--- a/src/api/models/user.ts
+++ b/src/api/models/user.ts
@@ -296,7 +296,7 @@ export const packForAp = (
 
 	if (!_user) return reject('invalid user arg.');
 
-	const userUrl = `${config.url}/${_user.username}`;
+	const userUrl = `${config.url}/@${_user.username}`;
 
 	resolve({
 		"@context": ["https://www.w3.org/ns/activitystreams", {
diff --git a/src/common/othello/ai/back.ts b/src/common/othello/ai/back.ts
index 42a256c0b..27dbc3952 100644
--- a/src/common/othello/ai/back.ts
+++ b/src/common/othello/ai/back.ts
@@ -47,8 +47,8 @@ process.on('message', async msg => {
 		const user = game.user1_id == id ? game.user2 : game.user1;
 		const isSettai = form[0].value === 0;
 		const text = isSettai
-			? `?[${user.name}](${conf.url}/${user.username})さんの接待を始めました!`
-			: `対局を?[${user.name}](${conf.url}/${user.username})さんと始めました! (強さ${form[0].value})`;
+			? `?[${user.name}](${conf.url}/@${user.username})さんの接待を始めました!`
+			: `対局を?[${user.name}](${conf.url}/@${user.username})さんと始めました! (強さ${form[0].value})`;
 
 		const res = await request.post(`${conf.api_url}/posts/create`, {
 			json: { i,
@@ -72,15 +72,15 @@ process.on('message', async msg => {
 		const isSettai = form[0].value === 0;
 		const text = isSettai
 			? msg.body.winner_id === null
-				? `?[${user.name}](${conf.url}/${user.username})さんに接待で引き分けました...`
+				? `?[${user.name}](${conf.url}/@${user.username})さんに接待で引き分けました...`
 				: msg.body.winner_id == id
-					? `?[${user.name}](${conf.url}/${user.username})さんに接待で勝ってしまいました...`
-					: `?[${user.name}](${conf.url}/${user.username})さんに接待で負けてあげました♪`
+					? `?[${user.name}](${conf.url}/@${user.username})さんに接待で勝ってしまいました...`
+					: `?[${user.name}](${conf.url}/@${user.username})さんに接待で負けてあげました♪`
 			: msg.body.winner_id === null
-				? `?[${user.name}](${conf.url}/${user.username})さんと引き分けました~`
+				? `?[${user.name}](${conf.url}/@${user.username})さんと引き分けました~`
 				: msg.body.winner_id == id
-					? `?[${user.name}](${conf.url}/${user.username})さんに勝ちました♪`
-					: `?[${user.name}](${conf.url}/${user.username})さんに負けました...`;
+					? `?[${user.name}](${conf.url}/@${user.username})さんに勝ちました♪`
+					: `?[${user.name}](${conf.url}/@${user.username})さんに負けました...`;
 
 		await request.post(`${conf.api_url}/posts/create`, {
 			json: { i,
diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index b5c6ce1e6..face824cf 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -165,7 +165,7 @@
 <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>
+		<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"/>
 		<span>ID:<i>{ post.user.username }</i></span>
diff --git a/src/web/app/ch/tags/header.tag b/src/web/app/ch/tags/header.tag
index 747bec357..901123d63 100644
--- a/src/web/app/ch/tags/header.tag
+++ b/src/web/app/ch/tags/header.tag
@@ -4,7 +4,7 @@
 	</div>
 	<div>
 		<a v-if="!$root.$data.os.isSignedIn" href={ _URL_ }>ログイン(新規登録)</a>
-		<a v-if="$root.$data.os.isSignedIn" href={ _URL_ + '/' + I.username }>{ I.username }</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/common/views/components/messaging-room.message.vue b/src/web/app/common/views/components/messaging-room.message.vue
index 56854ca2f..647e39a75 100644
--- a/src/web/app/common/views/components/messaging-room.message.vue
+++ b/src/web/app/common/views/components/messaging-room.message.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="message" :data-is-me="isMe">
-	<router-link class="avatar-anchor" :to="`/${message.user.username}`" :title="message.user.username" target="_blank">
+	<router-link class="avatar-anchor" :to="`/@${message.user.username}`" :title="message.user.username" target="_blank">
 		<img class="avatar" :src="`${message.user.avatar_url}?thumbnail&size=80`" alt=""/>
 	</router-link>
 	<div class="content">
diff --git a/src/web/app/common/views/components/post-html.ts b/src/web/app/common/views/components/post-html.ts
index dae118e82..56ee97d38 100644
--- a/src/web/app/common/views/components/post-html.ts
+++ b/src/web/app/common/views/components/post-html.ts
@@ -61,7 +61,7 @@ export default Vue.component('mk-post-html', {
 				case 'mention':
 					return (createElement as any)('a', {
 						attrs: {
-							href: `${url}/${token.username}`,
+							href: `${url}/@${token.username}`,
 							target: '_blank',
 							dataIsMe: (this as any).i && (this as any).i.username == token.username
 						},
diff --git a/src/web/app/common/views/components/signup.vue b/src/web/app/common/views/components/signup.vue
index 2ca1687be..c2e78aa8a 100644
--- a/src/web/app/common/views/components/signup.vue
+++ b/src/web/app/common/views/components/signup.vue
@@ -3,7 +3,7 @@
 	<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 @input="onChangeUsername"/>
-		<p class="profile-page-url-preview" v-if="shouldShowProfileUrl">{{ `${url}/${username}` }}</p>
+		<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>
diff --git a/src/web/app/common/views/components/welcome-timeline.vue b/src/web/app/common/views/components/welcome-timeline.vue
index 7e35e1f71..062ccda32 100644
--- a/src/web/app/common/views/components/welcome-timeline.vue
+++ b/src/web/app/common/views/components/welcome-timeline.vue
@@ -1,15 +1,15 @@
 <template>
 <div class="mk-welcome-timeline">
 	<div v-for="post in posts">
-		<router-link class="avatar-anchor" :to="`/${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=96`" alt="avatar"/>
 		</router-link>
 		<div class="body">
 			<header>
-				<router-link class="name" :to="`/${post.user.username}`" v-user-preview="post.user.id">{{ post.user.name }}</router-link>
+				<router-link class="name" :to="`/@${post.user.username}`" v-user-preview="post.user.id">{{ post.user.name }}</router-link>
 				<span class="username">@{{ post.user.username }}</span>
 				<div class="info">
-					<router-link class="created-at" :to="`/${post.user.username}/${post.id}`">
+					<router-link class="created-at" :to="`/@${post.user.username}/${post.id}`">
 						<mk-time :time="post.created_at"/>
 					</router-link>
 				</div>
diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
index 2362613cd..f2bcc6f8a 100644
--- a/src/web/app/desktop/script.ts
+++ b/src/web/app/desktop/script.ts
@@ -83,8 +83,8 @@ init(async (launch) => {
 		{ path: '/search', component: MkSearch },
 		{ path: '/othello', component: MkOthello },
 		{ path: '/othello/:game', component: MkOthello },
-		{ path: '/:user', component: MkUser },
-		{ path: '/:user/:post', component: MkPost }
+		{ path: '/@:user', component: MkUser },
+		{ path: '/@:user/:post', component: MkPost }
 	]);
 }, true);
 
diff --git a/src/web/app/desktop/views/components/friends-maker.vue b/src/web/app/desktop/views/components/friends-maker.vue
index ab35efc75..65adff7ce 100644
--- a/src/web/app/desktop/views/components/friends-maker.vue
+++ b/src/web/app/desktop/views/components/friends-maker.vue
@@ -3,11 +3,11 @@
 	<p class="title">気になるユーザーをフォロー:</p>
 	<div class="users" v-if="!fetching && users.length > 0">
 		<div class="user" v-for="user in users" :key="user.id">
-			<router-link class="avatar-anchor" :to="`/${user.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">
-				<router-link class="name" :to="`/${user.username}`" v-user-preview="user.id">{{ user.name }}</router-link>
+				<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"/>
diff --git a/src/web/app/desktop/views/components/notifications.vue b/src/web/app/desktop/views/components/notifications.vue
index d61397d53..86cd1ba4f 100644
--- a/src/web/app/desktop/views/components/notifications.vue
+++ b/src/web/app/desktop/views/components/notifications.vue
@@ -5,82 +5,82 @@
 			<div class="notification" :class="notification.type" :key="notification.id">
 				<mk-time :time="notification.created_at"/>
 				<template v-if="notification.type == 'reaction'">
-					<router-link class="avatar-anchor" :to="`/${notification.user.username}`" v-user-preview="notification.user.id">
+					<router-link class="avatar-anchor" :to="`/@${notification.user.username}`" v-user-preview="notification.user.id">
 						<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>
 							<mk-reaction-icon :reaction="notification.reaction"/>
-							<router-link :to="`/${notification.user.username}`" v-user-preview="notification.user.id">{{ notification.user.name }}</router-link>
+							<router-link :to="`/@${notification.user.username}`" v-user-preview="notification.user.id">{{ notification.user.name }}</router-link>
 						</p>
-						<router-link class="post-ref" :to="`/${notification.post.user.username}/${notification.post.id}`">
+						<router-link class="post-ref" :to="`/@${notification.post.user.username}/${notification.post.id}`">
 							%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%
 						</router-link>
 					</div>
 				</template>
 				<template v-if="notification.type == 'repost'">
-					<router-link class="avatar-anchor" :to="`/${notification.post.user.username}`" v-user-preview="notification.post.user_id">
+					<router-link class="avatar-anchor" :to="`/@${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"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:retweet%
-							<router-link :to="`/${notification.post.user.username}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</router-link>
+							<router-link :to="`/@${notification.post.user.username}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</router-link>
 						</p>
-						<router-link class="post-ref" :to="`/${notification.post.user.username}/${notification.post.id}`">
+						<router-link class="post-ref" :to="`/@${notification.post.user.username}/${notification.post.id}`">
 							%fa:quote-left%{{ getPostSummary(notification.post.repost) }}%fa:quote-right%
 						</router-link>
 					</div>
 				</template>
 				<template v-if="notification.type == 'quote'">
-					<router-link class="avatar-anchor" :to="`/${notification.post.user.username}`" v-user-preview="notification.post.user_id">
+					<router-link class="avatar-anchor" :to="`/@${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"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:quote-left%
-							<router-link :to="`/${notification.post.user.username}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</router-link>
+							<router-link :to="`/@${notification.post.user.username}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</router-link>
 						</p>
-						<router-link class="post-preview" :to="`/${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link>
+						<router-link class="post-preview" :to="`/@${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link>
 					</div>
 				</template>
 				<template v-if="notification.type == 'follow'">
-					<router-link class="avatar-anchor" :to="`/${notification.user.username}`" v-user-preview="notification.user.id">
+					<router-link class="avatar-anchor" :to="`/@${notification.user.username}`" v-user-preview="notification.user.id">
 						<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:user-plus%
-							<router-link :to="`/${notification.user.username}`" v-user-preview="notification.user.id">{{ notification.user.name }}</router-link>
+							<router-link :to="`/@${notification.user.username}`" v-user-preview="notification.user.id">{{ notification.user.name }}</router-link>
 						</p>
 					</div>
 				</template>
 				<template v-if="notification.type == 'reply'">
-					<router-link class="avatar-anchor" :to="`/${notification.post.user.username}`" v-user-preview="notification.post.user_id">
+					<router-link class="avatar-anchor" :to="`/@${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"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:reply%
-							<router-link :to="`/${notification.post.user.username}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</router-link>
+							<router-link :to="`/@${notification.post.user.username}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</router-link>
 						</p>
-						<router-link class="post-preview" :to="`/${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link>
+						<router-link class="post-preview" :to="`/@${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link>
 					</div>
 				</template>
 				<template v-if="notification.type == 'mention'">
-					<router-link class="avatar-anchor" :to="`/${notification.post.user.username}`" v-user-preview="notification.post.user_id">
+					<router-link class="avatar-anchor" :to="`/@${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"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:at%
-							<router-link :to="`/${notification.post.user.username}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</router-link>
+							<router-link :to="`/@${notification.post.user.username}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</router-link>
 						</p>
-						<a class="post-preview" :href="`/${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</a>
+						<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'">
-					<router-link class="avatar-anchor" :to="`/${notification.user.username}`" v-user-preview="notification.user.id">
+					<router-link class="avatar-anchor" :to="`/@${notification.user.username}`" v-user-preview="notification.user.id">
 						<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
-						<p>%fa:chart-pie%<a :href="`/${notification.user.username}`" v-user-preview="notification.user.id">{{ notification.user.name }}</a></p>
-						<router-link class="post-ref" :to="`/${notification.post.user.username}/${notification.post.id}`">
+						<p>%fa:chart-pie%<a :href="`/@${notification.user.username}`" v-user-preview="notification.user.id">{{ notification.user.name }}</a></p>
+						<router-link class="post-ref" :to="`/@${notification.post.user.username}/${notification.post.id}`">
 							%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%
 						</router-link>
 					</div>
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 c35a07d7c..53fc724fc 100644
--- a/src/web/app/desktop/views/components/post-detail.sub.vue
+++ b/src/web/app/desktop/views/components/post-detail.sub.vue
@@ -1,16 +1,16 @@
 <template>
 <div class="sub" :title="title">
-	<router-link class="avatar-anchor" :to="`/${post.user.username}`">
+	<router-link class="avatar-anchor" :to="`/@${post.user.username}`">
 		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar" v-user-preview="post.user_id"/>
 	</router-link>
 	<div class="main">
 		<header>
 			<div class="left">
-				<router-link class="name" :to="`/${post.user.username}`" v-user-preview="post.user_id">{{ post.user.name }}</router-link>
+				<router-link class="name" :to="`/@${post.user.username}`" v-user-preview="post.user_id">{{ post.user.name }}</router-link>
 				<span class="username">@{{ post.user.username }}</span>
 			</div>
 			<div class="right">
-				<router-link class="time" :to="`/${post.user.username}/${post.id}`">
+				<router-link class="time" :to="`/@${post.user.username}/${post.id}`">
 					<mk-time :time="post.created_at"/>
 				</router-link>
 			</div>
diff --git a/src/web/app/desktop/views/components/post-detail.vue b/src/web/app/desktop/views/components/post-detail.vue
index 5845ab4f8..9a8958679 100644
--- a/src/web/app/desktop/views/components/post-detail.vue
+++ b/src/web/app/desktop/views/components/post-detail.vue
@@ -18,22 +18,22 @@
 	</div>
 	<div class="repost" v-if="isRepost">
 		<p>
-			<router-link class="avatar-anchor" :to="`/${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"/>
 			</router-link>
 			%fa:retweet%
-			<router-link class="name" :href="`/${post.user.username}`">{{ post.user.name }}</router-link>
+			<router-link class="name" :href="`/@${post.user.username}`">{{ post.user.name }}</router-link>
 			がRepost
 		</p>
 	</div>
 	<article>
-		<router-link class="avatar-anchor" :to="`/${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"/>
 		</router-link>
 		<header>
-			<router-link class="name" :to="`/${p.user.username}`" v-user-preview="p.user.id">{{ p.user.name }}</router-link>
+			<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}`">
+			<router-link class="time" :to="`/@${p.user.username}/${p.id}`">
 				<mk-time :time="p.created_at"/>
 			</router-link>
 		</header>
diff --git a/src/web/app/desktop/views/components/post-preview.vue b/src/web/app/desktop/views/components/post-preview.vue
index ec3372f90..edb593457 100644
--- a/src/web/app/desktop/views/components/post-preview.vue
+++ b/src/web/app/desktop/views/components/post-preview.vue
@@ -1,13 +1,13 @@
 <template>
 <div class="mk-post-preview" :title="title">
-	<router-link class="avatar-anchor" :to="`/${post.user.username}`">
+	<router-link class="avatar-anchor" :to="`/@${post.user.username}`">
 		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar" v-user-preview="post.user_id"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/${post.user.username}`" v-user-preview="post.user_id">{{ post.user.name }}</router-link>
+			<router-link class="name" :to="`/@${post.user.username}`" v-user-preview="post.user_id">{{ post.user.name }}</router-link>
 			<span class="username">@{{ post.user.username }}</span>
-			<router-link class="time" :to="`/${post.user.username}/${post.id}`">
+			<router-link class="time" :to="`/@${post.user.username}/${post.id}`">
 				<mk-time :time="post.created_at"/>
 			</router-link>
 		</header>
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 69c88fed5..2fd8a9865 100644
--- a/src/web/app/desktop/views/components/posts.post.sub.vue
+++ b/src/web/app/desktop/views/components/posts.post.sub.vue
@@ -1,13 +1,13 @@
 <template>
 <div class="sub" :title="title">
-	<router-link class="avatar-anchor" :to="`/${post.user.username}`">
+	<router-link class="avatar-anchor" :to="`/@${post.user.username}`">
 		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar" v-user-preview="post.user_id"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/${post.user.username}`" v-user-preview="post.user_id">{{ post.user.name }}</router-link>
+			<router-link class="name" :to="`/@${post.user.username}`" v-user-preview="post.user_id">{{ post.user.name }}</router-link>
 			<span class="username">@{{ post.user.username }}</span>
-			<router-link class="created-at" :to="`/${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>
diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index 71cbbc68d..a525900b9 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -5,23 +5,23 @@
 	</div>
 	<div class="repost" v-if="isRepost">
 		<p>
-			<router-link class="avatar-anchor" :to="`/${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"/>
 			</router-link>
 			%fa:retweet%
 			<span>{{ '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('{')) }}</span>
-			<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>{{ '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1) }}</span>
 		</p>
 		<mk-time :time="post.created_at"/>
 	</div>
 	<article>
-		<router-link class="avatar-anchor" :to="`/${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"/>
 		</router-link>
 		<div class="main">
 			<header>
-				<router-link class="name" :to="`/${p.user.username}`" v-user-preview="p.user.id">{{ p.user.name }}</router-link>
+				<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.account.is_bot">bot</span>
 				<span class="username">@{{ p.user.username }}</span>
 				<div class="info">
@@ -135,7 +135,7 @@ export default Vue.extend({
 			return dateStringify(this.p.created_at);
 		},
 		url(): string {
-			return `/${this.p.user.username}/${this.p.id}`;
+			return `/@${this.p.user.username}/${this.p.id}`;
 		},
 		urls(): string[] {
 			if (this.p.ast) {
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 2cc2c1867..19b9d7779 100644
--- a/src/web/app/desktop/views/components/ui.header.account.vue
+++ b/src/web/app/desktop/views/components/ui.header.account.vue
@@ -8,7 +8,7 @@
 		<div class="menu" v-if="isOpen">
 			<ul>
 				<li>
-					<router-link :to="`/${ os.i.username }`">%fa:user%%i18n:desktop.tags.mk-ui-header-account.profile%%fa:angle-right%</router-link>
+					<router-link :to="`/@${ os.i.username }`">%fa:user%%i18n:desktop.tags.mk-ui-header-account.profile%%fa:angle-right%</router-link>
 				</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/user-preview.vue b/src/web/app/desktop/views/components/user-preview.vue
index cfb95961d..ffc959fac 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>
-		<router-link class="avatar" :to="`/${u.username}`">
+		<router-link class="avatar" :to="`/@${u.username}`">
 			<img :src="`${u.avatar_url}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="title">
-			<router-link class="name" :to="`/${u.username}`">{{ u.name }}</router-link>
+			<router-link class="name" :to="`/@${u.username}`">{{ u.name }}</router-link>
 			<p class="username">@{{ u.username }}</p>
 		</div>
 		<div class="description">{{ u.description }}</div>
diff --git a/src/web/app/desktop/views/components/users-list.item.vue b/src/web/app/desktop/views/components/users-list.item.vue
index 374f55b41..2d1e13347 100644
--- a/src/web/app/desktop/views/components/users-list.item.vue
+++ b/src/web/app/desktop/views/components/users-list.item.vue
@@ -1,11 +1,11 @@
 <template>
 <div class="root item">
-	<router-link class="avatar-anchor" :to="`/${user.username}`" v-user-preview="user.id">
+	<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"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/${user.username}`" v-user-preview="user.id">{{ user.name }}</router-link>
+			<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">
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 015b12d3d..20675c454 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 d27009a82..a60020f59 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.header.vue b/src/web/app/desktop/views/pages/user/user.header.vue
index 63542055d..e60b312ca 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.account.profile.location">%fa:map-marker%{{ user.account.profile.location }}</p>
 		</div>
 		<footer>
-			<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>
+			<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>
diff --git a/src/web/app/desktop/views/pages/welcome.vue b/src/web/app/desktop/views/pages/welcome.vue
index ad1dccae9..c514500b2 100644
--- a/src/web/app/desktop/views/pages/welcome.vue
+++ b/src/web/app/desktop/views/pages/welcome.vue
@@ -8,7 +8,7 @@
 					<p>ようこそ! <b>Misskey</b>はTwitter風ミニブログSNSです。思ったことや皆と共有したいことを投稿しましょう。タイムラインを見れば、皆の関心事をすぐにチェックすることもできます。<a :href="aboutUrl">詳しく...</a></p>
 					<p><button class="signup" @click="signup">はじめる</button><button class="signin" @click="signin">ログイン</button></p>
 					<div class="users">
-						<router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="`/${user.username}`" v-user-preview="user.id">
+						<router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="`/@${user.username}`" v-user-preview="user.id">
 							<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
 						</router-link>
 					</div>
diff --git a/src/web/app/desktop/views/widgets/channel.channel.post.vue b/src/web/app/desktop/views/widgets/channel.channel.post.vue
index faaf0fb73..b2fa92ad4 100644
--- a/src/web/app/desktop/views/widgets/channel.channel.post.vue
+++ b/src/web/app/desktop/views/widgets/channel.channel.post.vue
@@ -2,7 +2,7 @@
 <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>
+		<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>
diff --git a/src/web/app/desktop/views/widgets/polls.vue b/src/web/app/desktop/views/widgets/polls.vue
index fda4e17d8..636378d0b 100644
--- a/src/web/app/desktop/views/widgets/polls.vue
+++ b/src/web/app/desktop/views/widgets/polls.vue
@@ -5,8 +5,8 @@
 		<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>
+		<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>
diff --git a/src/web/app/desktop/views/widgets/profile.vue b/src/web/app/desktop/views/widgets/profile.vue
index e067a0eb2..394010619 100644
--- a/src/web/app/desktop/views/widgets/profile.vue
+++ b/src/web/app/desktop/views/widgets/profile.vue
@@ -15,7 +15,7 @@
 		title="クリックでアバター編集"
 		v-user-preview="os.i.id"
 	/>
-	<router-link class="name" :to="`/${os.i.username}`">{{ os.i.name }}</router-link>
+	<router-link class="name" :to="`/@${os.i.username}`">{{ os.i.name }}</router-link>
 	<p class="username">@{{ os.i.username }}</p>
 </div>
 </template>
diff --git a/src/web/app/desktop/views/widgets/trends.vue b/src/web/app/desktop/views/widgets/trends.vue
index 09cad9ba4..c006c811d 100644
--- a/src/web/app/desktop/views/widgets/trends.vue
+++ b/src/web/app/desktop/views/widgets/trends.vue
@@ -6,8 +6,8 @@
 	</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"><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>
+		<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>
diff --git a/src/web/app/desktop/views/widgets/users.vue b/src/web/app/desktop/views/widgets/users.vue
index f7af8205e..c0a85a08e 100644
--- a/src/web/app/desktop/views/widgets/users.vue
+++ b/src/web/app/desktop/views/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" :to="`/${_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">
-				<router-link class="name" :to="`/${_user.username}`" v-user-preview="_user.id">{{ _user.name }}</router-link>
+				<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"/>
diff --git a/src/web/app/mobile/script.ts b/src/web/app/mobile/script.ts
index 2b57b78ad..2fcb085ac 100644
--- a/src/web/app/mobile/script.ts
+++ b/src/web/app/mobile/script.ts
@@ -71,9 +71,9 @@ init((launch) => {
 		{ path: '/search', component: MkSearch },
 		{ path: '/othello', component: MkOthello },
 		{ path: '/othello/:game', component: MkOthello },
-		{ path: '/:user', component: MkUser },
-		{ path: '/:user/followers', component: MkFollowers },
-		{ path: '/:user/following', component: MkFollowing },
-		{ path: '/:user/:post', component: MkPost }
+		{ 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/views/components/notification.vue b/src/web/app/mobile/views/components/notification.vue
index 506ce3493..301fb81dd 100644
--- a/src/web/app/mobile/views/components/notification.vue
+++ b/src/web/app/mobile/views/components/notification.vue
@@ -2,15 +2,15 @@
 <div class="mk-notification">
 	<div class="notification reaction" v-if="notification.type == 'reaction'">
 		<mk-time :time="notification.created_at"/>
-		<router-link class="avatar-anchor" :to="`/${notification.user.username}`">
+		<router-link class="avatar-anchor" :to="`/@${notification.user.username}`">
 			<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="text">
 			<p>
 				<mk-reaction-icon :reaction="notification.reaction"/>
-				<router-link :to="`/${notification.user.username}`">{{ notification.user.name }}</router-link>
+				<router-link :to="`/@${notification.user.username}`">{{ notification.user.name }}</router-link>
 			</p>
-			<router-link class="post-ref" :to="`/${notification.post.user.username}/${notification.post.id}`">
+			<router-link class="post-ref" :to="`/@${notification.post.user.username}/${notification.post.id}`">
 				%fa:quote-left%{{ getPostSummary(notification.post) }}
 				%fa:quote-right%
 			</router-link>
@@ -19,15 +19,15 @@
 
 	<div class="notification repost" v-if="notification.type == 'repost'">
 		<mk-time :time="notification.created_at"/>
-		<router-link class="avatar-anchor" :to="`/${notification.post.user.username}`">
+		<router-link class="avatar-anchor" :to="`/@${notification.post.user.username}`">
 			<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="text">
 			<p>
 				%fa:retweet%
-				<router-link :to="`/${notification.post.user.username}`">{{ notification.post.user.name }}</router-link>
+				<router-link :to="`/@${notification.post.user.username}`">{{ notification.post.user.name }}</router-link>
 			</p>
-			<router-link class="post-ref" :to="`/${notification.post.user.username}/${notification.post.id}`">
+			<router-link class="post-ref" :to="`/@${notification.post.user.username}/${notification.post.id}`">
 				%fa:quote-left%{{ getPostSummary(notification.post.repost) }}%fa:quote-right%
 			</router-link>
 		</div>
@@ -39,13 +39,13 @@
 
 	<div class="notification follow" v-if="notification.type == 'follow'">
 		<mk-time :time="notification.created_at"/>
-		<router-link class="avatar-anchor" :to="`/${notification.user.username}`">
+		<router-link class="avatar-anchor" :to="`/@${notification.user.username}`">
 			<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="text">
 			<p>
 				%fa:user-plus%
-				<router-link :to="`/${notification.user.username}`">{{ notification.user.name }}</router-link>
+				<router-link :to="`/@${notification.user.username}`">{{ notification.user.name }}</router-link>
 			</p>
 		</div>
 	</div>
@@ -60,15 +60,15 @@
 
 	<div class="notification poll_vote" v-if="notification.type == 'poll_vote'">
 		<mk-time :time="notification.created_at"/>
-		<router-link class="avatar-anchor" :to="`/${notification.user.username}`">
+		<router-link class="avatar-anchor" :to="`/@${notification.user.username}`">
 			<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="text">
 			<p>
 				%fa:chart-pie%
-				<router-link :to="`/${notification.user.username}`">{{ notification.user.name }}</router-link>
+				<router-link :to="`/@${notification.user.username}`">{{ notification.user.name }}</router-link>
 			</p>
-			<router-link class="post-ref" :to="`/${notification.post.user.username}/${notification.post.id}`">
+			<router-link class="post-ref" :to="`/@${notification.post.user.username}/${notification.post.id}`">
 				%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%
 			</router-link>
 		</div>
diff --git a/src/web/app/mobile/views/components/post-card.vue b/src/web/app/mobile/views/components/post-card.vue
index 08a2bebfc..1b3b20d88 100644
--- a/src/web/app/mobile/views/components/post-card.vue
+++ b/src/web/app/mobile/views/components/post-card.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="mk-post-card">
-	<a :href="`/${post.user.username}/${post.id}`">
+	<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>
diff --git a/src/web/app/mobile/views/components/post-detail.sub.vue b/src/web/app/mobile/views/components/post-detail.sub.vue
index dff0cef51..153acf78e 100644
--- a/src/web/app/mobile/views/components/post-detail.sub.vue
+++ b/src/web/app/mobile/views/components/post-detail.sub.vue
@@ -1,13 +1,13 @@
 <template>
 <div class="root sub">
-	<router-link class="avatar-anchor" :to="`/${post.user.username}`">
+	<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>
-			<router-link class="name" :to="`/${post.user.username}`">{{ post.user.name }}</router-link>
+			<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}`">
+			<router-link class="time" :to="`/@${post.user.username}/${post.id}`">
 				<mk-time :time="post.created_at"/>
 			</router-link>
 		</header>
diff --git a/src/web/app/mobile/views/components/post-detail.vue b/src/web/app/mobile/views/components/post-detail.vue
index 9baa5de6d..f7af71eea 100644
--- a/src/web/app/mobile/views/components/post-detail.vue
+++ b/src/web/app/mobile/views/components/post-detail.vue
@@ -17,11 +17,11 @@
 	</div>
 	<div class="repost" v-if="isRepost">
 		<p>
-			<router-link class="avatar-anchor" :to="`/${post.user.username}`">
+			<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}`">
+			<router-link class="name" :to="`/@${post.user.username}`">
 				{{ post.user.name }}
 			</router-link>
 			がRepost
@@ -29,11 +29,11 @@
 	</div>
 	<article>
 		<header>
-			<router-link class="avatar-anchor" :to="`/${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"/>
 			</router-link>
 			<div>
-				<router-link class="name" :to="`/${p.user.username}`">{{ p.user.name }}</router-link>
+				<router-link class="name" :to="`/@${p.user.username}`">{{ p.user.name }}</router-link>
 				<span class="username">@{{ p.user.username }}</span>
 			</div>
 		</header>
@@ -53,7 +53,7 @@
 				<mk-post-preview :post="p.repost"/>
 			</div>
 		</div>
-		<router-link class="time" :to="`/${p.user.username}/${p.id}`">
+		<router-link class="time" :to="`/@${p.user.username}/${p.id}`">
 			<mk-time :time="p.created_at" mode="detail"/>
 		</router-link>
 		<footer>
diff --git a/src/web/app/mobile/views/components/post-preview.vue b/src/web/app/mobile/views/components/post-preview.vue
index e9a411925..787e1a3a7 100644
--- a/src/web/app/mobile/views/components/post-preview.vue
+++ b/src/web/app/mobile/views/components/post-preview.vue
@@ -1,13 +1,13 @@
 <template>
 <div class="mk-post-preview">
-	<router-link class="avatar-anchor" :to="`/${post.user.username}`">
+	<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>
-			<router-link class="name" :to="`/${post.user.username}`">{{ post.user.name }}</router-link>
+			<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}`">
+			<router-link class="time" :to="`/@${post.user.username}/${post.id}`">
 				<mk-time :time="post.created_at"/>
 			</router-link>
 		</header>
diff --git a/src/web/app/mobile/views/components/post.sub.vue b/src/web/app/mobile/views/components/post.sub.vue
index f1c858675..2427cefeb 100644
--- a/src/web/app/mobile/views/components/post.sub.vue
+++ b/src/web/app/mobile/views/components/post.sub.vue
@@ -1,13 +1,13 @@
 <template>
 <div class="sub">
-	<router-link class="avatar-anchor" :to="`/${post.user.username}`">
+	<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>
+			<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" :to="`/${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>
diff --git a/src/web/app/mobile/views/components/post.vue b/src/web/app/mobile/views/components/post.vue
index d53649b11..b8f9e95ee 100644
--- a/src/web/app/mobile/views/components/post.vue
+++ b/src/web/app/mobile/views/components/post.vue
@@ -5,23 +5,23 @@
 	</div>
 	<div class="repost" v-if="isRepost">
 		<p>
-			<router-link class="avatar-anchor" :to="`/${post.user.username}`">
+			<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%
 			<span>{{ '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('{')) }}</span>
-			<router-link class="name" :to="`/${post.user.username}`">{{ post.user.name }}</router-link>
+			<router-link class="name" :to="`/@${post.user.username}`">{{ post.user.name }}</router-link>
 			<span>{{ '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr('%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1) }}</span>
 		</p>
 		<mk-time :time="post.created_at"/>
 	</div>
 	<article>
-		<router-link class="avatar-anchor" :to="`/${p.user.username}`">
+		<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>
-				<router-link class="name" :to="`/${p.user.username}`">{{ p.user.name }}</router-link>
+				<router-link class="name" :to="`/@${p.user.username}`">{{ p.user.name }}</router-link>
 				<span class="is-bot" v-if="p.user.account.is_bot">bot</span>
 				<span class="username">@{{ p.user.username }}</span>
 				<div class="info">
@@ -110,7 +110,7 @@ export default Vue.extend({
 				: 0;
 		},
 		url(): string {
-			return `/${this.p.user.username}/${this.p.id}`;
+			return `/@${this.p.user.username}/${this.p.id}`;
 		},
 		urls(): string[] {
 			if (this.p.ast) {
diff --git a/src/web/app/mobile/views/components/ui.nav.vue b/src/web/app/mobile/views/components/ui.nav.vue
index b8bc2fb04..760a5b518 100644
--- a/src/web/app/mobile/views/components/ui.nav.vue
+++ b/src/web/app/mobile/views/components/ui.nav.vue
@@ -9,7 +9,7 @@
 	</transition>
 	<transition name="nav">
 		<div class="body" v-if="isOpen">
-			<router-link class="me" v-if="os.isSignedIn" :to="`/${os.i.username}`">
+			<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>
diff --git a/src/web/app/mobile/views/components/user-card.vue b/src/web/app/mobile/views/components/user-card.vue
index 729421616..bfc748866 100644
--- a/src/web/app/mobile/views/components/user-card.vue
+++ b/src/web/app/mobile/views/components/user-card.vue
@@ -1,11 +1,11 @@
 <template>
 <div class="mk-user-card">
 	<header :style="user.banner_url ? `background-image: url(${user.banner_url}?thumbnail&size=1024)` : ''">
-		<a :href="`/${user.username}`">
+		<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>
+	<a class="name" :href="`/@${user.username}`" target="_blank">{{ user.name }}</a>
 	<p class="username">@{{ user.username }}</p>
 	<mk-follow-button :user="user"/>
 </div>
diff --git a/src/web/app/mobile/views/components/user-preview.vue b/src/web/app/mobile/views/components/user-preview.vue
index 3cbc20033..a3db311d1 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">
-	<router-link class="avatar-anchor" :to="`/${user.username}`">
+	<router-link class="avatar-anchor" :to="`/@${user.username}`">
 		<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/${user.username}`">{{ user.name }}</router-link>
+			<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/pages/user.vue b/src/web/app/mobile/views/pages/user.vue
index f283050b8..ba66052e0 100644
--- a/src/web/app/mobile/views/pages/user.vue
+++ b/src/web/app/mobile/views/pages/user.vue
@@ -30,11 +30,11 @@
 						<b>{{ user.posts_count | number }}</b>
 						<i>%i18n:mobile.tags.mk-user.posts%</i>
 					</a>
-					<a :href="`${user.username}/following`">
+					<a :href="`@${user.username}/following`">
 						<b>{{ user.following_count | number }}</b>
 						<i>%i18n:mobile.tags.mk-user.following%</i>
 					</a>
-					<a :href="`${user.username}/followers`">
+					<a :href="`@${user.username}/followers`">
 						<b>{{ user.followers_count | number }}</b>
 						<i>%i18n:mobile.tags.mk-user.followers%</i>
 					</a>
diff --git a/src/web/app/mobile/views/pages/user/home.followers-you-know.vue b/src/web/app/mobile/views/pages/user/home.followers-you-know.vue
index acefcaa10..7b02020b1 100644
--- a/src/web/app/mobile/views/pages/user/home.followers-you-know.vue
+++ b/src/web/app/mobile/views/pages/user/home.followers-you-know.vue
@@ -2,7 +2,7 @@
 <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}`">
+		<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>
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 ddbced608..385e5b8dd 100644
--- a/src/web/app/mobile/views/pages/user/home.photos.vue
+++ b/src/web/app/mobile/views/pages/user/home.photos.vue
@@ -5,7 +5,7 @@
 		<a v-for="image in images"
 			class="img"
 			:style="`background-image: url(${image.media.url}?thumbnail&size=256)`"
-			:href="`/${image.post.user.username}/${image.post.id}`"
+			: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>
diff --git a/src/web/app/mobile/views/pages/welcome.vue b/src/web/app/mobile/views/pages/welcome.vue
index 563d2b28c..3384ee699 100644
--- a/src/web/app/mobile/views/pages/welcome.vue
+++ b/src/web/app/mobile/views/pages/welcome.vue
@@ -21,7 +21,7 @@
 		<mk-welcome-timeline/>
 	</div>
 	<div class="users">
-		<router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="`/${user.username}`">
+		<router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="`/@${user.username}`">
 			<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 	</div>
diff --git a/src/web/app/mobile/views/widgets/profile.vue b/src/web/app/mobile/views/widgets/profile.vue
index 6bc7bfffc..1c9d038b4 100644
--- a/src/web/app/mobile/views/widgets/profile.vue
+++ b/src/web/app/mobile/views/widgets/profile.vue
@@ -8,7 +8,7 @@
 			:src="`${os.i.avatar_url}?thumbnail&size=96`"
 			alt="avatar"
 		/>
-		<router-link :class="$style.name" :to="`/${os.i.username}`">{{ os.i.name }}</router-link>
+		<router-link :class="$style.name" :to="`/@${os.i.username}`">{{ os.i.name }}</router-link>
 	</mk-widget-container>
 </div>
 </template>

From 3e8042d4a8003a4b2cbf6ff883c0a8ba141185bd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 27 Mar 2018 12:55:58 +0900
Subject: [PATCH 0872/1250] Update docs

---
 docs/setup.en.md | 33 +++++----------------------------
 docs/setup.ja.md | 34 +++++-----------------------------
 2 files changed, 10 insertions(+), 57 deletions(-)

diff --git a/docs/setup.en.md b/docs/setup.en.md
index e7de83689..08cd16857 100644
--- a/docs/setup.en.md
+++ b/docs/setup.en.md
@@ -8,30 +8,7 @@ This guide describes how to install and setup Misskey.
 
 ----------------------------------------------------------------
 
-If you can use Docker, please see [Setup with Docker](./docker.en.md).
-
-*1.* Domains
-----------------------------------------------------------------
-Misskey requires two domains called the primary domain and the secondary domain.
-
-* The primary-domain is used to provide the main service of Misskey.
-* The secondary-domain is used to avoid vulnerabilities such as XSS.
-
-**Ensure that the secondary domain is not a subdomain of the primary domain.**
-
-### Subdomains
-Note that Misskey uses following subdomains:
-
-* **api**.*{primary domain}*
-* **auth**.*{primary domain}*
-* **docs**.*{primary domain}*
-* **ch**.*{primary domain}*
-* **stats**.*{primary domain}*
-* **status**.*{primary domain}*
-* **dev**.*{primary domain}*
-* **file**.*{secondary domain}*
-
-*2.* reCAPTCHA tokens
+*1.* reCAPTCHA tokens
 ----------------------------------------------------------------
 Misskey requires reCAPTCHA tokens.
 Please visit https://www.google.com/recaptcha/intro/ and generate keys.
@@ -45,7 +22,7 @@ npm install web-push -g
 web-push generate-vapid-keys
 ```
 
-*3.* Install dependencies
+*2.* Install dependencies
 ----------------------------------------------------------------
 Please install and setup these softwares:
 
@@ -58,13 +35,13 @@ Please install and setup these softwares:
 ##### Optional
 * [Elasticsearch](https://www.elastic.co/) - used to provide searching feature instead of MongoDB
 
-*4.* Prepare configuration
+*3.* Prepare configuration
 ----------------------------------------------------------------
 First, you need to create a `.config` directory in the directory that
 Misskey installed. And then you need to create a `default.yml` file in
 the directory. The template of configuration is available [here](./config.md).
 
-*5.* Install Misskey
+*4.* Install and build Misskey
 ----------------------------------------------------------------
 
 1. `git clone -b master git://github.com/syuilo/misskey.git`
@@ -77,7 +54,7 @@ the directory. The template of configuration is available [here](./config.md).
 2. `npm install`
 3. `npm run build`
 
-*6.* That is it.
+*5.* That is it.
 ----------------------------------------------------------------
 Well done! Now, you have an environment that run to Misskey.
 
diff --git a/docs/setup.ja.md b/docs/setup.ja.md
index 9528d1aae..9fa56acb2 100644
--- a/docs/setup.ja.md
+++ b/docs/setup.ja.md
@@ -8,31 +8,7 @@ Misskeyサーバーの構築にご関心をお寄せいただきありがとう
 
 ----------------------------------------------------------------
 
-Dockerを利用してMisskeyを構築することもできます: [Setup with Docker](./docker.en.md)。
-その場合、*3番目*以降の手順はスキップできます。
-
-*1.* ドメインの用意
-----------------------------------------------------------------
-Misskeyはプライマリ ドメインとセカンダリ ドメインを必要とします。
-
-* プライマリ ドメインはMisskeyの主要な部分を提供するために使われます。
-* セカンダリ ドメインはXSSといった脆弱性の対策に使われます。
-
-**セカンダリ ドメインがプライマリ ドメインのサブドメインであってはなりません。**
-
-### サブドメイン
-Misskeyは以下のサブドメインを使います:
-
-* **api**.*{primary domain}*
-* **auth**.*{primary domain}*
-* **docs**.*{primary domain}*
-* **ch**.*{primary domain}*
-* **stats**.*{primary domain}*
-* **status**.*{primary domain}*
-* **dev**.*{primary domain}*
-* **file**.*{secondary domain}*
-
-*2.* reCAPTCHAトークンの用意
+*1.* reCAPTCHAトークンの用意
 ----------------------------------------------------------------
 MisskeyはreCAPTCHAトークンを必要とします。
 https://www.google.com/recaptcha/intro/ にアクセスしてトークンを生成してください。
@@ -46,7 +22,7 @@ npm install web-push -g
 web-push generate-vapid-keys
 ```
 
-*3.* 依存関係をインストールする
+*2.* 依存関係をインストールする
 ----------------------------------------------------------------
 これらのソフトウェアをインストール・設定してください:
 
@@ -59,13 +35,13 @@ web-push generate-vapid-keys
 ##### オプション
 * [Elasticsearch](https://www.elastic.co/) - 検索機能を向上させるために用います。
 
-*4.* 設定ファイルを用意する
+*3.* 設定ファイルを用意する
 ----------------------------------------------------------------
 Misskeyをインストールしたディレクトリに、`.config`というディレクトリを作成し、
 その中に`default.yml`という名前で設定ファイルを作ってください。
 設定ファイルの下書きは[ここ](./config.md)にありますので、コピペしてご利用ください。
 
-*5.* Misskeyのインストール
+*4.* Misskeyのインストール(とビルド)
 ----------------------------------------------------------------
 
 1. `git clone -b master git://github.com/syuilo/misskey.git`
@@ -78,7 +54,7 @@ Misskeyをインストールしたディレクトリに、`.config`というデ
 2. `npm install`
 3. `npm run build`
 
-*6.* 以上です!
+*5.* 以上です!
 ----------------------------------------------------------------
 お疲れ様でした。これでMisskeyを動かす準備は整いました。
 

From 753fce41ec9bd6833a1de05a231ea13653608999 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 27 Mar 2018 14:13:12 +0900
Subject: [PATCH 0873/1250] Bye bye subdomains

---
 src/config.ts                 | 12 ++++-----
 src/server.ts                 |  1 -
 src/web/app/boot.js           | 16 +++++------
 src/web/app/desktop/script.ts | 39 +++++++++++++++------------
 src/web/app/dev/script.ts     | 23 +++++++++-------
 src/web/app/init.ts           |  8 +++---
 src/web/app/mobile/script.ts  | 51 +++++++++++++++++++----------------
 src/web/server.ts             |  5 +---
 8 files changed, 81 insertions(+), 74 deletions(-)

diff --git a/src/config.ts b/src/config.ts
index 349b161de..6d3e7740b 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -109,7 +109,6 @@ type Mixin = {
 	ws_url: string;
 	auth_url: string;
 	docs_url: string;
-	ch_url: string;
 	stats_url: string;
 	status_url: string;
 	dev_url: string;
@@ -135,12 +134,11 @@ export default function load() {
 	mixin.ws_scheme = mixin.scheme.replace('http', 'ws');
 	mixin.ws_url = `${mixin.ws_scheme}://${mixin.host}`;
 	mixin.api_url = `${mixin.scheme}://${mixin.host}/api`;
-	mixin.auth_url = `${mixin.scheme}://auth.${mixin.host}`;
-	mixin.ch_url = `${mixin.scheme}://ch.${mixin.host}`;
-	mixin.dev_url = `${mixin.scheme}://dev.${mixin.host}`;
-	mixin.docs_url = `${mixin.scheme}://docs.${mixin.host}`;
-	mixin.stats_url = `${mixin.scheme}://stats.${mixin.host}`;
-	mixin.status_url = `${mixin.scheme}://status.${mixin.host}`;
+	mixin.auth_url = `${mixin.scheme}://${mixin.host}/auth`;
+	mixin.dev_url = `${mixin.scheme}://${mixin.host}/dev`;
+	mixin.docs_url = `${mixin.scheme}://${mixin.host}/docs`;
+	mixin.stats_url = `${mixin.scheme}://${mixin.host}/stats`;
+	mixin.status_url = `${mixin.scheme}://${mixin.host}/status`;
 	mixin.drive_url = `${mixin.scheme}://${mixin.host}/files`;
 
 	return Object.assign(config, mixin);
diff --git a/src/server.ts b/src/server.ts
index 00d09f153..0e030002a 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -9,7 +9,6 @@ import * as cluster from 'cluster';
 import * as express from 'express';
 import * as morgan from 'morgan';
 import Accesses from 'accesses';
-import vhost = require('vhost');
 
 import log from './log-request';
 import config from './conf';
diff --git a/src/web/app/boot.js b/src/web/app/boot.js
index 00ac9daad..0846e4bd5 100644
--- a/src/web/app/boot.js
+++ b/src/web/app/boot.js
@@ -21,13 +21,13 @@
 	// Get the current url information
 	const url = new URL(location.href);
 
-	// Extarct the (sub) domain part of the current url
-	//
-	// e.g.
-	//   misskey.alice               => misskey
-	//   misskey.strawberry.pasta    => misskey
-	//   dev.misskey.arisu.tachibana => dev
-	let app = url.host === HOST ? 'misskey' : url.host.substr(0, -HOST.length);
+	//#region Detect app name
+	let app = null;
+
+	if (url.pathname == '/docs') app = 'docs';
+	if (url.pathname == '/dev') app = 'dev';
+	if (url.pathname == '/auth') app = 'auth';
+	//#endregion
 
 	// Detect the user language
 	// Note: The default language is English
@@ -57,7 +57,7 @@
 	}
 
 	// Switch desktop or mobile version
-	if (app == 'misskey') {
+	if (app == null) {
 		app = isMobile ? 'mobile' : 'desktop';
 	}
 
diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
index f2bcc6f8a..145cc45db 100644
--- a/src/web/app/desktop/script.ts
+++ b/src/web/app/desktop/script.ts
@@ -2,13 +2,15 @@
  * Desktop Client
  */
 
+import VueRouter from 'vue-router';
+
 // Style
 import './style.styl';
 import '../../element.scss';
 
 import init from '../init';
 import fuckAdBlock from '../common/scripts/fuck-ad-block';
-import HomeStreamManager from '../common/scripts/streaming/home-stream-manager';
+import { HomeStreamManager } from '../common/scripts/streaming/home';
 import composeNotification from '../common/scripts/compose-notification';
 
 import chooseDriveFolder from './api/choose-drive-folder';
@@ -41,8 +43,26 @@ init(async (launch) => {
 	require('./views/components');
 	require('./views/widgets');
 
+	// Init router
+	const router = new VueRouter({
+		mode: 'history',
+		routes: [
+			{ 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 },
+			{ path: '/search', component: MkSearch },
+			{ path: '/othello', component: MkOthello },
+			{ path: '/othello/:game', component: MkOthello },
+			{ path: '/@:user', component: MkUser },
+			{ path: '/@:user/:post', component: MkPost }
+		]
+	});
+
 	// Launch the app
-	const [app, os] = launch(os => ({
+	const [, os] = launch(router, os => ({
 		chooseDriveFolder,
 		chooseDriveFile,
 		dialog,
@@ -71,21 +91,6 @@ init(async (launch) => {
 			registerNotifications(os.stream);
 		}
 	}
-
-	// Routing
-	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 },
-		{ path: '/search', component: MkSearch },
-		{ path: '/othello', component: MkOthello },
-		{ path: '/othello/:game', component: MkOthello },
-		{ path: '/@:user', component: MkUser },
-		{ path: '/@:user/:post', component: MkPost }
-	]);
 }, true);
 
 function registerNotifications(stream: HomeStreamManager) {
diff --git a/src/web/app/dev/script.ts b/src/web/app/dev/script.ts
index 2f4a16fab..c043813b4 100644
--- a/src/web/app/dev/script.ts
+++ b/src/web/app/dev/script.ts
@@ -3,6 +3,7 @@
  */
 
 import Vue from 'vue';
+import VueRouter from 'vue-router';
 import BootstrapVue from 'bootstrap-vue';
 import 'bootstrap/dist/css/bootstrap.css';
 import 'bootstrap-vue/dist/bootstrap-vue.css';
@@ -26,14 +27,18 @@ Vue.component('mk-ui', ui);
  * init
  */
 init(launch => {
-	// Launch the app
-	const [app] = launch();
+	// Init router
+	const router = new VueRouter({
+		mode: 'history',
+		base: '/dev/',
+		routes: [
+			{ path: '/', component: Index },
+			{ path: '/apps', component: Apps },
+			{ path: '/app/new', component: AppNew },
+			{ path: '/app/:id', component: App },
+		]
+	});
 
-	// Routing
-	app.$router.addRoutes([
-		{ path: '/', component: Index },
-		{ path: '/apps', component: Apps },
-		{ path: '/app/new', component: AppNew },
-		{ path: '/app/:id', component: App },
-	]);
+	// Launch the app
+	launch(router);
 });
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index 6cc7ae6ce..521dade86 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -91,14 +91,14 @@ if (localStorage.getItem('should-refresh') == 'true') {
 }
 
 // MiOSを初期化してコールバックする
-export default (callback: (launch: (api?: (os: MiOS) => API) => [Vue, MiOS]) => void, sw = false) => {
+export default (callback: (launch: (router: VueRouter, 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?: (os: MiOS) => API) => {
+		const launch = (router: VueRouter, api?: (os: MiOS) => API) => {
 			os.apis = api ? api(os) : null;
 
 			Vue.mixin({
@@ -112,9 +112,7 @@ export default (callback: (launch: (api?: (os: MiOS) => API) => [Vue, MiOS]) =>
 			});
 
 			const app = new Vue({
-				router: new VueRouter({
-					mode: 'history'
-				}),
+				router,
 				created() {
 					this.$watch('os.i', i => {
 						// キャッシュ更新
diff --git a/src/web/app/mobile/script.ts b/src/web/app/mobile/script.ts
index 2fcb085ac..062a6d83d 100644
--- a/src/web/app/mobile/script.ts
+++ b/src/web/app/mobile/script.ts
@@ -2,6 +2,8 @@
  * Mobile Client
  */
 
+import VueRouter from 'vue-router';
+
 // Style
 import './style.styl';
 import '../../element.scss';
@@ -45,8 +47,33 @@ init((launch) => {
 	// http://qiita.com/junya/items/3ff380878f26ca447f85
 	document.body.setAttribute('ontouchstart', '');
 
+	// Init router
+	const router = new VueRouter({
+		mode: 'history',
+		routes: [
+			{ 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 },
+			{ path: '/i/drive', component: MkDrive },
+			{ path: '/i/drive/folder/:folder', component: MkDrive },
+			{ path: '/i/drive/file/:file', component: MkDrive },
+			{ path: '/selectdrive', component: MkSelectDrive },
+			{ path: '/search', component: MkSearch },
+			{ path: '/othello', component: MkOthello },
+			{ path: '/othello/:game', component: MkOthello },
+			{ path: '/@:user', component: MkUser },
+			{ path: '/@:user/followers', component: MkFollowers },
+			{ path: '/@:user/following', component: MkFollowing },
+			{ path: '/@:user/:post', component: MkPost }
+		]
+	});
+
 	// Launch the app
-	const [app] = launch(os => ({
+	launch(router, os => ({
 		chooseDriveFolder,
 		chooseDriveFile,
 		dialog,
@@ -54,26 +81,4 @@ init((launch) => {
 		post: post(os),
 		notify
 	}));
-
-	// Routing
-	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 },
-		{ path: '/i/drive', component: MkDrive },
-		{ path: '/i/drive/folder/:folder', component: MkDrive },
-		{ path: '/i/drive/file/:file', component: MkDrive },
-		{ path: '/selectdrive', component: MkSelectDrive },
-		{ path: '/search', component: MkSearch },
-		{ path: '/othello', component: MkOthello },
-		{ path: '/othello/:game', component: MkOthello },
-		{ 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/server.ts b/src/web/server.ts
index 062d1f197..b117f6ae8 100644
--- a/src/web/server.ts
+++ b/src/web/server.ts
@@ -10,9 +10,6 @@ import * as express from 'express';
 import * as bodyParser from 'body-parser';
 import * as favicon from 'serve-favicon';
 import * as compression from 'compression';
-import vhost = require('vhost');
-
-import config from '../conf';
 
 /**
  * Init app
@@ -20,7 +17,7 @@ import config from '../conf';
 const app = express();
 app.disable('x-powered-by');
 
-app.use(vhost(`docs.${config.host}`, require('./docs/server')));
+app.use('/docs', require('./docs/server'));
 
 app.use(bodyParser.urlencoded({ extended: true }));
 app.use(bodyParser.json({

From dc78aa1b11a96d41574dc55f8f5ad760534bef25 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 27 Mar 2018 14:17:38 +0900
Subject: [PATCH 0874/1250] =?UTF-8?q?=E3=81=97=E3=82=85=E3=81=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 locales/ja.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/locales/ja.yml b/locales/ja.yml
index 86b594990..f826b1b6c 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -24,7 +24,7 @@ common:
 
   reactions:
     like: "いいね"
-    love: "ハート"
+    love: "しゅき"
     laugh: "笑"
     hmm: "ふぅ~む"
     surprise: "わお"

From fa4a66120cab818cf32144f4c55316687565c1a5 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Tue, 27 Mar 2018 14:20:41 +0900
Subject: [PATCH 0875/1250] Update config.md

---
 docs/config.md | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/docs/config.md b/docs/config.md
index 45c6b7cfc..c4a54c0be 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -7,12 +7,9 @@ maintainer:
   # メンテナの連絡先(URLかmailto形式のURL)
   url:
 
-# プライマリURL
+# (Misskeyを動かす)URL
 url:
 
-# セカンダリURL
-secondary_url:
-
 # 待受ポート
 port:
 

From 8a2bf0116722cd87a2c088a84d9beeb01be96c55 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Tue, 27 Mar 2018 15:57:42 +0900
Subject: [PATCH 0876/1250] Add host_lower to User

---
 src/api/models/user.ts                              | 1 +
 src/api/private/signup.ts                           | 1 +
 tools/migration/shell.1522116710.user-host_lower.js | 1 +
 3 files changed, 3 insertions(+)
 create mode 100644 tools/migration/shell.1522116710.user-host_lower.js

diff --git a/src/api/models/user.ts b/src/api/models/user.ts
index 08d7fbb8c..a3ffbd1f7 100644
--- a/src/api/models/user.ts
+++ b/src/api/models/user.ts
@@ -59,6 +59,7 @@ export type IUser = {
 	is_suspended: boolean;
 	keywords: string[];
 	host: string;
+	host_lower: string;
 	account: {
 		keypair: string;
 		email: string;
diff --git a/src/api/private/signup.ts b/src/api/private/signup.ts
index a4c06b5f5..280153d4f 100644
--- a/src/api/private/signup.ts
+++ b/src/api/private/signup.ts
@@ -120,6 +120,7 @@ export default async (req: express.Request, res: express.Response) => {
 		username: username,
 		username_lower: username.toLowerCase(),
 		host: null,
+		host_lower: null,
 		account: {
 			keypair: generateKeypair(),
 			token: secret,
diff --git a/tools/migration/shell.1522116710.user-host_lower.js b/tools/migration/shell.1522116710.user-host_lower.js
new file mode 100644
index 000000000..31ec6c468
--- /dev/null
+++ b/tools/migration/shell.1522116710.user-host_lower.js
@@ -0,0 +1 @@
+db.users.update({ }, { $set: { host_lower: null } }, { multi: true });

From 055c522e6b8d1fecc813891deecb2da752afff44 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Tue, 27 Mar 2018 16:04:22 +0900
Subject: [PATCH 0877/1250] Revert a change for AuthSess query

---
 src/api/endpoints/auth/accept.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/api/endpoints/auth/accept.ts b/src/api/endpoints/auth/accept.ts
index 8955738eb..4ee20a6d2 100644
--- a/src/api/endpoints/auth/accept.ts
+++ b/src/api/endpoints/auth/accept.ts
@@ -45,7 +45,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Fetch token
 	const session = await AuthSess
-		.findOne({ 'account.token': token });
+		.findOne({ token: token });
 
 	if (session === null) {
 		return rej('session not found');

From d0e9d76ed2dd88f37a3db3b9e368be510ac50073 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Tue, 27 Mar 2018 16:40:10 +0900
Subject: [PATCH 0878/1250] Describe host field of user entity

---
 src/web/docs/api/entities/user.yaml | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/web/docs/api/entities/user.yaml b/src/web/docs/api/entities/user.yaml
index f0d5fdbba..a451a4080 100644
--- a/src/web/docs/api/entities/user.yaml
+++ b/src/web/docs/api/entities/user.yaml
@@ -105,6 +105,12 @@ props:
     desc:
       ja: "ドライブの容量(bytes)"
       en: "The capacity of drive of this user (bytes)"
+  - name: "host"
+    type: "string | null"
+    optional: false
+    desc:
+      ja: "ホスト (例: example.com:3000)"
+      en: "Host (e.g. example.com:3000)"
   - name: "account"
     type: "object"
     optional: false

From c20b22cf2271aa1ed89530b7d3dc84cf2f5fd324 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Tue, 27 Mar 2018 16:51:12 +0900
Subject: [PATCH 0879/1250] Implement remote account resolution

---
 package.json                                  |   3 +
 src/api/bot/core.ts                           |  14 +-
 src/api/bot/interfaces/line.ts                |  12 +-
 .../add-file.ts}                              |  15 +-
 src/api/common/drive/upload_from_url.ts       |  46 +++++
 src/api/common/get-host-lower.ts              |   5 +
 src/api/common/text/elements/mention.ts       |   7 +-
 src/api/endpoints/drive/files/create.ts       |   2 +-
 .../endpoints/drive/files/upload_from_url.ts  |  46 +----
 src/api/endpoints/posts/create.ts             |  12 +-
 src/api/endpoints/username/available.ts       |   1 +
 src/api/endpoints/users/posts.ts              |  13 +-
 src/api/endpoints/users/recommendation.ts     |  12 +-
 src/api/endpoints/users/show.ts               | 191 ++++++++++++++++--
 src/api/limitter.ts                           |   5 +-
 src/api/models/user.ts                        | 104 +++++-----
 src/api/private/signin.ts                     |  13 +-
 src/api/private/signup.ts                     |   3 +-
 src/api/service/twitter.ts                    |   3 +
 src/api/streaming.ts                          |   1 +
 src/common/get-user-summary.ts                |  12 --
 src/common/user/get-acct.ts                   |   3 +
 src/common/user/get-summary.ts                |  18 ++
 src/common/user/parse-acct.ts                 |   4 +
 src/web/app/ch/tags/channel.tag               |   7 +-
 .../common/views/components/autocomplete.vue  |   4 +-
 .../components/messaging-room.message.vue     |   6 +-
 .../app/common/views/components/messaging.vue |   8 +-
 .../app/common/views/components/post-html.ts  |   5 +-
 .../views/components/welcome-timeline.vue     |  10 +-
 src/web/app/desktop/script.ts                 |   2 +-
 .../views/components/friends-maker.vue        |   9 +-
 .../components/messaging-room-window.vue      |   3 +-
 .../views/components/notifications.vue        |  42 ++--
 .../views/components/post-detail.sub.vue      |  12 +-
 .../desktop/views/components/post-detail.vue  |  18 +-
 .../desktop/views/components/post-preview.vue |  12 +-
 .../views/components/posts.post.sub.vue       |  12 +-
 .../desktop/views/components/posts.post.vue   |  18 +-
 .../views/components/settings.mute.vue        |   6 +-
 .../desktop/views/components/user-preview.vue |  22 +-
 .../views/components/users-list.item.vue      |  15 +-
 .../desktop/views/pages/messaging-room.vue    |   5 +-
 .../pages/user/user.followers-you-know.vue    |   7 +-
 .../desktop/views/pages/user/user.friends.vue |  11 +-
 .../desktop/views/pages/user/user.header.vue  |  16 +-
 .../desktop/views/pages/user/user.home.vue    |   2 +-
 .../desktop/views/pages/user/user.profile.vue |   4 +-
 src/web/app/desktop/views/pages/user/user.vue |   5 +-
 src/web/app/desktop/views/pages/welcome.vue   |   4 +-
 .../views/widgets/channel.channel.post.vue    |  11 +-
 src/web/app/desktop/views/widgets/polls.vue   |  11 +-
 src/web/app/desktop/views/widgets/trends.vue  |  11 +-
 src/web/app/desktop/views/widgets/users.vue   |   8 +-
 src/web/app/mobile/script.ts                  |   2 +-
 .../mobile/views/components/notification.vue  |  28 ++-
 .../app/mobile/views/components/post-card.vue |   8 +-
 .../views/components/post-detail.sub.vue      |  17 +-
 .../mobile/views/components/post-detail.vue   |  19 +-
 .../mobile/views/components/post-preview.vue  |  17 +-
 .../app/mobile/views/components/post.sub.vue  |  17 +-
 src/web/app/mobile/views/components/post.vue  |  21 +-
 .../app/mobile/views/components/user-card.vue |  15 +-
 .../mobile/views/components/user-preview.vue  |  15 +-
 src/web/app/mobile/views/pages/followers.vue  |   5 +-
 src/web/app/mobile/views/pages/following.vue  |   5 +-
 .../app/mobile/views/pages/messaging-room.vue |   6 +-
 src/web/app/mobile/views/pages/messaging.vue  |   4 +-
 src/web/app/mobile/views/pages/user.vue       |  19 +-
 .../pages/user/home.followers-you-know.vue    |   7 +-
 .../mobile/views/pages/user/home.photos.vue   |   7 +-
 src/web/app/mobile/views/pages/user/home.vue  |   2 +-
 test/text.js                                  |   4 +-
 73 files changed, 735 insertions(+), 334 deletions(-)
 rename src/api/common/{add-file-to-drive.ts => drive/add-file.ts} (94%)
 create mode 100644 src/api/common/drive/upload_from_url.ts
 create mode 100644 src/api/common/get-host-lower.ts
 delete mode 100644 src/common/get-user-summary.ts
 create mode 100644 src/common/user/get-acct.ts
 create mode 100644 src/common/user/get-summary.ts
 create mode 100644 src/common/user/parse-acct.ts

diff --git a/package.json b/package.json
index eee658fbd..d9ed80b47 100644
--- a/package.json
+++ b/package.json
@@ -134,6 +134,7 @@
 		"is-root": "2.0.0",
 		"is-url": "1.2.3",
 		"js-yaml": "3.11.0",
+		"jsdom": "^11.6.2",
 		"license-checker": "18.0.0",
 		"loader-utils": "1.1.0",
 		"mecab-async": "0.1.2",
@@ -156,6 +157,7 @@
 		"prominence": "0.2.0",
 		"proxy-addr": "2.0.3",
 		"pug": "2.0.3",
+		"punycode": "^2.1.0",
 		"qrcode": "1.2.0",
 		"ratelimiter": "3.0.3",
 		"recaptcha-promise": "0.1.3",
@@ -198,6 +200,7 @@
 		"vue-template-compiler": "2.5.16",
 		"vuedraggable": "2.16.0",
 		"web-push": "3.3.0",
+		"webfinger.js": "^2.6.6",
 		"webpack": "4.2.0",
 		"webpack-cli": "2.0.13",
 		"webpack-replace-loader": "1.3.0",
diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
index ad29f1003..77a68aaee 100644
--- a/src/api/bot/core.ts
+++ b/src/api/bot/core.ts
@@ -1,10 +1,11 @@
 import * as EventEmitter from 'events';
 import * as bcrypt from 'bcryptjs';
 
-import User, { IUser, init as initUser } from '../models/user';
+import User, { ILocalAccount, IUser, init as initUser } from '../models/user';
 
 import getPostSummary from '../../common/get-post-summary';
-import getUserSummary from '../../common/get-user-summary';
+import getUserSummary from '../../common/user/get-summary';
+import parseAcct from '../../common/user/parse-acct';
 import getNotificationSummary from '../../common/get-notification-summary';
 
 const hmm = [
@@ -163,9 +164,7 @@ export default class BotCore extends EventEmitter {
 
 	public async showUserCommand(q: string): Promise<string> {
 		try {
-			const user = await require('../endpoints/users/show')({
-				username: q.substr(1)
-			}, this.user);
+			const user = await require('../endpoints/users/show')(parseAcct(q.substr(1)), this.user);
 
 			const text = getUserSummary(user);
 
@@ -209,7 +208,8 @@ class SigninContext extends Context {
 		if (this.temporaryUser == null) {
 			// Fetch user
 			const user: IUser = await User.findOne({
-				username_lower: query.toLowerCase()
+				username_lower: query.toLowerCase(),
+				host: null
 			}, {
 				fields: {
 					data: false
@@ -225,7 +225,7 @@ class SigninContext extends Context {
 			}
 		} else {
 			// Compare password
-			const same = await bcrypt.compare(query, this.temporaryUser.account.password);
+			const same = await bcrypt.compare(query, (this.temporaryUser.account as ILocalAccount).password);
 
 			if (same) {
 				this.bot.signin(this.temporaryUser);
diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts
index 6b2ebdec8..8036b2fde 100644
--- a/src/api/bot/interfaces/line.ts
+++ b/src/api/bot/interfaces/line.ts
@@ -7,6 +7,8 @@ import config from '../../../conf';
 import BotCore from '../core';
 import _redis from '../../../db/redis';
 import prominence = require('prominence');
+import getAcct from '../../../common/user/get-acct';
+import parseAcct from '../../../common/user/parse-acct';
 import getPostSummary from '../../../common/get-post-summary';
 
 const redis = prominence(_redis);
@@ -98,10 +100,9 @@ class LineBot extends BotCore {
 	}
 
 	public async showUserCommand(q: string) {
-		const user = await require('../../endpoints/users/show')({
-			username: q.substr(1)
-		}, this.user);
+		const user = await require('../../endpoints/users/show')(parseAcct(q.substr(1)), this.user);
 
+		const acct = getAcct(user);
 		const actions = [];
 
 		actions.push({
@@ -121,7 +122,7 @@ class LineBot extends BotCore {
 		actions.push({
 			type: 'uri',
 			label: 'Webで見る',
-			uri: `${config.url}/@${user.username}`
+			uri: `${config.url}/@${acct}`
 		});
 
 		this.reply([{
@@ -130,7 +131,7 @@ class LineBot extends BotCore {
 			template: {
 				type: 'buttons',
 				thumbnailImageUrl: `${user.avatar_url}?thumbnail&size=1024`,
-				title: `${user.name} (@${user.username})`,
+				title: `${user.name} (@${acct})`,
 				text: user.description || '(no description)',
 				actions: actions
 			}
@@ -171,6 +172,7 @@ module.exports = async (app: express.Application) => {
 
 		if (session == null) {
 			const user = await User.findOne({
+				host: null,
 				'account.line': {
 					user_id: sourceId
 				}
diff --git a/src/api/common/add-file-to-drive.ts b/src/api/common/drive/add-file.ts
similarity index 94%
rename from src/api/common/add-file-to-drive.ts
rename to src/api/common/drive/add-file.ts
index 1ee455c09..c4f2f212a 100644
--- a/src/api/common/add-file-to-drive.ts
+++ b/src/api/common/drive/add-file.ts
@@ -10,17 +10,18 @@ import * as debug from 'debug';
 import fileType = require('file-type');
 import prominence = require('prominence');
 
-import DriveFile, { getGridFSBucket } from '../models/drive-file';
-import DriveFolder from '../models/drive-folder';
-import { pack } from '../models/drive-file';
-import event, { publishDriveStream } from '../event';
-import config from '../../conf';
+import DriveFile, { getGridFSBucket } from '../../models/drive-file';
+import DriveFolder from '../../models/drive-folder';
+import { pack } from '../../models/drive-file';
+import event, { publishDriveStream } from '../../event';
+import getAcct from '../../../common/user/get-acct';
+import config from '../../../conf';
 
 const gm = _gm.subClass({
 	imageMagick: true
 });
 
-const log = debug('misskey:register-drive-file');
+const log = debug('misskey:drive:add-file');
 
 const tmpFile = (): Promise<string> => new Promise((resolve, reject) => {
 	tmp.file((e, path) => {
@@ -46,7 +47,7 @@ const addFile = async (
 	folderId: mongodb.ObjectID = null,
 	force: boolean = false
 ) => {
-	log(`registering ${name} (user: ${user.username}, path: ${path})`);
+	log(`registering ${name} (user: ${getAcct(user)}, path: ${path})`);
 
 	// Calculate hash, get content type and get file size
 	const [hash, [mime, ext], size] = await Promise.all([
diff --git a/src/api/common/drive/upload_from_url.ts b/src/api/common/drive/upload_from_url.ts
new file mode 100644
index 000000000..5dd969593
--- /dev/null
+++ b/src/api/common/drive/upload_from_url.ts
@@ -0,0 +1,46 @@
+import * as URL from 'url';
+import { IDriveFile, validateFileName } from '../../models/drive-file';
+import create from './add-file';
+import * as debug from 'debug';
+import * as tmp from 'tmp';
+import * as fs from 'fs';
+import * as request from 'request';
+
+const log = debug('misskey:common:drive:upload_from_url');
+
+export default async (url, user, folderId = null): Promise<IDriveFile> => {
+	let name = URL.parse(url).pathname.split('/').pop();
+	if (!validateFileName(name)) {
+		name = null;
+	}
+
+	// Create temp file
+	const path = await new Promise((res: (string) => void, rej) => {
+		tmp.file((e, path) => {
+			if (e) return rej(e);
+			res(path);
+		});
+	});
+
+	// write content at URL to temp file
+	await new Promise((res, rej) => {
+		const writable = fs.createWriteStream(path);
+		request(url)
+			.on('error', rej)
+			.on('end', () => {
+				writable.close();
+				res(path);
+			})
+			.pipe(writable)
+			.on('error', rej);
+	});
+
+	const driveFile = await create(user, path, name, null, folderId);
+
+	// clean-up
+	fs.unlink(path, (e) => {
+		if (e) log(e.stack);
+	});
+
+	return driveFile;
+};
diff --git a/src/api/common/get-host-lower.ts b/src/api/common/get-host-lower.ts
new file mode 100644
index 000000000..fc4b30439
--- /dev/null
+++ b/src/api/common/get-host-lower.ts
@@ -0,0 +1,5 @@
+import { toUnicode } from 'punycode';
+
+export default host => {
+	return toUnicode(host).replace(/[A-Z]+/, match => match.toLowerCase());
+};
diff --git a/src/api/common/text/elements/mention.ts b/src/api/common/text/elements/mention.ts
index e0fac4dd7..2025dfdaa 100644
--- a/src/api/common/text/elements/mention.ts
+++ b/src/api/common/text/elements/mention.ts
@@ -1,14 +1,17 @@
 /**
  * Mention
  */
+import parseAcct from '../../../../common/user/parse-acct';
 
 module.exports = text => {
-	const match = text.match(/^@[a-zA-Z0-9\-]+/);
+	const match = text.match(/^(?:@[a-zA-Z0-9\-]+){1,2}/);
 	if (!match) return null;
 	const mention = match[0];
+	const { username, host } = parseAcct(mention.substr(1));
 	return {
 		type: 'mention',
 		content: mention,
-		username: mention.substr(1)
+		username,
+		host
 	};
 };
diff --git a/src/api/endpoints/drive/files/create.ts b/src/api/endpoints/drive/files/create.ts
index 96bcace88..db801b61f 100644
--- a/src/api/endpoints/drive/files/create.ts
+++ b/src/api/endpoints/drive/files/create.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import { validateFileName, pack } from '../../../models/drive-file';
-import create from '../../../common/add-file-to-drive';
+import create from '../../../common/drive/add-file';
 
 /**
  * Create a file
diff --git a/src/api/endpoints/drive/files/upload_from_url.ts b/src/api/endpoints/drive/files/upload_from_url.ts
index 68428747e..346633c61 100644
--- a/src/api/endpoints/drive/files/upload_from_url.ts
+++ b/src/api/endpoints/drive/files/upload_from_url.ts
@@ -1,16 +1,9 @@
 /**
  * Module dependencies
  */
-import * as URL from 'url';
 import $ from 'cafy';
-import { validateFileName, pack } from '../../../models/drive-file';
-import create from '../../../common/add-file-to-drive';
-import * as debug from 'debug';
-import * as tmp from 'tmp';
-import * as fs from 'fs';
-import * as request from 'request';
-
-const log = debug('misskey:endpoint:upload_from_url');
+import { pack } from '../../../models/drive-file';
+import uploadFromUrl from '../../../common/drive/upload_from_url';
 
 /**
  * Create a file from a URL
@@ -25,42 +18,9 @@ module.exports = async (params, user): Promise<any> => {
 	const [url, urlErr] = $(params.url).string().$;
 	if (urlErr) throw 'invalid url param';
 
-	let name = URL.parse(url).pathname.split('/').pop();
-	if (!validateFileName(name)) {
-		name = null;
-	}
-
 	// Get 'folder_id' parameter
 	const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
 	if (folderIdErr) throw 'invalid folder_id param';
 
-	// Create temp file
-	const path = await new Promise((res: (string) => void, rej) => {
-		tmp.file((e, path) => {
-			if (e) return rej(e);
-			res(path);
-		});
-	});
-
-	// write content at URL to temp file
-	await new Promise((res, rej) => {
-		const writable = fs.createWriteStream(path);
-		request(url)
-			.on('error', rej)
-			.on('end', () => {
-				writable.close();
-				res(path);
-			})
-			.pipe(writable)
-			.on('error', rej);
-	});
-
-	const driveFile = await create(user, path, name, null, folderId);
-
-	// clean-up
-	fs.unlink(path, (e) => {
-		if (e) log(e.stack);
-	});
-
-	return pack(driveFile);
+	return pack(await uploadFromUrl(url, user, folderId));
 };
diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index f46a84e1f..286e18bb7 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -5,7 +5,7 @@ import $ from 'cafy';
 import deepEqual = require('deep-equal');
 import parse from '../../common/text';
 import { default as Post, IPost, isValidText } from '../../models/post';
-import { default as User, IUser } from '../../models/user';
+import { default as User, ILocalAccount, IUser } from '../../models/user';
 import { default as Channel, IChannel } from '../../models/channel';
 import Following from '../../models/following';
 import Mute from '../../models/mute';
@@ -16,6 +16,8 @@ import { pack } from '../../models/post';
 import notify from '../../common/notify';
 import watch from '../../common/watch-post';
 import event, { pushSw, publishChannelStream } from '../../event';
+import getAcct from '../../../common/user/get-acct';
+import parseAcct from '../../../common/user/parse-acct';
 import config from '../../../conf';
 
 /**
@@ -390,7 +392,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 			});
 
 		// この投稿をWatchする
-		if (user.account.settings.auto_watch !== false) {
+		if ((user.account as ILocalAccount).settings.auto_watch !== false) {
 			watch(user._id, reply);
 		}
 
@@ -477,7 +479,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		// Extract an '@' mentions
 		const atMentions = tokens
 			.filter(t => t.type == 'mention')
-			.map(m => m.username)
+			.map(getAcct)
 			// Drop dupulicates
 			.filter((v, i, s) => s.indexOf(v) == i);
 
@@ -486,9 +488,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 			// Fetch mentioned user
 			// SELECT _id
 			const mentionee = await User
-				.findOne({
-					username_lower: mention.toLowerCase()
-				}, { _id: true });
+				.findOne(parseAcct(mention), { _id: true });
 
 			// When mentioned user not found
 			if (mentionee == null) return;
diff --git a/src/api/endpoints/username/available.ts b/src/api/endpoints/username/available.ts
index 3be7bcba3..aac7fadf5 100644
--- a/src/api/endpoints/username/available.ts
+++ b/src/api/endpoints/username/available.ts
@@ -19,6 +19,7 @@ module.exports = async (params) => new Promise(async (res, rej) => {
 	// Get exist
 	const exist = await User
 		.count({
+			host: null,
 			username_lower: username.toLowerCase()
 		}, {
 			limit: 1
diff --git a/src/api/endpoints/users/posts.ts b/src/api/endpoints/users/posts.ts
index 0c8bceee3..3c84bf0d8 100644
--- a/src/api/endpoints/users/posts.ts
+++ b/src/api/endpoints/users/posts.ts
@@ -2,6 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
+import getHostLower from '../../common/get-host-lower';
 import Post, { pack } from '../../models/post';
 import User from '../../models/user';
 
@@ -22,7 +23,15 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	if (usernameErr) return rej('invalid username param');
 
 	if (userId === undefined && username === undefined) {
-		return rej('user_id or username is required');
+		return rej('user_id or pair of username and host is required');
+	}
+
+	// Get 'host' parameter
+	const [host, hostErr] = $(params.host).optional.string().$;
+	if (hostErr) return rej('invalid host param');
+
+	if (userId === undefined && host === undefined) {
+		return rej('user_id or pair of username and host is required');
 	}
 
 	// Get 'include_replies' parameter
@@ -60,7 +69,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	const q = userId !== undefined
 		? { _id: userId }
-		: { username_lower: username.toLowerCase() } ;
+		: { username_lower: username.toLowerCase(), host_lower: getHostLower(host) } ;
 
 	// Lookup user
 	const user = await User.findOne(q, {
diff --git a/src/api/endpoints/users/recommendation.ts b/src/api/endpoints/users/recommendation.ts
index f1f5bcd0a..45d90f422 100644
--- a/src/api/endpoints/users/recommendation.ts
+++ b/src/api/endpoints/users/recommendation.ts
@@ -30,9 +30,15 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 			_id: {
 				$nin: followingIds
 			},
-			'account.last_used_at': {
-				$gte: new Date(Date.now() - ms('7days'))
-			}
+			$or: [
+				{
+					'account.last_used_at': {
+						$gte: new Date(Date.now() - ms('7days'))
+					}
+				}, {
+					host: { $not: null }
+				}
+			]
 		}, {
 			limit: limit,
 			skip: offset,
diff --git a/src/api/endpoints/users/show.ts b/src/api/endpoints/users/show.ts
index 7aea59296..78df23f33 100644
--- a/src/api/endpoints/users/show.ts
+++ b/src/api/endpoints/users/show.ts
@@ -2,7 +2,49 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User, { pack } from '../../models/user';
+import { JSDOM } from 'jsdom';
+import { toUnicode, toASCII } from 'punycode';
+import uploadFromUrl from '../../common/drive/upload_from_url';
+import User, { pack, validateUsername, isValidName, isValidDescription } from '../../models/user';
+const request = require('request-promise-native');
+const WebFinger = require('webfinger.js');
+
+const webFinger = new WebFinger({});
+
+async function getCollectionCount(url) {
+	if (!url) {
+		return null;
+	}
+
+	try {
+		const collection = await request({ url, json: true });
+		return collection ? collection.totalItems : null;
+	} catch (exception) {
+		return null;
+	}
+}
+
+function findUser(q) {
+	return User.findOne(q, {
+		fields: {
+			data: false
+		}
+	});
+}
+
+function webFingerAndVerify(query, verifier) {
+	return new Promise((res, rej) => webFinger.lookup(query, (error, result) => {
+		if (error) {
+			return rej(error);
+		}
+
+		if (result.object.subject.toLowerCase().replace(/^acct:/, '') !== verifier) {
+			return rej('WebFinger verfification failed');
+		}
+
+		res(result.object);
+	}));
+}
 
 /**
  * Show a user
@@ -12,6 +54,8 @@ import User, { pack } from '../../models/user';
  * @return {Promise<any>}
  */
 module.exports = (params, me) => new Promise(async (res, rej) => {
+	let user;
+
 	// Get 'user_id' parameter
 	const [userId, userIdErr] = $(params.user_id).optional.id().$;
 	if (userIdErr) return rej('invalid user_id param');
@@ -20,23 +64,142 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [username, usernameErr] = $(params.username).optional.string().$;
 	if (usernameErr) return rej('invalid username param');
 
-	if (userId === undefined && username === undefined) {
-		return rej('user_id or username is required');
+	// Get 'host' parameter
+	const [host, hostErr] = $(params.host).optional.string().$;
+	if (hostErr) return rej('invalid username param');
+
+	if (userId === undefined && typeof username !== 'string') {
+		return rej('user_id or pair of username and host is required');
 	}
 
-	const q = userId !== undefined
-		? { _id: userId }
-		: { username_lower: username.toLowerCase() };
-
 	// Lookup user
-	const user = await User.findOne(q, {
-		fields: {
-			data: false
-		}
-	});
+	if (typeof host === 'string') {
+		const username_lower = username.toLowerCase();
+		const host_lower_ascii = toASCII(host).toLowerCase();
+		const host_lower = toUnicode(host_lower_ascii);
 
-	if (user === null) {
-		return rej('user not found');
+		user = await findUser({ username_lower, host_lower });
+
+		if (user === null) {
+			const acct_lower = `${username_lower}@${host_lower_ascii}`;
+			let activityStreams;
+			let finger;
+			let followers_count;
+			let following_count;
+			let likes_count;
+			let posts_count;
+
+			if (!validateUsername(username)) {
+				return rej('username validation failed');
+			}
+
+			try {
+				finger = await webFingerAndVerify(acct_lower, acct_lower);
+			} catch (exception) {
+				return rej('WebFinger lookup failed');
+			}
+
+			const self = finger.links.find(link => link.rel && link.rel.toLowerCase() === 'self');
+			if (!self) {
+				return rej('WebFinger has no reference to self representation');
+			}
+
+			try {
+				activityStreams = await request({
+					url: self.href,
+					headers: {
+						Accept: 'application/activity+json, application/ld+json'
+					},
+					json: true
+				});
+			} catch (exception) {
+				return rej('failed to retrieve ActivityStreams representation');
+			}
+
+			if (!(activityStreams &&
+				(Array.isArray(activityStreams['@context']) ?
+					activityStreams['@context'].includes('https://www.w3.org/ns/activitystreams') :
+					activityStreams['@context'] === 'https://www.w3.org/ns/activitystreams') &&
+				activityStreams.type === 'Person' &&
+				typeof activityStreams.preferredUsername === 'string' &&
+				activityStreams.preferredUsername.toLowerCase() === username_lower &&
+				isValidName(activityStreams.name) &&
+				isValidDescription(activityStreams.summary)
+			)) {
+				return rej('failed ActivityStreams validation');
+			}
+
+			try {
+				[followers_count, following_count, likes_count, posts_count] = await Promise.all([
+					getCollectionCount(activityStreams.followers),
+					getCollectionCount(activityStreams.following),
+					getCollectionCount(activityStreams.liked),
+					getCollectionCount(activityStreams.outbox),
+					webFingerAndVerify(activityStreams.id, acct_lower),
+				]);
+			} catch (exception) {
+				return rej('failed to fetch assets');
+			}
+
+			const summaryDOM = JSDOM.fragment(activityStreams.summary);
+
+			// Create user
+			user = await User.insert({
+				avatar_id: null,
+				banner_id: null,
+				created_at: new Date(),
+				description: summaryDOM.textContent,
+				followers_count,
+				following_count,
+				name: activityStreams.name,
+				posts_count,
+				likes_count,
+				liked_count: 0,
+				drive_capacity: 1073741824, // 1GB
+				username: username,
+				username_lower,
+				host: toUnicode(finger.subject.replace(/^.*?@/, '')),
+				host_lower,
+				account: {
+					uri: activityStreams.id,
+				},
+			});
+
+			const [icon, image] = await Promise.all([
+				activityStreams.icon,
+				activityStreams.image,
+			].map(async image => {
+				if (!image || image.type !== 'Image') {
+					return { _id: null };
+				}
+
+				try {
+					return await uploadFromUrl(image.url, user);
+				} catch (exception) {
+					return { _id: null };
+				}
+			}));
+
+			User.update({ _id: user._id }, {
+				$set: {
+					avatar_id: icon._id,
+					banner_id: image._id,
+				},
+			});
+
+			user.avatar_id = icon._id;
+			user.banner_id = icon._id;
+		}
+	} else {
+		const q = userId !== undefined
+			? { _id: userId }
+			: { username_lower: username.toLowerCase(), host: null };
+
+		user = await findUser(q);
+
+		if (user === null) {
+			return rej('user not found');
+		}
 	}
 
 	// Send response
diff --git a/src/api/limitter.ts b/src/api/limitter.ts
index 10c50c340..9d2c42d33 100644
--- a/src/api/limitter.ts
+++ b/src/api/limitter.ts
@@ -3,6 +3,7 @@ import * as debug from 'debug';
 import limiterDB from '../db/redis';
 import { Endpoint } from './endpoints';
 import { IAuthContext } from './authenticate';
+import getAcct from '../common/user/get-acct';
 
 const log = debug('misskey:limitter');
 
@@ -42,7 +43,7 @@ export default (endpoint: Endpoint, ctx: IAuthContext) => new Promise((ok, rejec
 				return reject('ERR');
 			}
 
-			log(`@${ctx.user.username} ${endpoint.name} min remaining: ${info.remaining}`);
+			log(`@${getAcct(ctx.user)} ${endpoint.name} min remaining: ${info.remaining}`);
 
 			if (info.remaining === 0) {
 				reject('BRIEF_REQUEST_INTERVAL');
@@ -70,7 +71,7 @@ export default (endpoint: Endpoint, ctx: IAuthContext) => new Promise((ok, rejec
 				return reject('ERR');
 			}
 
-			log(`@${ctx.user.username} ${endpoint.name} max remaining: ${info.remaining}`);
+			log(`@${getAcct(ctx.user)} ${endpoint.name} max remaining: ${info.remaining}`);
 
 			if (info.remaining === 0) {
 				reject('RATE_LIMIT_EXCEEDED');
diff --git a/src/api/models/user.ts b/src/api/models/user.ts
index 46d32963b..e73c95faf 100644
--- a/src/api/models/user.ts
+++ b/src/api/models/user.ts
@@ -39,6 +39,39 @@ export function isValidBirthday(birthday: string): boolean {
 	return typeof birthday == 'string' && /^([0-9]{4})\-([0-9]{2})-([0-9]{2})$/.test(birthday);
 }
 
+export type ILocalAccount = {
+	keypair: string;
+	email: string;
+	links: string[];
+	password: string;
+	token: string;
+	twitter: {
+		access_token: string;
+		access_token_secret: string;
+		user_id: string;
+		screen_name: string;
+	};
+	line: {
+		user_id: string;
+	};
+	profile: {
+		location: string;
+		birthday: string; // 'YYYY-MM-DD'
+		tags: string[];
+	};
+	last_used_at: Date;
+	is_bot: boolean;
+	is_pro: boolean;
+	two_factor_secret: string;
+	two_factor_enabled: boolean;
+	client_settings: any;
+	settings: any;
+};
+
+export type IRemoteAccount = {
+	uri: string;
+};
+
 export type IUser = {
 	_id: mongo.ObjectID;
 	created_at: Date;
@@ -60,34 +93,7 @@ export type IUser = {
 	keywords: string[];
 	host: string;
 	host_lower: string;
-	account: {
-		keypair: string;
-		email: string;
-		links: string[];
-		password: string;
-		token: string;
-		twitter: {
-			access_token: string;
-			access_token_secret: string;
-			user_id: string;
-			screen_name: string;
-		};
-		line: {
-			user_id: string;
-		};
-		profile: {
-			location: string;
-			birthday: string; // 'YYYY-MM-DD'
-			tags: string[];
-		};
-		last_used_at: Date;
-		is_bot: boolean;
-		is_pro: boolean;
-		two_factor_secret: string;
-		two_factor_enabled: boolean;
-		client_settings: any;
-		settings: any;
-	};
+	account: ILocalAccount | IRemoteAccount;
 };
 
 export function init(user): IUser {
@@ -162,28 +168,30 @@ export const pack = (
 	// Remove needless properties
 	delete _user.latest_post;
 
-	// Remove private properties
-	delete _user.account.keypair;
-	delete _user.account.password;
-	delete _user.account.token;
-	delete _user.account.two_factor_temp_secret;
-	delete _user.account.two_factor_secret;
-	delete _user.username_lower;
-	if (_user.account.twitter) {
-		delete _user.account.twitter.access_token;
-		delete _user.account.twitter.access_token_secret;
-	}
-	delete _user.account.line;
+	if (!_user.host) {
+		// Remove private properties
+		delete _user.account.keypair;
+		delete _user.account.password;
+		delete _user.account.token;
+		delete _user.account.two_factor_temp_secret;
+		delete _user.account.two_factor_secret;
+		delete _user.username_lower;
+		if (_user.account.twitter) {
+			delete _user.account.twitter.access_token;
+			delete _user.account.twitter.access_token_secret;
+		}
+		delete _user.account.line;
 
-	// Visible via only the official client
-	if (!opts.includeSecrets) {
-		delete _user.account.email;
-		delete _user.account.settings;
-		delete _user.account.client_settings;
-	}
+		// Visible via only the official client
+		if (!opts.includeSecrets) {
+			delete _user.account.email;
+			delete _user.account.settings;
+			delete _user.account.client_settings;
+		}
 
-	if (!opts.detail) {
-		delete _user.account.two_factor_enabled;
+		if (!opts.detail) {
+			delete _user.account.two_factor_enabled;
+		}
 	}
 
 	_user.avatar_url = _user.avatar_id != null
diff --git a/src/api/private/signin.ts b/src/api/private/signin.ts
index ae0be03c7..00dcb8afc 100644
--- a/src/api/private/signin.ts
+++ b/src/api/private/signin.ts
@@ -1,7 +1,7 @@
 import * as express from 'express';
 import * as bcrypt from 'bcryptjs';
 import * as speakeasy from 'speakeasy';
-import { default as User, IUser } from '../models/user';
+import { default as User, ILocalAccount, IUser } from '../models/user';
 import Signin, { pack } from '../models/signin';
 import event from '../event';
 import signin from '../common/signin';
@@ -32,7 +32,8 @@ export default async (req: express.Request, res: express.Response) => {
 
 	// Fetch user
 	const user: IUser = await User.findOne({
-		username_lower: username.toLowerCase()
+		username_lower: username.toLowerCase(),
+		host: null
 	}, {
 		fields: {
 			data: false,
@@ -47,13 +48,15 @@ export default async (req: express.Request, res: express.Response) => {
 		return;
 	}
 
+	const account = user.account as ILocalAccount;
+
 	// Compare password
-	const same = await bcrypt.compare(password, user.account.password);
+	const same = await bcrypt.compare(password, account.password);
 
 	if (same) {
-		if (user.account.two_factor_enabled) {
+		if (account.two_factor_enabled) {
 			const verified = (speakeasy as any).totp.verify({
-				secret: user.account.two_factor_secret,
+				secret: account.two_factor_secret,
 				encoding: 'base32',
 				token: token
 			});
diff --git a/src/api/private/signup.ts b/src/api/private/signup.ts
index 280153d4f..96e049570 100644
--- a/src/api/private/signup.ts
+++ b/src/api/private/signup.ts
@@ -64,7 +64,8 @@ export default async (req: express.Request, res: express.Response) => {
 	// Fetch exist user that same username
 	const usernameExist = await User
 		.count({
-			username_lower: username.toLowerCase()
+			username_lower: username.toLowerCase(),
+			host: null
 		}, {
 			limit: 1
 		});
diff --git a/src/api/service/twitter.ts b/src/api/service/twitter.ts
index 02b613454..c1f2e48a6 100644
--- a/src/api/service/twitter.ts
+++ b/src/api/service/twitter.ts
@@ -39,6 +39,7 @@ module.exports = (app: express.Application) => {
 		if (userToken == null) return res.send('plz signin');
 
 		const user = await User.findOneAndUpdate({
+			host: null,
 			'account.token': userToken
 		}, {
 			$set: {
@@ -126,6 +127,7 @@ module.exports = (app: express.Application) => {
 				const result = await twAuth.done(JSON.parse(ctx), req.query.oauth_verifier);
 
 				const user = await User.findOne({
+					host: null,
 					'account.twitter.user_id': result.userId
 				});
 
@@ -148,6 +150,7 @@ module.exports = (app: express.Application) => {
 				const result = await twAuth.done(JSON.parse(ctx), verifier);
 
 				const user = await User.findOneAndUpdate({
+					host: null,
 					'account.token': userToken
 				}, {
 					$set: {
diff --git a/src/api/streaming.ts b/src/api/streaming.ts
index 427e01afd..a6759e414 100644
--- a/src/api/streaming.ts
+++ b/src/api/streaming.ts
@@ -94,6 +94,7 @@ function authenticate(token: string): Promise<IUser> {
 			// Fetch user
 			const user: IUser = await User
 				.findOne({
+					host: null,
 					'account.token': token
 				});
 
diff --git a/src/common/get-user-summary.ts b/src/common/get-user-summary.ts
deleted file mode 100644
index 619814e8a..000000000
--- a/src/common/get-user-summary.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { IUser } from '../api/models/user';
-
-/**
- * ユーザーを表す文字列を取得します。
- * @param user ユーザー
- */
-export default function(user: IUser): string {
-	return `${user.name} (@${user.username})\n` +
-		`${user.posts_count}投稿、${user.following_count}フォロー、${user.followers_count}フォロワー\n` +
-		`場所: ${user.account.profile.location}、誕生日: ${user.account.profile.birthday}\n` +
-		`「${user.description}」`;
-}
diff --git a/src/common/user/get-acct.ts b/src/common/user/get-acct.ts
new file mode 100644
index 000000000..9afb03d88
--- /dev/null
+++ b/src/common/user/get-acct.ts
@@ -0,0 +1,3 @@
+export default user => {
+	return user.host === null ? user.username : `${user.username}@${user.host}`;
+};
diff --git a/src/common/user/get-summary.ts b/src/common/user/get-summary.ts
new file mode 100644
index 000000000..f9b7125e3
--- /dev/null
+++ b/src/common/user/get-summary.ts
@@ -0,0 +1,18 @@
+import { ILocalAccount, IUser } from '../../api/models/user';
+import getAcct from './get-acct';
+
+/**
+ * ユーザーを表す文字列を取得します。
+ * @param user ユーザー
+ */
+export default function(user: IUser): string {
+	let string = `${user.name} (@${getAcct(user)})\n` +
+		`${user.posts_count}投稿、${user.following_count}フォロー、${user.followers_count}フォロワー\n`;
+
+	if (user.host === null) {
+		const account = user.account as ILocalAccount;
+		string += `場所: ${account.profile.location}、誕生日: ${account.profile.birthday}\n`;
+	}
+
+	return string + `「${user.description}」`;
+}
diff --git a/src/common/user/parse-acct.ts b/src/common/user/parse-acct.ts
new file mode 100644
index 000000000..ef1f55405
--- /dev/null
+++ b/src/common/user/parse-acct.ts
@@ -0,0 +1,4 @@
+export default acct => {
+	const splitted = acct.split('@', 2);
+	return { username: splitted[0], host: splitted[1] || null };
+};
diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index face824cf..dc4b8e142 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -165,10 +165,10 @@
 <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>
+		<a class="name" href={ _URL_ + '/@' + acct }><b>{ post.user.name }</b></a>
 		<mk-time time={ post.created_at }/>
 		<mk-time time={ post.created_at } mode="detail"/>
-		<span>ID:<i>{ post.user.username }</i></span>
+		<span>ID:<i>{ acct }</i></span>
 	</header>
 	<div>
 		<a v-if="post.reply">&gt;&gt;{ post.reply.index }</a>
@@ -229,8 +229,11 @@
 
 	</style>
 	<script lang="typescript">
+		import getAcct from '../../../../common/user/get-acct';
+
 		this.post = this.opts.post;
 		this.form = this.opts.form;
+		this.acct = getAcct(this.post.user);
 
 		this.reply = () => {
 			this.form.update({
diff --git a/src/web/app/common/views/components/autocomplete.vue b/src/web/app/common/views/components/autocomplete.vue
index 6d7d5cd1b..8afa291e3 100644
--- a/src/web/app/common/views/components/autocomplete.vue
+++ b/src/web/app/common/views/components/autocomplete.vue
@@ -4,7 +4,7 @@
 		<li v-for="user in users" @click="complete(type, 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>
+			<span class="username">@{{ getAcct(user) }}</span>
 		</li>
 	</ol>
 	<ol class="emojis" ref="suggests" v-if="emojis.length > 0">
@@ -21,6 +21,7 @@
 import Vue from 'vue';
 import * as emojilib from 'emojilib';
 import contains from '../../../common/scripts/contains';
+import getAcct from '../../../../../common/user/get-acct';
 
 const lib = Object.entries(emojilib.lib).filter((x: any) => {
 	return x[1].category != 'flags';
@@ -105,6 +106,7 @@ export default Vue.extend({
 		});
 	},
 	methods: {
+		getAcct,
 		exec() {
 			this.select = -1;
 			if (this.$refs.suggests) {
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 647e39a75..5f2eb1ba8 100644
--- a/src/web/app/common/views/components/messaging-room.message.vue
+++ b/src/web/app/common/views/components/messaging-room.message.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="message" :data-is-me="isMe">
-	<router-link class="avatar-anchor" :to="`/@${message.user.username}`" :title="message.user.username" target="_blank">
+	<router-link class="avatar-anchor" :to="`/@${acct}`" :title="acct" target="_blank">
 		<img class="avatar" :src="`${message.user.avatar_url}?thumbnail&size=80`" alt=""/>
 	</router-link>
 	<div class="content">
@@ -34,10 +34,14 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import getAcct from '../../../../../common/user/get-acct';
 
 export default Vue.extend({
 	props: ['message'],
 	computed: {
+		acct() {
+			return getAcct(this.message.user);
+		},
 		isMe(): boolean {
 			return this.message.user_id == (this as any).os.i.id;
 		},
diff --git a/src/web/app/common/views/components/messaging.vue b/src/web/app/common/views/components/messaging.vue
index db60e9259..88574b94d 100644
--- a/src/web/app/common/views/components/messaging.vue
+++ b/src/web/app/common/views/components/messaging.vue
@@ -15,7 +15,7 @@
 				>
 					<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=32`" alt=""/>
 					<span class="name">{{ user.name }}</span>
-					<span class="username">@{{ user.username }}</span>
+					<span class="username">@{{ getAcct(user) }}</span>
 				</li>
 			</ol>
 		</div>
@@ -24,7 +24,7 @@
 		<template>
 			<a v-for="message in messages"
 				class="user"
-				:href="`/i/messaging/${isMe(message) ? message.recipient.username : message.user.username}`"
+				:href="`/i/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
 				:data-is-me="isMe(message)"
 				:data-is-read="message.is_read"
 				@click.prevent="navigate(isMe(message) ? message.recipient : message.user)"
@@ -34,7 +34,7 @@
 					<img class="avatar" :src="`${isMe(message) ? message.recipient.avatar_url : message.user.avatar_url}?thumbnail&size=64`" alt=""/>
 					<header>
 						<span class="name">{{ isMe(message) ? message.recipient.name : message.user.name }}</span>
-						<span class="username">@{{ isMe(message) ? message.recipient.username : message.user.username }}</span>
+						<span class="username">@{{ getAcct(isMe(message) ? message.recipient : message.user) }}</span>
 						<mk-time :time="message.created_at"/>
 					</header>
 					<div class="body">
@@ -51,6 +51,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import getAcct from '../../../../../common/user/get-acct';
 
 export default Vue.extend({
 	props: {
@@ -92,6 +93,7 @@ export default Vue.extend({
 		(this as any).os.streams.messagingIndexStream.dispose(this.connectionId);
 	},
 	methods: {
+		getAcct,
 		isMe(message) {
 			return message.user_id == (this as any).os.i.id;
 		},
diff --git a/src/web/app/common/views/components/post-html.ts b/src/web/app/common/views/components/post-html.ts
index 56ee97d38..98da86617 100644
--- a/src/web/app/common/views/components/post-html.ts
+++ b/src/web/app/common/views/components/post-html.ts
@@ -1,5 +1,6 @@
 import Vue from 'vue';
 import * as emojilib from 'emojilib';
+import getAcct from '../../../../../common/user/get-acct';
 import { url } from '../../../config';
 import MkUrl from './url.vue';
 
@@ -61,9 +62,9 @@ export default Vue.component('mk-post-html', {
 				case 'mention':
 					return (createElement as any)('a', {
 						attrs: {
-							href: `${url}/@${token.username}`,
+							href: `${url}/@${getAcct(token)}`,
 							target: '_blank',
-							dataIsMe: (this as any).i && (this as any).i.username == token.username
+							dataIsMe: (this as any).i && getAcct((this as any).i) == getAcct(token)
 						},
 						directives: [{
 							name: 'user-preview',
diff --git a/src/web/app/common/views/components/welcome-timeline.vue b/src/web/app/common/views/components/welcome-timeline.vue
index 062ccda32..7586e9264 100644
--- a/src/web/app/common/views/components/welcome-timeline.vue
+++ b/src/web/app/common/views/components/welcome-timeline.vue
@@ -1,15 +1,15 @@
 <template>
 <div class="mk-welcome-timeline">
 	<div v-for="post in posts">
-		<router-link class="avatar-anchor" :to="`/@${post.user.username}`" v-user-preview="post.user.id">
+		<router-link class="avatar-anchor" :to="`/@${getAcct(post.user)}`" v-user-preview="post.user.id">
 			<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=96`" alt="avatar"/>
 		</router-link>
 		<div class="body">
 			<header>
-				<router-link class="name" :to="`/@${post.user.username}`" v-user-preview="post.user.id">{{ post.user.name }}</router-link>
-				<span class="username">@{{ post.user.username }}</span>
+				<router-link class="name" :to="`/@${getAcct(post.user)}`" v-user-preview="post.user.id">{{ post.user.name }}</router-link>
+				<span class="username">@{{ getAcct(post.user) }}</span>
 				<div class="info">
-					<router-link class="created-at" :to="`/@${post.user.username}/${post.id}`">
+					<router-link class="created-at" :to="`/@${getAcct(post.user)}/${post.id}`">
 						<mk-time :time="post.created_at"/>
 					</router-link>
 				</div>
@@ -24,6 +24,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import getAcct from '../../../../../common/user/get-acct';
 
 export default Vue.extend({
 	data() {
@@ -36,6 +37,7 @@ export default Vue.extend({
 		this.fetch();
 	},
 	methods: {
+		getAcct,
 		fetch(cb?) {
 			this.fetching = true;
 			(this as any).api('posts', {
diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
index 145cc45db..b95e16854 100644
--- a/src/web/app/desktop/script.ts
+++ b/src/web/app/desktop/script.ts
@@ -49,7 +49,7 @@ init(async (launch) => {
 		routes: [
 			{ path: '/', name: 'index', component: MkIndex },
 			{ path: '/i/customize-home', component: MkHomeCustomize },
-			{ path: '/i/messaging/:username', component: MkMessagingRoom },
+			{ path: '/i/messaging/:user', 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/friends-maker.vue b/src/web/app/desktop/views/components/friends-maker.vue
index 65adff7ce..eed15e077 100644
--- a/src/web/app/desktop/views/components/friends-maker.vue
+++ b/src/web/app/desktop/views/components/friends-maker.vue
@@ -3,12 +3,12 @@
 	<p class="title">気になるユーザーをフォロー:</p>
 	<div class="users" v-if="!fetching && users.length > 0">
 		<div class="user" v-for="user in users" :key="user.id">
-			<router-link class="avatar-anchor" :to="`/@${user.username}`">
+			<router-link class="avatar-anchor" :to="`/@${getAcct(user)}`">
 				<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=42`" alt="" v-user-preview="user.id"/>
 			</router-link>
 			<div class="body">
-				<router-link class="name" :to="`/@${user.username}`" v-user-preview="user.id">{{ user.name }}</router-link>
-				<p class="username">@{{ user.username }}</p>
+				<router-link class="name" :to="`/@${getAcct(user)}`" v-user-preview="user.id">{{ user.name }}</router-link>
+				<p class="username">@{{ getAcct(user) }}</p>
 			</div>
 			<mk-follow-button :user="user"/>
 		</div>
@@ -22,6 +22,8 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import getAcct from '../../../../../common/user/get-acct';
+
 export default Vue.extend({
 	data() {
 		return {
@@ -35,6 +37,7 @@ export default Vue.extend({
 		this.fetch();
 	},
 	methods: {
+		getAcct,
 		fetch() {
 			this.fetching = true;
 			this.users = [];
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 66a9aa003..373526781 100644
--- a/src/web/app/desktop/views/components/messaging-room-window.vue
+++ b/src/web/app/desktop/views/components/messaging-room-window.vue
@@ -8,12 +8,13 @@
 <script lang="ts">
 import Vue from 'vue';
 import { url } from '../../../config';
+import getAcct from '../../../../../common/user/get-acct';
 
 export default Vue.extend({
 	props: ['user'],
 	computed: {
 		popout(): string {
-			return `${url}/i/messaging/${this.user.username}`;
+			return `${url}/i/messaging/${getAcct(this.user)}`;
 		}
 	}
 });
diff --git a/src/web/app/desktop/views/components/notifications.vue b/src/web/app/desktop/views/components/notifications.vue
index 86cd1ba4f..b48ffc174 100644
--- a/src/web/app/desktop/views/components/notifications.vue
+++ b/src/web/app/desktop/views/components/notifications.vue
@@ -5,82 +5,82 @@
 			<div class="notification" :class="notification.type" :key="notification.id">
 				<mk-time :time="notification.created_at"/>
 				<template v-if="notification.type == 'reaction'">
-					<router-link class="avatar-anchor" :to="`/@${notification.user.username}`" v-user-preview="notification.user.id">
+					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">
 						<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>
 							<mk-reaction-icon :reaction="notification.reaction"/>
-							<router-link :to="`/@${notification.user.username}`" v-user-preview="notification.user.id">{{ notification.user.name }}</router-link>
+							<router-link :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ notification.user.name }}</router-link>
 						</p>
-						<router-link class="post-ref" :to="`/@${notification.post.user.username}/${notification.post.id}`">
+						<router-link class="post-ref" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">
 							%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%
 						</router-link>
 					</div>
 				</template>
 				<template v-if="notification.type == 'repost'">
-					<router-link class="avatar-anchor" :to="`/@${notification.post.user.username}`" v-user-preview="notification.post.user_id">
+					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.user_id">
 						<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:retweet%
-							<router-link :to="`/@${notification.post.user.username}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</router-link>
+							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</router-link>
 						</p>
-						<router-link class="post-ref" :to="`/@${notification.post.user.username}/${notification.post.id}`">
+						<router-link class="post-ref" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">
 							%fa:quote-left%{{ getPostSummary(notification.post.repost) }}%fa:quote-right%
 						</router-link>
 					</div>
 				</template>
 				<template v-if="notification.type == 'quote'">
-					<router-link class="avatar-anchor" :to="`/@${notification.post.user.username}`" v-user-preview="notification.post.user_id">
+					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.user_id">
 						<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:quote-left%
-							<router-link :to="`/@${notification.post.user.username}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</router-link>
+							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</router-link>
 						</p>
-						<router-link class="post-preview" :to="`/@${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link>
+						<router-link class="post-preview" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link>
 					</div>
 				</template>
 				<template v-if="notification.type == 'follow'">
-					<router-link class="avatar-anchor" :to="`/@${notification.user.username}`" v-user-preview="notification.user.id">
+					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">
 						<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:user-plus%
-							<router-link :to="`/@${notification.user.username}`" v-user-preview="notification.user.id">{{ notification.user.name }}</router-link>
+							<router-link :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ notification.user.name }}</router-link>
 						</p>
 					</div>
 				</template>
 				<template v-if="notification.type == 'reply'">
-					<router-link class="avatar-anchor" :to="`/@${notification.post.user.username}`" v-user-preview="notification.post.user_id">
+					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.user_id">
 						<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:reply%
-							<router-link :to="`/@${notification.post.user.username}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</router-link>
+							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</router-link>
 						</p>
-						<router-link class="post-preview" :to="`/@${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link>
+						<router-link class="post-preview" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link>
 					</div>
 				</template>
 				<template v-if="notification.type == 'mention'">
-					<router-link class="avatar-anchor" :to="`/@${notification.post.user.username}`" v-user-preview="notification.post.user_id">
+					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.user_id">
 						<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:at%
-							<router-link :to="`/@${notification.post.user.username}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</router-link>
+							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</router-link>
 						</p>
-						<a class="post-preview" :href="`/@${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</a>
+						<a class="post-preview" :href="`/@${getAcct(notification.post.user)}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</a>
 					</div>
 				</template>
 				<template v-if="notification.type == 'poll_vote'">
-					<router-link class="avatar-anchor" :to="`/@${notification.user.username}`" v-user-preview="notification.user.id">
+					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">
 						<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
-						<p>%fa:chart-pie%<a :href="`/@${notification.user.username}`" v-user-preview="notification.user.id">{{ notification.user.name }}</a></p>
-						<router-link class="post-ref" :to="`/@${notification.post.user.username}/${notification.post.id}`">
+						<p>%fa:chart-pie%<a :href="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ notification.user.name }}</a></p>
+						<router-link class="post-ref" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">
 							%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%
 						</router-link>
 					</div>
@@ -102,6 +102,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import getAcct from '../../../../../common/user/get-acct';
 import getPostSummary from '../../../../../common/get-post-summary';
 
 export default Vue.extend({
@@ -152,6 +153,7 @@ export default Vue.extend({
 		(this as any).os.stream.dispose(this.connectionId);
 	},
 	methods: {
+		getAcct,
 		fetchMoreNotifications() {
 			this.fetchingMoreNotifications = true;
 
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 53fc724fc..59d8db04c 100644
--- a/src/web/app/desktop/views/components/post-detail.sub.vue
+++ b/src/web/app/desktop/views/components/post-detail.sub.vue
@@ -1,16 +1,16 @@
 <template>
 <div class="sub" :title="title">
-	<router-link class="avatar-anchor" :to="`/@${post.user.username}`">
+	<router-link class="avatar-anchor" :to="`/@${acct}`">
 		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar" v-user-preview="post.user_id"/>
 	</router-link>
 	<div class="main">
 		<header>
 			<div class="left">
-				<router-link class="name" :to="`/@${post.user.username}`" v-user-preview="post.user_id">{{ post.user.name }}</router-link>
-				<span class="username">@{{ post.user.username }}</span>
+				<router-link class="name" :to="`/@${acct}`" v-user-preview="post.user_id">{{ post.user.name }}</router-link>
+				<span class="username">@{{ acct }}</span>
 			</div>
 			<div class="right">
-				<router-link class="time" :to="`/@${post.user.username}/${post.id}`">
+				<router-link class="time" :to="`/@${acct}/${post.id}`">
 					<mk-time :time="post.created_at"/>
 				</router-link>
 			</div>
@@ -28,10 +28,14 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
+import getAcct from '../../../../../common/user/get-acct';
 
 export default Vue.extend({
 	props: ['post'],
 	computed: {
+		acct() {
+			return getAcct(this.post.user);
+		},
 		title(): string {
 			return dateStringify(this.post.created_at);
 		}
diff --git a/src/web/app/desktop/views/components/post-detail.vue b/src/web/app/desktop/views/components/post-detail.vue
index 9a8958679..f09bf4cbd 100644
--- a/src/web/app/desktop/views/components/post-detail.vue
+++ b/src/web/app/desktop/views/components/post-detail.vue
@@ -18,22 +18,22 @@
 	</div>
 	<div class="repost" v-if="isRepost">
 		<p>
-			<router-link class="avatar-anchor" :to="`/@${post.user.username}`" v-user-preview="post.user_id">
+			<router-link class="avatar-anchor" :to="`/@${acct}`" 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>
+			<router-link class="name" :href="`/@${acct}`">{{ post.user.name }}</router-link>
 			がRepost
 		</p>
 	</div>
 	<article>
-		<router-link class="avatar-anchor" :to="`/@${p.user.username}`">
+		<router-link class="avatar-anchor" :to="`/@${acct}`">
 			<img class="avatar" :src="`${p.user.avatar_url}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
 		</router-link>
 		<header>
-			<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}`">
+			<router-link class="name" :to="`/@${acct}`" v-user-preview="p.user.id">{{ p.user.name }}</router-link>
+			<span class="username">@{{ acct }}</span>
+			<router-link class="time" :to="`/@${acct}/${p.id}`">
 				<mk-time :time="p.created_at"/>
 			</router-link>
 		</header>
@@ -78,6 +78,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
+import getAcct from '../../../../../common/user/get-acct';
 
 import MkPostFormWindow from './post-form-window.vue';
 import MkRepostFormWindow from './repost-form-window.vue';
@@ -98,6 +99,11 @@ export default Vue.extend({
 			default: false
 		}
 	},
+	computed: {
+		acct() {
+			return getAcct(this.post.user);
+		}
+	},
 	data() {
 		return {
 			context: [],
diff --git a/src/web/app/desktop/views/components/post-preview.vue b/src/web/app/desktop/views/components/post-preview.vue
index edb593457..808220c0e 100644
--- a/src/web/app/desktop/views/components/post-preview.vue
+++ b/src/web/app/desktop/views/components/post-preview.vue
@@ -1,13 +1,13 @@
 <template>
 <div class="mk-post-preview" :title="title">
-	<router-link class="avatar-anchor" :to="`/@${post.user.username}`">
+	<router-link class="avatar-anchor" :to="`/@${acct}`">
 		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar" v-user-preview="post.user_id"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${post.user.username}`" v-user-preview="post.user_id">{{ post.user.name }}</router-link>
-			<span class="username">@{{ post.user.username }}</span>
-			<router-link class="time" :to="`/@${post.user.username}/${post.id}`">
+			<router-link class="name" :to="`/@${acct}`" v-user-preview="post.user_id">{{ post.user.name }}</router-link>
+			<span class="username">@{{ acct }}</span>
+			<router-link class="time" :to="`/@${acct}/${post.id}`">
 				<mk-time :time="post.created_at"/>
 			</router-link>
 		</header>
@@ -21,10 +21,14 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
+import getAcct from '../../../../../common/user/get-acct';
 
 export default Vue.extend({
 	props: ['post'],
 	computed: {
+		acct() {
+			return getAcct(this.post.user);
+		},
 		title(): string {
 			return dateStringify(this.post.created_at);
 		}
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 2fd8a9865..120700877 100644
--- a/src/web/app/desktop/views/components/posts.post.sub.vue
+++ b/src/web/app/desktop/views/components/posts.post.sub.vue
@@ -1,13 +1,13 @@
 <template>
 <div class="sub" :title="title">
-	<router-link class="avatar-anchor" :to="`/@${post.user.username}`">
+	<router-link class="avatar-anchor" :to="`/@${acct}`">
 		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar" v-user-preview="post.user_id"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${post.user.username}`" v-user-preview="post.user_id">{{ post.user.name }}</router-link>
-			<span class="username">@{{ post.user.username }}</span>
-			<router-link class="created-at" :to="`/@${post.user.username}/${post.id}`">
+			<router-link class="name" :to="`/@${acct}`" v-user-preview="post.user_id">{{ post.user.name }}</router-link>
+			<span class="username">@{{ acct }}</span>
+			<router-link class="created-at" :to="`/@${acct}/${post.id}`">
 				<mk-time :time="post.created_at"/>
 			</router-link>
 		</header>
@@ -21,10 +21,14 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
+import getAcct from '../../../../../common/user/get-acct';
 
 export default Vue.extend({
 	props: ['post'],
 	computed: {
+		acct() {
+			return getAcct(this.post.user);
+		},
 		title(): string {
 			return dateStringify(this.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 a525900b9..6b4d3d278 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -5,25 +5,25 @@
 	</div>
 	<div class="repost" v-if="isRepost">
 		<p>
-			<router-link class="avatar-anchor" :to="`/@${post.user.username}`" v-user-preview="post.user_id">
+			<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="post.user_id">
 				<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=32`" alt="avatar"/>
 			</router-link>
 			%fa:retweet%
 			<span>{{ '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('{')) }}</span>
-			<a class="name" :href="`/@${post.user.username}`" v-user-preview="post.user_id">{{ post.user.name }}</a>
+			<a class="name" :href="`/@${acct}`" v-user-preview="post.user_id">{{ post.user.name }}</a>
 			<span>{{ '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1) }}</span>
 		</p>
 		<mk-time :time="post.created_at"/>
 	</div>
 	<article>
-		<router-link class="avatar-anchor" :to="`/@${p.user.username}`">
+		<router-link class="avatar-anchor" :to="`/@${acct}`">
 			<img class="avatar" :src="`${p.user.avatar_url}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
 		</router-link>
 		<div class="main">
 			<header>
-				<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.account.is_bot">bot</span>
-				<span class="username">@{{ p.user.username }}</span>
+				<router-link class="name" :to="`/@${acct}`" v-user-preview="p.user.id">{{ acct }}</router-link>
+				<span class="is-bot" v-if="p.user.host === null && p.user.account.is_bot">bot</span>
+				<span class="username">@{{ acct }}</span>
 				<div class="info">
 					<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
 					<span class="mobile" v-if="p.via_mobile">%fa:mobile-alt%</span>
@@ -85,6 +85,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
+import getAcct from '../../../../../common/user/get-acct';
 import MkPostFormWindow from './post-form-window.vue';
 import MkRepostFormWindow from './repost-form-window.vue';
 import MkPostMenu from '../../../common/views/components/post-menu.vue';
@@ -115,6 +116,9 @@ export default Vue.extend({
 		};
 	},
 	computed: {
+		acct() {
+			return getAcct(this.p.user);
+		},
 		isRepost(): boolean {
 			return (this.post.repost &&
 				this.post.text == null &&
@@ -135,7 +139,7 @@ export default Vue.extend({
 			return dateStringify(this.p.created_at);
 		},
 		url(): string {
-			return `/@${this.p.user.username}/${this.p.id}`;
+			return `/@${this.acct}/${this.p.id}`;
 		},
 		urls(): string[] {
 			if (this.p.ast) {
diff --git a/src/web/app/desktop/views/components/settings.mute.vue b/src/web/app/desktop/views/components/settings.mute.vue
index 0768b54ef..a8dfe1060 100644
--- a/src/web/app/desktop/views/components/settings.mute.vue
+++ b/src/web/app/desktop/views/components/settings.mute.vue
@@ -5,7 +5,7 @@
 	</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>
+			<p><b>{{ user.name }}</b> @{{ getAcct(user) }}</p>
 		</div>
 	</div>
 </div>
@@ -13,6 +13,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import getAcct from '../../../../../common/user/get-acct';
 
 export default Vue.extend({
 	data() {
@@ -21,6 +22,9 @@ export default Vue.extend({
 			users: []
 		};
 	},
+	methods: {
+		getAcct
+	},
 	mounted() {
 		(this as any).api('mute/list').then(x => {
 			this.users = x.users;
diff --git a/src/web/app/desktop/views/components/user-preview.vue b/src/web/app/desktop/views/components/user-preview.vue
index ffc959fac..24d613f12 100644
--- a/src/web/app/desktop/views/components/user-preview.vue
+++ b/src/web/app/desktop/views/components/user-preview.vue
@@ -2,12 +2,12 @@
 <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>
-		<router-link class="avatar" :to="`/@${u.username}`">
+		<router-link class="avatar" :to="`/@${acct}`">
 			<img :src="`${u.avatar_url}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="title">
-			<router-link class="name" :to="`/@${u.username}`">{{ u.name }}</router-link>
-			<p class="username">@{{ u.username }}</p>
+			<router-link class="name" :to="`/@${acct}`">{{ u.name }}</router-link>
+			<p class="username">@{{ acct }}</p>
 		</div>
 		<div class="description">{{ u.description }}</div>
 		<div class="status">
@@ -29,6 +29,8 @@
 <script lang="ts">
 import Vue from 'vue';
 import * as anime from 'animejs';
+import getAcct from '../../../../../common/user/get-acct';
+import parseAcct from '../../../../../common/user/parse-acct';
 
 export default Vue.extend({
 	props: {
@@ -37,6 +39,11 @@ export default Vue.extend({
 			required: true
 		}
 	},
+	computed: {
+		acct() {
+			return getAcct(this.u);
+		}
+	},
 	data() {
 		return {
 			u: null
@@ -49,10 +56,11 @@ export default Vue.extend({
 				this.open();
 			});
 		} else {
-			(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 => {
+			const query = this.user[0] == '@' ?
+				parseAcct(this.user[0].substr(1)) :
+				{ user_id: this.user[0] };
+
+			(this as any).api('users/show', query).then(user => {
 				this.u = user;
 				this.open();
 			});
diff --git a/src/web/app/desktop/views/components/users-list.item.vue b/src/web/app/desktop/views/components/users-list.item.vue
index 2d1e13347..e02d1311d 100644
--- a/src/web/app/desktop/views/components/users-list.item.vue
+++ b/src/web/app/desktop/views/components/users-list.item.vue
@@ -1,12 +1,12 @@
 <template>
 <div class="root item">
-	<router-link class="avatar-anchor" :to="`/@${user.username}`" v-user-preview="user.id">
+	<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="user.id">
 		<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${user.username}`" v-user-preview="user.id">{{ user.name }}</router-link>
-			<span class="username">@{{ user.username }}</span>
+			<router-link class="name" :to="`/@${acct}`" v-user-preview="user.id">{{ user.name }}</router-link>
+			<span class="username">@{{ acct }}</span>
 		</header>
 		<div class="body">
 			<p class="followed" v-if="user.is_followed">フォローされています</p>
@@ -19,8 +19,15 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import getAcct from '../../../../../common/user/get-acct';
+
 export default Vue.extend({
-	props: ['user']
+	props: ['user'],
+	computed: {
+		acct() {
+			return getAcct(this.user);
+		}
+	}
 });
 </script>
 
diff --git a/src/web/app/desktop/views/pages/messaging-room.vue b/src/web/app/desktop/views/pages/messaging-room.vue
index 99279dc07..0cab1e0d1 100644
--- a/src/web/app/desktop/views/pages/messaging-room.vue
+++ b/src/web/app/desktop/views/pages/messaging-room.vue
@@ -7,6 +7,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
+import parseAcct from '../../../../../common/user/parse-acct';
 
 export default Vue.extend({
 	data() {
@@ -29,9 +30,7 @@ export default Vue.extend({
 			Progress.start();
 			this.fetching = true;
 
-			(this as any).api('users/show', {
-				username: this.$route.params.username
-			}).then(user => {
+			(this as any).api('users/show', parseAcct(this.$route.params.user)).then(user => {
 				this.user = user;
 				this.fetching = false;
 
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 20675c454..80b38e8ac 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="`/@${getAcct(user)}`" :key="user.id">
 		<img :src="`${user.avatar_url}?thumbnail&size=64`" :alt="user.name" v-user-preview="user.id"/>
 	</router-link>
 	</div>
@@ -13,6 +13,8 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import getAcct from '../../../../../../common/user/get-acct';
+
 export default Vue.extend({
 	props: ['user'],
 	data() {
@@ -21,6 +23,9 @@ export default Vue.extend({
 			fetching: true
 		};
 	},
+	method() {
+		getAcct
+	},
 	mounted() {
 		(this as any).api('users/followers', {
 			user_id: this.user.id,
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 a60020f59..57e6def27 100644
--- a/src/web/app/desktop/views/pages/user/user.friends.vue
+++ b/src/web/app/desktop/views/pages/user/user.friends.vue
@@ -4,12 +4,12 @@
 	<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="`/@${getAcct(friend)}`">
 				<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>
+				<router-link class="name" :to="`/@${getAcct(friend)}`" v-user-preview="friend.id">{{ friend.name }}</router-link>
+				<p class="username">@{{ getAcct(friend) }}</p>
 			</div>
 			<mk-follow-button :user="friend"/>
 		</div>
@@ -20,6 +20,8 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import getAcct from '../../../../../../common/user/get-acct';
+
 export default Vue.extend({
 	props: ['user'],
 	data() {
@@ -28,6 +30,9 @@ export default Vue.extend({
 			fetching: true
 		};
 	},
+	method() {
+		getAcct
+	},
 	mounted() {
 		(this as any).api('users/get_frequently_replied_users', {
 			user_id: this.user.id,
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 e60b312ca..3522e76bd 100644
--- a/src/web/app/desktop/views/pages/user/user.header.vue
+++ b/src/web/app/desktop/views/pages/user/user.header.vue
@@ -8,13 +8,13 @@
 		<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.account.profile.location">%fa:map-marker%{{ user.account.profile.location }}</p>
+			<p class="username">@{{ acct }}</p>
+			<p class="location" v-if="user.host === null && user.account.profile.location">%fa:map-marker%{{ user.account.profile.location }}</p>
 		</div>
 		<footer>
-			<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>
+			<router-link :to="`/@${acct}`" :data-active="$parent.page == 'home'">%fa:home%概要</router-link>
+			<router-link :to="`/@${acct}/media`" :data-active="$parent.page == 'media'">%fa:image%メディア</router-link>
+			<router-link :to="`/@${acct}/graphs`" :data-active="$parent.page == 'graphs'">%fa:chart-bar%グラフ</router-link>
 		</footer>
 	</div>
 </div>
@@ -22,9 +22,15 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import getAcct from '../../../../../../common/user/get-acct';
 
 export default Vue.extend({
 	props: ['user'],
+	computed: {
+		acct() {
+			return getAcct(this.user);
+		}
+	},
 	mounted() {
 		window.addEventListener('load', this.onScroll);
 		window.addEventListener('scroll', this.onScroll);
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 592d5cca6..2483a6c72 100644
--- a/src/web/app/desktop/views/pages/user/user.home.vue
+++ b/src/web/app/desktop/views/pages/user/user.home.vue
@@ -5,7 +5,7 @@
 			<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.account.last_used_at"/></b></p>
+			<p v-if="user.host === null">%i18n:desktop.tags.mk-user.last-used-at%: <b><mk-time :time="user.account.last_used_at"/></b></p>
 		</div>
 	</div>
 	<main>
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 1e7cb455b..b51aae18f 100644
--- a/src/web/app/desktop/views/pages/user/user.profile.vue
+++ b/src/web/app/desktop/views/pages/user/user.profile.vue
@@ -7,10 +7,10 @@
 		<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.account.profile.birthday">
+	<div class="birthday" v-if="user.host === null && user.account.profile.birthday">
 		<p>%fa:birthday-cake%{{ user.account.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳)</p>
 	</div>
-	<div class="twitter" v-if="user.account.twitter">
+	<div class="twitter" v-if="user.host === null && user.account.twitter">
 		<p>%fa:B twitter%<a :href="`https://twitter.com/${user.account.twitter.screen_name}`" target="_blank">@{{ user.account.twitter.screen_name }}</a></p>
 	</div>
 	<div class="status">
diff --git a/src/web/app/desktop/views/pages/user/user.vue b/src/web/app/desktop/views/pages/user/user.vue
index 1ce3fa27e..67cef9326 100644
--- a/src/web/app/desktop/views/pages/user/user.vue
+++ b/src/web/app/desktop/views/pages/user/user.vue
@@ -9,6 +9,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import parseAcct from '../../../../../../common/user/parse-acct';
 import Progress from '../../../../common/scripts/loading';
 import XHeader from './user.header.vue';
 import XHome from './user.home.vue';
@@ -39,9 +40,7 @@ export default Vue.extend({
 		fetch() {
 			this.fetching = true;
 			Progress.start();
-			(this as any).api('users/show', {
-				username: this.$route.params.user
-			}).then(user => {
+			(this as any).api('users/show', parseAcct(this.$route.params.user)).then(user => {
 				this.user = user;
 				this.fetching = false;
 				Progress.done();
diff --git a/src/web/app/desktop/views/pages/welcome.vue b/src/web/app/desktop/views/pages/welcome.vue
index c514500b2..927ddf575 100644
--- a/src/web/app/desktop/views/pages/welcome.vue
+++ b/src/web/app/desktop/views/pages/welcome.vue
@@ -8,7 +8,7 @@
 					<p>ようこそ! <b>Misskey</b>はTwitter風ミニブログSNSです。思ったことや皆と共有したいことを投稿しましょう。タイムラインを見れば、皆の関心事をすぐにチェックすることもできます。<a :href="aboutUrl">詳しく...</a></p>
 					<p><button class="signup" @click="signup">はじめる</button><button class="signin" @click="signin">ログイン</button></p>
 					<div class="users">
-						<router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="`/@${user.username}`" v-user-preview="user.id">
+						<router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="`/@${getAcct(user)}`" v-user-preview="user.id">
 							<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
 						</router-link>
 					</div>
@@ -43,6 +43,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import { docsUrl, copyright, lang } from '../../../config';
+import getAcct from '../../../../../common/user/get-acct';
 
 const shares = [
 	'Everything!',
@@ -97,6 +98,7 @@ export default Vue.extend({
 		clearInterval(this.clock);
 	},
 	methods: {
+		getAcct,
 		signup() {
 			this.$modal.show('signup');
 		},
diff --git a/src/web/app/desktop/views/widgets/channel.channel.post.vue b/src/web/app/desktop/views/widgets/channel.channel.post.vue
index b2fa92ad4..433f9a00a 100644
--- a/src/web/app/desktop/views/widgets/channel.channel.post.vue
+++ b/src/web/app/desktop/views/widgets/channel.channel.post.vue
@@ -2,8 +2,8 @@
 <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>
+		<router-link class="name" :to="`/@${acct}`" v-user-preview="post.user.id"><b>{{ post.user.name }}</b></router-link>
+		<span>ID:<i>{{ acct }}</i></span>
 	</header>
 	<div>
 		<a v-if="post.reply">&gt;&gt;{{ post.reply.index }}</a>
@@ -19,8 +19,15 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import getAcct from '../../../../../common/user/get-acct';
+
 export default Vue.extend({
 	props: ['post'],
+	computed: {
+		acct() {
+			return getAcct(this.post.user);
+		}
+	},
 	methods: {
 		reply() {
 			this.$emit('reply', this.post);
diff --git a/src/web/app/desktop/views/widgets/polls.vue b/src/web/app/desktop/views/widgets/polls.vue
index 636378d0b..e5db34fc7 100644
--- a/src/web/app/desktop/views/widgets/polls.vue
+++ b/src/web/app/desktop/views/widgets/polls.vue
@@ -5,8 +5,8 @@
 		<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>
+		<p v-if="poll.text"><router-link to="`/@${ acct }/${ poll.id }`">{{ poll.text }}</router-link></p>
+		<p v-if="!poll.text"><router-link to="`/@${ acct }/${ 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>
@@ -16,12 +16,19 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
+import getAcct from '../../../../../common/user/get-acct';
+
 export default define({
 	name: 'polls',
 	props: () => ({
 		compact: false
 	})
 }).extend({
+	computed: {
+		acct() {
+			return getAcct(this.poll.user);
+		},
+	},
 	data() {
 		return {
 			poll: null,
diff --git a/src/web/app/desktop/views/widgets/trends.vue b/src/web/app/desktop/views/widgets/trends.vue
index c006c811d..77779787e 100644
--- a/src/web/app/desktop/views/widgets/trends.vue
+++ b/src/web/app/desktop/views/widgets/trends.vue
@@ -6,8 +6,8 @@
 	</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"><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>
+		<p class="text"><router-link :to="`/@${ acct }/${ post.id }`">{{ post.text }}</router-link></p>
+		<p class="author">―<router-link :to="`/@${ acct }`">@{{ acct }}</router-link></p>
 	</div>
 	<p class="empty" v-else>%i18n:desktop.tags.mk-trends-home-widget.nothing%</p>
 </div>
@@ -15,12 +15,19 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
+import getAcct from '../../../../../common/user/get-acct';
+
 export default define({
 	name: 'trends',
 	props: () => ({
 		compact: false
 	})
 }).extend({
+	computed: {
+		acct() {
+			return getAcct(this.post.user);
+		},
+	},
 	data() {
 		return {
 			post: null,
diff --git a/src/web/app/desktop/views/widgets/users.vue b/src/web/app/desktop/views/widgets/users.vue
index c0a85a08e..10e3c529e 100644
--- a/src/web/app/desktop/views/widgets/users.vue
+++ b/src/web/app/desktop/views/widgets/users.vue
@@ -7,12 +7,12 @@
 	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 	<template v-else-if="users.length != 0">
 		<div class="user" v-for="_user in users">
-			<router-link class="avatar-anchor" :to="`/@${_user.username}`">
+			<router-link class="avatar-anchor" :to="`/@${getAcct(_user)}`">
 				<img class="avatar" :src="`${_user.avatar_url}?thumbnail&size=42`" alt="" v-user-preview="_user.id"/>
 			</router-link>
 			<div class="body">
-				<router-link class="name" :to="`/@${_user.username}`" v-user-preview="_user.id">{{ _user.name }}</router-link>
-				<p class="username">@{{ _user.username }}</p>
+				<router-link class="name" :to="`/@${getAcct(_user)}`" v-user-preview="_user.id">{{ _user.name }}</router-link>
+				<p class="username">@{{ getAcct(_user) }}</p>
 			</div>
 			<mk-follow-button :user="_user"/>
 		</div>
@@ -23,6 +23,7 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
+import getAcct from '../../../../../common/user/get-acct';
 
 const limit = 3;
 
@@ -43,6 +44,7 @@ export default define({
 		this.fetch();
 	},
 	methods: {
+		getAcct,
 		func() {
 			this.props.compact = !this.props.compact;
 		},
diff --git a/src/web/app/mobile/script.ts b/src/web/app/mobile/script.ts
index 062a6d83d..4776fccdd 100644
--- a/src/web/app/mobile/script.ts
+++ b/src/web/app/mobile/script.ts
@@ -57,7 +57,7 @@ init((launch) => {
 			{ path: '/i/settings/profile', component: MkProfileSetting },
 			{ path: '/i/notifications', component: MkNotifications },
 			{ path: '/i/messaging', component: MkMessaging },
-			{ path: '/i/messaging/:username', component: MkMessagingRoom },
+			{ path: '/i/messaging/:user', component: MkMessagingRoom },
 			{ path: '/i/drive', component: MkDrive },
 			{ path: '/i/drive/folder/:folder', component: MkDrive },
 			{ path: '/i/drive/file/:file', component: MkDrive },
diff --git a/src/web/app/mobile/views/components/notification.vue b/src/web/app/mobile/views/components/notification.vue
index 301fb81dd..150ac0fd8 100644
--- a/src/web/app/mobile/views/components/notification.vue
+++ b/src/web/app/mobile/views/components/notification.vue
@@ -2,15 +2,15 @@
 <div class="mk-notification">
 	<div class="notification reaction" v-if="notification.type == 'reaction'">
 		<mk-time :time="notification.created_at"/>
-		<router-link class="avatar-anchor" :to="`/@${notification.user.username}`">
+		<router-link class="avatar-anchor" :to="`/@${acct}`">
 			<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="text">
 			<p>
 				<mk-reaction-icon :reaction="notification.reaction"/>
-				<router-link :to="`/@${notification.user.username}`">{{ notification.user.name }}</router-link>
+				<router-link :to="`/@${acct}`">{{ notification.user.name }}</router-link>
 			</p>
-			<router-link class="post-ref" :to="`/@${notification.post.user.username}/${notification.post.id}`">
+			<router-link class="post-ref" :to="`/@${acct}/${notification.post.id}`">
 				%fa:quote-left%{{ getPostSummary(notification.post) }}
 				%fa:quote-right%
 			</router-link>
@@ -19,15 +19,15 @@
 
 	<div class="notification repost" v-if="notification.type == 'repost'">
 		<mk-time :time="notification.created_at"/>
-		<router-link class="avatar-anchor" :to="`/@${notification.post.user.username}`">
+		<router-link class="avatar-anchor" :to="`/@${acct}`">
 			<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="text">
 			<p>
 				%fa:retweet%
-				<router-link :to="`/@${notification.post.user.username}`">{{ notification.post.user.name }}</router-link>
+				<router-link :to="`/@${acct}`">{{ notification.post.user.name }}</router-link>
 			</p>
-			<router-link class="post-ref" :to="`/@${notification.post.user.username}/${notification.post.id}`">
+			<router-link class="post-ref" :to="`/@${acct}/${notification.post.id}`">
 				%fa:quote-left%{{ getPostSummary(notification.post.repost) }}%fa:quote-right%
 			</router-link>
 		</div>
@@ -39,13 +39,13 @@
 
 	<div class="notification follow" v-if="notification.type == 'follow'">
 		<mk-time :time="notification.created_at"/>
-		<router-link class="avatar-anchor" :to="`/@${notification.user.username}`">
+		<router-link class="avatar-anchor" :to="`/@${acct}`">
 			<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="text">
 			<p>
 				%fa:user-plus%
-				<router-link :to="`/@${notification.user.username}`">{{ notification.user.name }}</router-link>
+				<router-link :to="`/@${acct}`">{{ notification.user.name }}</router-link>
 			</p>
 		</div>
 	</div>
@@ -60,15 +60,15 @@
 
 	<div class="notification poll_vote" v-if="notification.type == 'poll_vote'">
 		<mk-time :time="notification.created_at"/>
-		<router-link class="avatar-anchor" :to="`/@${notification.user.username}`">
+		<router-link class="avatar-anchor" :to="`/@${acct}`">
 			<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="text">
 			<p>
 				%fa:chart-pie%
-				<router-link :to="`/@${notification.user.username}`">{{ notification.user.name }}</router-link>
+				<router-link :to="`/@${acct}`">{{ notification.user.name }}</router-link>
 			</p>
-			<router-link class="post-ref" :to="`/@${notification.post.user.username}/${notification.post.id}`">
+			<router-link class="post-ref" :to="`/@${acct}/${notification.post.id}`">
 				%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%
 			</router-link>
 		</div>
@@ -79,9 +79,15 @@
 <script lang="ts">
 import Vue from 'vue';
 import getPostSummary from '../../../../../common/get-post-summary';
+import getAcct from '../../../../../common/user/get-acct';
 
 export default Vue.extend({
 	props: ['notification'],
+	computed: {
+		acct() {
+			return getAcct(this.notification.user);
+		}
+	},
 	data() {
 		return {
 			getPostSummary
diff --git a/src/web/app/mobile/views/components/post-card.vue b/src/web/app/mobile/views/components/post-card.vue
index 1b3b20d88..8ca7550c2 100644
--- a/src/web/app/mobile/views/components/post-card.vue
+++ b/src/web/app/mobile/views/components/post-card.vue
@@ -1,8 +1,8 @@
 <template>
 <div class="mk-post-card">
-	<a :href="`/@${post.user.username}/${post.id}`">
+	<a :href="`/@${acct}/${post.id}`">
 		<header>
-			<img :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/><h3>{{ post.user.name }}</h3>
+			<img :src="`${acct}?thumbnail&size=64`" alt="avatar"/><h3>{{ post.user.name }}</h3>
 		</header>
 		<div>
 			{{ text }}
@@ -15,10 +15,14 @@
 <script lang="ts">
 import Vue from 'vue';
 import summary from '../../../../../common/get-post-summary';
+import getAcct from '../../../../../common/user/get-acct';
 
 export default Vue.extend({
 	props: ['post'],
 	computed: {
+		acct() {
+			return getAcct(this.post.user);
+		},
 		text(): string {
 			return summary(this.post);
 		}
diff --git a/src/web/app/mobile/views/components/post-detail.sub.vue b/src/web/app/mobile/views/components/post-detail.sub.vue
index 153acf78e..6906cf570 100644
--- a/src/web/app/mobile/views/components/post-detail.sub.vue
+++ b/src/web/app/mobile/views/components/post-detail.sub.vue
@@ -1,13 +1,13 @@
 <template>
 <div class="root sub">
-	<router-link class="avatar-anchor" :to="`/@${post.user.username}`">
+	<router-link class="avatar-anchor" :to="`/@${acct}`">
 		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" 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="time" :to="`/@${post.user.username}/${post.id}`">
+			<router-link class="name" :to="`/@${acct}`">{{ post.user.name }}</router-link>
+			<span class="username">@{{ acct }}</span>
+			<router-link class="time" :to="`/@${acct}/${post.id}`">
 				<mk-time :time="post.created_at"/>
 			</router-link>
 		</header>
@@ -20,8 +20,15 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import getAcct from '../../../../../common/user/get-acct';
+
 export default Vue.extend({
-	props: ['post']
+	props: ['post'],
+	computed: {
+		acct() {
+			return getAcct(this.post.user);
+		}
+	}
 });
 </script>
 
diff --git a/src/web/app/mobile/views/components/post-detail.vue b/src/web/app/mobile/views/components/post-detail.vue
index f7af71eea..b5c915830 100644
--- a/src/web/app/mobile/views/components/post-detail.vue
+++ b/src/web/app/mobile/views/components/post-detail.vue
@@ -17,11 +17,11 @@
 	</div>
 	<div class="repost" v-if="isRepost">
 		<p>
-			<router-link class="avatar-anchor" :to="`/@${post.user.username}`">
+			<router-link class="avatar-anchor" :to="`/@${acct}`">
 				<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}`">
+			<router-link class="name" :to="`/@${acct}`">
 				{{ post.user.name }}
 			</router-link>
 			がRepost
@@ -29,12 +29,12 @@
 	</div>
 	<article>
 		<header>
-			<router-link class="avatar-anchor" :to="`/@${p.user.username}`">
+			<router-link class="avatar-anchor" :to="`/@${pAcct}`">
 				<img class="avatar" :src="`${p.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
 			</router-link>
 			<div>
-				<router-link class="name" :to="`/@${p.user.username}`">{{ p.user.name }}</router-link>
-				<span class="username">@{{ p.user.username }}</span>
+				<router-link class="name" :to="`/@${pAcct}`">{{ p.user.name }}</router-link>
+				<span class="username">@{{ pAcct }}</span>
 			</div>
 		</header>
 		<div class="body">
@@ -53,7 +53,7 @@
 				<mk-post-preview :post="p.repost"/>
 			</div>
 		</div>
-		<router-link class="time" :to="`/@${p.user.username}/${p.id}`">
+		<router-link class="time" :to="`/@${pAcct}/${p.id}`">
 			<mk-time :time="p.created_at" mode="detail"/>
 		</router-link>
 		<footer>
@@ -80,6 +80,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import getAcct from '../../../../../common/user/get-acct';
 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';
@@ -105,6 +106,12 @@ export default Vue.extend({
 		};
 	},
 	computed: {
+		acct() {
+			return getAcct(this.post.user);
+		},
+		pAcct() {
+			return getAcct(this.p.user);
+		},
 		isRepost(): boolean {
 			return (this.post.repost &&
 				this.post.text == null &&
diff --git a/src/web/app/mobile/views/components/post-preview.vue b/src/web/app/mobile/views/components/post-preview.vue
index 787e1a3a7..0bd0a355b 100644
--- a/src/web/app/mobile/views/components/post-preview.vue
+++ b/src/web/app/mobile/views/components/post-preview.vue
@@ -1,13 +1,13 @@
 <template>
 <div class="mk-post-preview">
-	<router-link class="avatar-anchor" :to="`/@${post.user.username}`">
+	<router-link class="avatar-anchor" :to="`/@${acct}`">
 		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" 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="time" :to="`/@${post.user.username}/${post.id}`">
+			<router-link class="name" :to="`/@${acct}`">{{ post.user.name }}</router-link>
+			<span class="username">@{{ acct }}</span>
+			<router-link class="time" :to="`/@${acct}/${post.id}`">
 				<mk-time :time="post.created_at"/>
 			</router-link>
 		</header>
@@ -20,8 +20,15 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import getAcct from '../../../../../common/user/get-acct';
+
 export default Vue.extend({
-	props: ['post']
+	props: ['post'],
+	computed: {
+		acct() {
+			return getAcct(this.post.user);
+		}
+	}
 });
 </script>
 
diff --git a/src/web/app/mobile/views/components/post.sub.vue b/src/web/app/mobile/views/components/post.sub.vue
index 2427cefeb..b6ee7c1e0 100644
--- a/src/web/app/mobile/views/components/post.sub.vue
+++ b/src/web/app/mobile/views/components/post.sub.vue
@@ -1,13 +1,13 @@
 <template>
 <div class="sub">
-	<router-link class="avatar-anchor" :to="`/@${post.user.username}`">
+	<router-link class="avatar-anchor" :to="`/@${acct}`">
 		<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" :to="`/@${post.user.username}/${post.id}`">
+			<router-link class="name" :to="`/@${acct}`">{{ post.user.name }}</router-link>
+			<span class="username">@{{ acct }}</span>
+			<router-link class="created-at" :to="`/@${acct}/${post.id}`">
 				<mk-time :time="post.created_at"/>
 			</router-link>
 		</header>
@@ -20,8 +20,15 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import getAcct from '../../../../../common/user/get-acct';
+
 export default Vue.extend({
-	props: ['post']
+	props: ['post'],
+	computed: {
+		acct() {
+			return getAcct(this.post.user);
+		}
+	}
 });
 </script>
 
diff --git a/src/web/app/mobile/views/components/post.vue b/src/web/app/mobile/views/components/post.vue
index b8f9e95ee..e5bc96479 100644
--- a/src/web/app/mobile/views/components/post.vue
+++ b/src/web/app/mobile/views/components/post.vue
@@ -5,25 +5,25 @@
 	</div>
 	<div class="repost" v-if="isRepost">
 		<p>
-			<router-link class="avatar-anchor" :to="`/@${post.user.username}`">
+			<router-link class="avatar-anchor" :to="`/@${acct}`">
 				<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
 			</router-link>
 			%fa:retweet%
 			<span>{{ '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('{')) }}</span>
-			<router-link class="name" :to="`/@${post.user.username}`">{{ post.user.name }}</router-link>
+			<router-link class="name" :to="`/@${acct}`">{{ post.user.name }}</router-link>
 			<span>{{ '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr('%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1) }}</span>
 		</p>
 		<mk-time :time="post.created_at"/>
 	</div>
 	<article>
-		<router-link class="avatar-anchor" :to="`/@${p.user.username}`">
+		<router-link class="avatar-anchor" :to="`/@${pAcct}`">
 			<img class="avatar" :src="`${p.user.avatar_url}?thumbnail&size=96`" alt="avatar"/>
 		</router-link>
 		<div class="main">
 			<header>
-				<router-link class="name" :to="`/@${p.user.username}`">{{ p.user.name }}</router-link>
-				<span class="is-bot" v-if="p.user.account.is_bot">bot</span>
-				<span class="username">@{{ p.user.username }}</span>
+				<router-link class="name" :to="`/@${pAcct}`">{{ p.user.name }}</router-link>
+				<span class="is-bot" v-if="p.user.host === null && p.user.account.is_bot">bot</span>
+				<span class="username">@{{ pAcct }}</span>
 				<div class="info">
 					<span class="mobile" v-if="p.via_mobile">%fa:mobile-alt%</span>
 					<router-link class="created-at" :to="url">
@@ -77,6 +77,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import getAcct from '../../../../../common/user/get-acct';
 import MkPostMenu from '../../../common/views/components/post-menu.vue';
 import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
 import XSub from './post.sub.vue';
@@ -93,6 +94,12 @@ export default Vue.extend({
 		};
 	},
 	computed: {
+		acct() {
+			return getAcct(this.post.user);
+		},
+		pAcct() {
+			return getAcct(this.p.user);
+		},
 		isRepost(): boolean {
 			return (this.post.repost &&
 				this.post.text == null &&
@@ -110,7 +117,7 @@ export default Vue.extend({
 				: 0;
 		},
 		url(): string {
-			return `/@${this.p.user.username}/${this.p.id}`;
+			return `/@${this.pAcct}/${this.p.id}`;
 		},
 		urls(): string[] {
 			if (this.p.ast) {
diff --git a/src/web/app/mobile/views/components/user-card.vue b/src/web/app/mobile/views/components/user-card.vue
index bfc748866..5a7309cfd 100644
--- a/src/web/app/mobile/views/components/user-card.vue
+++ b/src/web/app/mobile/views/components/user-card.vue
@@ -1,20 +1,27 @@
 <template>
 <div class="mk-user-card">
 	<header :style="user.banner_url ? `background-image: url(${user.banner_url}?thumbnail&size=1024)` : ''">
-		<a :href="`/@${user.username}`">
+		<a :href="`/@${acct}`">
 			<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>
+	<a class="name" :href="`/@${acct}`" target="_blank">{{ user.name }}</a>
+	<p class="username">@{{ acct }}</p>
 	<mk-follow-button :user="user"/>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import getAcct from '../../../../../common/user/get-acct';
+
 export default Vue.extend({
-	props: ['user']
+	props: ['user'],
+	computed: {
+		acct() {
+			return getAcct(this.user);
+		}
+	}
 });
 </script>
 
diff --git a/src/web/app/mobile/views/components/user-preview.vue b/src/web/app/mobile/views/components/user-preview.vue
index a3db311d1..be80582ca 100644
--- a/src/web/app/mobile/views/components/user-preview.vue
+++ b/src/web/app/mobile/views/components/user-preview.vue
@@ -1,12 +1,12 @@
 <template>
 <div class="mk-user-preview">
-	<router-link class="avatar-anchor" :to="`/@${user.username}`">
+	<router-link class="avatar-anchor" :to="`/@${acct}`">
 		<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${user.username}`">{{ user.name }}</router-link>
-			<span class="username">@{{ user.username }}</span>
+			<router-link class="name" :to="`/@${acct}`">{{ user.name }}</router-link>
+			<span class="username">@{{ acct }}</span>
 		</header>
 		<div class="body">
 			<div class="description">{{ user.description }}</div>
@@ -17,8 +17,15 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import getAcct from '../../../../../common/user/get-acct';
+
 export default Vue.extend({
-	props: ['user']
+	props: ['user'],
+	computed: {
+		acct() {
+			return getAcct(this.user);
+		}
+	}
 });
 </script>
 
diff --git a/src/web/app/mobile/views/pages/followers.vue b/src/web/app/mobile/views/pages/followers.vue
index c2b6b90e2..1edf4e38a 100644
--- a/src/web/app/mobile/views/pages/followers.vue
+++ b/src/web/app/mobile/views/pages/followers.vue
@@ -19,6 +19,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
+import parseAcct from '../../../../../common/user/parse-acct';
 
 export default Vue.extend({
 	data() {
@@ -41,9 +42,7 @@ export default Vue.extend({
 			Progress.start();
 			this.fetching = true;
 
-			(this as any).api('users/show', {
-				username: this.$route.params.user
-			}).then(user => {
+			(this as any).api('users/show', parseAcct(this.$route.params.user)).then(user => {
 				this.user = 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 6365d3b37..0dd171cce 100644
--- a/src/web/app/mobile/views/pages/following.vue
+++ b/src/web/app/mobile/views/pages/following.vue
@@ -19,6 +19,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
+import parseAcct from '../../../../../common/user/parse-acct';
 
 export default Vue.extend({
 	data() {
@@ -41,9 +42,7 @@ export default Vue.extend({
 			Progress.start();
 			this.fetching = true;
 
-			(this as any).api('users/show', {
-				username: this.$route.params.user
-			}).then(user => {
+			(this as any).api('users/show', parseAcct(this.$route.params.user)).then(user => {
 				this.user = user;
 				this.fetching = false;
 
diff --git a/src/web/app/mobile/views/pages/messaging-room.vue b/src/web/app/mobile/views/pages/messaging-room.vue
index eb5439915..193c41179 100644
--- a/src/web/app/mobile/views/pages/messaging-room.vue
+++ b/src/web/app/mobile/views/pages/messaging-room.vue
@@ -10,6 +10,8 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import parseAcct from '../../../../../common/user/parse-acct';
+
 export default Vue.extend({
 	data() {
 		return {
@@ -27,9 +29,7 @@ export default Vue.extend({
 	methods: {
 		fetch() {
 			this.fetching = true;
-			(this as any).api('users/show', {
-				username: (this as any).$route.params.username
-			}).then(user => {
+			(this as any).api('users/show', parseAcct(this.$route.params.user)).then(user => {
 				this.user = user;
 				this.fetching = false;
 
diff --git a/src/web/app/mobile/views/pages/messaging.vue b/src/web/app/mobile/views/pages/messaging.vue
index e0fdb4944..e92068eda 100644
--- a/src/web/app/mobile/views/pages/messaging.vue
+++ b/src/web/app/mobile/views/pages/messaging.vue
@@ -7,6 +7,8 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import getAcct from '../../../../../common/user/get-acct';
+
 export default Vue.extend({
 	mounted() {
 		document.title = 'Misskey %i18n:mobile.tags.mk-messaging-page.message%';
@@ -14,7 +16,7 @@ export default Vue.extend({
 	},
 	methods: {
 		navigate(user) {
-			(this as any).$router.push(`/i/messaging/${user.username}`);
+			(this as any).$router.push(`/i/messaging/${getAcct(user)}`);
 		}
 	}
 });
diff --git a/src/web/app/mobile/views/pages/user.vue b/src/web/app/mobile/views/pages/user.vue
index ba66052e0..7ff897e42 100644
--- a/src/web/app/mobile/views/pages/user.vue
+++ b/src/web/app/mobile/views/pages/user.vue
@@ -13,15 +13,15 @@
 				</div>
 				<div class="title">
 					<h1>{{ user.name }}</h1>
-					<span class="username">@{{ user.username }}</span>
+					<span class="username">@{{ acct }}</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.account.profile.location">
+					<p class="location" v-if="user.host === null && user.account.profile.location">
 						%fa:map-marker%{{ user.account.profile.location }}
 					</p>
-					<p class="birthday" v-if="user.account.profile.birthday">
+					<p class="birthday" v-if="user.host === null && user.account.profile.birthday">
 						%fa:birthday-cake%{{ user.account.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳)
 					</p>
 				</div>
@@ -30,11 +30,11 @@
 						<b>{{ user.posts_count | number }}</b>
 						<i>%i18n:mobile.tags.mk-user.posts%</i>
 					</a>
-					<a :href="`@${user.username}/following`">
+					<a :href="`@${acct}/following`">
 						<b>{{ user.following_count | number }}</b>
 						<i>%i18n:mobile.tags.mk-user.following%</i>
 					</a>
-					<a :href="`@${user.username}/followers`">
+					<a :href="`@${acct}/followers`">
 						<b>{{ user.followers_count | number }}</b>
 						<i>%i18n:mobile.tags.mk-user.followers%</i>
 					</a>
@@ -60,6 +60,8 @@
 <script lang="ts">
 import Vue from 'vue';
 import * as age from 's-age';
+import getAcct from '../../../../../common/user/get-acct';
+import getAcct from '../../../../../common/user/parse-acct';
 import Progress from '../../../common/scripts/loading';
 import XHome from './user/home.vue';
 
@@ -75,6 +77,9 @@ export default Vue.extend({
 		};
 	},
 	computed: {
+		acct() {
+			return this.getAcct(this.user);
+		},
 		age(): number {
 			return age(this.user.account.profile.birthday);
 		}
@@ -92,9 +97,7 @@ export default Vue.extend({
 		fetch() {
 			Progress.start();
 
-			(this as any).api('users/show', {
-				username: this.$route.params.user
-			}).then(user => {
+			(this as any).api('users/show', parseAcct(this.$route.params.user)).then(user => {
 				this.user = user;
 				this.fetching = false;
 
diff --git a/src/web/app/mobile/views/pages/user/home.followers-you-know.vue b/src/web/app/mobile/views/pages/user/home.followers-you-know.vue
index 7b02020b1..1a2b8f708 100644
--- a/src/web/app/mobile/views/pages/user/home.followers-you-know.vue
+++ b/src/web/app/mobile/views/pages/user/home.followers-you-know.vue
@@ -2,7 +2,7 @@
 <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}`">
+		<a v-for="user in users" :key="user.id" :href="`/@${getAcct(user)}`">
 			<img :src="`${user.avatar_url}?thumbnail&size=64`" :alt="user.name"/>
 		</a>
 	</div>
@@ -12,6 +12,8 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import getAcct from '../../../../../../common/user/get-acct';
+
 export default Vue.extend({
 	props: ['user'],
 	data() {
@@ -20,6 +22,9 @@ export default Vue.extend({
 			users: []
 		};
 	},
+	methods: {
+		getAcct
+	},
 	mounted() {
 		(this as any).api('users/followers', {
 			user_id: this.user.id,
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 385e5b8dd..f12f59a40 100644
--- a/src/web/app/mobile/views/pages/user/home.photos.vue
+++ b/src/web/app/mobile/views/pages/user/home.photos.vue
@@ -5,7 +5,7 @@
 		<a v-for="image in images"
 			class="img"
 			:style="`background-image: url(${image.media.url}?thumbnail&size=256)`"
-			:href="`/@${image.post.user.username}/${image.post.id}`"
+			:href="`/@${getAcct(image.post.user)}/${image.post.id}`"
 		></a>
 	</div>
 	<p class="empty" v-if="!fetching && images.length == 0">%i18n:mobile.tags.mk-user-overview-photos.no-photos%</p>
@@ -14,6 +14,8 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import getAcct from '../../../../../../common/user/get-acct';
+
 export default Vue.extend({
 	props: ['user'],
 	data() {
@@ -22,6 +24,9 @@ export default Vue.extend({
 			images: []
 		};
 	},
+	methods: {
+		getAcct
+	},
 	mounted() {
 		(this as any).api('users/posts', {
 			user_id: this.user.id,
diff --git a/src/web/app/mobile/views/pages/user/home.vue b/src/web/app/mobile/views/pages/user/home.vue
index dabb3f60b..e3def6151 100644
--- a/src/web/app/mobile/views/pages/user/home.vue
+++ b/src/web/app/mobile/views/pages/user/home.vue
@@ -31,7 +31,7 @@
 			<x-followers-you-know :user="user"/>
 		</div>
 	</section>
-	<p>%i18n:mobile.tags.mk-user-overview.last-used-at%: <b><mk-time :time="user.account.last_used_at"/></b></p>
+	<p v-if="user.host === null">%i18n:mobile.tags.mk-user-overview.last-used-at%: <b><mk-time :time="user.account.last_used_at"/></b></p>
 </div>
 </template>
 
diff --git a/test/text.js b/test/text.js
index 24800ac44..3b27aa23d 100644
--- a/test/text.js
+++ b/test/text.js
@@ -11,7 +11,7 @@ describe('Text', () => {
 	it('can be analyzed', () => {
 		const tokens = analyze('@himawari お腹ペコい :cat: #yryr');
 		assert.deepEqual([
-			{ type: 'mention', content: '@himawari', username: 'himawari' },
+			{ type: 'mention', content: '@himawari', username: 'himawari', host: null },
 			{ type: 'text', content: ' お腹ペコい ' },
 			{ type: 'emoji', content: ':cat:', emoji: 'cat'},
 			{ type: 'text', content: ' '},
@@ -36,7 +36,7 @@ describe('Text', () => {
 		it('mention', () => {
 			const tokens = analyze('@himawari お腹ペコい');
 			assert.deepEqual([
-				{ type: 'mention', content: '@himawari', username: 'himawari' },
+				{ type: 'mention', content: '@himawari', username: 'himawari', host: null },
 				{ type: 'text', content: ' お腹ペコい' }
 			], tokens);
 		});

From 95721a2f0aafe022d330d0828e3fd0e16dacca62 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 09:10:05 +0900
Subject: [PATCH 0880/1250] wip

---
 src/api/models/access-token.ts      | 18 +++++--
 src/api/models/app.ts               | 20 ++++---
 src/api/models/appdata.ts           |  3 --
 src/api/models/auth-session.ts      |  9 ++--
 src/api/models/channel-watching.ts  | 13 ++++-
 src/api/models/channel.ts           | 11 ++--
 src/api/models/drive-file.ts        | 14 ++---
 src/api/models/drive-folder.ts      | 14 ++---
 src/api/models/drive-tag.ts         |  3 --
 src/api/models/favorite.ts          | 11 +++-
 src/api/models/following.ts         | 12 ++++-
 tools/migration/shell.camel-case.js | 81 +++++++++++++++++++++++++++++
 12 files changed, 166 insertions(+), 43 deletions(-)
 delete mode 100644 src/api/models/appdata.ts
 delete mode 100644 src/api/models/drive-tag.ts
 create mode 100644 tools/migration/shell.camel-case.js

diff --git a/src/api/models/access-token.ts b/src/api/models/access-token.ts
index 9985be501..9e1cb6474 100644
--- a/src/api/models/access-token.ts
+++ b/src/api/models/access-token.ts
@@ -1,8 +1,16 @@
+import * as mongo from 'mongodb';
 import db from '../../db/mongodb';
 
-const collection = db.get('access_tokens');
+const AccessToken = db.get<IAccessTokens>('accessTokens');
+AccessToken.createIndex('token');
+AccessToken.createIndex('hash');
+export default AccessToken;
 
-(collection as any).createIndex('token'); // fuck type definition
-(collection as any).createIndex('hash'); // fuck type definition
-
-export default collection as any; // fuck type definition
+export type IAccessTokens = {
+	_id: mongo.ObjectID;
+	createdAt: Date;
+	appId: mongo.ObjectID;
+	userId: mongo.ObjectID;
+	token: string;
+	hash: string;
+};
diff --git a/src/api/models/app.ts b/src/api/models/app.ts
index 34e9867db..20af049b2 100644
--- a/src/api/models/app.ts
+++ b/src/api/models/app.ts
@@ -5,16 +5,22 @@ import db from '../../db/mongodb';
 import config from '../../conf';
 
 const App = db.get<IApp>('apps');
-App.createIndex('name_id');
-App.createIndex('name_id_lower');
+App.createIndex('nameId');
+App.createIndex('nameIdLower');
 App.createIndex('secret');
 export default App;
 
 export type IApp = {
 	_id: mongo.ObjectID;
-	created_at: Date;
-	user_id: mongo.ObjectID;
+	createdAt: Date;
+	userId: mongo.ObjectID;
 	secret: string;
+	name: string;
+	nameId: string;
+	nameIdLower: string;
+	description: string;
+	permission: string;
+	callbackUrl: string;
 };
 
 export function isValidNameId(nameId: string): boolean {
@@ -70,7 +76,7 @@ export const pack = (
 	_app.id = _app._id;
 	delete _app._id;
 
-	delete _app.name_id_lower;
+	delete _app.nameIdLower;
 
 	// Visible by only owner
 	if (!opts.includeSecret) {
@@ -84,8 +90,8 @@ export const pack = (
 	if (me) {
 		// 既に連携しているか
 		const exist = await AccessToken.count({
-			app_id: _app.id,
-			user_id: me,
+			appId: _app.id,
+			userId: me,
 		}, {
 				limit: 1
 			});
diff --git a/src/api/models/appdata.ts b/src/api/models/appdata.ts
deleted file mode 100644
index 3e68354fa..000000000
--- a/src/api/models/appdata.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import db from '../../db/mongodb';
-
-export default db.get('appdata') as any; // fuck type definition
diff --git a/src/api/models/auth-session.ts b/src/api/models/auth-session.ts
index 997ec61c2..e6b8b2318 100644
--- a/src/api/models/auth-session.ts
+++ b/src/api/models/auth-session.ts
@@ -3,11 +3,15 @@ import deepcopy = require('deepcopy');
 import db from '../../db/mongodb';
 import { pack as packApp } from './app';
 
-const AuthSession = db.get('auth_sessions');
+const AuthSession = db.get<IAuthSession>('authSessions');
 export default AuthSession;
 
 export interface IAuthSession {
 	_id: mongo.ObjectID;
+	createdAt: Date;
+	appId: mongo.ObjectID;
+	userId: mongo.ObjectID;
+	token: string;
 }
 
 /**
@@ -24,7 +28,6 @@ export const pack = (
 	let _session: any;
 
 	// TODO: Populate session if it ID
-
 	_session = deepcopy(session);
 
 	// Me
@@ -39,7 +42,7 @@ export const pack = (
 	delete _session._id;
 
 	// Populate app
-	_session.app = await packApp(_session.app_id, me);
+	_session.app = await packApp(_session.appId, me);
 
 	resolve(_session);
 });
diff --git a/src/api/models/channel-watching.ts b/src/api/models/channel-watching.ts
index 6184ae408..23886d0c7 100644
--- a/src/api/models/channel-watching.ts
+++ b/src/api/models/channel-watching.ts
@@ -1,3 +1,14 @@
+import * as mongo from 'mongodb';
+
 import db from '../../db/mongodb';
 
-export default db.get('channel_watching') as any; // fuck type definition
+const ChannelWatching = db.get<IChannelWatching>('channelWatching');
+export default ChannelWatching;
+
+export interface IChannelWatching {
+	_id: mongo.ObjectID;
+	createdAt: Date;
+	deletedAt: Date;
+	channel_id: mongo.ObjectID;
+	userId: mongo.ObjectID;
+}
diff --git a/src/api/models/channel.ts b/src/api/models/channel.ts
index 815d53593..a753a4eba 100644
--- a/src/api/models/channel.ts
+++ b/src/api/models/channel.ts
@@ -9,10 +9,11 @@ export default Channel;
 
 export type IChannel = {
 	_id: mongo.ObjectID;
-	created_at: Date;
+	createdAt: Date;
 	title: string;
-	user_id: mongo.ObjectID;
+	userId: mongo.ObjectID;
 	index: number;
+	watchingCount: number;
 };
 
 /**
@@ -47,7 +48,7 @@ export const pack = (
 	delete _channel._id;
 
 	// Remove needless properties
-	delete _channel.user_id;
+	delete _channel.userId;
 
 	// Me
 	const meId: mongo.ObjectID = me
@@ -61,9 +62,9 @@ export const pack = (
 	if (me) {
 		//#region Watchしているかどうか
 		const watch = await Watching.findOne({
-			user_id: meId,
+			userId: meId,
 			channel_id: _channel.id,
-			deleted_at: { $exists: false }
+			deletedAt: { $exists: false }
 		});
 
 		_channel.is_watching = watch !== null;
diff --git a/src/api/models/drive-file.ts b/src/api/models/drive-file.ts
index 2a46d8dc4..b0e4d1db0 100644
--- a/src/api/models/drive-file.ts
+++ b/src/api/models/drive-file.ts
@@ -4,14 +4,14 @@ import { pack as packFolder } from './drive-folder';
 import config from '../../conf';
 import monkDb, { nativeDbConn } from '../../db/mongodb';
 
-const DriveFile = monkDb.get<IDriveFile>('drive_files.files');
+const DriveFile = monkDb.get<IDriveFile>('driveFiles.files');
 
 export default DriveFile;
 
 const getGridFSBucket = async (): Promise<mongodb.GridFSBucket> => {
 	const db = await nativeDbConn();
 	const bucket = new mongodb.GridFSBucket(db, {
-		bucketName: 'drive_files'
+		bucketName: 'driveFiles'
 	});
 	return bucket;
 };
@@ -26,8 +26,8 @@ export type IDriveFile = {
 	contentType: string;
 	metadata: {
 		properties: any;
-		user_id: mongodb.ObjectID;
-		folder_id: mongodb.ObjectID;
+		userId: mongodb.ObjectID;
+		folderId: mongodb.ObjectID;
 	}
 };
 
@@ -79,7 +79,7 @@ export const pack = (
 	let _target: any = {};
 
 	_target.id = _file._id;
-	_target.created_at = _file.uploadDate;
+	_target.createdAt = _file.uploadDate;
 	_target.name = _file.filename;
 	_target.type = _file.contentType;
 	_target.datasize = _file.length;
@@ -92,9 +92,9 @@ export const pack = (
 	if (_target.properties == null) _target.properties = {};
 
 	if (opts.detail) {
-		if (_target.folder_id) {
+		if (_target.folderId) {
 			// Populate folder
-			_target.folder = await packFolder(_target.folder_id, {
+			_target.folder = await packFolder(_target.folderId, {
 				detail: true
 			});
 		}
diff --git a/src/api/models/drive-folder.ts b/src/api/models/drive-folder.ts
index 54b45049b..52f784e06 100644
--- a/src/api/models/drive-folder.ts
+++ b/src/api/models/drive-folder.ts
@@ -8,10 +8,10 @@ export default DriveFolder;
 
 export type IDriveFolder = {
 	_id: mongo.ObjectID;
-	created_at: Date;
+	createdAt: Date;
 	name: string;
-	user_id: mongo.ObjectID;
-	parent_id: mongo.ObjectID;
+	userId: mongo.ObjectID;
+	parentId: mongo.ObjectID;
 };
 
 export function isValidFolderName(name: string): boolean {
@@ -55,20 +55,20 @@ export const pack = (
 
 	if (opts.detail) {
 		const childFoldersCount = await DriveFolder.count({
-			parent_id: _folder.id
+			parentId: _folder.id
 		});
 
 		const childFilesCount = await DriveFile.count({
-			'metadata.folder_id': _folder.id
+			'metadata.folderId': _folder.id
 		});
 
 		_folder.folders_count = childFoldersCount;
 		_folder.files_count = childFilesCount;
 	}
 
-	if (opts.detail && _folder.parent_id) {
+	if (opts.detail && _folder.parentId) {
 		// Populate parent folder
-		_folder.parent = await pack(_folder.parent_id, {
+		_folder.parent = await pack(_folder.parentId, {
 			detail: true
 		});
 	}
diff --git a/src/api/models/drive-tag.ts b/src/api/models/drive-tag.ts
deleted file mode 100644
index 991c935e8..000000000
--- a/src/api/models/drive-tag.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import db from '../../db/mongodb';
-
-export default db.get('drive_tags') as any; // fuck type definition
diff --git a/src/api/models/favorite.ts b/src/api/models/favorite.ts
index e01d9e343..5ba55c4c9 100644
--- a/src/api/models/favorite.ts
+++ b/src/api/models/favorite.ts
@@ -1,3 +1,12 @@
+import * as mongo from 'mongodb';
 import db from '../../db/mongodb';
 
-export default db.get('favorites') as any; // fuck type definition
+const Favorites = db.get<IFavorites>('favorites');
+export default Favorites;
+
+export type IFavorites = {
+	_id: mongo.ObjectID;
+	createdAt: Date;
+	userId: mongo.ObjectID;
+	postId: mongo.ObjectID;
+};
diff --git a/src/api/models/following.ts b/src/api/models/following.ts
index cb3db9b53..1163bf6b3 100644
--- a/src/api/models/following.ts
+++ b/src/api/models/following.ts
@@ -1,3 +1,13 @@
+import * as mongo from 'mongodb';
 import db from '../../db/mongodb';
 
-export default db.get('following') as any; // fuck type definition
+const Following = db.get<IFollowing>('following');
+export default Following;
+
+export type IFollowing = {
+	_id: mongo.ObjectID;
+	createdAt: Date;
+	deletedAt: Date;
+	followeeId: mongo.ObjectID;
+	followerId: mongo.ObjectID;
+};
diff --git a/tools/migration/shell.camel-case.js b/tools/migration/shell.camel-case.js
new file mode 100644
index 000000000..6d6b01a07
--- /dev/null
+++ b/tools/migration/shell.camel-case.js
@@ -0,0 +1,81 @@
+db.access_tokens.renameCollection('accessTokens');
+db.accessTokens.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		app_id: 'appId',
+		user_id: 'userId',
+	}
+}, false, true);
+
+db.apps.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		user_id: 'userId',
+		name_id: 'nameId',
+		name_id_lower: 'nameIdLower',
+		callback_url: 'callbackUrl',
+	}
+}, false, true);
+
+db.auth_sessions.renameCollection('authSessions');
+db.authSessions.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		app_id: 'appId',
+		user_id: 'userId',
+	}
+}, false, true);
+
+db.channel_watching.renameCollection('channelWatching');
+db.channelWatching.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		deleted_at: 'deletedAt',
+		channel_id: 'channelId',
+		user_id: 'userId',
+	}
+}, false, true);
+
+db.channels.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		user_id: 'userId',
+		watching_count: 'watchingCount'
+	}
+}, false, true);
+
+db.drive_files.files.renameCollection('driveFiles.files');
+db.drive_files.chunks.renameCollection('driveFiles.chunks');
+db.driveFiles.files.update({}, {
+	$rename: {
+		'metadata.user_id': 'metadata.userId',
+		'metadata.folder_id': 'metadata.folderId',
+		'metadata.properties.average_color': 'metadata.properties.avgColor'
+	}
+}, false, true);
+
+db.drive_folders.renameCollection('driveFolders');
+db.driveFolders.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		user_id: 'userId',
+		parent_id: 'parentId',
+	}
+}, false, true);
+
+db.favorites.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		user_id: 'userId',
+		post_id: 'postId',
+	}
+}, false, true);
+
+db.following.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		deleted_at: 'deletedAt',
+		followee_id: 'followeeId',
+		follower_id: 'followerId',
+	}
+}, false, true);

From af192f8ab14ffc26b493a8c2b26648e1be063fe5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 09:15:58 +0900
Subject: [PATCH 0881/1250] Update dependencies :rocket:

---
 package.json | 46 +++++++++++++++++++++++-----------------------
 1 file changed, 23 insertions(+), 23 deletions(-)

diff --git a/package.json b/package.json
index d9ed80b47..4e3da7d0b 100644
--- a/package.json
+++ b/package.json
@@ -44,11 +44,11 @@
 		"@types/express": "4.11.1",
 		"@types/gm": "1.17.33",
 		"@types/gulp": "3.8.36",
-		"@types/gulp-htmlmin": "1.3.31",
-		"@types/gulp-mocha": "0.0.31",
+		"@types/gulp-htmlmin": "1.3.32",
+		"@types/gulp-mocha": "0.0.32",
 		"@types/gulp-rename": "0.0.33",
 		"@types/gulp-replace": "0.0.31",
-		"@types/gulp-uglify": "3.0.4",
+		"@types/gulp-uglify": "3.0.5",
 		"@types/gulp-util": "3.0.34",
 		"@types/inquirer": "0.0.38",
 		"@types/is-root": "1.0.0",
@@ -56,13 +56,13 @@
 		"@types/js-yaml": "3.10.1",
 		"@types/license-checker": "15.0.0",
 		"@types/mkdirp": "0.5.2",
-		"@types/mocha": "2.2.48",
-		"@types/mongodb": "3.0.8",
+		"@types/mocha": "5.0.0",
+		"@types/mongodb": "3.0.9",
 		"@types/monk": "6.0.0",
 		"@types/morgan": "1.7.35",
 		"@types/ms": "0.7.30",
 		"@types/multer": "1.3.6",
-		"@types/node": "9.4.7",
+		"@types/node": "9.6.0",
 		"@types/proxy-addr": "2.0.0",
 		"@types/pug": "2.0.4",
 		"@types/qrcode": "0.8.1",
@@ -76,13 +76,13 @@
 		"@types/speakeasy": "2.0.2",
 		"@types/tmp": "0.0.33",
 		"@types/uuid": "3.4.3",
-		"@types/webpack": "4.1.1",
+		"@types/webpack": "4.1.2",
 		"@types/webpack-stream": "3.2.10",
 		"@types/websocket": "0.0.38",
 		"@types/ws": "4.0.1",
 		"accesses": "2.5.0",
 		"animejs": "2.2.0",
-		"autosize": "4.0.0",
+		"autosize": "4.0.1",
 		"autwh": "0.0.1",
 		"bcryptjs": "2.4.3",
 		"body-parser": "1.18.2",
@@ -114,7 +114,7 @@
 		"fuckadblock": "3.2.1",
 		"gm": "1.23.1",
 		"gulp": "3.9.1",
-		"gulp-cssnano": "2.1.2",
+		"gulp-cssnano": "2.1.3",
 		"gulp-htmlmin": "4.0.0",
 		"gulp-imagemin": "4.1.0",
 		"gulp-mocha": "5.0.0",
@@ -130,25 +130,25 @@
 		"hard-source-webpack-plugin": "0.6.4",
 		"highlight.js": "9.12.0",
 		"html-minifier": "3.5.12",
-		"inquirer": "5.1.0",
+		"inquirer": "5.2.0",
 		"is-root": "2.0.0",
-		"is-url": "1.2.3",
+		"is-url": "1.2.4",
 		"js-yaml": "3.11.0",
-		"jsdom": "^11.6.2",
+		"jsdom": "11.6.2",
 		"license-checker": "18.0.0",
 		"loader-utils": "1.1.0",
 		"mecab-async": "0.1.2",
 		"mkdirp": "0.5.1",
-		"mocha": "5.0.4",
+		"mocha": "5.0.5",
 		"moji": "0.5.1",
-		"mongodb": "3.0.4",
+		"mongodb": "3.0.5",
 		"monk": "6.0.5",
 		"morgan": "1.9.0",
 		"ms": "2.1.1",
 		"multer": "1.3.0",
-		"nan": "^2.10.0",
-		"node-sass": "4.7.2",
-		"node-sass-json-importer": "3.1.5",
+		"nan": "2.10.0",
+		"node-sass": "4.8.3",
+		"node-sass-json-importer": "3.1.6",
 		"nprogress": "0.2.0",
 		"object-assign-deep": "0.3.1",
 		"on-build-webpack": "0.1.0",
@@ -157,7 +157,7 @@
 		"prominence": "0.2.0",
 		"proxy-addr": "2.0.3",
 		"pug": "2.0.3",
-		"punycode": "^2.1.0",
+		"punycode": "2.1.0",
 		"qrcode": "1.2.0",
 		"ratelimiter": "3.0.3",
 		"recaptcha-promise": "0.1.3",
@@ -181,10 +181,10 @@
 		"tcp-port-used": "0.1.2",
 		"textarea-caret": "3.1.0",
 		"tmp": "0.0.33",
-		"ts-loader": "4.0.1",
+		"ts-loader": "4.1.0",
 		"ts-node": "5.0.1",
 		"tslint": "5.9.1",
-		"typescript": "2.7.2",
+		"typescript": "2.8.1",
 		"typescript-eslint-parser": "14.0.0",
 		"uglify-es": "3.3.9",
 		"url-loader": "1.0.1",
@@ -195,13 +195,13 @@
 		"vue-cropperjs": "2.2.0",
 		"vue-js-modal": "1.3.12",
 		"vue-json-tree-view": "2.1.3",
-		"vue-loader": "14.2.1",
+		"vue-loader": "14.2.2",
 		"vue-router": "3.0.1",
 		"vue-template-compiler": "2.5.16",
 		"vuedraggable": "2.16.0",
 		"web-push": "3.3.0",
-		"webfinger.js": "^2.6.6",
-		"webpack": "4.2.0",
+		"webfinger.js": "2.6.6",
+		"webpack": "4.3.0",
 		"webpack-cli": "2.0.13",
 		"webpack-replace-loader": "1.3.0",
 		"websocket": "1.0.25",

From a1bc0e6769b8a447653cd13a9a320fd7579c55a3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 09:27:57 +0900
Subject: [PATCH 0882/1250] Prepare new codename

---
 package.json                                 | 1 +
 src/index.ts                                 | 1 -
 src/web/app/common/views/widgets/version.vue | 7 ++++---
 src/web/app/config.ts                        | 4 ++--
 src/web/app/init.ts                          | 4 ++--
 src/web/app/mobile/views/pages/settings.vue  | 7 ++++---
 swagger.js                                   | 6 +++---
 webpack.config.ts                            | 3 ++-
 8 files changed, 18 insertions(+), 15 deletions(-)

diff --git a/package.json b/package.json
index 4e3da7d0b..3f9e49738 100644
--- a/package.json
+++ b/package.json
@@ -2,6 +2,7 @@
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
 	"version": "0.0.4224",
+	"codename": "nighthike",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",
 	"bugs": "https://github.com/syuilo/misskey/issues",
diff --git a/src/index.ts b/src/index.ts
index 218455d6f..a8181acda 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -89,7 +89,6 @@ function workerMain() {
  */
 async function init(): Promise<Config> {
 	Logger.info('Welcome to Misskey!');
-	Logger.info(chalk.bold('Misskey <aoi>'));
 	Logger.info('Initializing...');
 
 	EnvironmentInfo.show();
diff --git a/src/web/app/common/views/widgets/version.vue b/src/web/app/common/views/widgets/version.vue
index 5072d9b74..30b632b39 100644
--- a/src/web/app/common/views/widgets/version.vue
+++ b/src/web/app/common/views/widgets/version.vue
@@ -1,16 +1,17 @@
 <template>
-<p>ver {{ v }} (葵 aoi)</p>
+<p>ver {{ version }} ({{ codename }})</p>
 </template>
 
 <script lang="ts">
-import { version } from '../../../config';
+import { version, codename } from '../../../config';
 import define from '../../../common/define-widget';
 export default define({
 	name: 'version'
 }).extend({
 	data() {
 		return {
-			v: version
+			version,
+			codename
 		};
 	}
 });
diff --git a/src/web/app/config.ts b/src/web/app/config.ts
index 8ea6f7010..522d7ff05 100644
--- a/src/web/app/config.ts
+++ b/src/web/app/config.ts
@@ -7,13 +7,13 @@ 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;
 declare const _THEME_COLOR_: string;
 declare const _COPYRIGHT_: string;
 declare const _VERSION_: string;
+declare const _CODENAME_: string;
 declare const _LICENSE_: string;
 declare const _GOOGLE_MAPS_API_KEY_: string;
 
@@ -26,12 +26,12 @@ 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_;
 export const themeColor = _THEME_COLOR_;
 export const copyright = _COPYRIGHT_;
 export const version = _VERSION_;
+export const codename = _CODENAME_;
 export const license = _LICENSE_;
 export const googleMapsApiKey = _GOOGLE_MAPS_API_KEY_;
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index 521dade86..3e5c38961 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -14,7 +14,7 @@ import ElementLocaleJa from 'element-ui/lib/locale/lang/ja';
 import App from './app.vue';
 import checkForUpdate from './common/scripts/check-for-update';
 import MiOS, { API } from './common/mios';
-import { version, hostname, lang } from './config';
+import { version, codename, hostname, lang } from './config';
 
 let elementLocale;
 switch (lang) {
@@ -51,7 +51,7 @@ Vue.mixin({
  * APP ENTRY POINT!
  */
 
-console.info(`Misskey v${version} (葵 aoi)`);
+console.info(`Misskey v${version} (${codename})`);
 console.info(
 	'%cここにコードを入力したり張り付けたりしないでください。アカウントが不正利用される可能性があります。',
 	'color: red; background: yellow; font-size: 16px; font-weight: bold;');
diff --git a/src/web/app/mobile/views/pages/settings.vue b/src/web/app/mobile/views/pages/settings.vue
index 3250999e1..a945a21c5 100644
--- a/src/web/app/mobile/views/pages/settings.vue
+++ b/src/web/app/mobile/views/pages/settings.vue
@@ -12,19 +12,20 @@
 		<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>
+		<p><small>ver {{ version }} ({{ codename }})</small></p>
 	</div>
 </mk-ui>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-import { version } from '../../../config';
+import { version, codename } from '../../../config';
 
 export default Vue.extend({
 	data() {
 		return {
-			v: version
+			version,
+			codename
 		};
 	},
 	mounted() {
diff --git a/swagger.js b/swagger.js
index 0cfd2fff0..f6c2ca7fd 100644
--- a/swagger.js
+++ b/swagger.js
@@ -23,7 +23,7 @@ const defaultSwagger = {
   "swagger": "2.0",
   "info": {
     "title": "Misskey API",
-    "version": "aoi"
+    "version": "nighthike"
   },
   "host": "api.misskey.xyz",
   "schemes": [
@@ -218,8 +218,8 @@ options.apis = files.map(c => {return `${apiRoot}/${c}`;});
 if(fs.existsSync('.config/config.yml')){
   var config = yaml.safeLoad(fs.readFileSync('./.config/config.yml', 'utf8'));
   options.swaggerDefinition.host = `api.${config.url.match(/\:\/\/(.+)$/)[1]}`;
-  options.swaggerDefinition.schemes = config.https.enable ? 
-                                      ['https'] : 
+  options.swaggerDefinition.schemes = config.https.enable ?
+                                      ['https'] :
                                       ['http'];
 }
 
diff --git a/webpack.config.ts b/webpack.config.ts
index 9a952c8ef..88af49eb5 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -20,6 +20,7 @@ import { licenseHtml } from './src/common/build/license';
 import locales from './locales';
 const meta = require('./package.json');
 const version = meta.version;
+const codename = meta.codename;
 
 //#region Replacer definitions
 global['faReplacement'] = faReplacement;
@@ -76,13 +77,13 @@ module.exports = entries.map(x => {
 		_THEME_COLOR_: constants.themeColor,
 		_COPYRIGHT_: constants.copyright,
 		_VERSION_: version,
+		_CODENAME_: codename,
 		_STATUS_URL_: config.status_url,
 		_STATS_URL_: config.stats_url,
 		_DOCS_URL_: config.docs_url,
 		_API_URL_: config.api_url,
 		_WS_URL_: config.ws_url,
 		_DEV_URL_: config.dev_url,
-		_CH_URL_: config.ch_url,
 		_LANG_: lang,
 		_HOST_: config.host,
 		_HOSTNAME_: config.hostname,

From 3dcd4f3d10bd3507a7bbfa405c26c554ca1df9fe Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 09:38:35 +0900
Subject: [PATCH 0883/1250] Add watch command

---
 package.json | 1 +
 1 file changed, 1 insertion(+)

diff --git a/package.json b/package.json
index 3f9e49738..eb65e41db 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
 		"swagger": "node ./swagger.js",
 		"build": "node --max_old_space_size=16384 ./node_modules/webpack/bin/webpack.js && gulp build",
 		"webpack": "node --max_old_space_size=16384 ./node_modules/webpack/bin/webpack.js",
+		"watch": "node --max_old_space_size=16384 ./node_modules/webpack/bin/webpack.js --watch",
 		"gulp": "gulp build",
 		"rebuild": "gulp rebuild",
 		"clean": "gulp clean",

From 0c4bbe63272f6baeee09b7df2837a5a07a70a1cc Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 09:59:36 +0900
Subject: [PATCH 0884/1250] Update README.md

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index c50566cc9..cb8821b1e 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ Key features
 * Two-Factor Authentication support
 * ServiceWorker support
 * Web API for third-party applications
-* No ads
+* ActivityPub compatible
 
 and more! You can touch with your own eyes at https://misskey.xyz/.
 

From 9ac196d11591a2677569a4fa86771de0834d6760 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 13:05:30 +0900
Subject: [PATCH 0885/1250] #1294

---
 locales/ja.yml                                |  2 +-
 src/api/bot/core.ts                           |  2 +-
 .../app/common/views/components/signin.vue    |  2 +-
 .../app/common/views/components/signup.vue    |  2 +-
 .../common/views/directives/autocomplete.ts   |  2 +-
 src/web/app/dev/views/new-app.vue             |  4 +-
 src/web/app/mobile/views/pages/welcome.vue    |  2 +-
 tools/migration/node.2018-03-28.appname.js    | 40 +++++++++++++++++++
 tools/migration/node.2018-03-28.username.js   | 40 +++++++++++++++++++
 9 files changed, 88 insertions(+), 8 deletions(-)
 create mode 100644 tools/migration/node.2018-03-28.appname.js
 create mode 100644 tools/migration/node.2018-03-28.username.js

diff --git a/locales/ja.yml b/locales/ja.yml
index f826b1b6c..fd140ecc3 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -147,7 +147,7 @@ common:
       available: "利用できます"
       unavailable: "既に利用されています"
       error: "通信エラー"
-      invalid-format: "a~z、A~Z、0~9、-(ハイフン)が使えます"
+      invalid-format: "a~z、A~Z、0~9、_が使えます"
       too-short: "3文字以上でお願いします!"
       too-long: "20文字以内でお願いします"
       password: "パスワード"
diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
index 77a68aaee..d6706e9a1 100644
--- a/src/api/bot/core.ts
+++ b/src/api/bot/core.ts
@@ -67,7 +67,7 @@ export default class BotCore extends EventEmitter {
 			return await this.context.q(query);
 		}
 
-		if (/^@[a-zA-Z0-9-]+$/.test(query)) {
+		if (/^@[a-zA-Z0-9_]+$/.test(query)) {
 			return await this.showUserCommand(query);
 		}
 
diff --git a/src/web/app/common/views/components/signin.vue b/src/web/app/common/views/components/signin.vue
index 243468408..273143262 100644
--- a/src/web/app/common/views/components/signin.vue
+++ b/src/web/app/common/views/components/signin.vue
@@ -1,7 +1,7 @@
 <template>
 <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%
+		<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%
diff --git a/src/web/app/common/views/components/signup.vue b/src/web/app/common/views/components/signup.vue
index c2e78aa8a..e77d849ad 100644
--- a/src/web/app/common/views/components/signup.vue
+++ b/src/web/app/common/views/components/signup.vue
@@ -2,7 +2,7 @@
 <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 @input="onChangeUsername"/>
+		<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>
diff --git a/src/web/app/common/views/directives/autocomplete.ts b/src/web/app/common/views/directives/autocomplete.ts
index 3440c4212..94635d301 100644
--- a/src/web/app/common/views/directives/autocomplete.ts
+++ b/src/web/app/common/views/directives/autocomplete.ts
@@ -77,7 +77,7 @@ class Autocomplete {
 
 		if (mentionIndex != -1 && mentionIndex > emojiIndex) {
 			const username = text.substr(mentionIndex + 1);
-			if (username != '' && username.match(/^[a-zA-Z0-9-]+$/)) {
+			if (username != '' && username.match(/^[a-zA-Z0-9_]+$/)) {
 				this.open('user', username);
 				opened = true;
 			}
diff --git a/src/web/app/dev/views/new-app.vue b/src/web/app/dev/views/new-app.vue
index 344e8468f..1a796299c 100644
--- a/src/web/app/dev/views/new-app.vue
+++ b/src/web/app/dev/views/new-app.vue
@@ -6,12 +6,12 @@
 				<b-form-input v-model="name" type="text" placeholder="ex) Misskey for iOS" autocomplete="off" required/>
 			</b-form-group>
 			<b-form-group label="ID" description="あなたのアプリのID。">
-				<b-input v-model="nid" type="text" pattern="^[a-zA-Z0-9-]{3,30}$" placeholder="ex) misskey-for-ios" autocomplete="off" required/>
+				<b-input v-model="nid" type="text" pattern="^[a-zA-Z0-9_]{3,30}$" placeholder="ex) misskey-for-ios" autocomplete="off" required/>
 				<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 == '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>
 			</b-form-group>
diff --git a/src/web/app/mobile/views/pages/welcome.vue b/src/web/app/mobile/views/pages/welcome.vue
index 3384ee699..855744834 100644
--- a/src/web/app/mobile/views/pages/welcome.vue
+++ b/src/web/app/mobile/views/pages/welcome.vue
@@ -6,7 +6,7 @@
 		<p>%fa:lock% ログイン</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="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.account.two_factor_enabled" v-model="token" type="number" placeholder="トークン" required/>
 				<button type="submit" :disabled="signing">{{ signing ? 'ログインしています' : 'ログイン' }}</button>
diff --git a/tools/migration/node.2018-03-28.appname.js b/tools/migration/node.2018-03-28.appname.js
new file mode 100644
index 000000000..9f16e4720
--- /dev/null
+++ b/tools/migration/node.2018-03-28.appname.js
@@ -0,0 +1,40 @@
+// for Node.js interpret
+
+const { default: App } = require('../../built/api/models/app');
+const { generate } = require('../../built/crypto_key');
+const { default: zip } = require('@prezzemolo/zip')
+
+const migrate = async (app) => {
+	const result = await User.update(app._id, {
+		$set: {
+			'name_id': app.name_id.replace(/\-/g, '_'),
+			'name_id_lower': app.name_id_lower.replace(/\-/g, '_')
+		}
+	});
+	return result.ok === 1;
+}
+
+async function main() {
+	const count = await App.count({});
+
+	const dop = Number.parseInt(process.argv[2]) || 5
+	const idop = ((count - (count % dop)) / dop) + 1
+
+	return zip(
+		1,
+		async (time) => {
+			console.log(`${time} / ${idop}`)
+			const doc = await App.find({}, {
+				limit: dop, skip: time * dop
+			})
+			return Promise.all(doc.map(migrate))
+		},
+		idop
+	).then(a => {
+		const rv = []
+		a.forEach(e => rv.push(...e))
+		return rv
+	})
+}
+
+main().then(console.dir).catch(console.error)
diff --git a/tools/migration/node.2018-03-28.username.js b/tools/migration/node.2018-03-28.username.js
new file mode 100644
index 000000000..222215210
--- /dev/null
+++ b/tools/migration/node.2018-03-28.username.js
@@ -0,0 +1,40 @@
+// for Node.js interpret
+
+const { default: User } = require('../../built/api/models/user');
+const { generate } = require('../../built/crypto_key');
+const { default: zip } = require('@prezzemolo/zip')
+
+const migrate = async (user) => {
+	const result = await User.update(user._id, {
+		$set: {
+			'username': user.username.replace(/\-/g, '_'),
+			'username_lower': user.username_lower.replace(/\-/g, '_')
+		}
+	});
+	return result.ok === 1;
+}
+
+async function main() {
+	const count = await User.count({});
+
+	const dop = Number.parseInt(process.argv[2]) || 5
+	const idop = ((count - (count % dop)) / dop) + 1
+
+	return zip(
+		1,
+		async (time) => {
+			console.log(`${time} / ${idop}`)
+			const doc = await User.find({}, {
+				limit: dop, skip: time * dop
+			})
+			return Promise.all(doc.map(migrate))
+		},
+		idop
+	).then(a => {
+		const rv = []
+		a.forEach(e => rv.push(...e))
+		return rv
+	})
+}
+
+main().then(console.dir).catch(console.error)

From 020819a125a98fbb8cee4cf09f1251c3e0fa7368 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 13:13:10 +0900
Subject: [PATCH 0886/1250] wip

---
 src/api/models/messaging-history.ts | 12 +++++++++++-
 src/api/models/messaging-message.ts | 11 ++++++-----
 tools/migration/shell.camel-case.js | 22 ++++++++++++++++++++++
 3 files changed, 39 insertions(+), 6 deletions(-)

diff --git a/src/api/models/messaging-history.ts b/src/api/models/messaging-history.ts
index c06987e45..1e79032ed 100644
--- a/src/api/models/messaging-history.ts
+++ b/src/api/models/messaging-history.ts
@@ -1,3 +1,13 @@
+import * as mongo from 'mongodb';
 import db from '../../db/mongodb';
 
-export default db.get('messaging_histories') as any; // fuck type definition
+const MessagingHistory = db.get<IMessagingHistory>('messagingHistories');
+export default MessagingHistory;
+
+export type IMessagingHistory = {
+	_id: mongo.ObjectID;
+	updatedAt: Date;
+	userId: mongo.ObjectID;
+	partnerId: mongo.ObjectID;
+	messageId: mongo.ObjectID;
+};
diff --git a/src/api/models/messaging-message.ts b/src/api/models/messaging-message.ts
index fcb356c5c..026b23cf3 100644
--- a/src/api/models/messaging-message.ts
+++ b/src/api/models/messaging-message.ts
@@ -5,16 +5,17 @@ import { pack as packFile } from './drive-file';
 import db from '../../db/mongodb';
 import parse from '../common/text';
 
-const MessagingMessage = db.get<IMessagingMessage>('messaging_messages');
+const MessagingMessage = db.get<IMessagingMessage>('messagingMessages');
 export default MessagingMessage;
 
 export interface IMessagingMessage {
 	_id: mongo.ObjectID;
-	created_at: Date;
+	createdAt: Date;
 	text: string;
-	user_id: mongo.ObjectID;
-	recipient_id: mongo.ObjectID;
-	is_read: boolean;
+	userId: mongo.ObjectID;
+	recipientId: mongo.ObjectID;
+	isRead: boolean;
+	fileId: mongo.ObjectID;
 }
 
 export function isValidText(text: string): boolean {
diff --git a/tools/migration/shell.camel-case.js b/tools/migration/shell.camel-case.js
index 6d6b01a07..ac0476af0 100644
--- a/tools/migration/shell.camel-case.js
+++ b/tools/migration/shell.camel-case.js
@@ -79,3 +79,25 @@ db.following.update({}, {
 		follower_id: 'followerId',
 	}
 }, false, true);
+
+db.messaging_histories.renameCollection('messagingHistories');
+db.messagingHistories.update({}, {
+	$rename: {
+		updated_at: 'updatedAt',
+		user_id: 'userId',
+		partner: 'partnerId',
+		message: 'messageId',
+	}
+}, false, true);
+
+db.messaging_messages.renameCollection('messagingMessages');
+db.messagingMessages.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		user_id: 'userId',
+		recipient_id: 'recipientId',
+		file_id: 'fileId',
+		is_read: 'isRead'
+	}
+}, false, true);
+

From 00e4ad08ddf018707f32c32014beb7fa966dc6e0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 13:16:52 +0900
Subject: [PATCH 0887/1250] wip

---
 src/api/models/meta.ts | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/api/models/meta.ts b/src/api/models/meta.ts
index c7dba8fcb..e885c5373 100644
--- a/src/api/models/meta.ts
+++ b/src/api/models/meta.ts
@@ -1,7 +1,8 @@
 import db from '../../db/mongodb';
 
-export default db.get('meta') as any; // fuck type definition
+const Meta = db.get<IMeta>('meta');
+export default Meta;
 
 export type IMeta = {
-	top_image: string;
+	broadcasts: any[];
 };

From b25a590ba90b91532010bd2e1756c47089828f1f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 13:20:19 +0900
Subject: [PATCH 0888/1250] wip

---
 src/api/models/mute.ts              | 12 +++++++++++-
 tools/migration/shell.camel-case.js |  9 +++++++++
 2 files changed, 20 insertions(+), 1 deletion(-)

diff --git a/src/api/models/mute.ts b/src/api/models/mute.ts
index 16018b82f..fdc8cc714 100644
--- a/src/api/models/mute.ts
+++ b/src/api/models/mute.ts
@@ -1,3 +1,13 @@
+import * as mongo from 'mongodb';
 import db from '../../db/mongodb';
 
-export default db.get('mute') as any; // fuck type definition
+const Mute = db.get<IMute>('mute');
+export default Mute;
+
+export interface IMute {
+	_id: mongo.ObjectID;
+	createdAt: Date;
+	deletedAt: Date;
+	muterId: mongo.ObjectID;
+	muteeId: mongo.ObjectID;
+}
diff --git a/tools/migration/shell.camel-case.js b/tools/migration/shell.camel-case.js
index ac0476af0..9f07bd946 100644
--- a/tools/migration/shell.camel-case.js
+++ b/tools/migration/shell.camel-case.js
@@ -101,3 +101,12 @@ db.messagingMessages.update({}, {
 	}
 }, false, true);
 
+db.mute.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		deleted_at: 'deletedAt',
+		mutee_id: 'muteeId',
+		muter_id: 'muterId',
+	}
+}, false, true);
+

From e7666624198d5371b0fa04bee7cd0e47db3ca8ff Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 13:32:05 +0900
Subject: [PATCH 0889/1250] wip

---
 src/api/endpoints/othello/games.ts      |  4 +--
 src/api/endpoints/othello/games/show.ts |  4 +--
 src/api/endpoints/othello/match.ts      |  4 +--
 src/api/models/othello-game.ts          | 34 ++++++++++++-------------
 src/api/stream/othello-game.ts          | 32 +++++++++++------------
 tools/migration/shell.camel-case.js     | 20 +++++++++++++++
 6 files changed, 59 insertions(+), 39 deletions(-)

diff --git a/src/api/endpoints/othello/games.ts b/src/api/endpoints/othello/games.ts
index 2a6bbb404..f6e38b8d8 100644
--- a/src/api/endpoints/othello/games.ts
+++ b/src/api/endpoints/othello/games.ts
@@ -1,5 +1,5 @@
 import $ from 'cafy';
-import Game, { pack } from '../../models/othello-game';
+import OthelloGame, { pack } from '../../models/othello-game';
 
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Get 'my' parameter
@@ -50,7 +50,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}
 
 	// Fetch games
-	const games = await Game.find(q, {
+	const games = await OthelloGame.find(q, {
 		sort,
 		limit
 	});
diff --git a/src/api/endpoints/othello/games/show.ts b/src/api/endpoints/othello/games/show.ts
index 2b0db4dd0..c7bd74a39 100644
--- a/src/api/endpoints/othello/games/show.ts
+++ b/src/api/endpoints/othello/games/show.ts
@@ -1,5 +1,5 @@
 import $ from 'cafy';
-import Game, { pack } from '../../../models/othello-game';
+import OthelloGame, { pack } from '../../../models/othello-game';
 import Othello from '../../../../common/othello/core';
 
 module.exports = (params, user) => new Promise(async (res, rej) => {
@@ -7,7 +7,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [gameId, gameIdErr] = $(params.game_id).id().$;
 	if (gameIdErr) return rej('invalid game_id param');
 
-	const game = await Game.findOne({ _id: gameId });
+	const game = await OthelloGame.findOne({ _id: gameId });
 
 	if (game == null) {
 		return rej('game not found');
diff --git a/src/api/endpoints/othello/match.ts b/src/api/endpoints/othello/match.ts
index b73e105ef..f73386ba7 100644
--- a/src/api/endpoints/othello/match.ts
+++ b/src/api/endpoints/othello/match.ts
@@ -1,6 +1,6 @@
 import $ from 'cafy';
 import Matching, { pack as packMatching } from '../../models/othello-matching';
-import Game, { pack as packGame } from '../../models/othello-game';
+import OthelloGame, { pack as packGame } from '../../models/othello-game';
 import User from '../../models/user';
 import publishUserStream, { publishOthelloStream } from '../../event';
 import { eighteight } from '../../../common/othello/maps';
@@ -28,7 +28,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		});
 
 		// Create game
-		const game = await Game.insert({
+		const game = await OthelloGame.insert({
 			created_at: new Date(),
 			user1_id: exist.parent_id,
 			user2_id: user._id,
diff --git a/src/api/models/othello-game.ts b/src/api/models/othello-game.ts
index 01c6ca6c0..b9e57632d 100644
--- a/src/api/models/othello-game.ts
+++ b/src/api/models/othello-game.ts
@@ -3,17 +3,17 @@ import deepcopy = require('deepcopy');
 import db from '../../db/mongodb';
 import { IUser, pack as packUser } from './user';
 
-const Game = db.get<IGame>('othello_games');
-export default Game;
+const OthelloGame = db.get<IOthelloGame>('othelloGames');
+export default OthelloGame;
 
-export interface IGame {
+export interface IOthelloGame {
 	_id: mongo.ObjectID;
-	created_at: Date;
-	started_at: Date;
-	user1_id: mongo.ObjectID;
-	user2_id: mongo.ObjectID;
-	user1_accepted: boolean;
-	user2_accepted: boolean;
+	createdAt: Date;
+	startedAt: Date;
+	user1Id: mongo.ObjectID;
+	user2Id: mongo.ObjectID;
+	user1Accepted: boolean;
+	user2Accepted: boolean;
 
 	/**
 	 * どちらのプレイヤーが先行(黒)か
@@ -22,9 +22,9 @@ export interface IGame {
 	 */
 	black: number;
 
-	is_started: boolean;
-	is_ended: boolean;
-	winner_id: mongo.ObjectID;
+	isStarted: boolean;
+	isEnded: boolean;
+	winnerId: mongo.ObjectID;
 	logs: Array<{
 		at: Date;
 		color: boolean;
@@ -33,9 +33,9 @@ export interface IGame {
 	settings: {
 		map: string[];
 		bw: string | number;
-		is_llotheo: boolean;
-		can_put_everywhere: boolean;
-		looped_board: boolean;
+		isLlotheo: boolean;
+		canPutEverywhere: boolean;
+		loopedBoard: boolean;
 	};
 	form1: any;
 	form2: any;
@@ -62,11 +62,11 @@ export const pack = (
 
 	// Populate the game if 'game' is ID
 	if (mongo.ObjectID.prototype.isPrototypeOf(game)) {
-		_game = await Game.findOne({
+		_game = await OthelloGame.findOne({
 			_id: game
 		});
 	} else if (typeof game === 'string') {
-		_game = await Game.findOne({
+		_game = await OthelloGame.findOne({
 			_id: new mongo.ObjectID(game)
 		});
 	} else {
diff --git a/src/api/stream/othello-game.ts b/src/api/stream/othello-game.ts
index 1c846f27a..45a931c7e 100644
--- a/src/api/stream/othello-game.ts
+++ b/src/api/stream/othello-game.ts
@@ -1,7 +1,7 @@
 import * as websocket from 'websocket';
 import * as redis from 'redis';
 import * as CRC32 from 'crc-32';
-import Game, { pack } from '../models/othello-game';
+import OthelloGame, { pack } from '../models/othello-game';
 import { publishOthelloGameStream } from '../event';
 import Othello from '../../common/othello/core';
 import * as maps from '../../common/othello/maps';
@@ -60,14 +60,14 @@ export default function(request: websocket.request, connection: websocket.connec
 	});
 
 	async function updateSettings(settings) {
-		const game = await Game.findOne({ _id: gameId });
+		const game = await OthelloGame.findOne({ _id: gameId });
 
 		if (game.is_started) return;
 		if (!game.user1_id.equals(user._id) && !game.user2_id.equals(user._id)) return;
 		if (game.user1_id.equals(user._id) && game.user1_accepted) return;
 		if (game.user2_id.equals(user._id) && game.user2_accepted) return;
 
-		await Game.update({ _id: gameId }, {
+		await OthelloGame.update({ _id: gameId }, {
 			$set: {
 				settings
 			}
@@ -77,7 +77,7 @@ export default function(request: websocket.request, connection: websocket.connec
 	}
 
 	async function initForm(form) {
-		const game = await Game.findOne({ _id: gameId });
+		const game = await OthelloGame.findOne({ _id: gameId });
 
 		if (game.is_started) return;
 		if (!game.user1_id.equals(user._id) && !game.user2_id.equals(user._id)) return;
@@ -88,7 +88,7 @@ export default function(request: websocket.request, connection: websocket.connec
 			form2: form
 		};
 
-		await Game.update({ _id: gameId }, {
+		await OthelloGame.update({ _id: gameId }, {
 			$set: set
 		});
 
@@ -99,7 +99,7 @@ export default function(request: websocket.request, connection: websocket.connec
 	}
 
 	async function updateForm(id, value) {
-		const game = await Game.findOne({ _id: gameId });
+		const game = await OthelloGame.findOne({ _id: gameId });
 
 		if (game.is_started) return;
 		if (!game.user1_id.equals(user._id) && !game.user2_id.equals(user._id)) return;
@@ -118,7 +118,7 @@ export default function(request: websocket.request, connection: websocket.connec
 			form1: form
 		};
 
-		await Game.update({ _id: gameId }, {
+		await OthelloGame.update({ _id: gameId }, {
 			$set: set
 		});
 
@@ -138,14 +138,14 @@ export default function(request: websocket.request, connection: websocket.connec
 	}
 
 	async function accept(accept: boolean) {
-		const game = await Game.findOne({ _id: gameId });
+		const game = await OthelloGame.findOne({ _id: gameId });
 
 		if (game.is_started) return;
 
 		let bothAccepted = false;
 
 		if (game.user1_id.equals(user._id)) {
-			await Game.update({ _id: gameId }, {
+			await OthelloGame.update({ _id: gameId }, {
 				$set: {
 					user1_accepted: accept
 				}
@@ -158,7 +158,7 @@ export default function(request: websocket.request, connection: websocket.connec
 
 			if (accept && game.user2_accepted) bothAccepted = true;
 		} else if (game.user2_id.equals(user._id)) {
-			await Game.update({ _id: gameId }, {
+			await OthelloGame.update({ _id: gameId }, {
 				$set: {
 					user2_accepted: accept
 				}
@@ -177,7 +177,7 @@ export default function(request: websocket.request, connection: websocket.connec
 		if (bothAccepted) {
 			// 3秒後、まだacceptされていたらゲーム開始
 			setTimeout(async () => {
-				const freshGame = await Game.findOne({ _id: gameId });
+				const freshGame = await OthelloGame.findOne({ _id: gameId });
 				if (freshGame == null || freshGame.is_started || freshGame.is_ended) return;
 				if (!freshGame.user1_accepted || !freshGame.user2_accepted) return;
 
@@ -196,7 +196,7 @@ export default function(request: websocket.request, connection: websocket.connec
 
 				const map = freshGame.settings.map != null ? freshGame.settings.map : getRandomMap();
 
-				await Game.update({ _id: gameId }, {
+				await OthelloGame.update({ _id: gameId }, {
 					$set: {
 						started_at: new Date(),
 						is_started: true,
@@ -222,7 +222,7 @@ export default function(request: websocket.request, connection: websocket.connec
 						winner = null;
 					}
 
-					await Game.update({
+					await OthelloGame.update({
 						_id: gameId
 					}, {
 						$set: {
@@ -245,7 +245,7 @@ export default function(request: websocket.request, connection: websocket.connec
 
 	// 石を打つ
 	async function set(pos) {
-		const game = await Game.findOne({ _id: gameId });
+		const game = await OthelloGame.findOne({ _id: gameId });
 
 		if (!game.is_started) return;
 		if (game.is_ended) return;
@@ -288,7 +288,7 @@ export default function(request: websocket.request, connection: websocket.connec
 
 		const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString());
 
-		await Game.update({
+		await OthelloGame.update({
 			_id: gameId
 		}, {
 			$set: {
@@ -314,7 +314,7 @@ export default function(request: websocket.request, connection: websocket.connec
 	}
 
 	async function check(crc32) {
-		const game = await Game.findOne({ _id: gameId });
+		const game = await OthelloGame.findOne({ _id: gameId });
 
 		if (!game.is_started) return;
 
diff --git a/tools/migration/shell.camel-case.js b/tools/migration/shell.camel-case.js
index 9f07bd946..11c6fe401 100644
--- a/tools/migration/shell.camel-case.js
+++ b/tools/migration/shell.camel-case.js
@@ -1,3 +1,5 @@
+// このスクリプトを走らせる前か後に notifications コレクションはdropしてください
+
 db.access_tokens.renameCollection('accessTokens');
 db.accessTokens.update({}, {
 	$rename: {
@@ -110,3 +112,21 @@ db.mute.update({}, {
 	}
 }, false, true);
 
+db.othello_games.renameCollection('othelloGames');
+db.othelloGames.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		started_at: 'startedAt',
+		is_started: 'isStarted',
+		is_ended: 'isEnded',
+		user1_id: 'user1Id',
+		user2_id: 'user2Id',
+		user1_accepted: 'user1Accepted',
+		user2_accepted: 'user2Accepted',
+		winner_id: 'winnerId',
+		'settings.is_llotheo': 'settings.isLlotheo',
+		'settings.can_put_everywhere': 'settings.canPutEverywhere',
+		'settings.looped_board': 'settings.loopedBoard',
+	}
+}, false, true);
+

From 3e37c71f0d620058b400afdb1ec426fa04c02214 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 28 Mar 2018 06:47:01 +0000
Subject: [PATCH 0890/1250] fix(package): update gulp-typescript to version
 4.0.2

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index eb65e41db..75eaa5741 100644
--- a/package.json
+++ b/package.json
@@ -126,7 +126,7 @@
 		"gulp-sourcemaps": "2.6.4",
 		"gulp-stylus": "2.7.0",
 		"gulp-tslint": "8.1.3",
-		"gulp-typescript": "4.0.1",
+		"gulp-typescript": "4.0.2",
 		"gulp-uglify": "3.0.0",
 		"gulp-util": "3.0.8",
 		"hard-source-webpack-plugin": "0.6.4",

From b339cd635acd5c32a7c07b3a259fa68ba053f93a Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 28 Mar 2018 06:50:48 +0000
Subject: [PATCH 0891/1250] fix(package): update element-ui to version 2.3.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index eb65e41db..efede0bac 100644
--- a/package.json
+++ b/package.json
@@ -103,7 +103,7 @@
 		"deepcopy": "0.6.3",
 		"diskusage": "0.2.4",
 		"elasticsearch": "14.2.1",
-		"element-ui": "2.2.2",
+		"element-ui": "2.3.0",
 		"emojilib": "2.2.12",
 		"escape-regexp": "0.0.1",
 		"eslint": "4.19.1",

From 69c97113809ce20a7a39a2bead1d9e17a2a131b1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 15:54:47 +0900
Subject: [PATCH 0892/1250] wip

---
 src/api/models/othello-matching.ts  |  8 ++++----
 src/api/models/poll-vote.ts         | 12 +++++++++++-
 tools/migration/shell.camel-case.js | 17 +++++++++++++++++
 3 files changed, 32 insertions(+), 5 deletions(-)

diff --git a/src/api/models/othello-matching.ts b/src/api/models/othello-matching.ts
index 5cc39cae1..9c84d7fb9 100644
--- a/src/api/models/othello-matching.ts
+++ b/src/api/models/othello-matching.ts
@@ -3,14 +3,14 @@ import deepcopy = require('deepcopy');
 import db from '../../db/mongodb';
 import { IUser, pack as packUser } from './user';
 
-const Matching = db.get<IMatching>('othello_matchings');
+const Matching = db.get<IMatching>('othelloMatchings');
 export default Matching;
 
 export interface IMatching {
 	_id: mongo.ObjectID;
-	created_at: Date;
-	parent_id: mongo.ObjectID;
-	child_id: mongo.ObjectID;
+	createdAt: Date;
+	parentId: mongo.ObjectID;
+	childId: mongo.ObjectID;
 }
 
 /**
diff --git a/src/api/models/poll-vote.ts b/src/api/models/poll-vote.ts
index af77a2643..3e883f213 100644
--- a/src/api/models/poll-vote.ts
+++ b/src/api/models/poll-vote.ts
@@ -1,3 +1,13 @@
+import * as mongo from 'mongodb';
 import db from '../../db/mongodb';
 
-export default db.get('poll_votes') as any; // fuck type definition
+const PollVote = db.get<IPollVote>('pollVotes');
+export default PollVote;
+
+export interface IPollVote {
+	_id: mongo.ObjectID;
+	createdAt: Date;
+	userId: mongo.ObjectID;
+	postId: mongo.ObjectID;
+	choice: number;
+}
diff --git a/tools/migration/shell.camel-case.js b/tools/migration/shell.camel-case.js
index 11c6fe401..6045dfa00 100644
--- a/tools/migration/shell.camel-case.js
+++ b/tools/migration/shell.camel-case.js
@@ -130,3 +130,20 @@ db.othelloGames.update({}, {
 	}
 }, false, true);
 
+db.othello_matchings.renameCollection('othelloMatchings');
+db.othelloMatchings.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		parent_id: 'parentId',
+		child_id: 'childId'
+	}
+}, false, true);
+
+db.poll_votes.renameCollection('pollVotes');
+db.pollVotes.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		user_id: 'userId',
+		post_id: 'postId'
+	}
+}, false, true);

From 3431cbb196fa8cd675b08465028ef99e876d7d0b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 15:59:34 +0900
Subject: [PATCH 0893/1250] wip

---
 src/api/models/post-reaction.ts     |  8 +++++---
 src/api/models/post-watching.ts     | 11 ++++++++++-
 tools/migration/shell.camel-case.js |  9 +++++++++
 3 files changed, 24 insertions(+), 4 deletions(-)

diff --git a/src/api/models/post-reaction.ts b/src/api/models/post-reaction.ts
index 639a70e00..f581f0153 100644
--- a/src/api/models/post-reaction.ts
+++ b/src/api/models/post-reaction.ts
@@ -4,13 +4,15 @@ import db from '../../db/mongodb';
 import Reaction from './post-reaction';
 import { pack as packUser } from './user';
 
-const PostReaction = db.get<IPostReaction>('post_reactions');
+const PostReaction = db.get<IPostReaction>('postReactions');
 export default PostReaction;
 
 export interface IPostReaction {
 	_id: mongo.ObjectID;
-	created_at: Date;
-	deleted_at: Date;
+	createdAt: Date;
+	deletedAt: Date;
+	postId: mongo.ObjectID;
+	userId: mongo.ObjectID;
 	reaction: string;
 }
 
diff --git a/src/api/models/post-watching.ts b/src/api/models/post-watching.ts
index 41d37e270..907909a50 100644
--- a/src/api/models/post-watching.ts
+++ b/src/api/models/post-watching.ts
@@ -1,3 +1,12 @@
+import * as mongo from 'mongodb';
 import db from '../../db/mongodb';
 
-export default db.get('post_watching') as any; // fuck type definition
+const PostWatching = db.get<IPostWatching>('postWatching');
+export default PostWatching;
+
+export interface IPostWatching {
+	_id: mongo.ObjectID;
+	createdAt: Date;
+	userId: mongo.ObjectID;
+	postId: mongo.ObjectID;
+}
diff --git a/tools/migration/shell.camel-case.js b/tools/migration/shell.camel-case.js
index 6045dfa00..2a5456b4d 100644
--- a/tools/migration/shell.camel-case.js
+++ b/tools/migration/shell.camel-case.js
@@ -147,3 +147,12 @@ db.pollVotes.update({}, {
 		post_id: 'postId'
 	}
 }, false, true);
+
+db.post_reactions.renameCollection('postReactions');
+db.postReactions.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		user_id: 'userId',
+		post_id: 'postId'
+	}
+}, false, true);

From 83aeea9f67c7fabb79ff59aa325e4d944fd301b3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 16:04:44 +0900
Subject: [PATCH 0894/1250] wip

---
 src/api/models/post.ts              | 18 ++++++++----------
 tools/migration/shell.camel-case.js | 22 ++++++++++++++++++++++
 2 files changed, 30 insertions(+), 10 deletions(-)

diff --git a/src/api/models/post.ts b/src/api/models/post.ts
index c37c8371c..fc4425651 100644
--- a/src/api/models/post.ts
+++ b/src/api/models/post.ts
@@ -20,18 +20,16 @@ export function isValidText(text: string): boolean {
 
 export type IPost = {
 	_id: mongo.ObjectID;
-	channel_id: mongo.ObjectID;
-	created_at: Date;
-	media_ids: mongo.ObjectID[];
-	reply_id: mongo.ObjectID;
-	repost_id: mongo.ObjectID;
+	channelId: mongo.ObjectID;
+	createdAt: Date;
+	mediaIds: mongo.ObjectID[];
+	replyId: mongo.ObjectID;
+	repostId: mongo.ObjectID;
 	poll: any; // todo
 	text: string;
-	user_id: mongo.ObjectID;
-	app_id: mongo.ObjectID;
-	category: string;
-	is_category_verified: boolean;
-	via_mobile: boolean;
+	userId: mongo.ObjectID;
+	appId: mongo.ObjectID;
+	viaMobile: boolean;
 	geo: {
 		latitude: number;
 		longitude: number;
diff --git a/tools/migration/shell.camel-case.js b/tools/migration/shell.camel-case.js
index 2a5456b4d..326d0a1b0 100644
--- a/tools/migration/shell.camel-case.js
+++ b/tools/migration/shell.camel-case.js
@@ -156,3 +156,25 @@ db.postReactions.update({}, {
 		post_id: 'postId'
 	}
 }, false, true);
+
+db.post_watching.renameCollection('postWatching');
+db.postWatching.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		user_id: 'userId',
+		post_id: 'postId'
+	}
+}, false, true);
+
+db.posts.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		channel_id: 'channelId',
+		user_id: 'userId',
+		app_id: 'appId',
+		media_ids: 'mediaIds',
+		reply_id: 'replyId',
+		repost_id: 'repostId',
+		via_mobile: 'viaMobile'
+	}
+}, false, true);

From 0597e1311955b58979e4005e32c879791bebfcc4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 16:07:48 +0900
Subject: [PATCH 0895/1250] wip

---
 tools/migration/shell.camel-case.js | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/tools/migration/shell.camel-case.js b/tools/migration/shell.camel-case.js
index 326d0a1b0..a7e8afcdc 100644
--- a/tools/migration/shell.camel-case.js
+++ b/tools/migration/shell.camel-case.js
@@ -175,6 +175,8 @@ db.posts.update({}, {
 		media_ids: 'mediaIds',
 		reply_id: 'replyId',
 		repost_id: 'repostId',
-		via_mobile: 'viaMobile'
+		via_mobile: 'viaMobile',
+		'_reply.user_id': '_reply.userId',
+		'_repost.user_id': '_repost.userId',
 	}
 }, false, true);

From 1c507b89f2c1a8a515bc17c71e228230d625a8d0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 16:09:49 +0900
Subject: [PATCH 0896/1250] wip

---
 src/api/models/signin.ts            | 5 +++++
 tools/migration/shell.camel-case.js | 7 +++++++
 2 files changed, 12 insertions(+)

diff --git a/src/api/models/signin.ts b/src/api/models/signin.ts
index 262c8707e..62ee796d8 100644
--- a/src/api/models/signin.ts
+++ b/src/api/models/signin.ts
@@ -7,6 +7,11 @@ export default Signin;
 
 export interface ISignin {
 	_id: mongo.ObjectID;
+	createdAt: Date;
+	userId: mongo.ObjectID;
+	ip: string;
+	headers: any;
+	success: boolean;
 }
 
 /**
diff --git a/tools/migration/shell.camel-case.js b/tools/migration/shell.camel-case.js
index a7e8afcdc..533868cdc 100644
--- a/tools/migration/shell.camel-case.js
+++ b/tools/migration/shell.camel-case.js
@@ -180,3 +180,10 @@ db.posts.update({}, {
 		'_repost.user_id': '_repost.userId',
 	}
 }, false, true);
+
+db.signin.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		user_id: 'userId',
+	}
+}, false, true);

From 61f476f9424157952afe8d04eef7670abbed3f30 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 16:11:50 +0900
Subject: [PATCH 0897/1250] wip

---
 src/api/models/sw-subscription.ts   | 12 +++++++++++-
 tools/migration/shell.camel-case.js |  7 +++++++
 2 files changed, 18 insertions(+), 1 deletion(-)

diff --git a/src/api/models/sw-subscription.ts b/src/api/models/sw-subscription.ts
index ecca04cb9..235c801c7 100644
--- a/src/api/models/sw-subscription.ts
+++ b/src/api/models/sw-subscription.ts
@@ -1,3 +1,13 @@
+import * as mongo from 'mongodb';
 import db from '../../db/mongodb';
 
-export default db.get('sw_subscriptions') as any; // fuck type definition
+const SwSubscription = db.get<ISwSubscription>('swSubscriptions');
+export default SwSubscription;
+
+export interface ISwSubscription {
+	_id: mongo.ObjectID;
+	userId: mongo.ObjectID;
+	endpoint: string;
+	auth: string;
+	publickey: string;
+}
diff --git a/tools/migration/shell.camel-case.js b/tools/migration/shell.camel-case.js
index 533868cdc..9cb0baaaf 100644
--- a/tools/migration/shell.camel-case.js
+++ b/tools/migration/shell.camel-case.js
@@ -187,3 +187,10 @@ db.signin.update({}, {
 		user_id: 'userId',
 	}
 }, false, true);
+
+db.sw_subscriptions.renameCollection('swSubscriptions');
+db.swSubscriptions.update({}, {
+	$rename: {
+		user_id: 'userId',
+	}
+}, false, true);

From 5082325cd3151bc38f14edce70e19c062d1cb0d0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 16:20:46 +0900
Subject: [PATCH 0898/1250] wip

---
 src/api/models/user.ts              |  2 +-
 src/api/private/signup.ts           |  2 --
 tools/migration/shell.camel-case.js | 33 +++++++++++++++++++++++++++++
 3 files changed, 34 insertions(+), 3 deletions(-)

diff --git a/src/api/models/user.ts b/src/api/models/user.ts
index e73c95faf..9aa7c4efa 100644
--- a/src/api/models/user.ts
+++ b/src/api/models/user.ts
@@ -46,7 +46,7 @@ export type ILocalAccount = {
 	password: string;
 	token: string;
 	twitter: {
-		access_token: string;
+		accessToken: string;
 		access_token_secret: string;
 		user_id: string;
 		screen_name: string;
diff --git a/src/api/private/signup.ts b/src/api/private/signup.ts
index 96e049570..29d75b62f 100644
--- a/src/api/private/signup.ts
+++ b/src/api/private/signup.ts
@@ -115,8 +115,6 @@ export default async (req: express.Request, res: express.Response) => {
 		following_count: 0,
 		name: name,
 		posts_count: 0,
-		likes_count: 0,
-		liked_count: 0,
 		drive_capacity: 1073741824, // 1GB
 		username: username,
 		username_lower: username.toLowerCase(),
diff --git a/tools/migration/shell.camel-case.js b/tools/migration/shell.camel-case.js
index 9cb0baaaf..afe831e5b 100644
--- a/tools/migration/shell.camel-case.js
+++ b/tools/migration/shell.camel-case.js
@@ -194,3 +194,36 @@ db.swSubscriptions.update({}, {
 		user_id: 'userId',
 	}
 }, false, true);
+
+db.users.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		deleted_at: 'deletedAt',
+		followers_count: 'followersCount',
+		following_count: 'followingCount',
+		posts_count: 'postsCount',
+		drive_capacity: 'driveCapacity',
+		username_lower: 'usernameLower',
+		avatar_id: 'avatarId',
+		banner_id: 'bannerId',
+		pinned_post_id: 'pinnedPostId',
+		is_suspended: 'isSuspended',
+		host_lower: 'hostLower',
+		'twitter.access_token': 'twitter.accessToken',
+		'twitter.access_token_secret': 'twitter.accessTokenSecret',
+		'twitter.user_id': 'twitter.userId',
+		'twitter.screen_name': 'twitter.screenName',
+		'line.user_id': 'line.userId',
+		last_used_at: 'lastUsedAt',
+		is_bot: 'isBot',
+		is_pro: 'isPro',
+		two_factor_secret: 'twoFactorSecret',
+		two_factor_enabled: 'twoFactorEnabled',
+		client_settings: 'clientSettings'
+	},
+	$unset: {
+		likes_count: '',
+		liked_count: '',
+		latest_post: ''
+	}
+}, false, true);

From c27c8a8f6afb6711b8bf287fe2b28ec36ac3fbb8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 16:39:14 +0900
Subject: [PATCH 0899/1250] wip

---
 src/api/authenticate.ts                       |   4 +-
 src/api/bot/core.ts                           |   2 +-
 src/api/bot/interfaces/line.ts                |  10 +-
 src/api/common/add-file-to-drive.ts           | 306 ++++++++++++++++++
 src/api/common/drive/add-file.ts              |  16 +-
 src/api/common/get-friends.ts                 |   8 +-
 src/api/common/notify.ts                      |  18 +-
 src/api/common/push-sw.ts                     |   4 +-
 src/api/common/read-messaging-message.ts      |  12 +-
 src/api/common/read-notification.ts           |   8 +-
 src/api/common/watch-post.ts                  |  14 +-
 src/api/endpoints/aggregation/posts.ts        |  16 +-
 .../endpoints/aggregation/posts/reaction.ts   |  16 +-
 .../endpoints/aggregation/posts/reactions.ts  |  16 +-
 src/api/endpoints/aggregation/posts/reply.ts  |  14 +-
 src/api/endpoints/aggregation/posts/repost.ts |  16 +-
 src/api/endpoints/aggregation/users.ts        |   8 +-
 .../endpoints/aggregation/users/activity.ts   |  24 +-
 .../endpoints/aggregation/users/followers.ts  |  20 +-
 .../endpoints/aggregation/users/following.ts  |  20 +-
 src/api/endpoints/aggregation/users/post.ts   |  24 +-
 .../endpoints/aggregation/users/reaction.ts   |  16 +-
 src/api/endpoints/app/create.ts               |  16 +-
 src/api/endpoints/app/name_id/available.ts    |  18 +-
 src/api/endpoints/app/show.ts                 |  24 +-
 src/api/endpoints/auth/accept.ts              |  14 +-
 src/api/endpoints/auth/session/generate.ts    |   4 +-
 src/api/endpoints/auth/session/show.ts        |   6 +-
 src/api/endpoints/auth/session/userkey.ts     |  12 +-
 src/api/endpoints/channels/create.ts          |  12 +-
 src/api/endpoints/channels/posts.ts           |   8 +-
 src/api/endpoints/channels/show.ts            |   6 +-
 src/api/endpoints/channels/unwatch.ts         |  16 +-
 src/api/endpoints/channels/watch.ts           |  20 +-
 src/api/endpoints/drive.ts                    |   4 +-
 src/api/endpoints/drive/files.ts              |  10 +-
 src/api/endpoints/drive/files/create.ts       |   6 +-
 src/api/endpoints/drive/files/find.ts         |  10 +-
 src/api/endpoints/drive/files/show.ts         |   8 +-
 src/api/endpoints/drive/files/update.ts       |  22 +-
 .../endpoints/drive/files/upload_from_url.ts  |   6 +-
 src/api/endpoints/drive/folders.ts            |  10 +-
 src/api/endpoints/drive/folders/create.ts     |  14 +-
 src/api/endpoints/drive/folders/find.ts       |  10 +-
 src/api/endpoints/drive/folders/show.ts       |   8 +-
 src/api/endpoints/drive/folders/update.ts     |  32 +-
 src/api/endpoints/drive/stream.ts             |   2 +-
 src/api/endpoints/following/create.ts         |  22 +-
 src/api/endpoints/following/delete.ts         |  18 +-
 src/api/endpoints/i.ts                        |   2 +-
 src/api/endpoints/i/2fa/done.ts               |   8 +-
 src/api/endpoints/i/2fa/register.ts           |   2 +-
 src/api/endpoints/i/2fa/unregister.ts         |   4 +-
 src/api/endpoints/i/appdata/get.ts            |   4 +-
 src/api/endpoints/i/appdata/set.ts            |   8 +-
 src/api/endpoints/i/authorized_apps.ts        |   4 +-
 src/api/endpoints/i/favorites.ts              |   2 +-
 src/api/endpoints/i/notifications.ts          |  12 +-
 src/api/endpoints/i/pin.ts                    |  10 +-
 src/api/endpoints/i/signin_history.ts         |   2 +-
 src/api/endpoints/i/update.ts                 |  30 +-
 src/api/endpoints/i/update_client_setting.ts  |   4 +-
 src/api/endpoints/i/update_home.ts            |   6 +-
 src/api/endpoints/i/update_mobile_home.ts     |   6 +-
 src/api/endpoints/messaging/history.ts        |  14 +-
 src/api/endpoints/messaging/messages.ts       |  14 +-
 .../endpoints/messaging/messages/create.ts    |  74 ++---
 src/api/endpoints/messaging/unread.ts         |  12 +-
 src/api/endpoints/mute/create.ts              |  18 +-
 src/api/endpoints/mute/delete.ts              |  14 +-
 src/api/endpoints/mute/list.ts                |   8 +-
 src/api/endpoints/my/apps.ts                  |   2 +-
 .../notifications/get_unread_count.ts         |  12 +-
 .../notifications/mark_as_read_all.ts         |   6 +-
 src/api/endpoints/othello/games.ts            |   8 +-
 src/api/endpoints/othello/games/show.ts       |   6 +-
 src/api/endpoints/othello/invitations.ts      |   2 +-
 src/api/endpoints/othello/match.ts            |  40 +--
 src/api/endpoints/othello/match/cancel.ts     |   2 +-
 src/api/endpoints/posts.ts                    |   8 +-
 src/api/endpoints/posts/categorize.ts         |   8 +-
 src/api/endpoints/posts/context.ts            |  14 +-
 src/api/endpoints/posts/create.ts             | 152 ++++-----
 src/api/endpoints/posts/favorites/create.ts   |  16 +-
 src/api/endpoints/posts/favorites/delete.ts   |  10 +-
 src/api/endpoints/posts/mentions.ts           |   2 +-
 .../endpoints/posts/polls/recommendation.ts   |   8 +-
 src/api/endpoints/posts/polls/vote.ts         |  32 +-
 src/api/endpoints/posts/reactions.ts          |  10 +-
 src/api/endpoints/posts/reactions/create.ts   |  42 +--
 src/api/endpoints/posts/reactions/delete.ts   |  14 +-
 src/api/endpoints/posts/replies.ts            |   8 +-
 src/api/endpoints/posts/reposts.ts            |   8 +-
 src/api/endpoints/posts/search.ts             |  74 ++---
 src/api/endpoints/posts/show.ts               |   6 +-
 src/api/endpoints/posts/timeline.ts           |  30 +-
 src/api/endpoints/posts/trend.ts              |   8 +-
 src/api/endpoints/stats.ts                    |   4 +-
 src/api/endpoints/sw/register.ts              |   6 +-
 src/api/endpoints/username/available.ts       |   2 +-
 src/api/endpoints/users.ts                    |   4 +-
 src/api/endpoints/users/followers.ts          |  14 +-
 src/api/endpoints/users/following.ts          |  14 +-
 .../users/get_frequently_replied_users.ts     |  20 +-
 src/api/endpoints/users/posts.ts              |  22 +-
 src/api/endpoints/users/recommendation.ts     |   4 +-
 src/api/endpoints/users/search.ts             |   2 +-
 src/api/endpoints/users/search_by_username.ts |   2 +-
 src/api/endpoints/users/show.ts               |  56 ++--
 src/api/models/channel-watching.ts            |   2 +-
 src/api/models/channel.ts                     |   2 +-
 src/api/models/messaging-message.ts           |   8 +-
 src/api/models/notification.ts                |  22 +-
 src/api/models/othello-game.ts                |   8 +-
 src/api/models/othello-matching.ts            |   4 +-
 src/api/models/post-reaction.ts               |   2 +-
 src/api/models/post.ts                        |  36 +--
 src/api/models/user.ts                        | 117 +++----
 src/api/private/signin.ts                     |  10 +-
 src/api/private/signup.ts                     |  22 +-
 src/api/service/github.ts                     |   2 +-
 src/api/service/twitter.ts                    |  10 +-
 src/api/stream/home.ts                        |  16 +-
 src/api/stream/othello-game.ts                |  96 +++---
 src/api/stream/othello.ts                     |   6 +-
 src/api/streaming.ts                          |   2 +-
 src/common/get-post-summary.ts                |   4 +-
 src/common/othello/ai/back.ts                 |  28 +-
 src/common/othello/ai/front.ts                |  16 +-
 src/common/user/get-summary.ts                |   2 +-
 src/config.ts                                 |   2 +-
 src/tools/analysis/extract-user-domains.ts    |   2 +-
 src/tools/analysis/extract-user-keywords.ts   |   2 +-
 src/tools/analysis/predict-user-interst.ts    |   2 +-
 src/web/app/auth/views/form.vue               |   2 +-
 src/web/app/ch/tags/channel.tag               |  20 +-
 src/web/app/common/define-widget.ts           |   4 +-
 src/web/app/common/mios.ts                    |   4 +-
 src/web/app/common/scripts/streaming/home.ts  |   2 +-
 .../views/components/messaging-room.form.vue  |   6 +-
 .../components/messaging-room.message.vue     |   6 +-
 .../views/components/messaging-room.vue       |  14 +-
 .../app/common/views/components/messaging.vue |  12 +-
 .../common/views/components/othello.game.vue  |  54 ++--
 .../views/components/othello.gameroom.vue     |   2 +-
 .../common/views/components/othello.room.vue  |  26 +-
 .../app/common/views/components/othello.vue   |  10 +-
 src/web/app/common/views/components/poll.vue  |   2 +-
 .../app/common/views/components/post-menu.vue |   4 +-
 .../views/components/reaction-picker.vue      |   2 +-
 .../app/common/views/components/signin.vue    |   4 +-
 .../views/components/twitter-setting.vue      |   4 +-
 .../app/common/views/components/uploader.vue  |   2 +-
 .../views/components/welcome-timeline.vue     |   2 +-
 .../app/common/views/widgets/slideshow.vue    |   2 +-
 src/web/app/desktop/api/update-avatar.ts      |   6 +-
 src/web/app/desktop/api/update-banner.ts      |   6 +-
 .../app/desktop/views/components/activity.vue |   2 +-
 .../desktop/views/components/drive.file.vue   |  14 +-
 .../desktop/views/components/drive.folder.vue |  10 +-
 .../views/components/drive.nav-folder.vue     |   8 +-
 .../app/desktop/views/components/drive.vue    |  28 +-
 .../views/components/follow-button.vue        |   4 +-
 .../desktop/views/components/followers.vue    |   4 +-
 .../desktop/views/components/following.vue    |   4 +-
 src/web/app/desktop/views/components/home.vue |  20 +-
 .../desktop/views/components/media-image.vue  |   2 +-
 .../views/components/notifications.vue        |  22 +-
 .../views/components/post-detail.sub.vue      |   8 +-
 .../desktop/views/components/post-detail.vue  |  16 +-
 .../desktop/views/components/post-form.vue    |   8 +-
 .../desktop/views/components/post-preview.vue |   8 +-
 .../views/components/posts.post.sub.vue       |   8 +-
 .../desktop/views/components/posts.post.vue   |  20 +-
 .../app/desktop/views/components/posts.vue    |   4 +-
 .../desktop/views/components/repost-form.vue  |   2 +-
 .../desktop/views/components/settings.2fa.vue |   8 +-
 .../views/components/settings.profile.vue     |   4 +-
 .../views/components/settings.signins.vue     |   2 +-
 .../app/desktop/views/components/settings.vue |  10 +-
 .../views/components/sub-post-content.vue     |   4 +-
 .../app/desktop/views/components/timeline.vue |   4 +-
 .../desktop/views/components/ui.header.vue    |   4 +-
 .../desktop/views/components/user-preview.vue |   8 +-
 .../views/components/widget-container.vue     |   4 +-
 .../app/desktop/views/components/window.vue   |   4 +-
 src/web/app/desktop/views/pages/home.vue      |   2 +-
 src/web/app/desktop/views/pages/post.vue      |   2 +-
 .../pages/user/user.followers-you-know.vue    |   2 +-
 .../desktop/views/pages/user/user.friends.vue |   2 +-
 .../desktop/views/pages/user/user.home.vue    |   6 +-
 .../desktop/views/pages/user/user.photos.vue  |   2 +-
 .../desktop/views/pages/user/user.profile.vue |  12 +-
 .../views/pages/user/user.timeline.vue        |   4 +-
 .../views/widgets/channel.channel.form.vue    |   4 +-
 .../desktop/views/widgets/channel.channel.vue |   2 +-
 src/web/app/desktop/views/widgets/channel.vue |   2 +-
 src/web/app/dev/views/app.vue                 |   2 +-
 src/web/app/dev/views/new-app.vue             |   6 +-
 src/web/app/mobile/api/post.ts                |   2 +-
 .../app/mobile/views/components/activity.vue  |   2 +-
 .../views/components/drive.file-detail.vue    |  14 +-
 .../mobile/views/components/drive.file.vue    |   4 +-
 src/web/app/mobile/views/components/drive.vue |  28 +-
 .../mobile/views/components/follow-button.vue |   4 +-
 .../mobile/views/components/media-image.vue   |   2 +-
 .../mobile/views/components/notification.vue  |   8 +-
 .../mobile/views/components/notifications.vue |   4 +-
 .../app/mobile/views/components/post-card.vue |   2 +-
 .../views/components/post-detail.sub.vue      |   2 +-
 .../mobile/views/components/post-detail.vue   |  12 +-
 .../app/mobile/views/components/post-form.vue |   8 +-
 .../mobile/views/components/post-preview.vue  |   2 +-
 .../app/mobile/views/components/post.sub.vue  |   2 +-
 src/web/app/mobile/views/components/post.vue  |  14 +-
 src/web/app/mobile/views/components/posts.vue |   4 +-
 .../views/components/sub-post-content.vue     |   4 +-
 .../app/mobile/views/components/timeline.vue  |   2 +-
 .../app/mobile/views/components/ui.header.vue |   4 +-
 .../mobile/views/components/user-timeline.vue |   4 +-
 src/web/app/mobile/views/pages/followers.vue  |   4 +-
 src/web/app/mobile/views/pages/following.vue  |   4 +-
 src/web/app/mobile/views/pages/home.vue       |  22 +-
 src/web/app/mobile/views/pages/post.vue       |   2 +-
 .../mobile/views/pages/profile-setting.vue    |   4 +-
 src/web/app/mobile/views/pages/user.vue       |   6 +-
 .../pages/user/home.followers-you-know.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  |   4 +-
 src/web/app/mobile/views/pages/welcome.vue    |   4 +-
 src/web/app/stats/tags/index.tag              |   2 +-
 src/web/docs/api/endpoints/posts/create.yaml  |   6 +-
 src/web/docs/api/entities/drive-file.yaml     |   6 +-
 src/web/docs/api/entities/post.yaml           |  12 +-
 src/web/docs/api/entities/user.yaml           |  26 +-
 237 files changed, 1661 insertions(+), 1354 deletions(-)
 create mode 100644 src/api/common/add-file-to-drive.ts

diff --git a/src/api/authenticate.ts b/src/api/authenticate.ts
index 537c3d1e1..7b3983a83 100644
--- a/src/api/authenticate.ts
+++ b/src/api/authenticate.ts
@@ -55,10 +55,10 @@ export default (req: express.Request) => new Promise<IAuthContext>(async (resolv
 		}
 
 		const app = await App
-			.findOne({ _id: accessToken.app_id });
+			.findOne({ _id: accessToken.appId });
 
 		const user = await User
-			.findOne({ _id: accessToken.user_id });
+			.findOne({ _id: accessToken.userId });
 
 		return resolve({
 			app: app,
diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
index d6706e9a1..9e699572d 100644
--- a/src/api/bot/core.ts
+++ b/src/api/bot/core.ts
@@ -208,7 +208,7 @@ class SigninContext extends Context {
 		if (this.temporaryUser == null) {
 			// Fetch user
 			const user: IUser = await User.findOne({
-				username_lower: query.toLowerCase(),
+				usernameLower: query.toLowerCase(),
 				host: null
 			}, {
 				fields: {
diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts
index 8036b2fde..dc600125c 100644
--- a/src/api/bot/interfaces/line.ts
+++ b/src/api/bot/interfaces/line.ts
@@ -115,7 +115,7 @@ class LineBot extends BotCore {
 			actions.push({
 				type: 'uri',
 				label: 'Twitterアカウントを見る',
-				uri: `https://twitter.com/${user.account.twitter.screen_name}`
+				uri: `https://twitter.com/${user.account.twitter.screenName}`
 			});
 		}
 
@@ -142,7 +142,7 @@ class LineBot extends BotCore {
 
 	public async showUserTimelinePostback(userId: string) {
 		const tl = await require('../../endpoints/users/posts')({
-			user_id: userId,
+			userId: userId,
 			limit: 5
 		}, this.user);
 
@@ -174,7 +174,7 @@ module.exports = async (app: express.Application) => {
 			const user = await User.findOne({
 				host: null,
 				'account.line': {
-					user_id: sourceId
+					userId: sourceId
 				}
 			});
 
@@ -184,7 +184,7 @@ module.exports = async (app: express.Application) => {
 				User.update(user._id, {
 					$set: {
 						'account.line': {
-							user_id: sourceId
+							userId: sourceId
 						}
 					}
 				});
@@ -194,7 +194,7 @@ module.exports = async (app: express.Application) => {
 				User.update(user._id, {
 					$set: {
 						'account.line': {
-							user_id: null
+							userId: null
 						}
 					}
 				});
diff --git a/src/api/common/add-file-to-drive.ts b/src/api/common/add-file-to-drive.ts
new file mode 100644
index 000000000..6bf5fcbc0
--- /dev/null
+++ b/src/api/common/add-file-to-drive.ts
@@ -0,0 +1,306 @@
+import { Buffer } from 'buffer';
+import * as fs from 'fs';
+import * as tmp from 'tmp';
+import * as stream from 'stream';
+
+import * as mongodb from 'mongodb';
+import * as crypto from 'crypto';
+import * as _gm from 'gm';
+import * as debug from 'debug';
+import fileType = require('file-type');
+import prominence = require('prominence');
+
+import DriveFile, { getGridFSBucket } from '../models/drive-file';
+import DriveFolder from '../models/drive-folder';
+import { pack } from '../models/drive-file';
+import event, { publishDriveStream } from '../event';
+import config from '../../conf';
+
+const gm = _gm.subClass({
+	imageMagick: true
+});
+
+const log = debug('misskey:register-drive-file');
+
+const tmpFile = (): Promise<string> => new Promise((resolve, reject) => {
+	tmp.file((e, path) => {
+		if (e) return reject(e);
+		resolve(path);
+	});
+});
+
+const addToGridFS = (name: string, readable: stream.Readable, type: string, metadata: any): Promise<any> =>
+	getGridFSBucket()
+		.then(bucket => new Promise((resolve, reject) => {
+			const writeStream = bucket.openUploadStream(name, { contentType: type, metadata });
+			writeStream.once('finish', (doc) => { resolve(doc); });
+			writeStream.on('error', reject);
+			readable.pipe(writeStream);
+		}));
+
+const addFile = async (
+	user: any,
+	path: string,
+	name: string = null,
+	comment: string = null,
+	folderId: mongodb.ObjectID = null,
+	force: boolean = false
+) => {
+	log(`registering ${name} (user: ${user.username}, path: ${path})`);
+
+	// Calculate hash, get content type and get file size
+	const [hash, [mime, ext], size] = await Promise.all([
+		// hash
+		((): Promise<string> => new Promise((res, rej) => {
+			const readable = fs.createReadStream(path);
+			const hash = crypto.createHash('md5');
+			const chunks = [];
+			readable
+				.on('error', rej)
+				.pipe(hash)
+				.on('error', rej)
+				.on('data', (chunk) => chunks.push(chunk))
+				.on('end', () => {
+					const buffer = Buffer.concat(chunks);
+					res(buffer.toString('hex'));
+				});
+		}))(),
+		// mime
+		((): Promise<[string, string | null]> => new Promise((res, rej) => {
+			const readable = fs.createReadStream(path);
+			readable
+				.on('error', rej)
+				.once('data', (buffer: Buffer) => {
+					readable.destroy();
+					const type = fileType(buffer);
+					if (type) {
+						return res([type.mime, type.ext]);
+					} else {
+						// 種類が同定できなかったら application/octet-stream にする
+						return res(['application/octet-stream', null]);
+					}
+				});
+		}))(),
+		// size
+		((): Promise<number> => new Promise((res, rej) => {
+			fs.stat(path, (err, stats) => {
+				if (err) return rej(err);
+				res(stats.size);
+			});
+		}))()
+	]);
+
+	log(`hash: ${hash}, mime: ${mime}, ext: ${ext}, size: ${size}`);
+
+	// detect name
+	const detectedName: string = name || (ext ? `untitled.${ext}` : 'untitled');
+
+	if (!force) {
+		// Check if there is a file with the same hash
+		const much = await DriveFile.findOne({
+			md5: hash,
+			'metadata.userId': user._id
+		});
+
+		if (much !== null) {
+			log('file with same hash is found');
+			return much;
+		} else {
+			log('file with same hash is not found');
+		}
+	}
+
+	const [wh, avgColor, folder] = await Promise.all([
+		// Width and height (when image)
+		(async () => {
+			// 画像かどうか
+			if (!/^image\/.*$/.test(mime)) {
+				return null;
+			}
+
+			const imageType = mime.split('/')[1];
+
+			// 画像でもPNGかJPEGかGIFでないならスキップ
+			if (imageType != 'png' && imageType != 'jpeg' && imageType != 'gif') {
+				return null;
+			}
+
+			log('calculate image width and height...');
+
+			// Calculate width and height
+			const g = gm(fs.createReadStream(path), name);
+			const size = await prominence(g).size();
+
+			log(`image width and height is calculated: ${size.width}, ${size.height}`);
+
+			return [size.width, size.height];
+		})(),
+		// average color (when image)
+		(async () => {
+			// 画像かどうか
+			if (!/^image\/.*$/.test(mime)) {
+				return null;
+			}
+
+			const imageType = mime.split('/')[1];
+
+			// 画像でもPNGかJPEGでないならスキップ
+			if (imageType != 'png' && imageType != 'jpeg') {
+				return null;
+			}
+
+			log('calculate average color...');
+
+			const buffer = await prominence(gm(fs.createReadStream(path), name)
+				.setFormat('ppm')
+				.resize(1, 1)) // 1pxのサイズに縮小して平均色を取得するというハック
+				.toBuffer();
+
+			const r = buffer.readUInt8(buffer.length - 3);
+			const g = buffer.readUInt8(buffer.length - 2);
+			const b = buffer.readUInt8(buffer.length - 1);
+
+			log(`average color is calculated: ${r}, ${g}, ${b}`);
+
+			return [r, g, b];
+		})(),
+		// folder
+		(async () => {
+			if (!folderId) {
+				return null;
+			}
+			const driveFolder = await DriveFolder.findOne({
+				_id: folderId,
+				userId: user._id
+			});
+			if (!driveFolder) {
+				throw 'folder-not-found';
+			}
+			return driveFolder;
+		})(),
+		// usage checker
+		(async () => {
+			// Calculate drive usage
+			const usage = await DriveFile
+				.aggregate([{
+					$match: { 'metadata.userId': user._id }
+				}, {
+					$project: {
+						length: true
+					}
+				}, {
+					$group: {
+						_id: null,
+						usage: { $sum: '$length' }
+					}
+				}])
+				.then((aggregates: any[]) => {
+					if (aggregates.length > 0) {
+						return aggregates[0].usage;
+					}
+					return 0;
+				});
+
+			log(`drive usage is ${usage}`);
+
+			// If usage limit exceeded
+			if (usage + size > user.driveCapacity) {
+				throw 'no-free-space';
+			}
+		})()
+	]);
+
+	const readable = fs.createReadStream(path);
+
+	const properties = {};
+
+	if (wh) {
+		properties['width'] = wh[0];
+		properties['height'] = wh[1];
+	}
+
+	if (avgColor) {
+		properties['avgColor'] = avgColor;
+	}
+
+	return addToGridFS(detectedName, readable, mime, {
+		userId: user._id,
+		folderId: folder !== null ? folder._id : null,
+		comment: comment,
+		properties: properties
+	});
+};
+
+/**
+ * Add file to drive
+ *
+ * @param user User who wish to add file
+ * @param file File path or readableStream
+ * @param comment Comment
+ * @param type File type
+ * @param folderId Folder ID
+ * @param force If set to true, forcibly upload the file even if there is a file with the same hash.
+ * @return Object that represents added file
+ */
+export default (user: any, file: string | stream.Readable, ...args) => new Promise<any>((resolve, reject) => {
+	// Get file path
+	new Promise((res: (v: [string, boolean]) => void, rej) => {
+		if (typeof file === 'string') {
+			res([file, false]);
+			return;
+		}
+		if (typeof file === 'object' && typeof file.read === 'function') {
+			tmpFile()
+				.then(path => {
+					const readable: stream.Readable = file;
+					const writable = fs.createWriteStream(path);
+					readable
+						.on('error', rej)
+						.on('end', () => {
+							res([path, true]);
+						})
+						.pipe(writable)
+						.on('error', rej);
+				})
+				.catch(rej);
+		}
+		rej(new Error('un-compatible file.'));
+	})
+	.then(([path, shouldCleanup]): Promise<any> => new Promise((res, rej) => {
+		addFile(user, path, ...args)
+			.then(file => {
+				res(file);
+				if (shouldCleanup) {
+					fs.unlink(path, (e) => {
+						if (e) log(e.stack);
+					});
+				}
+			})
+			.catch(rej);
+	}))
+	.then(file => {
+		log(`drive file has been created ${file._id}`);
+		resolve(file);
+
+		pack(file).then(serializedFile => {
+			// Publish drive_file_created event
+			event(user._id, 'drive_file_created', serializedFile);
+			publishDriveStream(user._id, 'file_created', serializedFile);
+
+			// Register to search database
+			if (config.elasticsearch.enable) {
+				const es = require('../../db/elasticsearch');
+				es.index({
+					index: 'misskey',
+					type: 'drive_file',
+					id: file._id.toString(),
+					body: {
+						name: file.name,
+						userId: user._id.toString()
+					}
+				});
+			}
+		});
+	})
+	.catch(reject);
+});
diff --git a/src/api/common/drive/add-file.ts b/src/api/common/drive/add-file.ts
index c4f2f212a..b10f9e381 100644
--- a/src/api/common/drive/add-file.ts
+++ b/src/api/common/drive/add-file.ts
@@ -100,7 +100,7 @@ const addFile = async (
 		// Check if there is a file with the same hash
 		const much = await DriveFile.findOne({
 			md5: hash,
-			'metadata.user_id': user._id
+			'metadata.userId': user._id
 		});
 
 		if (much !== null) {
@@ -172,7 +172,7 @@ const addFile = async (
 			}
 			const driveFolder = await DriveFolder.findOne({
 				_id: folderId,
-				user_id: user._id
+				userId: user._id
 			});
 			if (!driveFolder) {
 				throw 'folder-not-found';
@@ -184,7 +184,7 @@ const addFile = async (
 			// Calculate drive usage
 			const usage = await DriveFile
 				.aggregate([{
-					$match: { 'metadata.user_id': user._id }
+					$match: { 'metadata.userId': user._id }
 				}, {
 					$project: {
 						length: true
@@ -205,7 +205,7 @@ const addFile = async (
 			log(`drive usage is ${usage}`);
 
 			// If usage limit exceeded
-			if (usage + size > user.drive_capacity) {
+			if (usage + size > user.driveCapacity) {
 				throw 'no-free-space';
 			}
 		})()
@@ -221,12 +221,12 @@ const addFile = async (
 	}
 
 	if (averageColor) {
-		properties['average_color'] = averageColor;
+		properties['avgColor'] = averageColor;
 	}
 
 	return addToGridFS(detectedName, readable, mime, {
-		user_id: user._id,
-		folder_id: folder !== null ? folder._id : null,
+		userId: user._id,
+		folderId: folder !== null ? folder._id : null,
 		comment: comment,
 		properties: properties
 	});
@@ -297,7 +297,7 @@ export default (user: any, file: string | stream.Readable, ...args) => new Promi
 					id: file._id.toString(),
 					body: {
 						name: file.name,
-						user_id: user._id.toString()
+						userId: user._id.toString()
 					}
 				});
 			}
diff --git a/src/api/common/get-friends.ts b/src/api/common/get-friends.ts
index db6313816..7f548b3bb 100644
--- a/src/api/common/get-friends.ts
+++ b/src/api/common/get-friends.ts
@@ -6,17 +6,17 @@ export default async (me: mongodb.ObjectID, includeMe: boolean = true) => {
 	// SELECT followee
 	const myfollowing = await Following
 		.find({
-			follower_id: me,
+			followerId: me,
 			// 削除されたドキュメントは除く
-			deleted_at: { $exists: false }
+			deletedAt: { $exists: false }
 		}, {
 			fields: {
-				followee_id: true
+				followeeId: true
 			}
 		});
 
 	// ID list of other users who the I follows
-	const myfollowingIds = myfollowing.map(follow => follow.followee_id);
+	const myfollowingIds = myfollowing.map(follow => follow.followeeId);
 
 	if (includeMe) {
 		myfollowingIds.push(me);
diff --git a/src/api/common/notify.ts b/src/api/common/notify.ts
index ae5669b84..c4df17f88 100644
--- a/src/api/common/notify.ts
+++ b/src/api/common/notify.ts
@@ -16,11 +16,11 @@ export default (
 
 	// Create notification
 	const notification = await Notification.insert(Object.assign({
-		created_at: new Date(),
-		notifiee_id: notifiee,
-		notifier_id: notifier,
+		createdAt: new Date(),
+		notifieeId: notifiee,
+		notifierId: notifier,
 		type: type,
-		is_read: false
+		isRead: false
 	}, content));
 
 	resolve(notification);
@@ -31,14 +31,14 @@ export default (
 
 	// 3秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
 	setTimeout(async () => {
-		const fresh = await Notification.findOne({ _id: notification._id }, { is_read: true });
-		if (!fresh.is_read) {
+		const fresh = await Notification.findOne({ _id: notification._id }, { isRead: true });
+		if (!fresh.isRead) {
 			//#region ただしミュートしているユーザーからの通知なら無視
 			const mute = await Mute.find({
-				muter_id: notifiee,
-				deleted_at: { $exists: false }
+				muterId: notifiee,
+				deletedAt: { $exists: false }
 			});
-			const mutedUserIds = mute.map(m => m.mutee_id.toString());
+			const mutedUserIds = mute.map(m => m.muteeId.toString());
 			if (mutedUserIds.indexOf(notifier.toString()) != -1) {
 				return;
 			}
diff --git a/src/api/common/push-sw.ts b/src/api/common/push-sw.ts
index 2993c760e..f90233233 100644
--- a/src/api/common/push-sw.ts
+++ b/src/api/common/push-sw.ts
@@ -20,7 +20,7 @@ export default async function(userId: mongo.ObjectID | string, type, body?) {
 
 	// Fetch
 	const subscriptions = await Subscription.find({
-		user_id: userId
+		userId: userId
 	});
 
 	subscriptions.forEach(subscription => {
@@ -41,7 +41,7 @@ export default async function(userId: mongo.ObjectID | string, type, body?) {
 
 			if (err.statusCode == 410) {
 				Subscription.remove({
-					user_id: userId,
+					userId: userId,
 					endpoint: subscription.endpoint,
 					auth: subscription.auth,
 					publickey: subscription.publickey
diff --git a/src/api/common/read-messaging-message.ts b/src/api/common/read-messaging-message.ts
index 8e5e5b2b6..9047edec8 100644
--- a/src/api/common/read-messaging-message.ts
+++ b/src/api/common/read-messaging-message.ts
@@ -37,12 +37,12 @@ export default (
 	// Update documents
 	await Message.update({
 		_id: { $in: ids },
-		user_id: otherpartyId,
-		recipient_id: userId,
-		is_read: false
+		userId: otherpartyId,
+		recipientId: userId,
+		isRead: false
 	}, {
 		$set: {
-			is_read: true
+			isRead: true
 		}
 	}, {
 		multi: true
@@ -55,8 +55,8 @@ export default (
 	// Calc count of my unread messages
 	const count = await Message
 		.count({
-			recipient_id: userId,
-			is_read: false
+			recipientId: userId,
+			isRead: false
 		});
 
 	if (count == 0) {
diff --git a/src/api/common/read-notification.ts b/src/api/common/read-notification.ts
index 3009cc5d0..5bbf13632 100644
--- a/src/api/common/read-notification.ts
+++ b/src/api/common/read-notification.ts
@@ -29,10 +29,10 @@ export default (
 	// Update documents
 	await Notification.update({
 		_id: { $in: ids },
-		is_read: false
+		isRead: false
 	}, {
 		$set: {
-			is_read: true
+			isRead: true
 		}
 	}, {
 		multi: true
@@ -41,8 +41,8 @@ export default (
 	// Calc count of my unread notifications
 	const count = await Notification
 		.count({
-			notifiee_id: userId,
-			is_read: false
+			notifieeId: userId,
+			isRead: false
 		});
 
 	if (count == 0) {
diff --git a/src/api/common/watch-post.ts b/src/api/common/watch-post.ts
index 1a50f0eda..61ea44443 100644
--- a/src/api/common/watch-post.ts
+++ b/src/api/common/watch-post.ts
@@ -3,15 +3,15 @@ import Watching from '../models/post-watching';
 
 export default async (me: mongodb.ObjectID, post: object) => {
 	// 自分の投稿はwatchできない
-	if (me.equals((post as any).user_id)) {
+	if (me.equals((post as any).userId)) {
 		return;
 	}
 
 	// if watching now
 	const exist = await Watching.findOne({
-		post_id: (post as any)._id,
-		user_id: me,
-		deleted_at: { $exists: false }
+		postId: (post as any)._id,
+		userId: me,
+		deletedAt: { $exists: false }
 	});
 
 	if (exist !== null) {
@@ -19,8 +19,8 @@ export default async (me: mongodb.ObjectID, post: object) => {
 	}
 
 	await Watching.insert({
-		created_at: new Date(),
-		post_id: (post as any)._id,
-		user_id: me
+		createdAt: new Date(),
+		postId: (post as any)._id,
+		userId: me
 	});
 };
diff --git a/src/api/endpoints/aggregation/posts.ts b/src/api/endpoints/aggregation/posts.ts
index 9d8bccbdb..67d261964 100644
--- a/src/api/endpoints/aggregation/posts.ts
+++ b/src/api/endpoints/aggregation/posts.ts
@@ -18,23 +18,23 @@ module.exports = params => new Promise(async (res, rej) => {
 	const datas = await Post
 		.aggregate([
 			{ $project: {
-				repost_id: '$repost_id',
-				reply_id: '$reply_id',
-				created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
+				repostId: '$repostId',
+				replyId: '$replyId',
+				createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
 			}},
 			{ $project: {
 				date: {
-					year: { $year: '$created_at' },
-					month: { $month: '$created_at' },
-					day: { $dayOfMonth: '$created_at' }
+					year: { $year: '$createdAt' },
+					month: { $month: '$createdAt' },
+					day: { $dayOfMonth: '$createdAt' }
 				},
 				type: {
 					$cond: {
-						if: { $ne: ['$repost_id', null] },
+						if: { $ne: ['$repostId', null] },
 						then: 'repost',
 						else: {
 							$cond: {
-								if: { $ne: ['$reply_id', null] },
+								if: { $ne: ['$replyId', null] },
 								then: 'reply',
 								else: 'post'
 							}
diff --git a/src/api/endpoints/aggregation/posts/reaction.ts b/src/api/endpoints/aggregation/posts/reaction.ts
index eb99b9d08..9f9a4f37e 100644
--- a/src/api/endpoints/aggregation/posts/reaction.ts
+++ b/src/api/endpoints/aggregation/posts/reaction.ts
@@ -12,9 +12,9 @@ import Reaction from '../../../models/post-reaction';
  * @return {Promise<any>}
  */
 module.exports = (params) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Lookup post
 	const post = await Post.findOne({
@@ -27,15 +27,15 @@ module.exports = (params) => new Promise(async (res, rej) => {
 
 	const datas = await Reaction
 		.aggregate([
-			{ $match: { post_id: post._id } },
+			{ $match: { postId: post._id } },
 			{ $project: {
-				created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
+				createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
 			}},
 			{ $project: {
 				date: {
-					year: { $year: '$created_at' },
-					month: { $month: '$created_at' },
-					day: { $dayOfMonth: '$created_at' }
+					year: { $year: '$createdAt' },
+					month: { $month: '$createdAt' },
+					day: { $dayOfMonth: '$createdAt' }
 				}
 			}},
 			{ $group: {
diff --git a/src/api/endpoints/aggregation/posts/reactions.ts b/src/api/endpoints/aggregation/posts/reactions.ts
index 790b523be..2dc989281 100644
--- a/src/api/endpoints/aggregation/posts/reactions.ts
+++ b/src/api/endpoints/aggregation/posts/reactions.ts
@@ -12,9 +12,9 @@ import Reaction from '../../../models/post-reaction';
  * @return {Promise<any>}
  */
 module.exports = (params) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Lookup post
 	const post = await Post.findOne({
@@ -29,10 +29,10 @@ module.exports = (params) => new Promise(async (res, rej) => {
 
 	const reactions = await Reaction
 		.find({
-			post_id: post._id,
+			postId: post._id,
 			$or: [
-				{ deleted_at: { $exists: false } },
-				{ deleted_at: { $gt: startTime } }
+				{ deletedAt: { $exists: false } },
+				{ deletedAt: { $gt: startTime } }
 			]
 		}, {
 			sort: {
@@ -40,7 +40,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
 			},
 			fields: {
 				_id: false,
-				post_id: false
+				postId: false
 			}
 		});
 
@@ -55,7 +55,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
 		// day = day.getTime();
 
 		const count = reactions.filter(r =>
-			r.created_at < day && (r.deleted_at == null || r.deleted_at > day)
+			r.createdAt < day && (r.deletedAt == null || r.deletedAt > day)
 		).length;
 
 		graph.push({
diff --git a/src/api/endpoints/aggregation/posts/reply.ts b/src/api/endpoints/aggregation/posts/reply.ts
index b114c34e1..3b050582a 100644
--- a/src/api/endpoints/aggregation/posts/reply.ts
+++ b/src/api/endpoints/aggregation/posts/reply.ts
@@ -11,9 +11,9 @@ import Post from '../../../models/post';
  * @return {Promise<any>}
  */
 module.exports = (params) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Lookup post
 	const post = await Post.findOne({
@@ -28,13 +28,13 @@ module.exports = (params) => new Promise(async (res, rej) => {
 		.aggregate([
 			{ $match: { reply: post._id } },
 			{ $project: {
-				created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
+				createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
 			}},
 			{ $project: {
 				date: {
-					year: { $year: '$created_at' },
-					month: { $month: '$created_at' },
-					day: { $dayOfMonth: '$created_at' }
+					year: { $year: '$createdAt' },
+					month: { $month: '$createdAt' },
+					day: { $dayOfMonth: '$createdAt' }
 				}
 			}},
 			{ $group: {
diff --git a/src/api/endpoints/aggregation/posts/repost.ts b/src/api/endpoints/aggregation/posts/repost.ts
index 217159caa..d9f3e36a0 100644
--- a/src/api/endpoints/aggregation/posts/repost.ts
+++ b/src/api/endpoints/aggregation/posts/repost.ts
@@ -11,9 +11,9 @@ import Post from '../../../models/post';
  * @return {Promise<any>}
  */
 module.exports = (params) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Lookup post
 	const post = await Post.findOne({
@@ -26,15 +26,15 @@ module.exports = (params) => new Promise(async (res, rej) => {
 
 	const datas = await Post
 		.aggregate([
-			{ $match: { repost_id: post._id } },
+			{ $match: { repostId: post._id } },
 			{ $project: {
-				created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
+				createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
 			}},
 			{ $project: {
 				date: {
-					year: { $year: '$created_at' },
-					month: { $month: '$created_at' },
-					day: { $dayOfMonth: '$created_at' }
+					year: { $year: '$createdAt' },
+					month: { $month: '$createdAt' },
+					day: { $dayOfMonth: '$createdAt' }
 				}
 			}},
 			{ $group: {
diff --git a/src/api/endpoints/aggregation/users.ts b/src/api/endpoints/aggregation/users.ts
index e38ce92ff..a4e91a228 100644
--- a/src/api/endpoints/aggregation/users.ts
+++ b/src/api/endpoints/aggregation/users.ts
@@ -22,8 +22,8 @@ module.exports = params => new Promise(async (res, rej) => {
 			},
 			fields: {
 				_id: false,
-				created_at: true,
-				deleted_at: true
+				createdAt: true,
+				deletedAt: true
 			}
 		});
 
@@ -44,11 +44,11 @@ module.exports = params => new Promise(async (res, rej) => {
 		// day = day.getTime();
 
 		const total = users.filter(u =>
-			u.created_at < dayEnd && (u.deleted_at == null || u.deleted_at > dayEnd)
+			u.createdAt < dayEnd && (u.deletedAt == null || u.deletedAt > dayEnd)
 		).length;
 
 		const created = users.filter(u =>
-			u.created_at < dayEnd && u.created_at > dayStart
+			u.createdAt < dayEnd && u.createdAt > dayStart
 		).length;
 
 		graph.push({
diff --git a/src/api/endpoints/aggregation/users/activity.ts b/src/api/endpoints/aggregation/users/activity.ts
index 102a71d7c..d47761657 100644
--- a/src/api/endpoints/aggregation/users/activity.ts
+++ b/src/api/endpoints/aggregation/users/activity.ts
@@ -18,9 +18,9 @@ module.exports = (params) => new Promise(async (res, rej) => {
 	const [limit = 365, limitErr] = $(params.limit).optional.number().range(1, 365).$;
 	if (limitErr) return rej('invalid limit param');
 
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [userId, userIdErr] = $(params.userId).id().$;
+	if (userIdErr) return rej('invalid userId param');
 
 	// Lookup user
 	const user = await User.findOne({
@@ -37,25 +37,25 @@ module.exports = (params) => new Promise(async (res, rej) => {
 
 	const datas = await Post
 		.aggregate([
-			{ $match: { user_id: user._id } },
+			{ $match: { userId: user._id } },
 			{ $project: {
-				repost_id: '$repost_id',
-				reply_id: '$reply_id',
-				created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
+				repostId: '$repostId',
+				replyId: '$replyId',
+				createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
 			}},
 			{ $project: {
 				date: {
-					year: { $year: '$created_at' },
-					month: { $month: '$created_at' },
-					day: { $dayOfMonth: '$created_at' }
+					year: { $year: '$createdAt' },
+					month: { $month: '$createdAt' },
+					day: { $dayOfMonth: '$createdAt' }
 				},
 				type: {
 					$cond: {
-						if: { $ne: ['$repost_id', null] },
+						if: { $ne: ['$repostId', null] },
 						then: 'repost',
 						else: {
 							$cond: {
-								if: { $ne: ['$reply_id', null] },
+								if: { $ne: ['$replyId', null] },
 								then: 'reply',
 								else: 'post'
 							}
diff --git a/src/api/endpoints/aggregation/users/followers.ts b/src/api/endpoints/aggregation/users/followers.ts
index 3022b2b00..73a30281b 100644
--- a/src/api/endpoints/aggregation/users/followers.ts
+++ b/src/api/endpoints/aggregation/users/followers.ts
@@ -12,9 +12,9 @@ import Following from '../../../models/following';
  * @return {Promise<any>}
  */
 module.exports = (params) => new Promise(async (res, rej) => {
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [userId, userIdErr] = $(params.userId).id().$;
+	if (userIdErr) return rej('invalid userId param');
 
 	// Lookup user
 	const user = await User.findOne({
@@ -33,17 +33,17 @@ module.exports = (params) => new Promise(async (res, rej) => {
 
 	const following = await Following
 		.find({
-			followee_id: user._id,
+			followeeId: user._id,
 			$or: [
-				{ deleted_at: { $exists: false } },
-				{ deleted_at: { $gt: startTime } }
+				{ deletedAt: { $exists: false } },
+				{ deletedAt: { $gt: startTime } }
 			]
 		}, {
 			_id: false,
-			follower_id: false,
-			followee_id: false
+			followerId: false,
+			followeeId: false
 		}, {
-			sort: { created_at: -1 }
+			sort: { createdAt: -1 }
 		});
 
 	const graph = [];
@@ -57,7 +57,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
 		// day = day.getTime();
 
 		const count = following.filter(f =>
-			f.created_at < day && (f.deleted_at == null || f.deleted_at > day)
+			f.createdAt < day && (f.deletedAt == null || f.deletedAt > day)
 		).length;
 
 		graph.push({
diff --git a/src/api/endpoints/aggregation/users/following.ts b/src/api/endpoints/aggregation/users/following.ts
index 92da7e692..16fc568d5 100644
--- a/src/api/endpoints/aggregation/users/following.ts
+++ b/src/api/endpoints/aggregation/users/following.ts
@@ -12,9 +12,9 @@ import Following from '../../../models/following';
  * @return {Promise<any>}
  */
 module.exports = (params) => new Promise(async (res, rej) => {
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [userId, userIdErr] = $(params.userId).id().$;
+	if (userIdErr) return rej('invalid userId param');
 
 	// Lookup user
 	const user = await User.findOne({
@@ -33,17 +33,17 @@ module.exports = (params) => new Promise(async (res, rej) => {
 
 	const following = await Following
 		.find({
-			follower_id: user._id,
+			followerId: user._id,
 			$or: [
-				{ deleted_at: { $exists: false } },
-				{ deleted_at: { $gt: startTime } }
+				{ deletedAt: { $exists: false } },
+				{ deletedAt: { $gt: startTime } }
 			]
 		}, {
 			_id: false,
-			follower_id: false,
-			followee_id: false
+			followerId: false,
+			followeeId: false
 		}, {
-			sort: { created_at: -1 }
+			sort: { createdAt: -1 }
 		});
 
 	const graph = [];
@@ -56,7 +56,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
 		day = new Date(day.setHours(23));
 
 		const count = following.filter(f =>
-			f.created_at < day && (f.deleted_at == null || f.deleted_at > day)
+			f.createdAt < day && (f.deletedAt == null || f.deletedAt > day)
 		).length;
 
 		graph.push({
diff --git a/src/api/endpoints/aggregation/users/post.ts b/src/api/endpoints/aggregation/users/post.ts
index c6a75eee3..c98874859 100644
--- a/src/api/endpoints/aggregation/users/post.ts
+++ b/src/api/endpoints/aggregation/users/post.ts
@@ -12,9 +12,9 @@ import Post from '../../../models/post';
  * @return {Promise<any>}
  */
 module.exports = (params) => new Promise(async (res, rej) => {
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [userId, userIdErr] = $(params.userId).id().$;
+	if (userIdErr) return rej('invalid userId param');
 
 	// Lookup user
 	const user = await User.findOne({
@@ -31,25 +31,25 @@ module.exports = (params) => new Promise(async (res, rej) => {
 
 	const datas = await Post
 		.aggregate([
-			{ $match: { user_id: user._id } },
+			{ $match: { userId: user._id } },
 			{ $project: {
-				repost_id: '$repost_id',
-				reply_id: '$reply_id',
-				created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
+				repostId: '$repostId',
+				replyId: '$replyId',
+				createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
 			}},
 			{ $project: {
 				date: {
-					year: { $year: '$created_at' },
-					month: { $month: '$created_at' },
-					day: { $dayOfMonth: '$created_at' }
+					year: { $year: '$createdAt' },
+					month: { $month: '$createdAt' },
+					day: { $dayOfMonth: '$createdAt' }
 				},
 				type: {
 					$cond: {
-						if: { $ne: ['$repost_id', null] },
+						if: { $ne: ['$repostId', null] },
 						then: 'repost',
 						else: {
 							$cond: {
-								if: { $ne: ['$reply_id', null] },
+								if: { $ne: ['$replyId', null] },
 								then: 'reply',
 								else: 'post'
 							}
diff --git a/src/api/endpoints/aggregation/users/reaction.ts b/src/api/endpoints/aggregation/users/reaction.ts
index 0a082ed1b..60b33e9d1 100644
--- a/src/api/endpoints/aggregation/users/reaction.ts
+++ b/src/api/endpoints/aggregation/users/reaction.ts
@@ -12,9 +12,9 @@ import Reaction from '../../../models/post-reaction';
  * @return {Promise<any>}
  */
 module.exports = (params) => new Promise(async (res, rej) => {
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [userId, userIdErr] = $(params.userId).id().$;
+	if (userIdErr) return rej('invalid userId param');
 
 	// Lookup user
 	const user = await User.findOne({
@@ -31,15 +31,15 @@ module.exports = (params) => new Promise(async (res, rej) => {
 
 	const datas = await Reaction
 		.aggregate([
-			{ $match: { user_id: user._id } },
+			{ $match: { userId: user._id } },
 			{ $project: {
-				created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
+				createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
 			}},
 			{ $project: {
 				date: {
-					year: { $year: '$created_at' },
-					month: { $month: '$created_at' },
-					day: { $dayOfMonth: '$created_at' }
+					year: { $year: '$createdAt' },
+					month: { $month: '$createdAt' },
+					day: { $dayOfMonth: '$createdAt' }
 				}
 			}},
 			{ $group: {
diff --git a/src/api/endpoints/app/create.ts b/src/api/endpoints/app/create.ts
index 0f688792a..cde4846e0 100644
--- a/src/api/endpoints/app/create.ts
+++ b/src/api/endpoints/app/create.ts
@@ -13,7 +13,7 @@ import App, { isValidNameId, pack } from '../../models/app';
  *     parameters:
  *       - $ref: "#/parameters/AccessToken"
  *       -
- *         name: name_id
+ *         name: nameId
  *         description: Application unique name
  *         in: formData
  *         required: true
@@ -66,9 +66,9 @@ import App, { isValidNameId, pack } from '../../models/app';
  * @return {Promise<any>}
  */
 module.exports = async (params, user) => new Promise(async (res, rej) => {
-	// Get 'name_id' parameter
-	const [nameId, nameIdErr] = $(params.name_id).string().pipe(isValidNameId).$;
-	if (nameIdErr) return rej('invalid name_id param');
+	// Get 'nameId' parameter
+	const [nameId, nameIdErr] = $(params.nameId).string().pipe(isValidNameId).$;
+	if (nameIdErr) return rej('invalid nameId param');
 
 	// Get 'name' parameter
 	const [name, nameErr] = $(params.name).string().$;
@@ -92,11 +92,11 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 	// Create account
 	const app = await App.insert({
-		created_at: new Date(),
-		user_id: user._id,
+		createdAt: new Date(),
+		userId: user._id,
 		name: name,
-		name_id: nameId,
-		name_id_lower: nameId.toLowerCase(),
+		nameId: nameId,
+		nameIdLower: nameId.toLowerCase(),
 		description: description,
 		permission: permission,
 		callback_url: callbackUrl,
diff --git a/src/api/endpoints/app/name_id/available.ts b/src/api/endpoints/app/name_id/available.ts
index 3d2c71032..6d02b26d2 100644
--- a/src/api/endpoints/app/name_id/available.ts
+++ b/src/api/endpoints/app/name_id/available.ts
@@ -7,12 +7,12 @@ import { isValidNameId } from '../../../models/app';
 
 /**
  * @swagger
- * /app/name_id/available:
+ * /app/nameId/available:
  *   post:
- *     summary: Check available name_id on creation an application
+ *     summary: Check available nameId on creation an application
  *     parameters:
  *       -
- *         name: name_id
+ *         name: nameId
  *         description: Application unique name
  *         in: formData
  *         required: true
@@ -25,7 +25,7 @@ import { isValidNameId } from '../../../models/app';
  *           type: object
  *           properties:
  *             available:
- *               description: Whether name_id is available
+ *               description: Whether nameId is available
  *               type: boolean
  *
  *       default:
@@ -35,20 +35,20 @@ import { isValidNameId } from '../../../models/app';
  */
 
 /**
- * Check available name_id of app
+ * Check available nameId of app
  *
  * @param {any} params
  * @return {Promise<any>}
  */
 module.exports = async (params) => new Promise(async (res, rej) => {
-	// Get 'name_id' parameter
-	const [nameId, nameIdErr] = $(params.name_id).string().pipe(isValidNameId).$;
-	if (nameIdErr) return rej('invalid name_id param');
+	// Get 'nameId' parameter
+	const [nameId, nameIdErr] = $(params.nameId).string().pipe(isValidNameId).$;
+	if (nameIdErr) return rej('invalid nameId param');
 
 	// Get exist
 	const exist = await App
 		.count({
-			name_id_lower: nameId.toLowerCase()
+			nameIdLower: nameId.toLowerCase()
 		}, {
 			limit: 1
 		});
diff --git a/src/api/endpoints/app/show.ts b/src/api/endpoints/app/show.ts
index 8bc3dda42..34bb958ee 100644
--- a/src/api/endpoints/app/show.ts
+++ b/src/api/endpoints/app/show.ts
@@ -9,15 +9,15 @@ import App, { pack } from '../../models/app';
  * /app/show:
  *   post:
  *     summary: Show an application's information
- *     description: Require app_id or name_id
+ *     description: Require appId or nameId
  *     parameters:
  *       -
- *         name: app_id
+ *         name: appId
  *         description: Application ID
  *         in: formData
  *         type: string
  *       -
- *         name: name_id
+ *         name: nameId
  *         description: Application unique name
  *         in: formData
  *         type: string
@@ -44,22 +44,22 @@ import App, { pack } from '../../models/app';
  * @return {Promise<any>}
  */
 module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) => {
-	// Get 'app_id' parameter
-	const [appId, appIdErr] = $(params.app_id).optional.id().$;
-	if (appIdErr) return rej('invalid app_id param');
+	// Get 'appId' parameter
+	const [appId, appIdErr] = $(params.appId).optional.id().$;
+	if (appIdErr) return rej('invalid appId param');
 
-	// Get 'name_id' parameter
-	const [nameId, nameIdErr] = $(params.name_id).optional.string().$;
-	if (nameIdErr) return rej('invalid name_id param');
+	// Get 'nameId' parameter
+	const [nameId, nameIdErr] = $(params.nameId).optional.string().$;
+	if (nameIdErr) return rej('invalid nameId param');
 
 	if (appId === undefined && nameId === undefined) {
-		return rej('app_id or name_id is required');
+		return rej('appId or nameId is required');
 	}
 
 	// Lookup app
 	const app = appId !== undefined
 		? await App.findOne({ _id: appId })
-		: await App.findOne({ name_id_lower: nameId.toLowerCase() });
+		: await App.findOne({ nameIdLower: nameId.toLowerCase() });
 
 	if (app === null) {
 		return rej('app not found');
@@ -67,6 +67,6 @@ module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) =>
 
 	// Send response
 	res(await pack(app, user, {
-		includeSecret: isSecure && app.user_id.equals(user._id)
+		includeSecret: isSecure && app.userId.equals(user._id)
 	}));
 });
diff --git a/src/api/endpoints/auth/accept.ts b/src/api/endpoints/auth/accept.ts
index 4ee20a6d2..5a1925144 100644
--- a/src/api/endpoints/auth/accept.ts
+++ b/src/api/endpoints/auth/accept.ts
@@ -56,14 +56,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Fetch exist access token
 	const exist = await AccessToken.findOne({
-		app_id: session.app_id,
-		user_id: user._id,
+		appId: session.appId,
+		userId: user._id,
 	});
 
 	if (exist === null) {
 		// Lookup app
 		const app = await App.findOne({
-			_id: session.app_id
+			_id: session.appId
 		});
 
 		// Generate Hash
@@ -73,9 +73,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 		// Insert access token doc
 		await AccessToken.insert({
-			created_at: new Date(),
-			app_id: session.app_id,
-			user_id: user._id,
+			createdAt: new Date(),
+			appId: session.appId,
+			userId: user._id,
 			token: accessToken,
 			hash: hash
 		});
@@ -84,7 +84,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Update session
 	await AuthSess.update(session._id, {
 		$set: {
-			user_id: user._id
+			userId: user._id
 		}
 	});
 
diff --git a/src/api/endpoints/auth/session/generate.ts b/src/api/endpoints/auth/session/generate.ts
index 510382247..81db188ee 100644
--- a/src/api/endpoints/auth/session/generate.ts
+++ b/src/api/endpoints/auth/session/generate.ts
@@ -63,8 +63,8 @@ module.exports = (params) => new Promise(async (res, rej) => {
 
 	// Create session token document
 	const doc = await AuthSess.insert({
-		created_at: new Date(),
-		app_id: app._id,
+		createdAt: new Date(),
+		appId: app._id,
 		token: token
 	});
 
diff --git a/src/api/endpoints/auth/session/show.ts b/src/api/endpoints/auth/session/show.ts
index 73ac3185f..869b714e7 100644
--- a/src/api/endpoints/auth/session/show.ts
+++ b/src/api/endpoints/auth/session/show.ts
@@ -23,17 +23,17 @@ import AuthSess, { pack } from '../../../models/auth-session';
  *         schema:
  *           type: object
  *           properties:
- *             created_at:
+ *             createdAt:
  *               type: string
  *               format: date-time
  *               description: Date and time of the session creation
- *             app_id:
+ *             appId:
  *               type: string
  *               description: Application ID
  *             token:
  *               type: string
  *               description: Session Token
- *             user_id:
+ *             userId:
  *               type: string
  *               description: ID of user who create the session
  *             app:
diff --git a/src/api/endpoints/auth/session/userkey.ts b/src/api/endpoints/auth/session/userkey.ts
index fc989bf8c..74f025c8a 100644
--- a/src/api/endpoints/auth/session/userkey.ts
+++ b/src/api/endpoints/auth/session/userkey.ts
@@ -71,21 +71,21 @@ module.exports = (params) => new Promise(async (res, rej) => {
 	const session = await AuthSess
 		.findOne({
 			token: token,
-			app_id: app._id
+			appId: app._id
 		});
 
 	if (session === null) {
 		return rej('session not found');
 	}
 
-	if (session.user_id == null) {
+	if (session.userId == null) {
 		return rej('this session is not allowed yet');
 	}
 
 	// Lookup access token
 	const accessToken = await AccessToken.findOne({
-		app_id: app._id,
-		user_id: session.user_id
+		appId: app._id,
+		userId: session.userId
 	});
 
 	// Delete session
@@ -101,8 +101,8 @@ module.exports = (params) => new Promise(async (res, rej) => {
 
 	// Response
 	res({
-		access_token: accessToken.token,
-		user: await pack(session.user_id, null, {
+		accessToken: accessToken.token,
+		user: await pack(session.userId, null, {
 			detail: true
 		})
 	});
diff --git a/src/api/endpoints/channels/create.ts b/src/api/endpoints/channels/create.ts
index 695b4515b..1dc453c4a 100644
--- a/src/api/endpoints/channels/create.ts
+++ b/src/api/endpoints/channels/create.ts
@@ -20,11 +20,11 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 	// Create a channel
 	const channel = await Channel.insert({
-		created_at: new Date(),
-		user_id: user._id,
+		createdAt: new Date(),
+		userId: user._id,
 		title: title,
 		index: 0,
-		watching_count: 1
+		watchingCount: 1
 	});
 
 	// Response
@@ -32,8 +32,8 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 	// Create Watching
 	await Watching.insert({
-		created_at: new Date(),
-		user_id: user._id,
-		channel_id: channel._id
+		createdAt: new Date(),
+		userId: user._id,
+		channelId: channel._id
 	});
 });
diff --git a/src/api/endpoints/channels/posts.ts b/src/api/endpoints/channels/posts.ts
index d722589c2..753666405 100644
--- a/src/api/endpoints/channels/posts.ts
+++ b/src/api/endpoints/channels/posts.ts
@@ -30,9 +30,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		return rej('cannot set since_id and until_id');
 	}
 
-	// Get 'channel_id' parameter
-	const [channelId, channelIdErr] = $(params.channel_id).id().$;
-	if (channelIdErr) return rej('invalid channel_id param');
+	// Get 'channelId' parameter
+	const [channelId, channelIdErr] = $(params.channelId).id().$;
+	if (channelIdErr) return rej('invalid channelId param');
 
 	// Fetch channel
 	const channel: IChannel = await Channel.findOne({
@@ -49,7 +49,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	};
 
 	const query = {
-		channel_id: channel._id
+		channelId: channel._id
 	} as any;
 
 	if (sinceId) {
diff --git a/src/api/endpoints/channels/show.ts b/src/api/endpoints/channels/show.ts
index 332da6467..5874ed18a 100644
--- a/src/api/endpoints/channels/show.ts
+++ b/src/api/endpoints/channels/show.ts
@@ -12,9 +12,9 @@ import Channel, { IChannel, pack } from '../../models/channel';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'channel_id' parameter
-	const [channelId, channelIdErr] = $(params.channel_id).id().$;
-	if (channelIdErr) return rej('invalid channel_id param');
+	// Get 'channelId' parameter
+	const [channelId, channelIdErr] = $(params.channelId).id().$;
+	if (channelIdErr) return rej('invalid channelId param');
 
 	// Fetch channel
 	const channel: IChannel = await Channel.findOne({
diff --git a/src/api/endpoints/channels/unwatch.ts b/src/api/endpoints/channels/unwatch.ts
index 19d3be118..709313bc6 100644
--- a/src/api/endpoints/channels/unwatch.ts
+++ b/src/api/endpoints/channels/unwatch.ts
@@ -13,9 +13,9 @@ import Watching from '../../models/channel-watching';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'channel_id' parameter
-	const [channelId, channelIdErr] = $(params.channel_id).id().$;
-	if (channelIdErr) return rej('invalid channel_id param');
+	// Get 'channelId' parameter
+	const [channelId, channelIdErr] = $(params.channelId).id().$;
+	if (channelIdErr) return rej('invalid channelId param');
 
 	//#region Fetch channel
 	const channel = await Channel.findOne({
@@ -29,9 +29,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	//#region Check whether not watching
 	const exist = await Watching.findOne({
-		user_id: user._id,
-		channel_id: channel._id,
-		deleted_at: { $exists: false }
+		userId: user._id,
+		channelId: channel._id,
+		deletedAt: { $exists: false }
 	});
 
 	if (exist === null) {
@@ -44,7 +44,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		_id: exist._id
 	}, {
 		$set: {
-			deleted_at: new Date()
+			deletedAt: new Date()
 		}
 	});
 
@@ -54,7 +54,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Decrement watching count
 	Channel.update(channel._id, {
 		$inc: {
-			watching_count: -1
+			watchingCount: -1
 		}
 	});
 });
diff --git a/src/api/endpoints/channels/watch.ts b/src/api/endpoints/channels/watch.ts
index 030e0dd41..df9e70d5c 100644
--- a/src/api/endpoints/channels/watch.ts
+++ b/src/api/endpoints/channels/watch.ts
@@ -13,9 +13,9 @@ import Watching from '../../models/channel-watching';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'channel_id' parameter
-	const [channelId, channelIdErr] = $(params.channel_id).id().$;
-	if (channelIdErr) return rej('invalid channel_id param');
+	// Get 'channelId' parameter
+	const [channelId, channelIdErr] = $(params.channelId).id().$;
+	if (channelIdErr) return rej('invalid channelId param');
 
 	//#region Fetch channel
 	const channel = await Channel.findOne({
@@ -29,9 +29,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	//#region Check whether already watching
 	const exist = await Watching.findOne({
-		user_id: user._id,
-		channel_id: channel._id,
-		deleted_at: { $exists: false }
+		userId: user._id,
+		channelId: channel._id,
+		deletedAt: { $exists: false }
 	});
 
 	if (exist !== null) {
@@ -41,9 +41,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Create Watching
 	await Watching.insert({
-		created_at: new Date(),
-		user_id: user._id,
-		channel_id: channel._id
+		createdAt: new Date(),
+		userId: user._id,
+		channelId: channel._id
 	});
 
 	// Send response
@@ -52,7 +52,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Increment watching count
 	Channel.update(channel._id, {
 		$inc: {
-			watching_count: 1
+			watchingCount: 1
 		}
 	});
 });
diff --git a/src/api/endpoints/drive.ts b/src/api/endpoints/drive.ts
index d92473633..eb2185391 100644
--- a/src/api/endpoints/drive.ts
+++ b/src/api/endpoints/drive.ts
@@ -14,7 +14,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Calculate drive usage
 	const usage = ((await DriveFile
 		.aggregate([
-			{ $match: { 'metadata.user_id': user._id } },
+			{ $match: { 'metadata.userId': user._id } },
 			{
 				$project: {
 					length: true
@@ -31,7 +31,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		}).usage;
 
 	res({
-		capacity: user.drive_capacity,
+		capacity: user.driveCapacity,
 		usage: usage
 	});
 });
diff --git a/src/api/endpoints/drive/files.ts b/src/api/endpoints/drive/files.ts
index 89915331e..1ce855932 100644
--- a/src/api/endpoints/drive/files.ts
+++ b/src/api/endpoints/drive/files.ts
@@ -30,9 +30,9 @@ module.exports = async (params, user, app) => {
 		throw 'cannot set since_id and until_id';
 	}
 
-	// Get 'folder_id' parameter
-	const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
-	if (folderIdErr) throw 'invalid folder_id param';
+	// Get 'folderId' parameter
+	const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$;
+	if (folderIdErr) throw 'invalid folderId param';
 
 	// Get 'type' parameter
 	const [type, typeErr] = $(params.type).optional.string().match(/^[a-zA-Z\/\-\*]+$/).$;
@@ -43,8 +43,8 @@ module.exports = async (params, user, app) => {
 		_id: -1
 	};
 	const query = {
-		'metadata.user_id': user._id,
-		'metadata.folder_id': folderId
+		'metadata.userId': user._id,
+		'metadata.folderId': folderId
 	} as any;
 	if (sinceId) {
 		sort._id = 1;
diff --git a/src/api/endpoints/drive/files/create.ts b/src/api/endpoints/drive/files/create.ts
index db801b61f..2cd89a8fa 100644
--- a/src/api/endpoints/drive/files/create.ts
+++ b/src/api/endpoints/drive/files/create.ts
@@ -33,9 +33,9 @@ module.exports = async (file, params, user): Promise<any> => {
 		name = null;
 	}
 
-	// Get 'folder_id' parameter
-	const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
-	if (folderIdErr) throw 'invalid folder_id param';
+	// Get 'folderId' parameter
+	const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$;
+	if (folderIdErr) throw 'invalid folderId param';
 
 	try {
 		// Create file
diff --git a/src/api/endpoints/drive/files/find.ts b/src/api/endpoints/drive/files/find.ts
index e026afe93..47ce89130 100644
--- a/src/api/endpoints/drive/files/find.ts
+++ b/src/api/endpoints/drive/files/find.ts
@@ -16,16 +16,16 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [name, nameErr] = $(params.name).string().$;
 	if (nameErr) return rej('invalid name param');
 
-	// Get 'folder_id' parameter
-	const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
-	if (folderIdErr) return rej('invalid folder_id param');
+	// Get 'folderId' parameter
+	const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$;
+	if (folderIdErr) return rej('invalid folderId param');
 
 	// Issue query
 	const files = await DriveFile
 		.find({
 			filename: name,
-			'metadata.user_id': user._id,
-			'metadata.folder_id': folderId
+			'metadata.userId': user._id,
+			'metadata.folderId': folderId
 		});
 
 	// Serialize
diff --git a/src/api/endpoints/drive/files/show.ts b/src/api/endpoints/drive/files/show.ts
index 21664f7ba..63920db7f 100644
--- a/src/api/endpoints/drive/files/show.ts
+++ b/src/api/endpoints/drive/files/show.ts
@@ -12,15 +12,15 @@ import DriveFile, { pack } from '../../../models/drive-file';
  * @return {Promise<any>}
  */
 module.exports = async (params, user) => {
-	// Get 'file_id' parameter
-	const [fileId, fileIdErr] = $(params.file_id).id().$;
-	if (fileIdErr) throw 'invalid file_id param';
+	// Get 'fileId' parameter
+	const [fileId, fileIdErr] = $(params.fileId).id().$;
+	if (fileIdErr) throw 'invalid fileId param';
 
 	// Fetch file
 	const file = await DriveFile
 		.findOne({
 			_id: fileId,
-			'metadata.user_id': user._id
+			'metadata.userId': user._id
 		});
 
 	if (file === null) {
diff --git a/src/api/endpoints/drive/files/update.ts b/src/api/endpoints/drive/files/update.ts
index 83da46211..bfad45b0a 100644
--- a/src/api/endpoints/drive/files/update.ts
+++ b/src/api/endpoints/drive/files/update.ts
@@ -14,15 +14,15 @@ import { publishDriveStream } from '../../../event';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'file_id' parameter
-	const [fileId, fileIdErr] = $(params.file_id).id().$;
-	if (fileIdErr) return rej('invalid file_id param');
+	// Get 'fileId' parameter
+	const [fileId, fileIdErr] = $(params.fileId).id().$;
+	if (fileIdErr) return rej('invalid fileId param');
 
 	// Fetch file
 	const file = await DriveFile
 		.findOne({
 			_id: fileId,
-			'metadata.user_id': user._id
+			'metadata.userId': user._id
 		});
 
 	if (file === null) {
@@ -34,33 +34,33 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	if (nameErr) return rej('invalid name param');
 	if (name) file.filename = name;
 
-	// Get 'folder_id' parameter
-	const [folderId, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
-	if (folderIdErr) return rej('invalid folder_id param');
+	// Get 'folderId' parameter
+	const [folderId, folderIdErr] = $(params.folderId).optional.nullable.id().$;
+	if (folderIdErr) return rej('invalid folderId param');
 
 	if (folderId !== undefined) {
 		if (folderId === null) {
-			file.metadata.folder_id = null;
+			file.metadata.folderId = null;
 		} else {
 			// Fetch folder
 			const folder = await DriveFolder
 				.findOne({
 					_id: folderId,
-					user_id: user._id
+					userId: user._id
 				});
 
 			if (folder === null) {
 				return rej('folder-not-found');
 			}
 
-			file.metadata.folder_id = folder._id;
+			file.metadata.folderId = folder._id;
 		}
 	}
 
 	await DriveFile.update(file._id, {
 		$set: {
 			filename: file.filename,
-			'metadata.folder_id': file.metadata.folder_id
+			'metadata.folderId': file.metadata.folderId
 		}
 	});
 
diff --git a/src/api/endpoints/drive/files/upload_from_url.ts b/src/api/endpoints/drive/files/upload_from_url.ts
index 346633c61..1a4ce0bf0 100644
--- a/src/api/endpoints/drive/files/upload_from_url.ts
+++ b/src/api/endpoints/drive/files/upload_from_url.ts
@@ -18,9 +18,9 @@ module.exports = async (params, user): Promise<any> => {
 	const [url, urlErr] = $(params.url).string().$;
 	if (urlErr) throw 'invalid url param';
 
-	// Get 'folder_id' parameter
-	const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
-	if (folderIdErr) throw 'invalid folder_id param';
+	// Get 'folderId' parameter
+	const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$;
+	if (folderIdErr) throw 'invalid folderId param';
 
 	return pack(await uploadFromUrl(url, user, folderId));
 };
diff --git a/src/api/endpoints/drive/folders.ts b/src/api/endpoints/drive/folders.ts
index 428bde350..c25964635 100644
--- a/src/api/endpoints/drive/folders.ts
+++ b/src/api/endpoints/drive/folders.ts
@@ -30,17 +30,17 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
 		return rej('cannot set since_id and until_id');
 	}
 
-	// Get 'folder_id' parameter
-	const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
-	if (folderIdErr) return rej('invalid folder_id param');
+	// Get 'folderId' parameter
+	const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$;
+	if (folderIdErr) return rej('invalid folderId param');
 
 	// Construct query
 	const sort = {
 		_id: -1
 	};
 	const query = {
-		user_id: user._id,
-		parent_id: folderId
+		userId: user._id,
+		parentId: folderId
 	} as any;
 	if (sinceId) {
 		sort._id = 1;
diff --git a/src/api/endpoints/drive/folders/create.ts b/src/api/endpoints/drive/folders/create.ts
index 03f396ddc..564558606 100644
--- a/src/api/endpoints/drive/folders/create.ts
+++ b/src/api/endpoints/drive/folders/create.ts
@@ -17,9 +17,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [name = '無題のフォルダー', nameErr] = $(params.name).optional.string().pipe(isValidFolderName).$;
 	if (nameErr) return rej('invalid name param');
 
-	// Get 'parent_id' parameter
-	const [parentId = null, parentIdErr] = $(params.parent_id).optional.nullable.id().$;
-	if (parentIdErr) return rej('invalid parent_id param');
+	// Get 'parentId' parameter
+	const [parentId = null, parentIdErr] = $(params.parentId).optional.nullable.id().$;
+	if (parentIdErr) return rej('invalid parentId param');
 
 	// If the parent folder is specified
 	let parent = null;
@@ -28,7 +28,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		parent = await DriveFolder
 			.findOne({
 				_id: parentId,
-				user_id: user._id
+				userId: user._id
 			});
 
 		if (parent === null) {
@@ -38,10 +38,10 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Create folder
 	const folder = await DriveFolder.insert({
-		created_at: new Date(),
+		createdAt: new Date(),
 		name: name,
-		parent_id: parent !== null ? parent._id : null,
-		user_id: user._id
+		parentId: parent !== null ? parent._id : null,
+		userId: user._id
 	});
 
 	// Serialize
diff --git a/src/api/endpoints/drive/folders/find.ts b/src/api/endpoints/drive/folders/find.ts
index fc84766bc..f46aaedd3 100644
--- a/src/api/endpoints/drive/folders/find.ts
+++ b/src/api/endpoints/drive/folders/find.ts
@@ -16,16 +16,16 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [name, nameErr] = $(params.name).string().$;
 	if (nameErr) return rej('invalid name param');
 
-	// Get 'parent_id' parameter
-	const [parentId = null, parentIdErr] = $(params.parent_id).optional.nullable.id().$;
-	if (parentIdErr) return rej('invalid parent_id param');
+	// Get 'parentId' parameter
+	const [parentId = null, parentIdErr] = $(params.parentId).optional.nullable.id().$;
+	if (parentIdErr) return rej('invalid parentId param');
 
 	// Issue query
 	const folders = await DriveFolder
 		.find({
 			name: name,
-			user_id: user._id,
-			parent_id: parentId
+			userId: user._id,
+			parentId: parentId
 		});
 
 	// Serialize
diff --git a/src/api/endpoints/drive/folders/show.ts b/src/api/endpoints/drive/folders/show.ts
index e07d14d20..a6d7e86df 100644
--- a/src/api/endpoints/drive/folders/show.ts
+++ b/src/api/endpoints/drive/folders/show.ts
@@ -12,15 +12,15 @@ import DriveFolder, { pack } from '../../../models/drive-folder';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'folder_id' parameter
-	const [folderId, folderIdErr] = $(params.folder_id).id().$;
-	if (folderIdErr) return rej('invalid folder_id param');
+	// Get 'folderId' parameter
+	const [folderId, folderIdErr] = $(params.folderId).id().$;
+	if (folderIdErr) return rej('invalid folderId param');
 
 	// Get folder
 	const folder = await DriveFolder
 		.findOne({
 			_id: folderId,
-			user_id: user._id
+			userId: user._id
 		});
 
 	if (folder === null) {
diff --git a/src/api/endpoints/drive/folders/update.ts b/src/api/endpoints/drive/folders/update.ts
index d3df8bdae..fcfd24124 100644
--- a/src/api/endpoints/drive/folders/update.ts
+++ b/src/api/endpoints/drive/folders/update.ts
@@ -13,15 +13,15 @@ import { publishDriveStream } from '../../../event';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'folder_id' parameter
-	const [folderId, folderIdErr] = $(params.folder_id).id().$;
-	if (folderIdErr) return rej('invalid folder_id param');
+	// Get 'folderId' parameter
+	const [folderId, folderIdErr] = $(params.folderId).id().$;
+	if (folderIdErr) return rej('invalid folderId param');
 
 	// Fetch folder
 	const folder = await DriveFolder
 		.findOne({
 			_id: folderId,
-			user_id: user._id
+			userId: user._id
 		});
 
 	if (folder === null) {
@@ -33,18 +33,18 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	if (nameErr) return rej('invalid name param');
 	if (name) folder.name = name;
 
-	// Get 'parent_id' parameter
-	const [parentId, parentIdErr] = $(params.parent_id).optional.nullable.id().$;
-	if (parentIdErr) return rej('invalid parent_id param');
+	// Get 'parentId' parameter
+	const [parentId, parentIdErr] = $(params.parentId).optional.nullable.id().$;
+	if (parentIdErr) return rej('invalid parentId param');
 	if (parentId !== undefined) {
 		if (parentId === null) {
-			folder.parent_id = null;
+			folder.parentId = null;
 		} else {
 			// Get parent folder
 			const parent = await DriveFolder
 				.findOne({
 					_id: parentId,
-					user_id: user._id
+					userId: user._id
 				});
 
 			if (parent === null) {
@@ -58,25 +58,25 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 					_id: folderId
 				}, {
 					_id: true,
-					parent_id: true
+					parentId: true
 				});
 
 				if (folder2._id.equals(folder._id)) {
 					return true;
-				} else if (folder2.parent_id) {
-					return await checkCircle(folder2.parent_id);
+				} else if (folder2.parentId) {
+					return await checkCircle(folder2.parentId);
 				} else {
 					return false;
 				}
 			}
 
-			if (parent.parent_id !== null) {
-				if (await checkCircle(parent.parent_id)) {
+			if (parent.parentId !== null) {
+				if (await checkCircle(parent.parentId)) {
 					return rej('detected-circular-definition');
 				}
 			}
 
-			folder.parent_id = parent._id;
+			folder.parentId = parent._id;
 		}
 	}
 
@@ -84,7 +84,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	DriveFolder.update(folder._id, {
 		$set: {
 			name: folder.name,
-			parent_id: folder.parent_id
+			parentId: folder.parentId
 		}
 	});
 
diff --git a/src/api/endpoints/drive/stream.ts b/src/api/endpoints/drive/stream.ts
index 8352c7dd4..0f9cea9f1 100644
--- a/src/api/endpoints/drive/stream.ts
+++ b/src/api/endpoints/drive/stream.ts
@@ -38,7 +38,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		_id: -1
 	};
 	const query = {
-		'metadata.user_id': user._id
+		'metadata.userId': user._id
 	} as any;
 	if (sinceId) {
 		sort._id = 1;
diff --git a/src/api/endpoints/following/create.ts b/src/api/endpoints/following/create.ts
index 767b837b3..983d8040f 100644
--- a/src/api/endpoints/following/create.ts
+++ b/src/api/endpoints/following/create.ts
@@ -17,9 +17,9 @@ import event from '../../event';
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	const follower = user;
 
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [userId, userIdErr] = $(params.userId).id().$;
+	if (userIdErr) return rej('invalid userId param');
 
 	// 自分自身
 	if (user._id.equals(userId)) {
@@ -42,9 +42,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Check if already following
 	const exist = await Following.findOne({
-		follower_id: follower._id,
-		followee_id: followee._id,
-		deleted_at: { $exists: false }
+		followerId: follower._id,
+		followeeId: followee._id,
+		deletedAt: { $exists: false }
 	});
 
 	if (exist !== null) {
@@ -53,9 +53,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Create following
 	await Following.insert({
-		created_at: new Date(),
-		follower_id: follower._id,
-		followee_id: followee._id
+		createdAt: new Date(),
+		followerId: follower._id,
+		followeeId: followee._id
 	});
 
 	// Send response
@@ -64,14 +64,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Increment following count
 	User.update(follower._id, {
 		$inc: {
-			following_count: 1
+			followingCount: 1
 		}
 	});
 
 	// Increment followers count
 	User.update({ _id: followee._id }, {
 		$inc: {
-			followers_count: 1
+			followersCount: 1
 		}
 	});
 
diff --git a/src/api/endpoints/following/delete.ts b/src/api/endpoints/following/delete.ts
index 64b9a8cec..25eba8b26 100644
--- a/src/api/endpoints/following/delete.ts
+++ b/src/api/endpoints/following/delete.ts
@@ -16,9 +16,9 @@ import event from '../../event';
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	const follower = user;
 
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [userId, userIdErr] = $(params.userId).id().$;
+	if (userIdErr) return rej('invalid userId param');
 
 	// Check if the followee is yourself
 	if (user._id.equals(userId)) {
@@ -41,9 +41,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Check not following
 	const exist = await Following.findOne({
-		follower_id: follower._id,
-		followee_id: followee._id,
-		deleted_at: { $exists: false }
+		followerId: follower._id,
+		followeeId: followee._id,
+		deletedAt: { $exists: false }
 	});
 
 	if (exist === null) {
@@ -55,7 +55,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		_id: exist._id
 	}, {
 		$set: {
-			deleted_at: new Date()
+			deletedAt: new Date()
 		}
 	});
 
@@ -65,14 +65,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Decrement following count
 	User.update({ _id: follower._id }, {
 		$inc: {
-			following_count: -1
+			followingCount: -1
 		}
 	});
 
 	// Decrement followers count
 	User.update({ _id: followee._id }, {
 		$inc: {
-			followers_count: -1
+			followersCount: -1
 		}
 	});
 
diff --git a/src/api/endpoints/i.ts b/src/api/endpoints/i.ts
index 32b0382fa..f5e92b4de 100644
--- a/src/api/endpoints/i.ts
+++ b/src/api/endpoints/i.ts
@@ -22,7 +22,7 @@ module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) =>
 	// Update lastUsedAt
 	User.update({ _id: user._id }, {
 		$set: {
-			'account.last_used_at': new Date()
+			'account.lastUsedAt': new Date()
 		}
 	});
 });
diff --git a/src/api/endpoints/i/2fa/done.ts b/src/api/endpoints/i/2fa/done.ts
index 0f1db7382..d61ebbe6f 100644
--- a/src/api/endpoints/i/2fa/done.ts
+++ b/src/api/endpoints/i/2fa/done.ts
@@ -12,12 +12,12 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 	const _token = token.replace(/\s/g, '');
 
-	if (user.two_factor_temp_secret == null) {
+	if (user.twoFactorTempSecret == null) {
 		return rej('二段階認証の設定が開始されていません');
 	}
 
 	const verified = (speakeasy as any).totp.verify({
-		secret: user.two_factor_temp_secret,
+		secret: user.twoFactorTempSecret,
 		encoding: 'base32',
 		token: _token
 	});
@@ -28,8 +28,8 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 	await User.update(user._id, {
 		$set: {
-			'account.two_factor_secret': user.two_factor_temp_secret,
-			'account.two_factor_enabled': true
+			'account.twoFactorSecret': user.twoFactorTempSecret,
+			'account.twoFactorEnabled': true
 		}
 	});
 
diff --git a/src/api/endpoints/i/2fa/register.ts b/src/api/endpoints/i/2fa/register.ts
index 24abfcdfc..b498a2414 100644
--- a/src/api/endpoints/i/2fa/register.ts
+++ b/src/api/endpoints/i/2fa/register.ts
@@ -27,7 +27,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 	await User.update(user._id, {
 		$set: {
-			two_factor_temp_secret: secret.base32
+			twoFactorTempSecret: secret.base32
 		}
 	});
 
diff --git a/src/api/endpoints/i/2fa/unregister.ts b/src/api/endpoints/i/2fa/unregister.ts
index c43f9ccc4..0221ecb96 100644
--- a/src/api/endpoints/i/2fa/unregister.ts
+++ b/src/api/endpoints/i/2fa/unregister.ts
@@ -19,8 +19,8 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 	await User.update(user._id, {
 		$set: {
-			'account.two_factor_secret': null,
-			'account.two_factor_enabled': false
+			'account.twoFactorSecret': null,
+			'account.twoFactorEnabled': false
 		}
 	});
 
diff --git a/src/api/endpoints/i/appdata/get.ts b/src/api/endpoints/i/appdata/get.ts
index 571208d46..0b34643f7 100644
--- a/src/api/endpoints/i/appdata/get.ts
+++ b/src/api/endpoints/i/appdata/get.ts
@@ -25,8 +25,8 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
 		select[`data.${key}`] = true;
 	}
 	const appdata = await Appdata.findOne({
-		app_id: app._id,
-		user_id: user._id
+		appId: app._id,
+		userId: user._id
 	}, {
 		fields: select
 	});
diff --git a/src/api/endpoints/i/appdata/set.ts b/src/api/endpoints/i/appdata/set.ts
index 2804a14cb..1e3232ce3 100644
--- a/src/api/endpoints/i/appdata/set.ts
+++ b/src/api/endpoints/i/appdata/set.ts
@@ -43,11 +43,11 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
 	}
 
 	await Appdata.update({
-		app_id: app._id,
-		user_id: user._id
+		appId: app._id,
+		userId: user._id
 	}, Object.assign({
-		app_id: app._id,
-		user_id: user._id
+		appId: app._id,
+		userId: user._id
 	}, {
 			$set: set
 		}), {
diff --git a/src/api/endpoints/i/authorized_apps.ts b/src/api/endpoints/i/authorized_apps.ts
index 40ce7a68c..5a38d7c18 100644
--- a/src/api/endpoints/i/authorized_apps.ts
+++ b/src/api/endpoints/i/authorized_apps.ts
@@ -28,7 +28,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Get tokens
 	const tokens = await AccessToken
 		.find({
-			user_id: user._id
+			userId: user._id
 		}, {
 			limit: limit,
 			skip: offset,
@@ -39,5 +39,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(tokens.map(async token =>
-		await pack(token.app_id))));
+		await pack(token.appId))));
 });
diff --git a/src/api/endpoints/i/favorites.ts b/src/api/endpoints/i/favorites.ts
index eb464cf0f..22a439954 100644
--- a/src/api/endpoints/i/favorites.ts
+++ b/src/api/endpoints/i/favorites.ts
@@ -28,7 +28,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Get favorites
 	const favorites = await Favorite
 		.find({
-			user_id: user._id
+			userId: user._id
 		}, {
 			limit: limit,
 			skip: offset,
diff --git a/src/api/endpoints/i/notifications.ts b/src/api/endpoints/i/notifications.ts
index 688039a0d..e3447c17e 100644
--- a/src/api/endpoints/i/notifications.ts
+++ b/src/api/endpoints/i/notifications.ts
@@ -47,15 +47,15 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}
 
 	const mute = await Mute.find({
-		muter_id: user._id,
-		deleted_at: { $exists: false }
+		muterId: user._id,
+		deletedAt: { $exists: false }
 	});
 
 	const query = {
-		notifiee_id: user._id,
+		notifieeId: user._id,
 		$and: [{
-			notifier_id: {
-				$nin: mute.map(m => m.mutee_id)
+			notifierId: {
+				$nin: mute.map(m => m.muteeId)
 			}
 		}]
 	} as any;
@@ -69,7 +69,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		const followingIds = await getFriends(user._id);
 
 		query.$and.push({
-			notifier_id: {
+			notifierId: {
 				$in: followingIds
 			}
 		});
diff --git a/src/api/endpoints/i/pin.ts b/src/api/endpoints/i/pin.ts
index ff546fc2b..886a3edeb 100644
--- a/src/api/endpoints/i/pin.ts
+++ b/src/api/endpoints/i/pin.ts
@@ -14,14 +14,14 @@ import { pack } from '../../models/user';
  * @return {Promise<any>}
  */
 module.exports = async (params, user) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Fetch pinee
 	const post = await Post.findOne({
 		_id: postId,
-		user_id: user._id
+		userId: user._id
 	});
 
 	if (post === null) {
@@ -30,7 +30,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 	await User.update(user._id, {
 		$set: {
-			pinned_post_id: post._id
+			pinnedPostId: post._id
 		}
 	});
 
diff --git a/src/api/endpoints/i/signin_history.ts b/src/api/endpoints/i/signin_history.ts
index 859e81653..5b794d0a4 100644
--- a/src/api/endpoints/i/signin_history.ts
+++ b/src/api/endpoints/i/signin_history.ts
@@ -30,7 +30,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}
 
 	const query = {
-		user_id: user._id
+		userId: user._id
 	} as any;
 
 	const sort = {
diff --git a/src/api/endpoints/i/update.ts b/src/api/endpoints/i/update.ts
index db8a3f25b..664575187 100644
--- a/src/api/endpoints/i/update.ts
+++ b/src/api/endpoints/i/update.ts
@@ -36,20 +36,20 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re
 	if (birthdayErr) return rej('invalid birthday param');
 	if (birthday !== undefined) user.account.profile.birthday = birthday;
 
-	// Get 'avatar_id' parameter
-	const [avatarId, avatarIdErr] = $(params.avatar_id).optional.id().$;
-	if (avatarIdErr) return rej('invalid avatar_id param');
-	if (avatarId) user.avatar_id = avatarId;
+	// Get 'avatarId' parameter
+	const [avatarId, avatarIdErr] = $(params.avatarId).optional.id().$;
+	if (avatarIdErr) return rej('invalid avatarId param');
+	if (avatarId) user.avatarId = avatarId;
 
-	// Get 'banner_id' parameter
-	const [bannerId, bannerIdErr] = $(params.banner_id).optional.id().$;
-	if (bannerIdErr) return rej('invalid banner_id param');
-	if (bannerId) user.banner_id = bannerId;
+	// Get 'bannerId' parameter
+	const [bannerId, bannerIdErr] = $(params.bannerId).optional.id().$;
+	if (bannerIdErr) return rej('invalid bannerId param');
+	if (bannerId) user.bannerId = bannerId;
 
-	// Get 'is_bot' parameter
-	const [isBot, isBotErr] = $(params.is_bot).optional.boolean().$;
-	if (isBotErr) return rej('invalid is_bot param');
-	if (isBot != null) user.account.is_bot = isBot;
+	// Get 'isBot' parameter
+	const [isBot, isBotErr] = $(params.isBot).optional.boolean().$;
+	if (isBotErr) return rej('invalid isBot param');
+	if (isBot != null) user.account.isBot = isBot;
 
 	// Get 'auto_watch' parameter
 	const [autoWatch, autoWatchErr] = $(params.auto_watch).optional.boolean().$;
@@ -60,10 +60,10 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re
 		$set: {
 			name: user.name,
 			description: user.description,
-			avatar_id: user.avatar_id,
-			banner_id: user.banner_id,
+			avatarId: user.avatarId,
+			bannerId: user.bannerId,
 			'account.profile': user.account.profile,
-			'account.is_bot': user.account.is_bot,
+			'account.isBot': user.account.isBot,
 			'account.settings': user.account.settings
 		}
 	});
diff --git a/src/api/endpoints/i/update_client_setting.ts b/src/api/endpoints/i/update_client_setting.ts
index c772ed5dc..a0bef5e59 100644
--- a/src/api/endpoints/i/update_client_setting.ts
+++ b/src/api/endpoints/i/update_client_setting.ts
@@ -22,14 +22,14 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	if (valueErr) return rej('invalid value param');
 
 	const x = {};
-	x[`account.client_settings.${name}`] = value;
+	x[`account.clientSettings.${name}`] = value;
 
 	await User.update(user._id, {
 		$set: x
 	});
 
 	// Serialize
-	user.account.client_settings[name] = value;
+	user.account.clientSettings[name] = value;
 	const iObj = await pack(user, user, {
 		detail: true,
 		includeSecrets: true
diff --git a/src/api/endpoints/i/update_home.ts b/src/api/endpoints/i/update_home.ts
index 9ce44e25e..151c3e205 100644
--- a/src/api/endpoints/i/update_home.ts
+++ b/src/api/endpoints/i/update_home.ts
@@ -26,7 +26,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	if (home) {
 		await User.update(user._id, {
 			$set: {
-				'account.client_settings.home': home
+				'account.clientSettings.home': home
 			}
 		});
 
@@ -38,7 +38,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	} else {
 		if (id == null && data == null) return rej('you need to set id and data params if home param unset');
 
-		const _home = user.account.client_settings.home;
+		const _home = user.account.clientSettings.home;
 		const widget = _home.find(w => w.id == id);
 
 		if (widget == null) return rej('widget not found');
@@ -47,7 +47,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 		await User.update(user._id, {
 			$set: {
-				'account.client_settings.home': _home
+				'account.clientSettings.home': _home
 			}
 		});
 
diff --git a/src/api/endpoints/i/update_mobile_home.ts b/src/api/endpoints/i/update_mobile_home.ts
index 1daddf42b..a8436b940 100644
--- a/src/api/endpoints/i/update_mobile_home.ts
+++ b/src/api/endpoints/i/update_mobile_home.ts
@@ -25,7 +25,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	if (home) {
 		await User.update(user._id, {
 			$set: {
-				'account.client_settings.mobile_home': home
+				'account.clientSettings.mobile_home': home
 			}
 		});
 
@@ -37,7 +37,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	} else {
 		if (id == null && data == null) return rej('you need to set id and data params if home param unset');
 
-		const _home = user.account.client_settings.mobile_home || [];
+		const _home = user.account.clientSettings.mobile_home || [];
 		const widget = _home.find(w => w.id == id);
 
 		if (widget == null) return rej('widget not found');
@@ -46,7 +46,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 		await User.update(user._id, {
 			$set: {
-				'account.client_settings.mobile_home': _home
+				'account.clientSettings.mobile_home': _home
 			}
 		});
 
diff --git a/src/api/endpoints/messaging/history.ts b/src/api/endpoints/messaging/history.ts
index 1683ca7a8..2bf3ed996 100644
--- a/src/api/endpoints/messaging/history.ts
+++ b/src/api/endpoints/messaging/history.ts
@@ -19,25 +19,25 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	if (limitErr) return rej('invalid limit param');
 
 	const mute = await Mute.find({
-		muter_id: user._id,
-		deleted_at: { $exists: false }
+		muterId: user._id,
+		deletedAt: { $exists: false }
 	});
 
 	// Get history
 	const history = await History
 		.find({
-			user_id: user._id,
-			partner: {
-				$nin: mute.map(m => m.mutee_id)
+			userId: user._id,
+			partnerId: {
+				$nin: mute.map(m => m.muteeId)
 			}
 		}, {
 			limit: limit,
 			sort: {
-				updated_at: -1
+				updatedAt: -1
 			}
 		});
 
 	// Serialize
 	res(await Promise.all(history.map(async h =>
-		await pack(h.message, user))));
+		await pack(h.messageId, user))));
 });
diff --git a/src/api/endpoints/messaging/messages.ts b/src/api/endpoints/messaging/messages.ts
index 67ba5e9d6..ba8ca7d1c 100644
--- a/src/api/endpoints/messaging/messages.ts
+++ b/src/api/endpoints/messaging/messages.ts
@@ -15,9 +15,9 @@ import read from '../../common/read-messaging-message';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'user_id' parameter
-	const [recipientId, recipientIdErr] = $(params.user_id).id().$;
-	if (recipientIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [recipientId, recipientIdErr] = $(params.userId).id().$;
+	if (recipientIdErr) return rej('invalid userId param');
 
 	// Fetch recipient
 	const recipient = await User.findOne({
@@ -55,11 +55,11 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	const query = {
 		$or: [{
-			user_id: user._id,
-			recipient_id: recipient._id
+			userId: user._id,
+			recipientId: recipient._id
 		}, {
-			user_id: recipient._id,
-			recipient_id: user._id
+			userId: recipient._id,
+			recipientId: user._id
 		}]
 	} as any;
 
diff --git a/src/api/endpoints/messaging/messages/create.ts b/src/api/endpoints/messaging/messages/create.ts
index 1b8a5f59e..b94a90678 100644
--- a/src/api/endpoints/messaging/messages/create.ts
+++ b/src/api/endpoints/messaging/messages/create.ts
@@ -21,9 +21,9 @@ import config from '../../../../conf';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'user_id' parameter
-	const [recipientId, recipientIdErr] = $(params.user_id).id().$;
-	if (recipientIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [recipientId, recipientIdErr] = $(params.userId).id().$;
+	if (recipientIdErr) return rej('invalid userId param');
 
 	// Myself
 	if (recipientId.equals(user._id)) {
@@ -47,15 +47,15 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [text, textErr] = $(params.text).optional.string().pipe(isValidText).$;
 	if (textErr) return rej('invalid text');
 
-	// Get 'file_id' parameter
-	const [fileId, fileIdErr] = $(params.file_id).optional.id().$;
-	if (fileIdErr) return rej('invalid file_id param');
+	// Get 'fileId' parameter
+	const [fileId, fileIdErr] = $(params.fileId).optional.id().$;
+	if (fileIdErr) return rej('invalid fileId param');
 
 	let file = null;
 	if (fileId !== undefined) {
 		file = await DriveFile.findOne({
 			_id: fileId,
-			'metadata.user_id': user._id
+			'metadata.userId': user._id
 		});
 
 		if (file === null) {
@@ -70,12 +70,12 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// メッセージを作成
 	const message = await Message.insert({
-		created_at: new Date(),
-		file_id: file ? file._id : undefined,
-		recipient_id: recipient._id,
+		createdAt: new Date(),
+		fileId: file ? file._id : undefined,
+		recipientId: recipient._id,
 		text: text ? text : undefined,
-		user_id: user._id,
-		is_read: false
+		userId: user._id,
+		isRead: false
 	});
 
 	// Serialize
@@ -85,32 +85,32 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	res(messageObj);
 
 	// 自分のストリーム
-	publishMessagingStream(message.user_id, message.recipient_id, 'message', messageObj);
-	publishMessagingIndexStream(message.user_id, 'message', messageObj);
-	publishUserStream(message.user_id, 'messaging_message', messageObj);
+	publishMessagingStream(message.userId, message.recipientId, 'message', messageObj);
+	publishMessagingIndexStream(message.userId, 'message', messageObj);
+	publishUserStream(message.userId, 'messaging_message', messageObj);
 
 	// 相手のストリーム
-	publishMessagingStream(message.recipient_id, message.user_id, 'message', messageObj);
-	publishMessagingIndexStream(message.recipient_id, 'message', messageObj);
-	publishUserStream(message.recipient_id, 'messaging_message', messageObj);
+	publishMessagingStream(message.recipientId, message.userId, 'message', messageObj);
+	publishMessagingIndexStream(message.recipientId, 'message', messageObj);
+	publishUserStream(message.recipientId, 'messaging_message', messageObj);
 
 	// 3秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する
 	setTimeout(async () => {
-		const freshMessage = await Message.findOne({ _id: message._id }, { is_read: true });
-		if (!freshMessage.is_read) {
+		const freshMessage = await Message.findOne({ _id: message._id }, { isRead: true });
+		if (!freshMessage.isRead) {
 			//#region ただしミュートされているなら発行しない
 			const mute = await Mute.find({
-				muter_id: recipient._id,
-				deleted_at: { $exists: false }
+				muterId: recipient._id,
+				deletedAt: { $exists: false }
 			});
-			const mutedUserIds = mute.map(m => m.mutee_id.toString());
+			const mutedUserIds = mute.map(m => m.muteeId.toString());
 			if (mutedUserIds.indexOf(user._id.toString()) != -1) {
 				return;
 			}
 			//#endregion
 
-			publishUserStream(message.recipient_id, 'unread_messaging_message', messageObj);
-			pushSw(message.recipient_id, 'unread_messaging_message', messageObj);
+			publishUserStream(message.recipientId, 'unread_messaging_message', messageObj);
+			pushSw(message.recipientId, 'unread_messaging_message', messageObj);
 		}
 	}, 3000);
 
@@ -130,26 +130,26 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// 履歴作成(自分)
 	History.update({
-		user_id: user._id,
-		partner: recipient._id
+		userId: user._id,
+		partnerId: recipient._id
 	}, {
-		updated_at: new Date(),
-		user_id: user._id,
-		partner: recipient._id,
-		message: message._id
+		updatedAt: new Date(),
+		userId: user._id,
+		partnerId: recipient._id,
+		messageId: message._id
 	}, {
 		upsert: true
 	});
 
 	// 履歴作成(相手)
 	History.update({
-		user_id: recipient._id,
-		partner: user._id
+		userId: recipient._id,
+		partnerId: user._id
 	}, {
-		updated_at: new Date(),
-		user_id: recipient._id,
-		partner: user._id,
-		message: message._id
+		updatedAt: new Date(),
+		userId: recipient._id,
+		partnerId: user._id,
+		messageId: message._id
 	}, {
 		upsert: true
 	});
diff --git a/src/api/endpoints/messaging/unread.ts b/src/api/endpoints/messaging/unread.ts
index c4326e1d2..f7f4047b6 100644
--- a/src/api/endpoints/messaging/unread.ts
+++ b/src/api/endpoints/messaging/unread.ts
@@ -13,18 +13,18 @@ import Mute from '../../models/mute';
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	const mute = await Mute.find({
-		muter_id: user._id,
-		deleted_at: { $exists: false }
+		muterId: user._id,
+		deletedAt: { $exists: false }
 	});
-	const mutedUserIds = mute.map(m => m.mutee_id);
+	const mutedUserIds = mute.map(m => m.muteeId);
 
 	const count = await Message
 		.count({
-			user_id: {
+			userId: {
 				$nin: mutedUserIds
 			},
-			recipient_id: user._id,
-			is_read: false
+			recipientId: user._id,
+			isRead: false
 		});
 
 	res({
diff --git a/src/api/endpoints/mute/create.ts b/src/api/endpoints/mute/create.ts
index f99b40d32..e86023508 100644
--- a/src/api/endpoints/mute/create.ts
+++ b/src/api/endpoints/mute/create.ts
@@ -15,9 +15,9 @@ import Mute from '../../models/mute';
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	const muter = user;
 
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [userId, userIdErr] = $(params.userId).id().$;
+	if (userIdErr) return rej('invalid userId param');
 
 	// 自分自身
 	if (user._id.equals(userId)) {
@@ -40,9 +40,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Check if already muting
 	const exist = await Mute.findOne({
-		muter_id: muter._id,
-		mutee_id: mutee._id,
-		deleted_at: { $exists: false }
+		muterId: muter._id,
+		muteeId: mutee._id,
+		deletedAt: { $exists: false }
 	});
 
 	if (exist !== null) {
@@ -51,9 +51,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Create mute
 	await Mute.insert({
-		created_at: new Date(),
-		muter_id: muter._id,
-		mutee_id: mutee._id,
+		createdAt: new Date(),
+		muterId: muter._id,
+		muteeId: mutee._id,
 	});
 
 	// Send response
diff --git a/src/api/endpoints/mute/delete.ts b/src/api/endpoints/mute/delete.ts
index 36e2fd101..7e361b479 100644
--- a/src/api/endpoints/mute/delete.ts
+++ b/src/api/endpoints/mute/delete.ts
@@ -15,9 +15,9 @@ import Mute from '../../models/mute';
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	const muter = user;
 
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [userId, userIdErr] = $(params.userId).id().$;
+	if (userIdErr) return rej('invalid userId param');
 
 	// Check if the mutee is yourself
 	if (user._id.equals(userId)) {
@@ -40,9 +40,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Check not muting
 	const exist = await Mute.findOne({
-		muter_id: muter._id,
-		mutee_id: mutee._id,
-		deleted_at: { $exists: false }
+		muterId: muter._id,
+		muteeId: mutee._id,
+		deletedAt: { $exists: false }
 	});
 
 	if (exist === null) {
@@ -54,7 +54,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		_id: exist._id
 	}, {
 		$set: {
-			deleted_at: new Date()
+			deletedAt: new Date()
 		}
 	});
 
diff --git a/src/api/endpoints/mute/list.ts b/src/api/endpoints/mute/list.ts
index 19e3b157e..3401fba64 100644
--- a/src/api/endpoints/mute/list.ts
+++ b/src/api/endpoints/mute/list.ts
@@ -28,15 +28,15 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Construct query
 	const query = {
-		muter_id: me._id,
-		deleted_at: { $exists: false }
+		muterId: me._id,
+		deletedAt: { $exists: false }
 	} as any;
 
 	if (iknow) {
 		// Get my friends
 		const myFriends = await getFriends(me._id);
 
-		query.mutee_id = {
+		query.muteeId = {
 			$in: myFriends
 		};
 	}
@@ -63,7 +63,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Serialize
 	const users = await Promise.all(mutes.map(async m =>
-		await pack(m.mutee_id, me, { detail: true })));
+		await pack(m.muteeId, me, { detail: true })));
 
 	// Response
 	res({
diff --git a/src/api/endpoints/my/apps.ts b/src/api/endpoints/my/apps.ts
index b23619050..bc1290cac 100644
--- a/src/api/endpoints/my/apps.ts
+++ b/src/api/endpoints/my/apps.ts
@@ -21,7 +21,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	if (offsetErr) return rej('invalid offset param');
 
 	const query = {
-		user_id: user._id
+		userId: user._id
 	};
 
 	// Execute query
diff --git a/src/api/endpoints/notifications/get_unread_count.ts b/src/api/endpoints/notifications/get_unread_count.ts
index 845d6b29c..8f9719fff 100644
--- a/src/api/endpoints/notifications/get_unread_count.ts
+++ b/src/api/endpoints/notifications/get_unread_count.ts
@@ -13,18 +13,18 @@ import Mute from '../../models/mute';
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	const mute = await Mute.find({
-		muter_id: user._id,
-		deleted_at: { $exists: false }
+		muterId: user._id,
+		deletedAt: { $exists: false }
 	});
-	const mutedUserIds = mute.map(m => m.mutee_id);
+	const mutedUserIds = mute.map(m => m.muteeId);
 
 	const count = await Notification
 		.count({
-			notifiee_id: user._id,
-			notifier_id: {
+			notifieeId: user._id,
+			notifierId: {
 				$nin: mutedUserIds
 			},
-			is_read: false
+			isRead: false
 		});
 
 	res({
diff --git a/src/api/endpoints/notifications/mark_as_read_all.ts b/src/api/endpoints/notifications/mark_as_read_all.ts
index 3550e344c..693de3d0e 100644
--- a/src/api/endpoints/notifications/mark_as_read_all.ts
+++ b/src/api/endpoints/notifications/mark_as_read_all.ts
@@ -14,11 +14,11 @@ import event from '../../event';
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Update documents
 	await Notification.update({
-		notifiee_id: user._id,
-		is_read: false
+		notifieeId: user._id,
+		isRead: false
 	}, {
 		$set: {
-			is_read: true
+			isRead: true
 		}
 	}, {
 		multi: true
diff --git a/src/api/endpoints/othello/games.ts b/src/api/endpoints/othello/games.ts
index f6e38b8d8..5c71f9882 100644
--- a/src/api/endpoints/othello/games.ts
+++ b/src/api/endpoints/othello/games.ts
@@ -24,14 +24,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}
 
 	const q: any = my ? {
-		is_started: true,
+		isStarted: true,
 		$or: [{
-			user1_id: user._id
+			user1Id: user._id
 		}, {
-			user2_id: user._id
+			user2Id: user._id
 		}]
 	} : {
-		is_started: true
+		isStarted: true
 	};
 
 	const sort = {
diff --git a/src/api/endpoints/othello/games/show.ts b/src/api/endpoints/othello/games/show.ts
index c7bd74a39..19f5d0fef 100644
--- a/src/api/endpoints/othello/games/show.ts
+++ b/src/api/endpoints/othello/games/show.ts
@@ -14,9 +14,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}
 
 	const o = new Othello(game.settings.map, {
-		isLlotheo: game.settings.is_llotheo,
-		canPutEverywhere: game.settings.can_put_everywhere,
-		loopedBoard: game.settings.looped_board
+		isLlotheo: game.settings.isLlotheo,
+		canPutEverywhere: game.settings.canPutEverywhere,
+		loopedBoard: game.settings.loopedBoard
 	});
 
 	game.logs.forEach(log => {
diff --git a/src/api/endpoints/othello/invitations.ts b/src/api/endpoints/othello/invitations.ts
index 02fb421fb..f6e0071a6 100644
--- a/src/api/endpoints/othello/invitations.ts
+++ b/src/api/endpoints/othello/invitations.ts
@@ -3,7 +3,7 @@ import Matching, { pack as packMatching } from '../../models/othello-matching';
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Find session
 	const invitations = await Matching.find({
-		child_id: user._id
+		childId: user._id
 	}, {
 		sort: {
 			_id: -1
diff --git a/src/api/endpoints/othello/match.ts b/src/api/endpoints/othello/match.ts
index f73386ba7..f503c5834 100644
--- a/src/api/endpoints/othello/match.ts
+++ b/src/api/endpoints/othello/match.ts
@@ -6,19 +6,19 @@ import publishUserStream, { publishOthelloStream } from '../../event';
 import { eighteight } from '../../../common/othello/maps';
 
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'user_id' parameter
-	const [childId, childIdErr] = $(params.user_id).id().$;
-	if (childIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [childId, childIdErr] = $(params.userId).id().$;
+	if (childIdErr) return rej('invalid userId param');
 
 	// Myself
 	if (childId.equals(user._id)) {
-		return rej('invalid user_id param');
+		return rej('invalid userId param');
 	}
 
 	// Find session
 	const exist = await Matching.findOne({
-		parent_id: childId,
-		child_id: user._id
+		parentId: childId,
+		childId: user._id
 	});
 
 	if (exist) {
@@ -29,28 +29,28 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 		// Create game
 		const game = await OthelloGame.insert({
-			created_at: new Date(),
-			user1_id: exist.parent_id,
-			user2_id: user._id,
-			user1_accepted: false,
-			user2_accepted: false,
-			is_started: false,
-			is_ended: false,
+			createdAt: new Date(),
+			user1Id: exist.parentId,
+			user2Id: user._id,
+			user1Accepted: false,
+			user2Accepted: false,
+			isStarted: false,
+			isEnded: false,
 			logs: [],
 			settings: {
 				map: eighteight.data,
 				bw: 'random',
-				is_llotheo: false
+				isLlotheo: false
 			}
 		});
 
 		// Reponse
 		res(await packGame(game, user));
 
-		publishOthelloStream(exist.parent_id, 'matched', await packGame(game, exist.parent_id));
+		publishOthelloStream(exist.parentId, 'matched', await packGame(game, exist.parentId));
 
 		const other = await Matching.count({
-			child_id: user._id
+			childId: user._id
 		});
 
 		if (other == 0) {
@@ -72,14 +72,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 		// 以前のセッションはすべて削除しておく
 		await Matching.remove({
-			parent_id: user._id
+			parentId: user._id
 		});
 
 		// セッションを作成
 		const matching = await Matching.insert({
-			created_at: new Date(),
-			parent_id: user._id,
-			child_id: child._id
+			createdAt: new Date(),
+			parentId: user._id,
+			childId: child._id
 		});
 
 		// Reponse
diff --git a/src/api/endpoints/othello/match/cancel.ts b/src/api/endpoints/othello/match/cancel.ts
index 6f751ef83..ee0f82a61 100644
--- a/src/api/endpoints/othello/match/cancel.ts
+++ b/src/api/endpoints/othello/match/cancel.ts
@@ -2,7 +2,7 @@ import Matching from '../../../models/othello-matching';
 
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	await Matching.remove({
-		parent_id: user._id
+		parentId: user._id
 	});
 
 	res();
diff --git a/src/api/endpoints/posts.ts b/src/api/endpoints/posts.ts
index 7df744d2a..7e9ff3ad7 100644
--- a/src/api/endpoints/posts.ts
+++ b/src/api/endpoints/posts.ts
@@ -65,15 +65,15 @@ module.exports = (params) => new Promise(async (res, rej) => {
 	}
 
 	if (reply != undefined) {
-		query.reply_id = reply ? { $exists: true, $ne: null } : null;
+		query.replyId = reply ? { $exists: true, $ne: null } : null;
 	}
 
 	if (repost != undefined) {
-		query.repost_id = repost ? { $exists: true, $ne: null } : null;
+		query.repostId = repost ? { $exists: true, $ne: null } : null;
 	}
 
 	if (media != undefined) {
-		query.media_ids = media ? { $exists: true, $ne: null } : null;
+		query.mediaIds = media ? { $exists: true, $ne: null } : null;
 	}
 
 	if (poll != undefined) {
@@ -82,7 +82,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
 
 	// TODO
 	//if (bot != undefined) {
-	//	query.is_bot = bot;
+	//	query.isBot = bot;
 	//}
 
 	// Issue query
diff --git a/src/api/endpoints/posts/categorize.ts b/src/api/endpoints/posts/categorize.ts
index 0c85c2b4e..0436c8e69 100644
--- a/src/api/endpoints/posts/categorize.ts
+++ b/src/api/endpoints/posts/categorize.ts
@@ -12,13 +12,13 @@ import Post from '../../models/post';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	if (!user.account.is_pro) {
+	if (!user.account.isPro) {
 		return rej('This endpoint is available only from a Pro account');
 	}
 
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Get categorizee
 	const post = await Post.findOne({
diff --git a/src/api/endpoints/posts/context.ts b/src/api/endpoints/posts/context.ts
index 5ba375897..44a77d102 100644
--- a/src/api/endpoints/posts/context.ts
+++ b/src/api/endpoints/posts/context.ts
@@ -12,9 +12,9 @@ import Post, { pack } from '../../models/post';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Get 'limit' parameter
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
@@ -48,13 +48,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 			return;
 		}
 
-		if (p.reply_id) {
-			await get(p.reply_id);
+		if (p.replyId) {
+			await get(p.replyId);
 		}
 	}
 
-	if (post.reply_id) {
-		await get(post.reply_id);
+	if (post.replyId) {
+		await get(post.replyId);
 	}
 
 	// Serialize
diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index 286e18bb7..2a3d974fe 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -33,9 +33,9 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 	const [text, textErr] = $(params.text).optional.string().pipe(isValidText).$;
 	if (textErr) return rej('invalid text');
 
-	// Get 'via_mobile' parameter
-	const [viaMobile = false, viaMobileErr] = $(params.via_mobile).optional.boolean().$;
-	if (viaMobileErr) return rej('invalid via_mobile');
+	// Get 'viaMobile' parameter
+	const [viaMobile = false, viaMobileErr] = $(params.viaMobile).optional.boolean().$;
+	if (viaMobileErr) return rej('invalid viaMobile');
 
 	// Get 'tags' parameter
 	const [tags = [], tagsErr] = $(params.tags).optional.array('string').unique().eachQ(t => t.range(1, 32)).$;
@@ -53,9 +53,9 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		.$;
 	if (geoErr) return rej('invalid geo');
 
-	// Get 'media_ids' parameter
-	const [mediaIds, mediaIdsErr] = $(params.media_ids).optional.array('id').unique().range(1, 4).$;
-	if (mediaIdsErr) return rej('invalid media_ids');
+	// Get 'mediaIds' parameter
+	const [mediaIds, mediaIdsErr] = $(params.mediaIds).optional.array('id').unique().range(1, 4).$;
+	if (mediaIdsErr) return rej('invalid mediaIds');
 
 	let files = [];
 	if (mediaIds !== undefined) {
@@ -67,7 +67,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 			// SELECT _id
 			const entity = await DriveFile.findOne({
 				_id: mediaId,
-				'metadata.user_id': user._id
+				'metadata.userId': user._id
 			});
 
 			if (entity === null) {
@@ -80,9 +80,9 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		files = null;
 	}
 
-	// Get 'repost_id' parameter
-	const [repostId, repostIdErr] = $(params.repost_id).optional.id().$;
-	if (repostIdErr) return rej('invalid repost_id');
+	// Get 'repostId' parameter
+	const [repostId, repostIdErr] = $(params.repostId).optional.id().$;
+	if (repostIdErr) return rej('invalid repostId');
 
 	let repost: IPost = null;
 	let isQuote = false;
@@ -94,13 +94,13 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 
 		if (repost == null) {
 			return rej('repostee is not found');
-		} else if (repost.repost_id && !repost.text && !repost.media_ids) {
+		} else if (repost.repostId && !repost.text && !repost.mediaIds) {
 			return rej('cannot repost to repost');
 		}
 
 		// Fetch recently post
 		const latestPost = await Post.findOne({
-			user_id: user._id
+			userId: user._id
 		}, {
 			sort: {
 				_id: -1
@@ -111,8 +111,8 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 
 		// 直近と同じRepost対象かつ引用じゃなかったらエラー
 		if (latestPost &&
-			latestPost.repost_id &&
-			latestPost.repost_id.equals(repost._id) &&
+			latestPost.repostId &&
+			latestPost.repostId.equals(repost._id) &&
 			!isQuote) {
 			return rej('cannot repost same post that already reposted in your latest post');
 		}
@@ -125,9 +125,9 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		}
 	}
 
-	// Get 'reply_id' parameter
-	const [replyId, replyIdErr] = $(params.reply_id).optional.id().$;
-	if (replyIdErr) return rej('invalid reply_id');
+	// Get 'replyId' parameter
+	const [replyId, replyIdErr] = $(params.replyId).optional.id().$;
+	if (replyIdErr) return rej('invalid replyId');
 
 	let reply: IPost = null;
 	if (replyId !== undefined) {
@@ -141,14 +141,14 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		}
 
 		// 返信対象が引用でないRepostだったらエラー
-		if (reply.repost_id && !reply.text && !reply.media_ids) {
+		if (reply.repostId && !reply.text && !reply.mediaIds) {
 			return rej('cannot reply to repost');
 		}
 	}
 
-	// Get 'channel_id' parameter
-	const [channelId, channelIdErr] = $(params.channel_id).optional.id().$;
-	if (channelIdErr) return rej('invalid channel_id');
+	// Get 'channelId' parameter
+	const [channelId, channelIdErr] = $(params.channelId).optional.id().$;
+	if (channelIdErr) return rej('invalid channelId');
 
 	let channel: IChannel = null;
 	if (channelId !== undefined) {
@@ -162,12 +162,12 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		}
 
 		// 返信対象の投稿がこのチャンネルじゃなかったらダメ
-		if (reply && !channelId.equals(reply.channel_id)) {
+		if (reply && !channelId.equals(reply.channelId)) {
 			return rej('チャンネル内部からチャンネル外部の投稿に返信することはできません');
 		}
 
 		// Repost対象の投稿がこのチャンネルじゃなかったらダメ
-		if (repost && !channelId.equals(repost.channel_id)) {
+		if (repost && !channelId.equals(repost.channelId)) {
 			return rej('チャンネル内部からチャンネル外部の投稿をRepostすることはできません');
 		}
 
@@ -177,12 +177,12 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		}
 	} else {
 		// 返信対象の投稿がチャンネルへの投稿だったらダメ
-		if (reply && reply.channel_id != null) {
+		if (reply && reply.channelId != null) {
 			return rej('チャンネル外部からチャンネル内部の投稿に返信することはできません');
 		}
 
 		// Repost対象の投稿がチャンネルへの投稿だったらダメ
-		if (repost && repost.channel_id != null) {
+		if (repost && repost.channelId != null) {
 			return rej('チャンネル外部からチャンネル内部の投稿をRepostすることはできません');
 		}
 	}
@@ -206,7 +206,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 
 	// テキストが無いかつ添付ファイルが無いかつRepostも無いかつ投票も無かったらエラー
 	if (text === undefined && files === null && repost === null && poll === undefined) {
-		return rej('text, media_ids, repost_id or poll is required');
+		return rej('text, mediaIds, repostId or poll is required');
 	}
 
 	// 直近の投稿と重複してたらエラー
@@ -214,14 +214,14 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 	if (user.latest_post) {
 		if (deepEqual({
 			text: user.latest_post.text,
-			reply: user.latest_post.reply_id ? user.latest_post.reply_id.toString() : null,
-			repost: user.latest_post.repost_id ? user.latest_post.repost_id.toString() : null,
-			media_ids: (user.latest_post.media_ids || []).map(id => id.toString())
+			reply: user.latest_post.replyId ? user.latest_post.replyId.toString() : null,
+			repost: user.latest_post.repostId ? user.latest_post.repostId.toString() : null,
+			mediaIds: (user.latest_post.mediaIds || []).map(id => id.toString())
 		}, {
 			text: text,
 			reply: reply ? reply._id.toString() : null,
 			repost: repost ? repost._id.toString() : null,
-			media_ids: (files || []).map(file => file._id.toString())
+			mediaIds: (files || []).map(file => file._id.toString())
 		})) {
 			return rej('duplicate');
 		}
@@ -246,23 +246,23 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 
 	// 投稿を作成
 	const post = await Post.insert({
-		created_at: new Date(),
-		channel_id: channel ? channel._id : undefined,
+		createdAt: new Date(),
+		channelId: channel ? channel._id : undefined,
 		index: channel ? channel.index + 1 : undefined,
-		media_ids: files ? files.map(file => file._id) : undefined,
-		reply_id: reply ? reply._id : undefined,
-		repost_id: repost ? repost._id : undefined,
+		mediaIds: files ? files.map(file => file._id) : undefined,
+		replyId: reply ? reply._id : undefined,
+		repostId: repost ? repost._id : undefined,
 		poll: poll,
 		text: text,
 		tags: tags,
-		user_id: user._id,
-		app_id: app ? app._id : null,
-		via_mobile: viaMobile,
+		userId: user._id,
+		appId: app ? app._id : null,
+		viaMobile: viaMobile,
 		geo,
 
 		// 以下非正規化データ
-		_reply: reply ? { user_id: reply.user_id } : undefined,
-		_repost: repost ? { user_id: repost.user_id } : undefined,
+		_reply: reply ? { userId: reply.userId } : undefined,
+		_repost: repost ? { userId: repost.userId } : undefined,
 	});
 
 	// Serialize
@@ -293,10 +293,10 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		// Publish event
 		if (!user._id.equals(mentionee)) {
 			const mentioneeMutes = await Mute.find({
-				muter_id: mentionee,
-				deleted_at: { $exists: false }
+				muterId: mentionee,
+				deletedAt: { $exists: false }
 			});
-			const mentioneesMutedUserIds = mentioneeMutes.map(m => m.mutee_id.toString());
+			const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString());
 			if (mentioneesMutedUserIds.indexOf(user._id.toString()) == -1) {
 				event(mentionee, reason, postObj);
 				pushSw(mentionee, reason, postObj);
@@ -312,17 +312,17 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		// Fetch all followers
 		const followers = await Following
 			.find({
-				followee_id: user._id,
+				followeeId: user._id,
 				// 削除されたドキュメントは除く
-				deleted_at: { $exists: false }
+				deletedAt: { $exists: false }
 			}, {
-				follower_id: true,
+				followerId: true,
 				_id: false
 			});
 
 		// Publish event to followers stream
 		followers.forEach(following =>
-			event(following.follower_id, 'post', postObj));
+			event(following.followerId, 'post', postObj));
 	}
 
 	// チャンネルへの投稿
@@ -339,21 +339,21 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 
 		// Get channel watchers
 		const watches = await ChannelWatching.find({
-			channel_id: channel._id,
+			channelId: channel._id,
 			// 削除されたドキュメントは除く
-			deleted_at: { $exists: false }
+			deletedAt: { $exists: false }
 		});
 
 		// チャンネルの視聴者(のタイムライン)に配信
 		watches.forEach(w => {
-			event(w.user_id, 'post', postObj);
+			event(w.userId, 'post', postObj);
 		});
 	}
 
 	// Increment my posts count
 	User.update({ _id: user._id }, {
 		$inc: {
-			posts_count: 1
+			postsCount: 1
 		}
 	});
 
@@ -367,26 +367,26 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		});
 
 		// 自分自身へのリプライでない限りは通知を作成
-		notify(reply.user_id, user._id, 'reply', {
-			post_id: post._id
+		notify(reply.userId, user._id, 'reply', {
+			postId: post._id
 		});
 
 		// Fetch watchers
 		Watching
 			.find({
-				post_id: reply._id,
-				user_id: { $ne: user._id },
+				postId: reply._id,
+				userId: { $ne: user._id },
 				// 削除されたドキュメントは除く
-				deleted_at: { $exists: false }
+				deletedAt: { $exists: false }
 			}, {
 				fields: {
-					user_id: true
+					userId: true
 				}
 			})
 			.then(watchers => {
 				watchers.forEach(watcher => {
-					notify(watcher.user_id, user._id, 'reply', {
-						post_id: post._id
+					notify(watcher.userId, user._id, 'reply', {
+						postId: post._id
 					});
 				});
 			});
@@ -397,33 +397,33 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		}
 
 		// Add mention
-		addMention(reply.user_id, 'reply');
+		addMention(reply.userId, 'reply');
 	}
 
 	// If it is repost
 	if (repost) {
 		// Notify
 		const type = text ? 'quote' : 'repost';
-		notify(repost.user_id, user._id, type, {
-			post_id: post._id
+		notify(repost.userId, user._id, type, {
+			postId: post._id
 		});
 
 		// Fetch watchers
 		Watching
 			.find({
-				post_id: repost._id,
-				user_id: { $ne: user._id },
+				postId: repost._id,
+				userId: { $ne: user._id },
 				// 削除されたドキュメントは除く
-				deleted_at: { $exists: false }
+				deletedAt: { $exists: false }
 			}, {
 				fields: {
-					user_id: true
+					userId: true
 				}
 			})
 			.then(watchers => {
 				watchers.forEach(watcher => {
-					notify(watcher.user_id, user._id, type, {
-						post_id: post._id
+					notify(watcher.userId, user._id, type, {
+						postId: post._id
 					});
 				});
 			});
@@ -436,18 +436,18 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		// If it is quote repost
 		if (text) {
 			// Add mention
-			addMention(repost.user_id, 'quote');
+			addMention(repost.userId, 'quote');
 		} else {
 			// Publish event
-			if (!user._id.equals(repost.user_id)) {
-				event(repost.user_id, 'repost', postObj);
+			if (!user._id.equals(repost.userId)) {
+				event(repost.userId, 'repost', postObj);
 			}
 		}
 
 		// 今までで同じ投稿をRepostしているか
 		const existRepost = await Post.findOne({
-			user_id: user._id,
-			repost_id: repost._id,
+			userId: user._id,
+			repostId: repost._id,
 			_id: {
 				$ne: post._id
 			}
@@ -494,15 +494,15 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 			if (mentionee == null) return;
 
 			// 既に言及されたユーザーに対する返信や引用repostの場合も無視
-			if (reply && reply.user_id.equals(mentionee._id)) return;
-			if (repost && repost.user_id.equals(mentionee._id)) return;
+			if (reply && reply.userId.equals(mentionee._id)) return;
+			if (repost && repost.userId.equals(mentionee._id)) return;
 
 			// Add mention
 			addMention(mentionee._id, 'mention');
 
 			// Create notification
 			notify(mentionee._id, user._id, 'mention', {
-				post_id: post._id
+				postId: post._id
 			});
 
 			return;
diff --git a/src/api/endpoints/posts/favorites/create.ts b/src/api/endpoints/posts/favorites/create.ts
index f9dee271b..6100e10b2 100644
--- a/src/api/endpoints/posts/favorites/create.ts
+++ b/src/api/endpoints/posts/favorites/create.ts
@@ -13,9 +13,9 @@ import Post from '../../../models/post';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Get favoritee
 	const post = await Post.findOne({
@@ -28,8 +28,8 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// if already favorited
 	const exist = await Favorite.findOne({
-		post_id: post._id,
-		user_id: user._id
+		postId: post._id,
+		userId: user._id
 	});
 
 	if (exist !== null) {
@@ -38,9 +38,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Create favorite
 	await Favorite.insert({
-		created_at: new Date(),
-		post_id: post._id,
-		user_id: user._id
+		createdAt: new Date(),
+		postId: post._id,
+		userId: user._id
 	});
 
 	// Send response
diff --git a/src/api/endpoints/posts/favorites/delete.ts b/src/api/endpoints/posts/favorites/delete.ts
index c4fe7d323..b1b4fcebc 100644
--- a/src/api/endpoints/posts/favorites/delete.ts
+++ b/src/api/endpoints/posts/favorites/delete.ts
@@ -13,9 +13,9 @@ import Post from '../../../models/post';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Get favoritee
 	const post = await Post.findOne({
@@ -28,8 +28,8 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// if already favorited
 	const exist = await Favorite.findOne({
-		post_id: post._id,
-		user_id: user._id
+		postId: post._id,
+		userId: user._id
 	});
 
 	if (exist === null) {
diff --git a/src/api/endpoints/posts/mentions.ts b/src/api/endpoints/posts/mentions.ts
index 7127db0ad..da90583bb 100644
--- a/src/api/endpoints/posts/mentions.ts
+++ b/src/api/endpoints/posts/mentions.ts
@@ -48,7 +48,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	if (following) {
 		const followingIds = await getFriends(user._id);
 
-		query.user_id = {
+		query.userId = {
 			$in: followingIds
 		};
 	}
diff --git a/src/api/endpoints/posts/polls/recommendation.ts b/src/api/endpoints/posts/polls/recommendation.ts
index 4a3fa3f55..19ef0975f 100644
--- a/src/api/endpoints/posts/polls/recommendation.ts
+++ b/src/api/endpoints/posts/polls/recommendation.ts
@@ -23,22 +23,22 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Get votes
 	const votes = await Vote.find({
-		user_id: user._id
+		userId: user._id
 	}, {
 		fields: {
 			_id: false,
-			post_id: true
+			postId: true
 		}
 	});
 
-	const nin = votes && votes.length != 0 ? votes.map(v => v.post_id) : [];
+	const nin = votes && votes.length != 0 ? votes.map(v => v.postId) : [];
 
 	const posts = await Post
 		.find({
 			_id: {
 				$nin: nin
 			},
-			user_id: {
+			userId: {
 				$ne: user._id
 			},
 			poll: {
diff --git a/src/api/endpoints/posts/polls/vote.ts b/src/api/endpoints/posts/polls/vote.ts
index 16ce76a6f..e87474ae6 100644
--- a/src/api/endpoints/posts/polls/vote.ts
+++ b/src/api/endpoints/posts/polls/vote.ts
@@ -17,9 +17,9 @@ import { publishPostStream } from '../../../event';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Get votee
 	const post = await Post.findOne({
@@ -43,8 +43,8 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// if already voted
 	const exist = await Vote.findOne({
-		post_id: post._id,
-		user_id: user._id
+		postId: post._id,
+		userId: user._id
 	});
 
 	if (exist !== null) {
@@ -53,9 +53,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Create vote
 	await Vote.insert({
-		created_at: new Date(),
-		post_id: post._id,
-		user_id: user._id,
+		createdAt: new Date(),
+		postId: post._id,
+		userId: user._id,
 		choice: choice
 	});
 
@@ -73,27 +73,27 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	publishPostStream(post._id, 'poll_voted');
 
 	// Notify
-	notify(post.user_id, user._id, 'poll_vote', {
-		post_id: post._id,
+	notify(post.userId, user._id, 'poll_vote', {
+		postId: post._id,
 		choice: choice
 	});
 
 	// Fetch watchers
 	Watching
 		.find({
-			post_id: post._id,
-			user_id: { $ne: user._id },
+			postId: post._id,
+			userId: { $ne: user._id },
 			// 削除されたドキュメントは除く
-			deleted_at: { $exists: false }
+			deletedAt: { $exists: false }
 		}, {
 			fields: {
-				user_id: true
+				userId: true
 			}
 		})
 		.then(watchers => {
 			watchers.forEach(watcher => {
-				notify(watcher.user_id, user._id, 'poll_vote', {
-					post_id: post._id,
+				notify(watcher.userId, user._id, 'poll_vote', {
+					postId: post._id,
 					choice: choice
 				});
 			});
diff --git a/src/api/endpoints/posts/reactions.ts b/src/api/endpoints/posts/reactions.ts
index feb140ab4..f753ba7c2 100644
--- a/src/api/endpoints/posts/reactions.ts
+++ b/src/api/endpoints/posts/reactions.ts
@@ -13,9 +13,9 @@ import Reaction, { pack } from '../../models/post-reaction';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Get 'limit' parameter
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
@@ -41,8 +41,8 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Issue query
 	const reactions = await Reaction
 		.find({
-			post_id: post._id,
-			deleted_at: { $exists: false }
+			postId: post._id,
+			deletedAt: { $exists: false }
 		}, {
 			limit: limit,
 			skip: offset,
diff --git a/src/api/endpoints/posts/reactions/create.ts b/src/api/endpoints/posts/reactions/create.ts
index f77afed40..7031d28e5 100644
--- a/src/api/endpoints/posts/reactions/create.ts
+++ b/src/api/endpoints/posts/reactions/create.ts
@@ -18,9 +18,9 @@ import { publishPostStream, pushSw } from '../../../event';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Get 'reaction' parameter
 	const [reaction, reactionErr] = $(params.reaction).string().or([
@@ -46,15 +46,15 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}
 
 	// Myself
-	if (post.user_id.equals(user._id)) {
+	if (post.userId.equals(user._id)) {
 		return rej('cannot react to my post');
 	}
 
 	// if already reacted
 	const exist = await Reaction.findOne({
-		post_id: post._id,
-		user_id: user._id,
-		deleted_at: { $exists: false }
+		postId: post._id,
+		userId: user._id,
+		deletedAt: { $exists: false }
 	});
 
 	if (exist !== null) {
@@ -63,9 +63,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Create reaction
 	await Reaction.insert({
-		created_at: new Date(),
-		post_id: post._id,
-		user_id: user._id,
+		createdAt: new Date(),
+		postId: post._id,
+		userId: user._id,
 		reaction: reaction
 	});
 
@@ -83,33 +83,33 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	publishPostStream(post._id, 'reacted');
 
 	// Notify
-	notify(post.user_id, user._id, 'reaction', {
-		post_id: post._id,
+	notify(post.userId, user._id, 'reaction', {
+		postId: post._id,
 		reaction: reaction
 	});
 
-	pushSw(post.user_id, 'reaction', {
-		user: await packUser(user, post.user_id),
-		post: await packPost(post, post.user_id),
+	pushSw(post.userId, 'reaction', {
+		user: await packUser(user, post.userId),
+		post: await packPost(post, post.userId),
 		reaction: reaction
 	});
 
 	// Fetch watchers
 	Watching
 		.find({
-			post_id: post._id,
-			user_id: { $ne: user._id },
+			postId: post._id,
+			userId: { $ne: user._id },
 			// 削除されたドキュメントは除く
-			deleted_at: { $exists: false }
+			deletedAt: { $exists: false }
 		}, {
 			fields: {
-				user_id: true
+				userId: true
 			}
 		})
 		.then(watchers => {
 			watchers.forEach(watcher => {
-				notify(watcher.user_id, user._id, 'reaction', {
-					post_id: post._id,
+				notify(watcher.userId, user._id, 'reaction', {
+					postId: post._id,
 					reaction: reaction
 				});
 			});
diff --git a/src/api/endpoints/posts/reactions/delete.ts b/src/api/endpoints/posts/reactions/delete.ts
index 922c57ab1..18fdabcdc 100644
--- a/src/api/endpoints/posts/reactions/delete.ts
+++ b/src/api/endpoints/posts/reactions/delete.ts
@@ -14,9 +14,9 @@ import Post from '../../../models/post';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Fetch unreactee
 	const post = await Post.findOne({
@@ -29,9 +29,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// if already unreacted
 	const exist = await Reaction.findOne({
-		post_id: post._id,
-		user_id: user._id,
-		deleted_at: { $exists: false }
+		postId: post._id,
+		userId: user._id,
+		deletedAt: { $exists: false }
 	});
 
 	if (exist === null) {
@@ -43,7 +43,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		_id: exist._id
 	}, {
 			$set: {
-				deleted_at: new Date()
+				deletedAt: new Date()
 			}
 		});
 
diff --git a/src/api/endpoints/posts/replies.ts b/src/api/endpoints/posts/replies.ts
index 613c4fa24..db021505f 100644
--- a/src/api/endpoints/posts/replies.ts
+++ b/src/api/endpoints/posts/replies.ts
@@ -12,9 +12,9 @@ import Post, { pack } from '../../models/post';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Get 'limit' parameter
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
@@ -39,7 +39,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Issue query
 	const replies = await Post
-		.find({ reply_id: post._id }, {
+		.find({ replyId: post._id }, {
 			limit: limit,
 			skip: offset,
 			sort: {
diff --git a/src/api/endpoints/posts/reposts.ts b/src/api/endpoints/posts/reposts.ts
index 89ab0e3d5..c1645117f 100644
--- a/src/api/endpoints/posts/reposts.ts
+++ b/src/api/endpoints/posts/reposts.ts
@@ -12,9 +12,9 @@ import Post, { pack } from '../../models/post';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Get 'limit' parameter
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
@@ -47,7 +47,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		_id: -1
 	};
 	const query = {
-		repost_id: post._id
+		repostId: post._id
 	} as any;
 	if (sinceId) {
 		sort._id = 1;
diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index a36d1178a..e7906c95c 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -21,13 +21,13 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [text, textError] = $(params.text).optional.string().$;
 	if (textError) return rej('invalid text param');
 
-	// Get 'include_user_ids' parameter
-	const [includeUserIds = [], includeUserIdsErr] = $(params.include_user_ids).optional.array('id').$;
-	if (includeUserIdsErr) return rej('invalid include_user_ids param');
+	// Get 'include_userIds' parameter
+	const [includeUserIds = [], includeUserIdsErr] = $(params.include_userIds).optional.array('id').$;
+	if (includeUserIdsErr) return rej('invalid include_userIds param');
 
-	// Get 'exclude_user_ids' parameter
-	const [excludeUserIds = [], excludeUserIdsErr] = $(params.exclude_user_ids).optional.array('id').$;
-	if (excludeUserIdsErr) return rej('invalid exclude_user_ids param');
+	// Get 'exclude_userIds' parameter
+	const [excludeUserIds = [], excludeUserIdsErr] = $(params.exclude_userIds).optional.array('id').$;
+	if (excludeUserIdsErr) return rej('invalid exclude_userIds param');
 
 	// Get 'include_user_usernames' parameter
 	const [includeUserUsernames = [], includeUserUsernamesErr] = $(params.include_user_usernames).optional.array('string').$;
@@ -81,7 +81,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	if (includeUserUsernames != null) {
 		const ids = (await Promise.all(includeUserUsernames.map(async (username) => {
 			const _user = await User.findOne({
-				username_lower: username.toLowerCase()
+				usernameLower: username.toLowerCase()
 			});
 			return _user ? _user._id : null;
 		}))).filter(id => id != null);
@@ -92,7 +92,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	if (excludeUserUsernames != null) {
 		const ids = (await Promise.all(excludeUserUsernames.map(async (username) => {
 			const _user = await User.findOne({
-				username_lower: username.toLowerCase()
+				usernameLower: username.toLowerCase()
 			});
 			return _user ? _user._id : null;
 		}))).filter(id => id != null);
@@ -143,13 +143,13 @@ async function search(
 
 	if (includeUserIds && includeUserIds.length != 0) {
 		push({
-			user_id: {
+			userId: {
 				$in: includeUserIds
 			}
 		});
 	} else if (excludeUserIds && excludeUserIds.length != 0) {
 		push({
-			user_id: {
+			userId: {
 				$nin: excludeUserIds
 			}
 		});
@@ -158,7 +158,7 @@ async function search(
 	if (following != null && me != null) {
 		const ids = await getFriends(me._id, false);
 		push({
-			user_id: following ? {
+			userId: following ? {
 				$in: ids
 			} : {
 				$nin: ids.concat(me._id)
@@ -168,45 +168,45 @@ async function search(
 
 	if (me != null) {
 		const mutes = await Mute.find({
-			muter_id: me._id,
-			deleted_at: { $exists: false }
+			muterId: me._id,
+			deletedAt: { $exists: false }
 		});
-		const mutedUserIds = mutes.map(m => m.mutee_id);
+		const mutedUserIds = mutes.map(m => m.muteeId);
 
 		switch (mute) {
 			case 'mute_all':
 				push({
-					user_id: {
+					userId: {
 						$nin: mutedUserIds
 					},
-					'_reply.user_id': {
+					'_reply.userId': {
 						$nin: mutedUserIds
 					},
-					'_repost.user_id': {
+					'_repost.userId': {
 						$nin: mutedUserIds
 					}
 				});
 				break;
 			case 'mute_related':
 				push({
-					'_reply.user_id': {
+					'_reply.userId': {
 						$nin: mutedUserIds
 					},
-					'_repost.user_id': {
+					'_repost.userId': {
 						$nin: mutedUserIds
 					}
 				});
 				break;
 			case 'mute_direct':
 				push({
-					user_id: {
+					userId: {
 						$nin: mutedUserIds
 					}
 				});
 				break;
 			case 'direct_only':
 				push({
-					user_id: {
+					userId: {
 						$in: mutedUserIds
 					}
 				});
@@ -214,11 +214,11 @@ async function search(
 			case 'related_only':
 				push({
 					$or: [{
-						'_reply.user_id': {
+						'_reply.userId': {
 							$in: mutedUserIds
 						}
 					}, {
-						'_repost.user_id': {
+						'_repost.userId': {
 							$in: mutedUserIds
 						}
 					}]
@@ -227,15 +227,15 @@ async function search(
 			case 'all_only':
 				push({
 					$or: [{
-						user_id: {
+						userId: {
 							$in: mutedUserIds
 						}
 					}, {
-						'_reply.user_id': {
+						'_reply.userId': {
 							$in: mutedUserIds
 						}
 					}, {
-						'_repost.user_id': {
+						'_repost.userId': {
 							$in: mutedUserIds
 						}
 					}]
@@ -247,7 +247,7 @@ async function search(
 	if (reply != null) {
 		if (reply) {
 			push({
-				reply_id: {
+				replyId: {
 					$exists: true,
 					$ne: null
 				}
@@ -255,11 +255,11 @@ async function search(
 		} else {
 			push({
 				$or: [{
-					reply_id: {
+					replyId: {
 						$exists: false
 					}
 				}, {
-					reply_id: null
+					replyId: null
 				}]
 			});
 		}
@@ -268,7 +268,7 @@ async function search(
 	if (repost != null) {
 		if (repost) {
 			push({
-				repost_id: {
+				repostId: {
 					$exists: true,
 					$ne: null
 				}
@@ -276,11 +276,11 @@ async function search(
 		} else {
 			push({
 				$or: [{
-					repost_id: {
+					repostId: {
 						$exists: false
 					}
 				}, {
-					repost_id: null
+					repostId: null
 				}]
 			});
 		}
@@ -289,7 +289,7 @@ async function search(
 	if (media != null) {
 		if (media) {
 			push({
-				media_ids: {
+				mediaIds: {
 					$exists: true,
 					$ne: null
 				}
@@ -297,11 +297,11 @@ async function search(
 		} else {
 			push({
 				$or: [{
-					media_ids: {
+					mediaIds: {
 						$exists: false
 					}
 				}, {
-					media_ids: null
+					mediaIds: null
 				}]
 			});
 		}
@@ -330,7 +330,7 @@ async function search(
 
 	if (sinceDate) {
 		push({
-			created_at: {
+			createdAt: {
 				$gt: new Date(sinceDate)
 			}
 		});
@@ -338,7 +338,7 @@ async function search(
 
 	if (untilDate) {
 		push({
-			created_at: {
+			createdAt: {
 				$lt: new Date(untilDate)
 			}
 		});
diff --git a/src/api/endpoints/posts/show.ts b/src/api/endpoints/posts/show.ts
index 383949059..bb4bcdb79 100644
--- a/src/api/endpoints/posts/show.ts
+++ b/src/api/endpoints/posts/show.ts
@@ -12,9 +12,9 @@ import Post, { pack } from '../../models/post';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Get post
 	const post = await Post.findOne({
diff --git a/src/api/endpoints/posts/timeline.ts b/src/api/endpoints/posts/timeline.ts
index c41cfdb8b..c7cb8032e 100644
--- a/src/api/endpoints/posts/timeline.ts
+++ b/src/api/endpoints/posts/timeline.ts
@@ -49,17 +49,17 @@ module.exports = async (params, user, app) => {
 
 		// Watchしているチャンネルを取得
 		watchingChannelIds: ChannelWatching.find({
-			user_id: user._id,
+			userId: user._id,
 			// 削除されたドキュメントは除く
-			deleted_at: { $exists: false }
-		}).then(watches => watches.map(w => w.channel_id)),
+			deletedAt: { $exists: false }
+		}).then(watches => watches.map(w => w.channelId)),
 
 		// ミュートしているユーザーを取得
 		mutedUserIds: Mute.find({
-			muter_id: user._id,
+			muterId: user._id,
 			// 削除されたドキュメントは除く
-			deleted_at: { $exists: false }
-		}).then(ms => ms.map(m => m.mutee_id))
+			deletedAt: { $exists: false }
+		}).then(ms => ms.map(m => m.muteeId))
 	});
 
 	//#region Construct query
@@ -70,31 +70,31 @@ module.exports = async (params, user, app) => {
 	const query = {
 		$or: [{
 			// フォローしている人のタイムラインへの投稿
-			user_id: {
+			userId: {
 				$in: followingIds
 			},
 			// 「タイムラインへの」投稿に限定するためにチャンネルが指定されていないもののみに限る
 			$or: [{
-				channel_id: {
+				channelId: {
 					$exists: false
 				}
 			}, {
-				channel_id: null
+				channelId: null
 			}]
 		}, {
 			// Watchしているチャンネルへの投稿
-			channel_id: {
+			channelId: {
 				$in: watchingChannelIds
 			}
 		}],
 		// mute
-		user_id: {
+		userId: {
 			$nin: mutedUserIds
 		},
-		'_reply.user_id': {
+		'_reply.userId': {
 			$nin: mutedUserIds
 		},
-		'_repost.user_id': {
+		'_repost.userId': {
 			$nin: mutedUserIds
 		},
 	} as any;
@@ -110,11 +110,11 @@ module.exports = async (params, user, app) => {
 		};
 	} else if (sinceDate) {
 		sort._id = 1;
-		query.created_at = {
+		query.createdAt = {
 			$gt: new Date(sinceDate)
 		};
 	} else if (untilDate) {
-		query.created_at = {
+		query.createdAt = {
 			$lt: new Date(untilDate)
 		};
 	}
diff --git a/src/api/endpoints/posts/trend.ts b/src/api/endpoints/posts/trend.ts
index caded92bf..3f92f0616 100644
--- a/src/api/endpoints/posts/trend.ts
+++ b/src/api/endpoints/posts/trend.ts
@@ -38,7 +38,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	if (pollErr) return rej('invalid poll param');
 
 	const query = {
-		created_at: {
+		createdAt: {
 			$gte: new Date(Date.now() - ms('1days'))
 		},
 		repost_count: {
@@ -47,15 +47,15 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	} as any;
 
 	if (reply != undefined) {
-		query.reply_id = reply ? { $exists: true, $ne: null } : null;
+		query.replyId = reply ? { $exists: true, $ne: null } : null;
 	}
 
 	if (repost != undefined) {
-		query.repost_id = repost ? { $exists: true, $ne: null } : null;
+		query.repostId = repost ? { $exists: true, $ne: null } : null;
 	}
 
 	if (media != undefined) {
-		query.media_ids = media ? { $exists: true, $ne: null } : null;
+		query.mediaIds = media ? { $exists: true, $ne: null } : null;
 	}
 
 	if (poll != undefined) {
diff --git a/src/api/endpoints/stats.ts b/src/api/endpoints/stats.ts
index a6084cd17..eee6f4870 100644
--- a/src/api/endpoints/stats.ts
+++ b/src/api/endpoints/stats.ts
@@ -15,7 +15,7 @@ import User from '../models/user';
  *         schema:
  *           type: object
  *           properties:
- *             posts_count:
+ *             postsCount:
  *               description: count of all posts of misskey
  *               type: number
  *             users_count:
@@ -42,7 +42,7 @@ module.exports = params => new Promise(async (res, rej) => {
 		.count();
 
 	res({
-		posts_count: postsCount,
+		postsCount: postsCount,
 		users_count: usersCount
 	});
 });
diff --git a/src/api/endpoints/sw/register.ts b/src/api/endpoints/sw/register.ts
index 99406138d..1542e1dbe 100644
--- a/src/api/endpoints/sw/register.ts
+++ b/src/api/endpoints/sw/register.ts
@@ -28,11 +28,11 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re
 
 	// if already subscribed
 	const exist = await Subscription.findOne({
-		user_id: user._id,
+		userId: user._id,
 		endpoint: endpoint,
 		auth: auth,
 		publickey: publickey,
-		deleted_at: { $exists: false }
+		deletedAt: { $exists: false }
 	});
 
 	if (exist !== null) {
@@ -40,7 +40,7 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re
 	}
 
 	await Subscription.insert({
-		user_id: user._id,
+		userId: user._id,
 		endpoint: endpoint,
 		auth: auth,
 		publickey: publickey
diff --git a/src/api/endpoints/username/available.ts b/src/api/endpoints/username/available.ts
index aac7fadf5..f23cdbd85 100644
--- a/src/api/endpoints/username/available.ts
+++ b/src/api/endpoints/username/available.ts
@@ -20,7 +20,7 @@ module.exports = async (params) => new Promise(async (res, rej) => {
 	const exist = await User
 		.count({
 			host: null,
-			username_lower: username.toLowerCase()
+			usernameLower: username.toLowerCase()
 		}, {
 			limit: 1
 		});
diff --git a/src/api/endpoints/users.ts b/src/api/endpoints/users.ts
index 4acc13c28..393c3479c 100644
--- a/src/api/endpoints/users.ts
+++ b/src/api/endpoints/users.ts
@@ -29,11 +29,11 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	if (sort) {
 		if (sort == '+follower') {
 			_sort = {
-				followers_count: -1
+				followersCount: -1
 			};
 		} else if (sort == '-follower') {
 			_sort = {
-				followers_count: 1
+				followersCount: 1
 			};
 		}
 	} else {
diff --git a/src/api/endpoints/users/followers.ts b/src/api/endpoints/users/followers.ts
index b0fb83c68..fc09cfa2c 100644
--- a/src/api/endpoints/users/followers.ts
+++ b/src/api/endpoints/users/followers.ts
@@ -15,9 +15,9 @@ import getFriends from '../../common/get-friends';
  * @return {Promise<any>}
  */
 module.exports = (params, me) => new Promise(async (res, rej) => {
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [userId, userIdErr] = $(params.userId).id().$;
+	if (userIdErr) return rej('invalid userId param');
 
 	// Get 'iknow' parameter
 	const [iknow = false, iknowErr] = $(params.iknow).optional.boolean().$;
@@ -46,8 +46,8 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Construct query
 	const query = {
-		followee_id: user._id,
-		deleted_at: { $exists: false }
+		followeeId: user._id,
+		deletedAt: { $exists: false }
 	} as any;
 
 	// ログインしていてかつ iknow フラグがあるとき
@@ -55,7 +55,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 		// Get my friends
 		const myFriends = await getFriends(me._id);
 
-		query.follower_id = {
+		query.followerId = {
 			$in: myFriends
 		};
 	}
@@ -82,7 +82,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Serialize
 	const users = await Promise.all(following.map(async f =>
-		await pack(f.follower_id, me, { detail: true })));
+		await pack(f.followerId, me, { detail: true })));
 
 	// Response
 	res({
diff --git a/src/api/endpoints/users/following.ts b/src/api/endpoints/users/following.ts
index 8e88431e9..3387dab36 100644
--- a/src/api/endpoints/users/following.ts
+++ b/src/api/endpoints/users/following.ts
@@ -15,9 +15,9 @@ import getFriends from '../../common/get-friends';
  * @return {Promise<any>}
  */
 module.exports = (params, me) => new Promise(async (res, rej) => {
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [userId, userIdErr] = $(params.userId).id().$;
+	if (userIdErr) return rej('invalid userId param');
 
 	// Get 'iknow' parameter
 	const [iknow = false, iknowErr] = $(params.iknow).optional.boolean().$;
@@ -46,8 +46,8 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Construct query
 	const query = {
-		follower_id: user._id,
-		deleted_at: { $exists: false }
+		followerId: user._id,
+		deletedAt: { $exists: false }
 	} as any;
 
 	// ログインしていてかつ iknow フラグがあるとき
@@ -55,7 +55,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 		// Get my friends
 		const myFriends = await getFriends(me._id);
 
-		query.followee_id = {
+		query.followeeId = {
 			$in: myFriends
 		};
 	}
@@ -82,7 +82,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Serialize
 	const users = await Promise.all(following.map(async f =>
-		await pack(f.followee_id, me, { detail: true })));
+		await pack(f.followeeId, me, { detail: true })));
 
 	// Response
 	res({
diff --git a/src/api/endpoints/users/get_frequently_replied_users.ts b/src/api/endpoints/users/get_frequently_replied_users.ts
index 87f4f77a5..991c5555b 100644
--- a/src/api/endpoints/users/get_frequently_replied_users.ts
+++ b/src/api/endpoints/users/get_frequently_replied_users.ts
@@ -6,9 +6,9 @@ import Post from '../../models/post';
 import User, { pack } from '../../models/user';
 
 module.exports = (params, me) => new Promise(async (res, rej) => {
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [userId, userIdErr] = $(params.userId).id().$;
+	if (userIdErr) return rej('invalid userId param');
 
 	// Get 'limit' parameter
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
@@ -29,8 +29,8 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Fetch recent posts
 	const recentPosts = await Post.find({
-		user_id: user._id,
-		reply_id: {
+		userId: user._id,
+		replyId: {
 			$exists: true,
 			$ne: null
 		}
@@ -41,7 +41,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 		limit: 1000,
 		fields: {
 			_id: false,
-			reply_id: true
+			replyId: true
 		}
 	});
 
@@ -52,15 +52,15 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	const replyTargetPosts = await Post.find({
 		_id: {
-			$in: recentPosts.map(p => p.reply_id)
+			$in: recentPosts.map(p => p.replyId)
 		},
-		user_id: {
+		userId: {
 			$ne: user._id
 		}
 	}, {
 		fields: {
 			_id: false,
-			user_id: true
+			userId: true
 		}
 	});
 
@@ -68,7 +68,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Extract replies from recent posts
 	replyTargetPosts.forEach(post => {
-		const userId = post.user_id.toString();
+		const userId = post.userId.toString();
 		if (repliedUsers[userId]) {
 			repliedUsers[userId]++;
 		} else {
diff --git a/src/api/endpoints/users/posts.ts b/src/api/endpoints/users/posts.ts
index 3c84bf0d8..f08be91c4 100644
--- a/src/api/endpoints/users/posts.ts
+++ b/src/api/endpoints/users/posts.ts
@@ -14,16 +14,16 @@ import User from '../../models/user';
  * @return {Promise<any>}
  */
 module.exports = (params, me) => new Promise(async (res, rej) => {
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).optional.id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [userId, userIdErr] = $(params.userId).optional.id().$;
+	if (userIdErr) return rej('invalid userId param');
 
 	// Get 'username' parameter
 	const [username, usernameErr] = $(params.username).optional.string().$;
 	if (usernameErr) return rej('invalid username param');
 
 	if (userId === undefined && username === undefined) {
-		return rej('user_id or pair of username and host is required');
+		return rej('userId or pair of username and host is required');
 	}
 
 	// Get 'host' parameter
@@ -31,7 +31,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	if (hostErr) return rej('invalid host param');
 
 	if (userId === undefined && host === undefined) {
-		return rej('user_id or pair of username and host is required');
+		return rej('userId or pair of username and host is required');
 	}
 
 	// Get 'include_replies' parameter
@@ -69,7 +69,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	const q = userId !== undefined
 		? { _id: userId }
-		: { username_lower: username.toLowerCase(), host_lower: getHostLower(host) } ;
+		: { usernameLower: username.toLowerCase(), hostLower: getHostLower(host) } ;
 
 	// Lookup user
 	const user = await User.findOne(q, {
@@ -88,7 +88,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	};
 
 	const query = {
-		user_id: user._id
+		userId: user._id
 	} as any;
 
 	if (sinceId) {
@@ -102,21 +102,21 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 		};
 	} else if (sinceDate) {
 		sort._id = 1;
-		query.created_at = {
+		query.createdAt = {
 			$gt: new Date(sinceDate)
 		};
 	} else if (untilDate) {
-		query.created_at = {
+		query.createdAt = {
 			$lt: new Date(untilDate)
 		};
 	}
 
 	if (!includeReplies) {
-		query.reply_id = null;
+		query.replyId = null;
 	}
 
 	if (withMedia) {
-		query.media_ids = {
+		query.mediaIds = {
 			$exists: true,
 			$ne: null
 		};
diff --git a/src/api/endpoints/users/recommendation.ts b/src/api/endpoints/users/recommendation.ts
index 45d90f422..c5297cdc5 100644
--- a/src/api/endpoints/users/recommendation.ts
+++ b/src/api/endpoints/users/recommendation.ts
@@ -32,7 +32,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 			},
 			$or: [
 				{
-					'account.last_used_at': {
+					'account.lastUsedAt': {
 						$gte: new Date(Date.now() - ms('7days'))
 					}
 				}, {
@@ -43,7 +43,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 			limit: limit,
 			skip: offset,
 			sort: {
-				followers_count: -1
+				followersCount: -1
 			}
 		});
 
diff --git a/src/api/endpoints/users/search.ts b/src/api/endpoints/users/search.ts
index 39e2ff989..1cc25e61b 100644
--- a/src/api/endpoints/users/search.ts
+++ b/src/api/endpoints/users/search.ts
@@ -41,7 +41,7 @@ async function byNative(res, rej, me, query, offset, max) {
 	const users = await User
 		.find({
 			$or: [{
-				username_lower: new RegExp(escapedQuery.replace('@', '').toLowerCase())
+				usernameLower: new RegExp(escapedQuery.replace('@', '').toLowerCase())
 			}, {
 				name: new RegExp(escapedQuery)
 			}]
diff --git a/src/api/endpoints/users/search_by_username.ts b/src/api/endpoints/users/search_by_username.ts
index 9c5e1905a..24e9c98e7 100644
--- a/src/api/endpoints/users/search_by_username.ts
+++ b/src/api/endpoints/users/search_by_username.ts
@@ -26,7 +26,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	const users = await User
 		.find({
-			username_lower: new RegExp(query.toLowerCase())
+			usernameLower: new RegExp(query.toLowerCase())
 		}, {
 			limit: limit,
 			skip: offset
diff --git a/src/api/endpoints/users/show.ts b/src/api/endpoints/users/show.ts
index 78df23f33..16411dddc 100644
--- a/src/api/endpoints/users/show.ts
+++ b/src/api/endpoints/users/show.ts
@@ -56,9 +56,9 @@ function webFingerAndVerify(query, verifier) {
 module.exports = (params, me) => new Promise(async (res, rej) => {
 	let user;
 
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).optional.id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [userId, userIdErr] = $(params.userId).optional.id().$;
+	if (userIdErr) return rej('invalid userId param');
 
 	// Get 'username' parameter
 	const [username, usernameErr] = $(params.username).optional.string().$;
@@ -69,25 +69,25 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	if (hostErr) return rej('invalid username param');
 
 	if (userId === undefined && typeof username !== 'string') {
-		return rej('user_id or pair of username and host is required');
+		return rej('userId or pair of username and host is required');
 	}
 
 	// Lookup user
 	if (typeof host === 'string') {
-		const username_lower = username.toLowerCase();
-		const host_lower_ascii = toASCII(host).toLowerCase();
-		const host_lower = toUnicode(host_lower_ascii);
+		const usernameLower = username.toLowerCase();
+		const hostLower_ascii = toASCII(host).toLowerCase();
+		const hostLower = toUnicode(hostLower_ascii);
 
-		user = await findUser({ username_lower, host_lower });
+		user = await findUser({ usernameLower, hostLower });
 
 		if (user === null) {
-			const acct_lower = `${username_lower}@${host_lower_ascii}`;
+			const acct_lower = `${usernameLower}@${hostLower_ascii}`;
 			let activityStreams;
 			let finger;
-			let followers_count;
-			let following_count;
+			let followersCount;
+			let followingCount;
 			let likes_count;
-			let posts_count;
+			let postsCount;
 
 			if (!validateUsername(username)) {
 				return rej('username validation failed');
@@ -122,7 +122,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 					activityStreams['@context'] === 'https://www.w3.org/ns/activitystreams') &&
 				activityStreams.type === 'Person' &&
 				typeof activityStreams.preferredUsername === 'string' &&
-				activityStreams.preferredUsername.toLowerCase() === username_lower &&
+				activityStreams.preferredUsername.toLowerCase() === usernameLower &&
 				isValidName(activityStreams.name) &&
 				isValidDescription(activityStreams.summary)
 			)) {
@@ -130,7 +130,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 			}
 
 			try {
-				[followers_count, following_count, likes_count, posts_count] = await Promise.all([
+				[followersCount, followingCount, likes_count, postsCount] = await Promise.all([
 					getCollectionCount(activityStreams.followers),
 					getCollectionCount(activityStreams.following),
 					getCollectionCount(activityStreams.liked),
@@ -145,21 +145,21 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 			// Create user
 			user = await User.insert({
-				avatar_id: null,
-				banner_id: null,
-				created_at: new Date(),
+				avatarId: null,
+				bannerId: null,
+				createdAt: new Date(),
 				description: summaryDOM.textContent,
-				followers_count,
-				following_count,
+				followersCount,
+				followingCount,
 				name: activityStreams.name,
-				posts_count,
+				postsCount,
 				likes_count,
 				liked_count: 0,
-				drive_capacity: 1073741824, // 1GB
+				driveCapacity: 1073741824, // 1GB
 				username: username,
-				username_lower,
+				usernameLower,
 				host: toUnicode(finger.subject.replace(/^.*?@/, '')),
-				host_lower,
+				hostLower,
 				account: {
 					uri: activityStreams.id,
 				},
@@ -182,18 +182,18 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 			User.update({ _id: user._id }, {
 				$set: {
-					avatar_id: icon._id,
-					banner_id: image._id,
+					avatarId: icon._id,
+					bannerId: image._id,
 				},
 			});
 
-			user.avatar_id = icon._id;
-			user.banner_id = icon._id;
+			user.avatarId = icon._id;
+			user.bannerId = icon._id;
 		}
 	} else {
 		const q = userId !== undefined
 			? { _id: userId }
-			: { username_lower: username.toLowerCase(), host: null };
+			: { usernameLower: username.toLowerCase(), host: null };
 
 		user = await findUser(q);
 
diff --git a/src/api/models/channel-watching.ts b/src/api/models/channel-watching.ts
index 23886d0c7..ec0c8135d 100644
--- a/src/api/models/channel-watching.ts
+++ b/src/api/models/channel-watching.ts
@@ -9,6 +9,6 @@ export interface IChannelWatching {
 	_id: mongo.ObjectID;
 	createdAt: Date;
 	deletedAt: Date;
-	channel_id: mongo.ObjectID;
+	channelId: mongo.ObjectID;
 	userId: mongo.ObjectID;
 }
diff --git a/src/api/models/channel.ts b/src/api/models/channel.ts
index a753a4eba..aab21db07 100644
--- a/src/api/models/channel.ts
+++ b/src/api/models/channel.ts
@@ -63,7 +63,7 @@ export const pack = (
 		//#region Watchしているかどうか
 		const watch = await Watching.findOne({
 			userId: meId,
-			channel_id: _channel.id,
+			channelId: _channel.id,
 			deletedAt: { $exists: false }
 		});
 
diff --git a/src/api/models/messaging-message.ts b/src/api/models/messaging-message.ts
index 026b23cf3..9a2f81387 100644
--- a/src/api/models/messaging-message.ts
+++ b/src/api/models/messaging-message.ts
@@ -66,16 +66,16 @@ export const pack = (
 	}
 
 	// Populate user
-	_message.user = await packUser(_message.user_id, me);
+	_message.user = await packUser(_message.userId, me);
 
-	if (_message.file_id) {
+	if (_message.fileId) {
 		// Populate file
-		_message.file = await packFile(_message.file_id);
+		_message.file = await packFile(_message.fileId);
 	}
 
 	if (opts.populateRecipient) {
 		// Populate recipient
-		_message.recipient = await packUser(_message.recipient_id, me);
+		_message.recipient = await packUser(_message.recipientId, me);
 	}
 
 	resolve(_message);
diff --git a/src/api/models/notification.ts b/src/api/models/notification.ts
index fa7049d31..910f53947 100644
--- a/src/api/models/notification.ts
+++ b/src/api/models/notification.ts
@@ -9,7 +9,7 @@ export default Notification;
 
 export interface INotification {
 	_id: mongo.ObjectID;
-	created_at: Date;
+	createdAt: Date;
 
 	/**
 	 * 通知の受信者
@@ -19,7 +19,7 @@ export interface INotification {
 	/**
 	 * 通知の受信者
 	 */
-	notifiee_id: mongo.ObjectID;
+	notifieeId: mongo.ObjectID;
 
 	/**
 	 * イニシエータ(initiator)、Origin。通知を行う原因となったユーザー
@@ -29,7 +29,7 @@ export interface INotification {
 	/**
 	 * イニシエータ(initiator)、Origin。通知を行う原因となったユーザー
 	 */
-	notifier_id: mongo.ObjectID;
+	notifierId: mongo.ObjectID;
 
 	/**
 	 * 通知の種類。
@@ -46,7 +46,7 @@ export interface INotification {
 	/**
 	 * 通知が読まれたかどうか
 	 */
-	is_read: Boolean;
+	isRead: Boolean;
 }
 
 /**
@@ -75,15 +75,15 @@ export const pack = (notification: any) => new Promise<any>(async (resolve, reje
 	_notification.id = _notification._id;
 	delete _notification._id;
 
-	// Rename notifier_id to user_id
-	_notification.user_id = _notification.notifier_id;
-	delete _notification.notifier_id;
+	// Rename notifierId to userId
+	_notification.userId = _notification.notifierId;
+	delete _notification.notifierId;
 
-	const me = _notification.notifiee_id;
-	delete _notification.notifiee_id;
+	const me = _notification.notifieeId;
+	delete _notification.notifieeId;
 
 	// Populate notifier
-	_notification.user = await packUser(_notification.user_id, me);
+	_notification.user = await packUser(_notification.userId, me);
 
 	switch (_notification.type) {
 		case 'follow':
@@ -96,7 +96,7 @@ export const pack = (notification: any) => new Promise<any>(async (resolve, reje
 		case 'reaction':
 		case 'poll_vote':
 			// Populate post
-			_notification.post = await packPost(_notification.post_id, me);
+			_notification.post = await packPost(_notification.postId, me);
 			break;
 		default:
 			console.error(`Unknown type: ${_notification.type}`);
diff --git a/src/api/models/othello-game.ts b/src/api/models/othello-game.ts
index b9e57632d..70e0c696c 100644
--- a/src/api/models/othello-game.ts
+++ b/src/api/models/othello-game.ts
@@ -97,10 +97,10 @@ export const pack = (
 	}
 
 	// Populate user
-	_game.user1 = await packUser(_game.user1_id, meId);
-	_game.user2 = await packUser(_game.user2_id, meId);
-	if (_game.winner_id) {
-		_game.winner = await packUser(_game.winner_id, meId);
+	_game.user1 = await packUser(_game.user1Id, meId);
+	_game.user2 = await packUser(_game.user2Id, meId);
+	if (_game.winnerId) {
+		_game.winner = await packUser(_game.winnerId, meId);
 	} else {
 		_game.winner = null;
 	}
diff --git a/src/api/models/othello-matching.ts b/src/api/models/othello-matching.ts
index 9c84d7fb9..8beedf72c 100644
--- a/src/api/models/othello-matching.ts
+++ b/src/api/models/othello-matching.ts
@@ -37,8 +37,8 @@ export const pack = (
 	delete _matching._id;
 
 	// Populate user
-	_matching.parent = await packUser(_matching.parent_id, meId);
-	_matching.child = await packUser(_matching.child_id, meId);
+	_matching.parent = await packUser(_matching.parentId, meId);
+	_matching.child = await packUser(_matching.childId, meId);
 
 	resolve(_matching);
 });
diff --git a/src/api/models/post-reaction.ts b/src/api/models/post-reaction.ts
index f581f0153..82613eb26 100644
--- a/src/api/models/post-reaction.ts
+++ b/src/api/models/post-reaction.ts
@@ -47,7 +47,7 @@ export const pack = (
 	delete _reaction._id;
 
 	// Populate user
-	_reaction.user = await packUser(_reaction.user_id, me);
+	_reaction.user = await packUser(_reaction.userId, me);
 
 	resolve(_reaction);
 });
diff --git a/src/api/models/post.ts b/src/api/models/post.ts
index fc4425651..4ab840b5e 100644
--- a/src/api/models/post.ts
+++ b/src/api/models/post.ts
@@ -100,21 +100,21 @@ export const pack = async (
 	}
 
 	// Populate user
-	_post.user = packUser(_post.user_id, meId);
+	_post.user = packUser(_post.userId, meId);
 
 	// Populate app
-	if (_post.app_id) {
-		_post.app = packApp(_post.app_id);
+	if (_post.appId) {
+		_post.app = packApp(_post.appId);
 	}
 
 	// Populate channel
-	if (_post.channel_id) {
-		_post.channel = packChannel(_post.channel_id);
+	if (_post.channelId) {
+		_post.channel = packChannel(_post.channelId);
 	}
 
 	// Populate media
-	if (_post.media_ids) {
-		_post.media = Promise.all(_post.media_ids.map(fileId =>
+	if (_post.mediaIds) {
+		_post.media = Promise.all(_post.mediaIds.map(fileId =>
 			packFile(fileId)
 		));
 	}
@@ -124,7 +124,7 @@ export const pack = async (
 		// Get previous post info
 		_post.prev = (async () => {
 			const prev = await Post.findOne({
-				user_id: _post.user_id,
+				userId: _post.userId,
 				_id: {
 					$lt: id
 				}
@@ -142,7 +142,7 @@ export const pack = async (
 		// Get next post info
 		_post.next = (async () => {
 			const next = await Post.findOne({
-				user_id: _post.user_id,
+				userId: _post.userId,
 				_id: {
 					$gt: id
 				}
@@ -157,16 +157,16 @@ export const pack = async (
 			return next ? next._id : null;
 		})();
 
-		if (_post.reply_id) {
+		if (_post.replyId) {
 			// Populate reply to post
-			_post.reply = pack(_post.reply_id, meId, {
+			_post.reply = pack(_post.replyId, meId, {
 				detail: false
 			});
 		}
 
-		if (_post.repost_id) {
+		if (_post.repostId) {
 			// Populate repost
-			_post.repost = pack(_post.repost_id, meId, {
+			_post.repost = pack(_post.repostId, meId, {
 				detail: _post.text == null
 			});
 		}
@@ -176,8 +176,8 @@ export const pack = async (
 			_post.poll = (async (poll) => {
 				const vote = await Vote
 					.findOne({
-						user_id: meId,
-						post_id: id
+						userId: meId,
+						postId: id
 					});
 
 				if (vote != null) {
@@ -196,9 +196,9 @@ export const pack = async (
 			_post.my_reaction = (async () => {
 				const reaction = await Reaction
 					.findOne({
-						user_id: meId,
-						post_id: id,
-						deleted_at: { $exists: false }
+						userId: meId,
+						postId: id,
+						deletedAt: { $exists: false }
 					});
 
 				if (reaction) {
diff --git a/src/api/models/user.ts b/src/api/models/user.ts
index 9aa7c4efa..8c68b06df 100644
--- a/src/api/models/user.ts
+++ b/src/api/models/user.ts
@@ -47,24 +47,25 @@ export type ILocalAccount = {
 	token: string;
 	twitter: {
 		accessToken: string;
-		access_token_secret: string;
-		user_id: string;
-		screen_name: string;
+		accessTokenSecret: string;
+		userId: string;
+		screenName: string;
 	};
 	line: {
-		user_id: string;
+		userId: string;
 	};
 	profile: {
 		location: string;
 		birthday: string; // 'YYYY-MM-DD'
 		tags: string[];
 	};
-	last_used_at: Date;
-	is_bot: boolean;
-	is_pro: boolean;
-	two_factor_secret: string;
-	two_factor_enabled: boolean;
-	client_settings: any;
+	lastUsedAt: Date;
+	isBot: boolean;
+	isPro: boolean;
+	twoFactorSecret: string;
+	twoFactorEnabled: boolean;
+	twoFactorTempSecret: string;
+	clientSettings: any;
 	settings: any;
 };
 
@@ -74,33 +75,33 @@ export type IRemoteAccount = {
 
 export type IUser = {
 	_id: mongo.ObjectID;
-	created_at: Date;
-	deleted_at: Date;
-	followers_count: number;
-	following_count: number;
+	createdAt: Date;
+	deletedAt: Date;
+	followersCount: number;
+	followingCount: number;
 	name: string;
-	posts_count: number;
-	drive_capacity: number;
+	postsCount: number;
+	driveCapacity: number;
 	username: string;
-	username_lower: string;
-	avatar_id: mongo.ObjectID;
-	banner_id: mongo.ObjectID;
+	usernameLower: string;
+	avatarId: mongo.ObjectID;
+	bannerId: mongo.ObjectID;
 	data: any;
 	description: string;
 	latest_post: IPost;
-	pinned_post_id: mongo.ObjectID;
-	is_suspended: boolean;
+	pinnedPostId: mongo.ObjectID;
+	isSuspended: boolean;
 	keywords: string[];
 	host: string;
-	host_lower: string;
+	hostLower: string;
 	account: ILocalAccount | IRemoteAccount;
 };
 
 export function init(user): IUser {
 	user._id = new mongo.ObjectID(user._id);
-	user.avatar_id = new mongo.ObjectID(user.avatar_id);
-	user.banner_id = new mongo.ObjectID(user.banner_id);
-	user.pinned_post_id = new mongo.ObjectID(user.pinned_post_id);
+	user.avatarId = new mongo.ObjectID(user.avatarId);
+	user.bannerId = new mongo.ObjectID(user.bannerId);
+	user.pinnedPostId = new mongo.ObjectID(user.pinnedPostId);
 	return user;
 }
 
@@ -131,7 +132,7 @@ export const pack = (
 	const fields = opts.detail ? {
 	} : {
 		'account.settings': false,
-		'account.client_settings': false,
+		'account.clientSettings': false,
 		'account.profile': false,
 		'account.keywords': false,
 		'account.domains': false
@@ -173,12 +174,12 @@ export const pack = (
 		delete _user.account.keypair;
 		delete _user.account.password;
 		delete _user.account.token;
-		delete _user.account.two_factor_temp_secret;
-		delete _user.account.two_factor_secret;
-		delete _user.username_lower;
+		delete _user.account.twoFactorTempSecret;
+		delete _user.account.twoFactorSecret;
+		delete _user.usernameLower;
 		if (_user.account.twitter) {
-			delete _user.account.twitter.access_token;
-			delete _user.account.twitter.access_token_secret;
+			delete _user.account.twitter.accessToken;
+			delete _user.account.twitter.accessTokenSecret;
 		}
 		delete _user.account.line;
 
@@ -186,36 +187,36 @@ export const pack = (
 		if (!opts.includeSecrets) {
 			delete _user.account.email;
 			delete _user.account.settings;
-			delete _user.account.client_settings;
+			delete _user.account.clientSettings;
 		}
 
 		if (!opts.detail) {
-			delete _user.account.two_factor_enabled;
+			delete _user.account.twoFactorEnabled;
 		}
 	}
 
-	_user.avatar_url = _user.avatar_id != null
-		? `${config.drive_url}/${_user.avatar_id}`
+	_user.avatar_url = _user.avatarId != null
+		? `${config.drive_url}/${_user.avatarId}`
 		: `${config.drive_url}/default-avatar.jpg`;
 
-	_user.banner_url = _user.banner_id != null
-		? `${config.drive_url}/${_user.banner_id}`
+	_user.banner_url = _user.bannerId != null
+		? `${config.drive_url}/${_user.bannerId}`
 		: null;
 
 	if (!meId || !meId.equals(_user.id) || !opts.detail) {
-		delete _user.avatar_id;
-		delete _user.banner_id;
+		delete _user.avatarId;
+		delete _user.bannerId;
 
-		delete _user.drive_capacity;
+		delete _user.driveCapacity;
 	}
 
 	if (meId && !meId.equals(_user.id)) {
 		// Whether the user is following
 		_user.is_following = (async () => {
 			const follow = await Following.findOne({
-				follower_id: meId,
-				followee_id: _user.id,
-				deleted_at: { $exists: false }
+				followerId: meId,
+				followeeId: _user.id,
+				deletedAt: { $exists: false }
 			});
 			return follow !== null;
 		})();
@@ -223,9 +224,9 @@ export const pack = (
 		// Whether the user is followed
 		_user.is_followed = (async () => {
 			const follow2 = await Following.findOne({
-				follower_id: _user.id,
-				followee_id: meId,
-				deleted_at: { $exists: false }
+				followerId: _user.id,
+				followeeId: meId,
+				deletedAt: { $exists: false }
 			});
 			return follow2 !== null;
 		})();
@@ -233,18 +234,18 @@ export const pack = (
 		// Whether the user is muted
 		_user.is_muted = (async () => {
 			const mute = await Mute.findOne({
-				muter_id: meId,
-				mutee_id: _user.id,
-				deleted_at: { $exists: false }
+				muterId: meId,
+				muteeId: _user.id,
+				deletedAt: { $exists: false }
 			});
 			return mute !== null;
 		})();
 	}
 
 	if (opts.detail) {
-		if (_user.pinned_post_id) {
+		if (_user.pinnedPostId) {
 			// Populate pinned post
-			_user.pinned_post = packPost(_user.pinned_post_id, meId, {
+			_user.pinnedPost = packPost(_user.pinnedPostId, meId, {
 				detail: true
 			});
 		}
@@ -254,16 +255,16 @@ export const pack = (
 
 			// Get following you know count
 			_user.following_you_know_count = Following.count({
-				followee_id: { $in: myFollowingIds },
-				follower_id: _user.id,
-				deleted_at: { $exists: false }
+				followeeId: { $in: myFollowingIds },
+				followerId: _user.id,
+				deletedAt: { $exists: false }
 			});
 
 			// Get followers you know count
 			_user.followers_you_know_count = Following.count({
-				followee_id: _user.id,
-				follower_id: { $in: myFollowingIds },
-				deleted_at: { $exists: false }
+				followeeId: _user.id,
+				followerId: { $in: myFollowingIds },
+				deletedAt: { $exists: false }
 			});
 		}
 	}
@@ -322,7 +323,7 @@ export const packForAp = (
 		"name": _user.name,
 		"summary": _user.description,
 		"icon": [
-			`${config.drive_url}/${_user.avatar_id}`
+			`${config.drive_url}/${_user.avatarId}`
 		]
 	});
 });
diff --git a/src/api/private/signin.ts b/src/api/private/signin.ts
index 00dcb8afc..c6b5d19ea 100644
--- a/src/api/private/signin.ts
+++ b/src/api/private/signin.ts
@@ -32,7 +32,7 @@ export default async (req: express.Request, res: express.Response) => {
 
 	// Fetch user
 	const user: IUser = await User.findOne({
-		username_lower: username.toLowerCase(),
+		usernameLower: username.toLowerCase(),
 		host: null
 	}, {
 		fields: {
@@ -54,9 +54,9 @@ export default async (req: express.Request, res: express.Response) => {
 	const same = await bcrypt.compare(password, account.password);
 
 	if (same) {
-		if (account.two_factor_enabled) {
+		if (account.twoFactorEnabled) {
 			const verified = (speakeasy as any).totp.verify({
-				secret: account.two_factor_secret,
+				secret: account.twoFactorSecret,
 				encoding: 'base32',
 				token: token
 			});
@@ -79,8 +79,8 @@ export default async (req: express.Request, res: express.Response) => {
 
 	// Append signin history
 	const record = await Signin.insert({
-		created_at: new Date(),
-		user_id: user._id,
+		createdAt: new Date(),
+		userId: user._id,
 		ip: req.ip,
 		headers: req.headers,
 		success: same
diff --git a/src/api/private/signup.ts b/src/api/private/signup.ts
index 29d75b62f..1304ccb54 100644
--- a/src/api/private/signup.ts
+++ b/src/api/private/signup.ts
@@ -64,7 +64,7 @@ export default async (req: express.Request, res: express.Response) => {
 	// Fetch exist user that same username
 	const usernameExist = await User
 		.count({
-			username_lower: username.toLowerCase(),
+			usernameLower: username.toLowerCase(),
 			host: null
 		}, {
 			limit: 1
@@ -107,19 +107,19 @@ export default async (req: express.Request, res: express.Response) => {
 
 	// Create account
 	const account: IUser = await User.insert({
-		avatar_id: null,
-		banner_id: null,
-		created_at: new Date(),
+		avatarId: null,
+		bannerId: null,
+		createdAt: new Date(),
 		description: null,
-		followers_count: 0,
-		following_count: 0,
+		followersCount: 0,
+		followingCount: 0,
 		name: name,
-		posts_count: 0,
-		drive_capacity: 1073741824, // 1GB
+		postsCount: 0,
+		driveCapacity: 1073741824, // 1GB
 		username: username,
-		username_lower: username.toLowerCase(),
+		usernameLower: username.toLowerCase(),
 		host: null,
-		host_lower: null,
+		hostLower: null,
 		account: {
 			keypair: generateKeypair(),
 			token: secret,
@@ -139,7 +139,7 @@ export default async (req: express.Request, res: express.Response) => {
 			settings: {
 				auto_watch: true
 			},
-			client_settings: {
+			clientSettings: {
 				home: homeData
 			}
 		}
diff --git a/src/api/service/github.ts b/src/api/service/github.ts
index 1c78267c0..598f36b0c 100644
--- a/src/api/service/github.ts
+++ b/src/api/service/github.ts
@@ -9,7 +9,7 @@ module.exports = async (app: express.Application) => {
 	if (config.github_bot == null) return;
 
 	const bot = await User.findOne({
-		username_lower: config.github_bot.username.toLowerCase()
+		usernameLower: config.github_bot.username.toLowerCase()
 	});
 
 	if (bot == null) {
diff --git a/src/api/service/twitter.ts b/src/api/service/twitter.ts
index c1f2e48a6..67c401efa 100644
--- a/src/api/service/twitter.ts
+++ b/src/api/service/twitter.ts
@@ -128,7 +128,7 @@ module.exports = (app: express.Application) => {
 
 				const user = await User.findOne({
 					host: null,
-					'account.twitter.user_id': result.userId
+					'account.twitter.userId': result.userId
 				});
 
 				if (user == null) {
@@ -155,10 +155,10 @@ module.exports = (app: express.Application) => {
 				}, {
 					$set: {
 						'account.twitter': {
-							access_token: result.accessToken,
-							access_token_secret: result.accessTokenSecret,
-							user_id: result.userId,
-							screen_name: result.screenName
+							accessToken: result.accessToken,
+							accessTokenSecret: result.accessTokenSecret,
+							userId: result.userId,
+							screenName: result.screenName
 						}
 					}
 				});
diff --git a/src/api/stream/home.ts b/src/api/stream/home.ts
index 1ef0f33b4..291be0824 100644
--- a/src/api/stream/home.ts
+++ b/src/api/stream/home.ts
@@ -14,10 +14,10 @@ export default async function(request: websocket.request, connection: websocket.
 	subscriber.subscribe(`misskey:user-stream:${user._id}`);
 
 	const mute = await Mute.find({
-		muter_id: user._id,
-		deleted_at: { $exists: false }
+		muterId: user._id,
+		deletedAt: { $exists: false }
 	});
-	const mutedUserIds = mute.map(m => m.mutee_id.toString());
+	const mutedUserIds = mute.map(m => m.muteeId.toString());
 
 	subscriber.on('message', async (channel, data) => {
 		switch (channel.split(':')[1]) {
@@ -26,17 +26,17 @@ export default async function(request: websocket.request, connection: websocket.
 					const x = JSON.parse(data);
 
 					if (x.type == 'post') {
-						if (mutedUserIds.indexOf(x.body.user_id) != -1) {
+						if (mutedUserIds.indexOf(x.body.userId) != -1) {
 							return;
 						}
-						if (x.body.reply != null && mutedUserIds.indexOf(x.body.reply.user_id) != -1) {
+						if (x.body.reply != null && mutedUserIds.indexOf(x.body.reply.userId) != -1) {
 							return;
 						}
-						if (x.body.repost != null && mutedUserIds.indexOf(x.body.repost.user_id) != -1) {
+						if (x.body.repost != null && mutedUserIds.indexOf(x.body.repost.userId) != -1) {
 							return;
 						}
 					} else if (x.type == 'notification') {
-						if (mutedUserIds.indexOf(x.body.user_id) != -1) {
+						if (mutedUserIds.indexOf(x.body.userId) != -1) {
 							return;
 						}
 					}
@@ -74,7 +74,7 @@ export default async function(request: websocket.request, connection: websocket.
 				// Update lastUsedAt
 				User.update({ _id: user._id }, {
 					$set: {
-						'account.last_used_at': new Date()
+						'account.lastUsedAt': new Date()
 					}
 				});
 				break;
diff --git a/src/api/stream/othello-game.ts b/src/api/stream/othello-game.ts
index 45a931c7e..e48d93cdd 100644
--- a/src/api/stream/othello-game.ts
+++ b/src/api/stream/othello-game.ts
@@ -62,10 +62,10 @@ export default function(request: websocket.request, connection: websocket.connec
 	async function updateSettings(settings) {
 		const game = await OthelloGame.findOne({ _id: gameId });
 
-		if (game.is_started) return;
-		if (!game.user1_id.equals(user._id) && !game.user2_id.equals(user._id)) return;
-		if (game.user1_id.equals(user._id) && game.user1_accepted) return;
-		if (game.user2_id.equals(user._id) && game.user2_accepted) return;
+		if (game.isStarted) return;
+		if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return;
+		if (game.user1Id.equals(user._id) && game.user1Accepted) return;
+		if (game.user2Id.equals(user._id) && game.user2Accepted) return;
 
 		await OthelloGame.update({ _id: gameId }, {
 			$set: {
@@ -79,10 +79,10 @@ export default function(request: websocket.request, connection: websocket.connec
 	async function initForm(form) {
 		const game = await OthelloGame.findOne({ _id: gameId });
 
-		if (game.is_started) return;
-		if (!game.user1_id.equals(user._id) && !game.user2_id.equals(user._id)) return;
+		if (game.isStarted) return;
+		if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return;
 
-		const set = game.user1_id.equals(user._id) ? {
+		const set = game.user1Id.equals(user._id) ? {
 			form1: form
 		} : {
 			form2: form
@@ -93,7 +93,7 @@ export default function(request: websocket.request, connection: websocket.connec
 		});
 
 		publishOthelloGameStream(gameId, 'init-form', {
-			user_id: user._id,
+			userId: user._id,
 			form
 		});
 	}
@@ -101,10 +101,10 @@ export default function(request: websocket.request, connection: websocket.connec
 	async function updateForm(id, value) {
 		const game = await OthelloGame.findOne({ _id: gameId });
 
-		if (game.is_started) return;
-		if (!game.user1_id.equals(user._id) && !game.user2_id.equals(user._id)) return;
+		if (game.isStarted) return;
+		if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return;
 
-		const form = game.user1_id.equals(user._id) ? game.form2 : game.form1;
+		const form = game.user1Id.equals(user._id) ? game.form2 : game.form1;
 
 		const item = form.find(i => i.id == id);
 
@@ -112,7 +112,7 @@ export default function(request: websocket.request, connection: websocket.connec
 
 		item.value = value;
 
-		const set = game.user1_id.equals(user._id) ? {
+		const set = game.user1Id.equals(user._id) ? {
 			form2: form
 		} : {
 			form1: form
@@ -123,7 +123,7 @@ export default function(request: websocket.request, connection: websocket.connec
 		});
 
 		publishOthelloGameStream(gameId, 'update-form', {
-			user_id: user._id,
+			userId: user._id,
 			id,
 			value
 		});
@@ -132,7 +132,7 @@ export default function(request: websocket.request, connection: websocket.connec
 	async function message(message) {
 		message.id = Math.random();
 		publishOthelloGameStream(gameId, 'message', {
-			user_id: user._id,
+			userId: user._id,
 			message
 		});
 	}
@@ -140,36 +140,36 @@ export default function(request: websocket.request, connection: websocket.connec
 	async function accept(accept: boolean) {
 		const game = await OthelloGame.findOne({ _id: gameId });
 
-		if (game.is_started) return;
+		if (game.isStarted) return;
 
 		let bothAccepted = false;
 
-		if (game.user1_id.equals(user._id)) {
+		if (game.user1Id.equals(user._id)) {
 			await OthelloGame.update({ _id: gameId }, {
 				$set: {
-					user1_accepted: accept
+					user1Accepted: accept
 				}
 			});
 
 			publishOthelloGameStream(gameId, 'change-accepts', {
 				user1: accept,
-				user2: game.user2_accepted
+				user2: game.user2Accepted
 			});
 
-			if (accept && game.user2_accepted) bothAccepted = true;
-		} else if (game.user2_id.equals(user._id)) {
+			if (accept && game.user2Accepted) bothAccepted = true;
+		} else if (game.user2Id.equals(user._id)) {
 			await OthelloGame.update({ _id: gameId }, {
 				$set: {
-					user2_accepted: accept
+					user2Accepted: accept
 				}
 			});
 
 			publishOthelloGameStream(gameId, 'change-accepts', {
-				user1: game.user1_accepted,
+				user1: game.user1Accepted,
 				user2: accept
 			});
 
-			if (accept && game.user1_accepted) bothAccepted = true;
+			if (accept && game.user1Accepted) bothAccepted = true;
 		} else {
 			return;
 		}
@@ -178,8 +178,8 @@ export default function(request: websocket.request, connection: websocket.connec
 			// 3秒後、まだacceptされていたらゲーム開始
 			setTimeout(async () => {
 				const freshGame = await OthelloGame.findOne({ _id: gameId });
-				if (freshGame == null || freshGame.is_started || freshGame.is_ended) return;
-				if (!freshGame.user1_accepted || !freshGame.user2_accepted) return;
+				if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return;
+				if (!freshGame.user1Accepted || !freshGame.user2Accepted) return;
 
 				let bw: number;
 				if (freshGame.settings.bw == 'random') {
@@ -198,8 +198,8 @@ export default function(request: websocket.request, connection: websocket.connec
 
 				await OthelloGame.update({ _id: gameId }, {
 					$set: {
-						started_at: new Date(),
-						is_started: true,
+						startedAt: new Date(),
+						isStarted: true,
 						black: bw,
 						'settings.map': map
 					}
@@ -207,17 +207,17 @@ export default function(request: websocket.request, connection: websocket.connec
 
 				//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
 				const o = new Othello(map, {
-					isLlotheo: freshGame.settings.is_llotheo,
-					canPutEverywhere: freshGame.settings.can_put_everywhere,
-					loopedBoard: freshGame.settings.looped_board
+					isLlotheo: freshGame.settings.isLlotheo,
+					canPutEverywhere: freshGame.settings.canPutEverywhere,
+					loopedBoard: freshGame.settings.loopedBoard
 				});
 
 				if (o.isEnded) {
 					let winner;
 					if (o.winner === true) {
-						winner = freshGame.black == 1 ? freshGame.user1_id : freshGame.user2_id;
+						winner = freshGame.black == 1 ? freshGame.user1Id : freshGame.user2Id;
 					} else if (o.winner === false) {
-						winner = freshGame.black == 1 ? freshGame.user2_id : freshGame.user1_id;
+						winner = freshGame.black == 1 ? freshGame.user2Id : freshGame.user1Id;
 					} else {
 						winner = null;
 					}
@@ -226,13 +226,13 @@ export default function(request: websocket.request, connection: websocket.connec
 						_id: gameId
 					}, {
 						$set: {
-							is_ended: true,
-							winner_id: winner
+							isEnded: true,
+							winnerId: winner
 						}
 					});
 
 					publishOthelloGameStream(gameId, 'ended', {
-						winner_id: winner,
+						winnerId: winner,
 						game: await pack(gameId, user)
 					});
 				}
@@ -247,14 +247,14 @@ export default function(request: websocket.request, connection: websocket.connec
 	async function set(pos) {
 		const game = await OthelloGame.findOne({ _id: gameId });
 
-		if (!game.is_started) return;
-		if (game.is_ended) return;
-		if (!game.user1_id.equals(user._id) && !game.user2_id.equals(user._id)) return;
+		if (!game.isStarted) return;
+		if (game.isEnded) return;
+		if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return;
 
 		const o = new Othello(game.settings.map, {
-			isLlotheo: game.settings.is_llotheo,
-			canPutEverywhere: game.settings.can_put_everywhere,
-			loopedBoard: game.settings.looped_board
+			isLlotheo: game.settings.isLlotheo,
+			canPutEverywhere: game.settings.canPutEverywhere,
+			loopedBoard: game.settings.loopedBoard
 		});
 
 		game.logs.forEach(log => {
@@ -262,7 +262,7 @@ export default function(request: websocket.request, connection: websocket.connec
 		});
 
 		const myColor =
-			(game.user1_id.equals(user._id) && game.black == 1) || (game.user2_id.equals(user._id) && game.black == 2)
+			(game.user1Id.equals(user._id) && game.black == 1) || (game.user2Id.equals(user._id) && game.black == 2)
 				? true
 				: false;
 
@@ -272,9 +272,9 @@ export default function(request: websocket.request, connection: websocket.connec
 		let winner;
 		if (o.isEnded) {
 			if (o.winner === true) {
-				winner = game.black == 1 ? game.user1_id : game.user2_id;
+				winner = game.black == 1 ? game.user1Id : game.user2Id;
 			} else if (o.winner === false) {
-				winner = game.black == 1 ? game.user2_id : game.user1_id;
+				winner = game.black == 1 ? game.user2Id : game.user1Id;
 			} else {
 				winner = null;
 			}
@@ -293,8 +293,8 @@ export default function(request: websocket.request, connection: websocket.connec
 		}, {
 			$set: {
 				crc32,
-				is_ended: o.isEnded,
-				winner_id: winner
+				isEnded: o.isEnded,
+				winnerId: winner
 			},
 			$push: {
 				logs: log
@@ -307,7 +307,7 @@ export default function(request: websocket.request, connection: websocket.connec
 
 		if (o.isEnded) {
 			publishOthelloGameStream(gameId, 'ended', {
-				winner_id: winner,
+				winnerId: winner,
 				game: await pack(gameId, user)
 			});
 		}
@@ -316,7 +316,7 @@ export default function(request: websocket.request, connection: websocket.connec
 	async function check(crc32) {
 		const game = await OthelloGame.findOne({ _id: gameId });
 
-		if (!game.is_started) return;
+		if (!game.isStarted) return;
 
 		// 互換性のため
 		if (game.crc32 == null) return;
diff --git a/src/api/stream/othello.ts b/src/api/stream/othello.ts
index bd3b4a763..55c993ec8 100644
--- a/src/api/stream/othello.ts
+++ b/src/api/stream/othello.ts
@@ -18,11 +18,11 @@ export default function(request: websocket.request, connection: websocket.connec
 			case 'ping':
 				if (msg.id == null) return;
 				const matching = await Matching.findOne({
-					parent_id: user._id,
-					child_id: new mongo.ObjectID(msg.id)
+					parentId: user._id,
+					childId: new mongo.ObjectID(msg.id)
 				});
 				if (matching == null) return;
-				publishUserStream(matching.child_id, 'othello_invited', await pack(matching, matching.child_id));
+				publishUserStream(matching.childId, 'othello_invited', await pack(matching, matching.childId));
 				break;
 		}
 	});
diff --git a/src/api/streaming.ts b/src/api/streaming.ts
index a6759e414..31319fcab 100644
--- a/src/api/streaming.ts
+++ b/src/api/streaming.ts
@@ -110,7 +110,7 @@ function authenticate(token: string): Promise<IUser> {
 
 			// Fetch user
 			const user: IUser = await User
-				.findOne({ _id: accessToken.user_id });
+				.findOne({ _id: accessToken.userId });
 
 			resolve(user);
 		}
diff --git a/src/common/get-post-summary.ts b/src/common/get-post-summary.ts
index 6e8f65708..8d0033064 100644
--- a/src/common/get-post-summary.ts
+++ b/src/common/get-post-summary.ts
@@ -22,7 +22,7 @@ const summarize = (post: any): string => {
 	}
 
 	// 返信のとき
-	if (post.reply_id) {
+	if (post.replyId) {
 		if (post.reply) {
 			summary += ` RE: ${summarize(post.reply)}`;
 		} else {
@@ -31,7 +31,7 @@ const summarize = (post: any): string => {
 	}
 
 	// Repostのとき
-	if (post.repost_id) {
+	if (post.repostId) {
 		if (post.repost) {
 			summary += ` RP: ${summarize(post.repost)}`;
 		} else {
diff --git a/src/common/othello/ai/back.ts b/src/common/othello/ai/back.ts
index 27dbc3952..52f01ff97 100644
--- a/src/common/othello/ai/back.ts
+++ b/src/common/othello/ai/back.ts
@@ -44,7 +44,7 @@ process.on('message', async msg => {
 		//#region TLに投稿する
 		const game = msg.body;
 		const url = `${conf.url}/othello/${game.id}`;
-		const user = game.user1_id == id ? game.user2 : game.user1;
+		const user = game.user1Id == id ? game.user2 : game.user1;
 		const isSettai = form[0].value === 0;
 		const text = isSettai
 			? `?[${user.name}](${conf.url}/@${user.username})さんの接待を始めました!`
@@ -68,23 +68,23 @@ process.on('message', async msg => {
 		});
 
 		//#region TLに投稿する
-		const user = game.user1_id == id ? game.user2 : game.user1;
+		const user = game.user1Id == id ? game.user2 : game.user1;
 		const isSettai = form[0].value === 0;
 		const text = isSettai
-			? msg.body.winner_id === null
+			? msg.body.winnerId === null
 				? `?[${user.name}](${conf.url}/@${user.username})さんに接待で引き分けました...`
-				: msg.body.winner_id == id
+				: msg.body.winnerId == id
 					? `?[${user.name}](${conf.url}/@${user.username})さんに接待で勝ってしまいました...`
 					: `?[${user.name}](${conf.url}/@${user.username})さんに接待で負けてあげました♪`
-			: msg.body.winner_id === null
+			: msg.body.winnerId === null
 				? `?[${user.name}](${conf.url}/@${user.username})さんと引き分けました~`
-				: msg.body.winner_id == id
+				: msg.body.winnerId == id
 					? `?[${user.name}](${conf.url}/@${user.username})さんに勝ちました♪`
 					: `?[${user.name}](${conf.url}/@${user.username})さんに負けました...`;
 
 		await request.post(`${conf.api_url}/posts/create`, {
 			json: { i,
-				repost_id: post.id,
+				repostId: post.id,
 				text: text
 			}
 		});
@@ -114,9 +114,9 @@ function onGameStarted(g) {
 
 	// オセロエンジン初期化
 	o = new Othello(game.settings.map, {
-		isLlotheo: game.settings.is_llotheo,
-		canPutEverywhere: game.settings.can_put_everywhere,
-		loopedBoard: game.settings.looped_board
+		isLlotheo: game.settings.isLlotheo,
+		canPutEverywhere: game.settings.canPutEverywhere,
+		loopedBoard: game.settings.loopedBoard
 	});
 
 	// 各マスの価値を計算しておく
@@ -141,7 +141,7 @@ function onGameStarted(g) {
 		return count >= 4 ? 1 : 0;
 	});
 
-	botColor = game.user1_id == id && game.black == 1 || game.user2_id == id && game.black == 2;
+	botColor = game.user1Id == id && game.black == 1 || game.user2Id == id && game.black == 2;
 
 	if (botColor) {
 		think();
@@ -188,7 +188,7 @@ function think() {
 		});
 
 		// ロセオならスコアを反転
-		if (game.settings.is_llotheo) score = -score;
+		if (game.settings.isLlotheo) score = -score;
 
 		// 接待ならスコアを反転
 		if (isSettai) score = -score;
@@ -234,7 +234,7 @@ function think() {
 
 			let score;
 
-			if (game.settings.is_llotheo) {
+			if (game.settings.isLlotheo) {
 				// 勝ちは勝ちでも、より自分の石を少なくした方が美しい勝ちだと判定する
 				score = o.winner ? base - (o.blackCount * 100) : base - (o.whiteCount * 100);
 			} else {
@@ -317,7 +317,7 @@ function think() {
 
 			let score;
 
-			if (game.settings.is_llotheo) {
+			if (game.settings.isLlotheo) {
 				// 勝ちは勝ちでも、より自分の石を少なくした方が美しい勝ちだと判定する
 				score = o.winner ? base - (o.blackCount * 100) : base - (o.whiteCount * 100);
 			} else {
diff --git a/src/common/othello/ai/front.ts b/src/common/othello/ai/front.ts
index d892afbed..e5496132f 100644
--- a/src/common/othello/ai/front.ts
+++ b/src/common/othello/ai/front.ts
@@ -48,12 +48,12 @@ homeStream.on('message', message => {
 	if (msg.type == 'mention' || msg.type == 'reply') {
 		const post = msg.body;
 
-		if (post.user_id == id) return;
+		if (post.userId == id) return;
 
 		// リアクションする
 		request.post(`${conf.api_url}/posts/reactions/create`, {
 			json: { i,
-				post_id: post.id,
+				postId: post.id,
 				reaction: 'love'
 			}
 		});
@@ -62,12 +62,12 @@ homeStream.on('message', message => {
 			if (post.text.indexOf('オセロ') > -1) {
 				request.post(`${conf.api_url}/posts/create`, {
 					json: { i,
-						reply_id: post.id,
+						replyId: post.id,
 						text: '良いですよ~'
 					}
 				});
 
-				invite(post.user_id);
+				invite(post.userId);
 			}
 		}
 	}
@@ -79,12 +79,12 @@ homeStream.on('message', message => {
 			if (message.text.indexOf('オセロ') > -1) {
 				request.post(`${conf.api_url}/messaging/messages/create`, {
 					json: { i,
-						user_id: message.user_id,
+						userId: message.userId,
 						text: '良いですよ~'
 					}
 				});
 
-				invite(message.user_id);
+				invite(message.userId);
 			}
 		}
 	}
@@ -94,7 +94,7 @@ homeStream.on('message', message => {
 function invite(userId) {
 	request.post(`${conf.api_url}/othello/match`, {
 		json: { i,
-			user_id: userId
+			userId: userId
 		}
 	});
 }
@@ -225,7 +225,7 @@ async function onInviteMe(inviter) {
 	const game = await request.post(`${conf.api_url}/othello/match`, {
 		json: {
 			i,
-			user_id: inviter.id
+			userId: inviter.id
 		}
 	});
 
diff --git a/src/common/user/get-summary.ts b/src/common/user/get-summary.ts
index f9b7125e3..b314a5cef 100644
--- a/src/common/user/get-summary.ts
+++ b/src/common/user/get-summary.ts
@@ -7,7 +7,7 @@ import getAcct from './get-acct';
  */
 export default function(user: IUser): string {
 	let string = `${user.name} (@${getAcct(user)})\n` +
-		`${user.posts_count}投稿、${user.following_count}フォロー、${user.followers_count}フォロワー\n`;
+		`${user.postsCount}投稿、${user.followingCount}フォロー、${user.followersCount}フォロワー\n`;
 
 	if (user.host === null) {
 		const account = user.account as ILocalAccount;
diff --git a/src/config.ts b/src/config.ts
index 6d3e7740b..0d8df39f4 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -80,7 +80,7 @@ type Source = {
 	};
 	line_bot?: {
 		channel_secret: string;
-		channel_access_token: string;
+		channel_accessToken: string;
 	};
 	analysis?: {
 		mecab_command?: string;
diff --git a/src/tools/analysis/extract-user-domains.ts b/src/tools/analysis/extract-user-domains.ts
index bc120f5c1..0c4249a1b 100644
--- a/src/tools/analysis/extract-user-domains.ts
+++ b/src/tools/analysis/extract-user-domains.ts
@@ -50,7 +50,7 @@ function extractDomainsOne(id) {
 
 		// Fetch recent posts
 		const recentPosts = await Post.find({
-			user_id: id,
+			userId: id,
 			text: {
 				$exists: true
 			}
diff --git a/src/tools/analysis/extract-user-keywords.ts b/src/tools/analysis/extract-user-keywords.ts
index b99ca9321..08d02e705 100644
--- a/src/tools/analysis/extract-user-keywords.ts
+++ b/src/tools/analysis/extract-user-keywords.ts
@@ -96,7 +96,7 @@ function extractKeywordsOne(id) {
 
 		// Fetch recent posts
 		const recentPosts = await Post.find({
-			user_id: id,
+			userId: id,
 			text: {
 				$exists: true
 			}
diff --git a/src/tools/analysis/predict-user-interst.ts b/src/tools/analysis/predict-user-interst.ts
index 99bdfa420..6904daeb0 100644
--- a/src/tools/analysis/predict-user-interst.ts
+++ b/src/tools/analysis/predict-user-interst.ts
@@ -6,7 +6,7 @@ export async function predictOne(id) {
 
 	// TODO: repostなども含める
 	const recentPosts = await Post.find({
-		user_id: id,
+		userId: id,
 		category: {
 			$exists: true
 		}
diff --git a/src/web/app/auth/views/form.vue b/src/web/app/auth/views/form.vue
index d86ed58b3..9d9e8cdb1 100644
--- a/src/web/app/auth/views/form.vue
+++ b/src/web/app/auth/views/form.vue
@@ -7,7 +7,7 @@
 	<div class="app">
 		<section>
 			<h2>{{ app.name }}</h2>
-			<p class="nid">{{ app.name_id }}</p>
+			<p class="nid">{{ app.nameId }}</p>
 			<p class="description">{{ app.description }}</p>
 		</section>
 		<section>
diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index dc4b8e142..225129088 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -77,7 +77,7 @@
 
 			// チャンネル概要読み込み
 			this.$root.$data.os.api('channels/show', {
-				channel_id: this.id
+				channelId: this.id
 			}).then(channel => {
 				if (fetched) {
 					Progress.done();
@@ -96,7 +96,7 @@
 
 			// 投稿読み込み
 			this.$root.$data.os.api('channels/posts', {
-				channel_id: this.id
+				channelId: this.id
 			}).then(posts => {
 				if (fetched) {
 					Progress.done();
@@ -125,7 +125,7 @@
 			this.posts.unshift(post);
 			this.update();
 
-			if (document.hidden && this.$root.$data.os.isSignedIn && post.user_id !== this.$root.$data.os.i.id) {
+			if (document.hidden && this.$root.$data.os.isSignedIn && post.userId !== this.$root.$data.os.i.id) {
 				this.unreadCount++;
 				document.title = `(${this.unreadCount}) ${this.channel.title} | Misskey`;
 			}
@@ -140,7 +140,7 @@
 
 		this.watch = () => {
 			this.$root.$data.os.api('channels/watch', {
-				channel_id: this.id
+				channelId: this.id
 			}).then(() => {
 				this.channel.is_watching = true;
 				this.update();
@@ -151,7 +151,7 @@
 
 		this.unwatch = () => {
 			this.$root.$data.os.api('channels/unwatch', {
-				channel_id: this.id
+				channelId: this.id
 			}).then(() => {
 				this.channel.is_watching = false;
 				this.update();
@@ -166,8 +166,8 @@
 	<header>
 		<a class="index" @click="reply">{ post.index }:</a>
 		<a class="name" href={ _URL_ + '/@' + acct }><b>{ post.user.name }</b></a>
-		<mk-time time={ post.created_at }/>
-		<mk-time time={ post.created_at } mode="detail"/>
+		<mk-time time={ post.createdAt }/>
+		<mk-time time={ post.createdAt } mode="detail"/>
 		<span>ID:<i>{ acct }</i></span>
 	</header>
 	<div>
@@ -328,9 +328,9 @@
 
 			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,
-				channel_id: this.channel.id
+				mediaIds: files,
+				replyId: this.reply ? this.reply.id : undefined,
+				channelId: this.channel.id
 			}).then(data => {
 				this.clear();
 			}).catch(err => {
diff --git a/src/web/app/common/define-widget.ts b/src/web/app/common/define-widget.ts
index d8d29873a..27db59b5e 100644
--- a/src/web/app/common/define-widget.ts
+++ b/src/web/app/common/define-widget.ts
@@ -56,14 +56,14 @@ export default function<T extends object>(data: {
 						id: this.id,
 						data: newProps
 					}).then(() => {
-						(this as any).os.i.account.client_settings.mobile_home.find(w => w.id == this.id).data = newProps;
+						(this as any).os.i.account.clientSettings.mobile_home.find(w => w.id == this.id).data = newProps;
 					});
 				} else {
 					(this as any).api('i/update_home', {
 						id: this.id,
 						data: newProps
 					}).then(() => {
-						(this as any).os.i.account.client_settings.home.find(w => w.id == this.id).data = newProps;
+						(this as any).os.i.account.clientSettings.home.find(w => w.id == this.id).data = newProps;
 					});
 				}
 			}, {
diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
index 2c6c9988e..bcb8b6067 100644
--- a/src/web/app/common/mios.ts
+++ b/src/web/app/common/mios.ts
@@ -294,12 +294,12 @@ export default class MiOS extends EventEmitter {
 		const fetched = me => {
 			if (me) {
 				// デフォルトの設定をマージ
-				me.account.client_settings = Object.assign({
+				me.account.clientSettings = Object.assign({
 					fetchOnScroll: true,
 					showMaps: true,
 					showPostFormOnTopOfTl: false,
 					gradientWindowHeader: false
-				}, me.account.client_settings);
+				}, me.account.clientSettings);
 
 				// ローカルストレージにキャッシュ
 				localStorage.setItem('me', JSON.stringify(me));
diff --git a/src/web/app/common/scripts/streaming/home.ts b/src/web/app/common/scripts/streaming/home.ts
index ffcf6e536..c19861940 100644
--- a/src/web/app/common/scripts/streaming/home.ts
+++ b/src/web/app/common/scripts/streaming/home.ts
@@ -16,7 +16,7 @@ export class HomeStream extends Stream {
 		// 最終利用日時を更新するため定期的にaliveメッセージを送信
 		setInterval(() => {
 			this.send({ type: 'alive' });
-			me.account.last_used_at = new Date();
+			me.account.lastUsedAt = new Date();
 		}, 1000 * 60);
 
 		// 自分の情報が更新されたとき
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 01886b19c..704f2016d 100644
--- a/src/web/app/common/views/components/messaging-room.form.vue
+++ b/src/web/app/common/views/components/messaging-room.form.vue
@@ -151,9 +151,9 @@ export default Vue.extend({
 		send() {
 			this.sending = true;
 			(this as any).api('messaging/messages/create', {
-				user_id: this.user.id,
+				userId: this.user.id,
 				text: this.text ? this.text : undefined,
-				file_id: this.file ? this.file.id : undefined
+				fileId: this.file ? this.file.id : undefined
 			}).then(message => {
 				this.clear();
 			}).catch(err => {
@@ -173,7 +173,7 @@ export default Vue.extend({
 			const data = JSON.parse(localStorage.getItem('message_drafts') || '{}');
 
 			data[this.draftId] = {
-				updated_at: new Date(),
+				updatedAt: new Date(),
 				data: {
 					text: this.text,
 					file: this.file
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 5f2eb1ba8..d21cce1a0 100644
--- a/src/web/app/common/views/components/messaging-room.message.vue
+++ b/src/web/app/common/views/components/messaging-room.message.vue
@@ -5,7 +5,7 @@
 	</router-link>
 	<div class="content">
 		<div class="balloon" :data-no-text="message.text == null">
-			<p class="read" v-if="isMe && message.is_read">%i18n:common.tags.mk-messaging-message.is-read%</p>
+			<p class="read" v-if="isMe && message.isRead">%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>
@@ -25,7 +25,7 @@
 		<div></div>
 		<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 		<footer>
-			<mk-time :time="message.created_at"/>
+			<mk-time :time="message.createdAt"/>
 			<template v-if="message.is_edited">%fa:pencil-alt%</template>
 		</footer>
 	</div>
@@ -43,7 +43,7 @@ export default Vue.extend({
 			return getAcct(this.message.user);
 		},
 		isMe(): boolean {
-			return this.message.user_id == (this as any).os.i.id;
+			return this.message.userId == (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 6ff808b61..36c53fd3f 100644
--- a/src/web/app/common/views/components/messaging-room.vue
+++ b/src/web/app/common/views/components/messaging-room.vue
@@ -52,8 +52,8 @@ export default Vue.extend({
 	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;
+				const date = new Date(message.createdAt).getDate();
+				const month = new Date(message.createdAt).getMonth() + 1;
 				message._date = date;
 				message._datetext = `${month}月 ${date}日`;
 				return message;
@@ -123,7 +123,7 @@ export default Vue.extend({
 				const max = this.existMoreMessages ? 20 : 10;
 
 				(this as any).api('messaging/messages', {
-					user_id: this.user.id,
+					userId: this.user.id,
 					limit: max + 1,
 					until_id: this.existMoreMessages ? this.messages[0].id : undefined
 				}).then(messages => {
@@ -158,7 +158,7 @@ export default Vue.extend({
 			const isBottom = this.isBottom();
 
 			this.messages.push(message);
-			if (message.user_id != (this as any).os.i.id && !document.hidden) {
+			if (message.userId != (this as any).os.i.id && !document.hidden) {
 				this.connection.send({
 					type: 'read',
 					id: message.id
@@ -170,7 +170,7 @@ export default Vue.extend({
 				this.$nextTick(() => {
 					this.scrollToBottom();
 				});
-			} else if (message.user_id != (this as any).os.i.id) {
+			} else if (message.userId != (this as any).os.i.id) {
 				// Notify
 				this.notify('%i18n:common.tags.mk-messaging-room.new-message%');
 			}
@@ -181,7 +181,7 @@ export default Vue.extend({
 			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.messages[exist].isRead = true;
 				}
 			});
 		},
@@ -223,7 +223,7 @@ export default Vue.extend({
 		onVisibilitychange() {
 			if (document.hidden) return;
 			this.messages.forEach(message => {
-				if (message.user_id !== (this as any).os.i.id && !message.is_read) {
+				if (message.userId !== (this as any).os.i.id && !message.isRead) {
 					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 88574b94d..0272c6707 100644
--- a/src/web/app/common/views/components/messaging.vue
+++ b/src/web/app/common/views/components/messaging.vue
@@ -26,7 +26,7 @@
 				class="user"
 				:href="`/i/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
 				:data-is-me="isMe(message)"
-				:data-is-read="message.is_read"
+				:data-is-read="message.isRead"
 				@click.prevent="navigate(isMe(message) ? message.recipient : message.user)"
 				:key="message.id"
 			>
@@ -35,7 +35,7 @@
 					<header>
 						<span class="name">{{ isMe(message) ? message.recipient.name : message.user.name }}</span>
 						<span class="username">@{{ getAcct(isMe(message) ? message.recipient : message.user) }}</span>
-						<mk-time :time="message.created_at"/>
+						<mk-time :time="message.createdAt"/>
 					</header>
 					<div class="body">
 						<p class="text"><span class="me" v-if="isMe(message)">%i18n:common.tags.mk-messaging.you%:</span>{{ message.text }}</p>
@@ -95,19 +95,19 @@ export default Vue.extend({
 	methods: {
 		getAcct,
 		isMe(message) {
-			return message.user_id == (this as any).os.i.id;
+			return message.userId == (this as any).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)));
+				(m.recipientId == message.recipientId && m.userId == message.userId) ||
+				(m.recipientId == message.userId && m.userId == message.recipientId)));
 
 			this.messages.unshift(message);
 		},
 		onRead(ids) {
 			ids.forEach(id => {
 				const found = this.messages.find(m => m.id == id);
-				if (found) found.is_read = true;
+				if (found) found.isRead = true;
 			});
 		},
 		search() {
diff --git a/src/web/app/common/views/components/othello.game.vue b/src/web/app/common/views/components/othello.game.vue
index 414d819a5..8150fe07f 100644
--- a/src/web/app/common/views/components/othello.game.vue
+++ b/src/web/app/common/views/components/othello.game.vue
@@ -3,19 +3,19 @@
 	<header><b>{{ blackUser.name }}</b>(黒) vs <b>{{ whiteUser.name }}</b>(白)</header>
 
 	<div style="overflow: hidden">
-		<p class="turn" v-if="!iAmPlayer && !game.is_ended">{{ turnUser.name }}のターンです<mk-ellipsis/></p>
+		<p class="turn" v-if="!iAmPlayer && !game.isEnded">{{ turnUser.name }}のターンです<mk-ellipsis/></p>
 		<p class="turn" v-if="logPos != logs.length">{{ turnUser.name }}のターン</p>
-		<p class="turn1" v-if="iAmPlayer && !game.is_ended && !isMyTurn">相手のターンです<mk-ellipsis/></p>
-		<p class="turn2" v-if="iAmPlayer && !game.is_ended && isMyTurn" v-animate-css="{ classes: 'tada', iteration: 'infinite' }">あなたのターンです</p>
-		<p class="result" v-if="game.is_ended && logPos == logs.length">
-			<template v-if="game.winner"><b>{{ game.winner.name }}</b>の勝ち{{ game.settings.is_llotheo ? ' (ロセオ)' : '' }}</template>
+		<p class="turn1" v-if="iAmPlayer && !game.isEnded && !isMyTurn">相手のターンです<mk-ellipsis/></p>
+		<p class="turn2" v-if="iAmPlayer && !game.isEnded && isMyTurn" v-animate-css="{ classes: 'tada', iteration: 'infinite' }">あなたのターンです</p>
+		<p class="result" v-if="game.isEnded && logPos == logs.length">
+			<template v-if="game.winner"><b>{{ game.winner.name }}</b>の勝ち{{ game.settings.isLlotheo ? ' (ロセオ)' : '' }}</template>
 			<template v-else>引き分け</template>
 		</p>
 	</div>
 
 	<div class="board" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map[0].length }, 1fr)` }">
 		<div v-for="(stone, i) in o.board"
-			:class="{ empty: stone == null, none: o.map[i] == 'null', isEnded: game.is_ended, myTurn: !game.is_ended && isMyTurn, can: turnUser ? o.canPut(turnUser.id == blackUser.id, i) : null, prev: o.prevPos == i }"
+			:class="{ empty: stone == null, none: o.map[i] == 'null', isEnded: game.isEnded, myTurn: !game.isEnded && isMyTurn, can: turnUser ? o.canPut(turnUser.id == blackUser.id, i) : null, prev: o.prevPos == i }"
 			@click="set(i)"
 			:title="'[' + (o.transformPosToXy(i)[0] + 1) + ', ' + (o.transformPosToXy(i)[1] + 1) + '] (' + i + ')'"
 		>
@@ -26,7 +26,7 @@
 
 	<p class="status"><b>{{ logPos }}ターン目</b> 黒:{{ o.blackCount }} 白:{{ o.whiteCount }} 合計:{{ o.blackCount + o.whiteCount }}</p>
 
-	<div class="player" v-if="game.is_ended">
+	<div class="player" v-if="game.isEnded">
 		<el-button-group>
 			<el-button type="primary" @click="logPos = 0" :disabled="logPos == 0">%fa:angle-double-left%</el-button>
 			<el-button type="primary" @click="logPos--" :disabled="logPos == 0">%fa:angle-left%</el-button>
@@ -62,12 +62,12 @@ export default Vue.extend({
 	computed: {
 		iAmPlayer(): boolean {
 			if (!(this as any).os.isSignedIn) return false;
-			return this.game.user1_id == (this as any).os.i.id || this.game.user2_id == (this as any).os.i.id;
+			return this.game.user1Id == (this as any).os.i.id || this.game.user2Id == (this as any).os.i.id;
 		},
 		myColor(): Color {
 			if (!this.iAmPlayer) return null;
-			if (this.game.user1_id == (this as any).os.i.id && this.game.black == 1) return true;
-			if (this.game.user2_id == (this as any).os.i.id && this.game.black == 2) return true;
+			if (this.game.user1Id == (this as any).os.i.id && this.game.black == 1) return true;
+			if (this.game.user2Id == (this as any).os.i.id && this.game.black == 2) return true;
 			return false;
 		},
 		opColor(): Color {
@@ -97,11 +97,11 @@ export default Vue.extend({
 
 	watch: {
 		logPos(v) {
-			if (!this.game.is_ended) return;
+			if (!this.game.isEnded) return;
 			this.o = new Othello(this.game.settings.map, {
-				isLlotheo: this.game.settings.is_llotheo,
-				canPutEverywhere: this.game.settings.can_put_everywhere,
-				loopedBoard: this.game.settings.looped_board
+				isLlotheo: this.game.settings.isLlotheo,
+				canPutEverywhere: this.game.settings.canPutEverywhere,
+				loopedBoard: this.game.settings.loopedBoard
 			});
 			this.logs.forEach((log, i) => {
 				if (i < v) {
@@ -116,9 +116,9 @@ export default Vue.extend({
 		this.game = this.initGame;
 
 		this.o = new Othello(this.game.settings.map, {
-			isLlotheo: this.game.settings.is_llotheo,
-			canPutEverywhere: this.game.settings.can_put_everywhere,
-			loopedBoard: this.game.settings.looped_board
+			isLlotheo: this.game.settings.isLlotheo,
+			canPutEverywhere: this.game.settings.canPutEverywhere,
+			loopedBoard: this.game.settings.loopedBoard
 		});
 
 		this.game.logs.forEach(log => {
@@ -129,7 +129,7 @@ export default Vue.extend({
 		this.logPos = this.logs.length;
 
 		// 通信を取りこぼしてもいいように定期的にポーリングさせる
-		if (this.game.is_started && !this.game.is_ended) {
+		if (this.game.isStarted && !this.game.isEnded) {
 			this.pollingClock = setInterval(() => {
 				const crc32 = CRC32.str(this.logs.map(x => x.pos.toString()).join(''));
 				this.connection.send({
@@ -154,7 +154,7 @@ export default Vue.extend({
 
 	methods: {
 		set(pos) {
-			if (this.game.is_ended) return;
+			if (this.game.isEnded) return;
 			if (!this.iAmPlayer) return;
 			if (!this.isMyTurn) return;
 			if (!this.o.canPut(this.myColor, pos)) return;
@@ -194,16 +194,16 @@ export default Vue.extend({
 		},
 
 		checkEnd() {
-			this.game.is_ended = this.o.isEnded;
-			if (this.game.is_ended) {
+			this.game.isEnded = this.o.isEnded;
+			if (this.game.isEnded) {
 				if (this.o.winner === true) {
-					this.game.winner_id = this.game.black == 1 ? this.game.user1_id : this.game.user2_id;
+					this.game.winnerId = this.game.black == 1 ? this.game.user1Id : this.game.user2Id;
 					this.game.winner = this.game.black == 1 ? this.game.user1 : this.game.user2;
 				} else if (this.o.winner === false) {
-					this.game.winner_id = this.game.black == 1 ? this.game.user2_id : this.game.user1_id;
+					this.game.winnerId = this.game.black == 1 ? this.game.user2Id : this.game.user1Id;
 					this.game.winner = this.game.black == 1 ? this.game.user2 : this.game.user1;
 				} else {
-					this.game.winner_id = null;
+					this.game.winnerId = null;
 					this.game.winner = null;
 				}
 			}
@@ -214,9 +214,9 @@ export default Vue.extend({
 			this.game = game;
 
 			this.o = new Othello(this.game.settings.map, {
-				isLlotheo: this.game.settings.is_llotheo,
-				canPutEverywhere: this.game.settings.can_put_everywhere,
-				loopedBoard: this.game.settings.looped_board
+				isLlotheo: this.game.settings.isLlotheo,
+				canPutEverywhere: this.game.settings.canPutEverywhere,
+				loopedBoard: this.game.settings.loopedBoard
 			});
 
 			this.game.logs.forEach(log => {
diff --git a/src/web/app/common/views/components/othello.gameroom.vue b/src/web/app/common/views/components/othello.gameroom.vue
index 38a25f668..dba9ccd16 100644
--- a/src/web/app/common/views/components/othello.gameroom.vue
+++ b/src/web/app/common/views/components/othello.gameroom.vue
@@ -1,6 +1,6 @@
 <template>
 <div>
-	<x-room v-if="!g.is_started" :game="g" :connection="connection"/>
+	<x-room v-if="!g.isStarted" :game="g" :connection="connection"/>
 	<x-game v-else :init-game="g" :connection="connection"/>
 </div>
 </template>
diff --git a/src/web/app/common/views/components/othello.room.vue b/src/web/app/common/views/components/othello.room.vue
index 396541483..a32be6b74 100644
--- a/src/web/app/common/views/components/othello.room.vue
+++ b/src/web/app/common/views/components/othello.room.vue
@@ -41,9 +41,9 @@
 			<div slot="header">
 				<span>ルール</span>
 			</div>
-			<mk-switch v-model="game.settings.is_llotheo" @change="updateSettings" text="石の少ない方が勝ち(ロセオ)"/>
-			<mk-switch v-model="game.settings.looped_board" @change="updateSettings" text="ループマップ"/>
-			<mk-switch v-model="game.settings.can_put_everywhere" @change="updateSettings" text="どこでも置けるモード"/>
+			<mk-switch v-model="game.settings.isLlotheo" @change="updateSettings" text="石の少ない方が勝ち(ロセオ)"/>
+			<mk-switch v-model="game.settings.loopedBoard" @change="updateSettings" text="ループマップ"/>
+			<mk-switch v-model="game.settings.canPutEverywhere" @change="updateSettings" text="どこでも置けるモード"/>
 		</el-card>
 
 		<el-card class="bot-form" v-if="form">
@@ -116,13 +116,13 @@ export default Vue.extend({
 			return categories.filter((item, pos) => categories.indexOf(item) == pos);
 		},
 		isAccepted(): boolean {
-			if (this.game.user1_id == (this as any).os.i.id && this.game.user1_accepted) return true;
-			if (this.game.user2_id == (this as any).os.i.id && this.game.user2_accepted) return true;
+			if (this.game.user1Id == (this as any).os.i.id && this.game.user1Accepted) return true;
+			if (this.game.user2Id == (this as any).os.i.id && this.game.user2Accepted) return true;
 			return false;
 		},
 		isOpAccepted(): boolean {
-			if (this.game.user1_id != (this as any).os.i.id && this.game.user1_accepted) return true;
-			if (this.game.user2_id != (this as any).os.i.id && this.game.user2_accepted) return true;
+			if (this.game.user1Id != (this as any).os.i.id && this.game.user1Accepted) return true;
+			if (this.game.user2Id != (this as any).os.i.id && this.game.user2Accepted) return true;
 			return false;
 		}
 	},
@@ -133,8 +133,8 @@ export default Vue.extend({
 		this.connection.on('init-form', this.onInitForm);
 		this.connection.on('message', this.onMessage);
 
-		if (this.game.user1_id != (this as any).os.i.id && this.game.settings.form1) this.form = this.game.settings.form1;
-		if (this.game.user2_id != (this as any).os.i.id && this.game.settings.form2) this.form = this.game.settings.form2;
+		if (this.game.user1Id != (this as any).os.i.id && this.game.settings.form1) this.form = this.game.settings.form1;
+		if (this.game.user2Id != (this as any).os.i.id && this.game.settings.form2) this.form = this.game.settings.form2;
 	},
 
 	beforeDestroy() {
@@ -162,8 +162,8 @@ export default Vue.extend({
 		},
 
 		onChangeAccepts(accepts) {
-			this.game.user1_accepted = accepts.user1;
-			this.game.user2_accepted = accepts.user2;
+			this.game.user1Accepted = accepts.user1;
+			this.game.user2Accepted = accepts.user2;
 			this.$forceUpdate();
 		},
 
@@ -185,12 +185,12 @@ export default Vue.extend({
 		},
 
 		onInitForm(x) {
-			if (x.user_id == (this as any).os.i.id) return;
+			if (x.userId == (this as any).os.i.id) return;
 			this.form = x.form;
 		},
 
 		onMessage(x) {
-			if (x.user_id == (this as any).os.i.id) return;
+			if (x.userId == (this as any).os.i.id) return;
 			this.messages.unshift(x.message);
 		},
 
diff --git a/src/web/app/common/views/components/othello.vue b/src/web/app/common/views/components/othello.vue
index d65032234..7bdb47100 100644
--- a/src/web/app/common/views/components/othello.vue
+++ b/src/web/app/common/views/components/othello.vue
@@ -34,7 +34,7 @@
 				<img :src="`${i.parent.avatar_url}?thumbnail&size=32`" alt="">
 				<span class="name"><b>{{ i.parent.name }}</b></span>
 				<span class="username">@{{ i.parent.username }}</span>
-				<mk-time :time="i.created_at"/>
+				<mk-time :time="i.createdAt"/>
 			</div>
 		</section>
 		<section v-if="myGames.length > 0">
@@ -43,7 +43,7 @@
 				<img :src="`${g.user1.avatar_url}?thumbnail&size=32`" alt="">
 				<img :src="`${g.user2.avatar_url}?thumbnail&size=32`" alt="">
 				<span><b>{{ g.user1.name }}</b> vs <b>{{ g.user2.name }}</b></span>
-				<span class="state">{{ g.is_ended ? '終了' : '進行中' }}</span>
+				<span class="state">{{ g.isEnded ? '終了' : '進行中' }}</span>
 			</a>
 		</section>
 		<section v-if="games.length > 0">
@@ -52,7 +52,7 @@
 				<img :src="`${g.user1.avatar_url}?thumbnail&size=32`" alt="">
 				<img :src="`${g.user2.avatar_url}?thumbnail&size=32`" alt="">
 				<span><b>{{ g.user1.name }}</b> vs <b>{{ g.user2.name }}</b></span>
-				<span class="state">{{ g.is_ended ? '終了' : '進行中' }}</span>
+				<span class="state">{{ g.isEnded ? '終了' : '進行中' }}</span>
 			</a>
 		</section>
 	</div>
@@ -147,7 +147,7 @@ export default Vue.extend({
 					username
 				}).then(user => {
 					(this as any).api('othello/match', {
-						user_id: user.id
+						userId: user.id
 					}).then(res => {
 						if (res == null) {
 							this.matching = user;
@@ -164,7 +164,7 @@ export default Vue.extend({
 		},
 		accept(invitation) {
 			(this as any).api('othello/match', {
-				user_id: invitation.parent.id
+				userId: invitation.parent.id
 			}).then(game => {
 				if (game) {
 					this.matching = null;
diff --git a/src/web/app/common/views/components/poll.vue b/src/web/app/common/views/components/poll.vue
index 8156c8bc5..e46e89f55 100644
--- a/src/web/app/common/views/components/poll.vue
+++ b/src/web/app/common/views/components/poll.vue
@@ -49,7 +49,7 @@ export default Vue.extend({
 		vote(id) {
 			if (this.poll.choices.some(c => c.is_voted)) return;
 			(this as any).api('posts/polls/vote', {
-				post_id: this.post.id,
+				postId: this.post.id,
 				choice: id
 			}).then(() => {
 				this.poll.choices.forEach(c => {
diff --git a/src/web/app/common/views/components/post-menu.vue b/src/web/app/common/views/components/post-menu.vue
index a53680e55..35116db7e 100644
--- a/src/web/app/common/views/components/post-menu.vue
+++ b/src/web/app/common/views/components/post-menu.vue
@@ -2,7 +2,7 @@
 <div class="mk-post-menu">
 	<div class="backdrop" ref="backdrop" @click="close"></div>
 	<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>
+		<button v-if="post.userId == os.i.id" @click="pin">%i18n:common.tags.mk-post-menu.pin%</button>
 	</div>
 </div>
 </template>
@@ -51,7 +51,7 @@ export default Vue.extend({
 	methods: {
 		pin() {
 			(this as any).api('i/pin', {
-				post_id: this.post.id
+				postId: 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 df8100f2f..bcb6b2b96 100644
--- a/src/web/app/common/views/components/reaction-picker.vue
+++ b/src/web/app/common/views/components/reaction-picker.vue
@@ -69,7 +69,7 @@ export default Vue.extend({
 	methods: {
 		react(reaction) {
 			(this as any).api('posts/reactions/create', {
-				post_id: this.post.id,
+				postId: this.post.id,
 				reaction: reaction
 			}).then(() => {
 				if (this.cb) this.cb();
diff --git a/src/web/app/common/views/components/signin.vue b/src/web/app/common/views/components/signin.vue
index 273143262..17154e6b3 100644
--- a/src/web/app/common/views/components/signin.vue
+++ b/src/web/app/common/views/components/signin.vue
@@ -6,7 +6,7 @@
 	<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.account.two_factor_enabled">
+	<label class="token" v-if="user && user.account.twoFactorEnabled">
 		<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>
@@ -43,7 +43,7 @@ export default Vue.extend({
 			(this as any).api('signin', {
 				username: this.username,
 				password: this.password,
-				token: this.user && this.user.account.two_factor_enabled ? this.token : undefined
+				token: this.user && this.user.account.twoFactorEnabled ? this.token : undefined
 			}).then(() => {
 				location.reload();
 			}).catch(() => {
diff --git a/src/web/app/common/views/components/twitter-setting.vue b/src/web/app/common/views/components/twitter-setting.vue
index 15968d20a..082d2b435 100644
--- a/src/web/app/common/views/components/twitter-setting.vue
+++ b/src/web/app/common/views/components/twitter-setting.vue
@@ -1,13 +1,13 @@
 <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.account.twitter" :title="`Twitter ID: ${os.i.account.twitter.user_id}`">%i18n:common.tags.mk-twitter-setting.connected-to%: <a :href="`https://twitter.com/${os.i.account.twitter.screen_name}`" target="_blank">@{{ os.i.account.twitter.screen_name }}</a></p>
+	<p class="account" v-if="os.i.account.twitter" :title="`Twitter ID: ${os.i.account.twitter.userId}`">%i18n:common.tags.mk-twitter-setting.connected-to%: <a :href="`https://twitter.com/${os.i.account.twitter.screenName}`" target="_blank">@{{ os.i.account.twitter.screenName }}</a></p>
 	<p>
 		<a :href="`${apiUrl}/connect/twitter`" target="_blank" @click.prevent="connect">{{ os.i.account.twitter ? '%i18n:common.tags.mk-twitter-setting.reconnect%' : '%i18n:common.tags.mk-twitter-setting.connect%' }}</a>
 		<span v-if="os.i.account.twitter"> or </span>
 		<a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="os.i.account.twitter" @click.prevent="disconnect">%i18n:common.tags.mk-twitter-setting.disconnect%</a>
 	</p>
-	<p class="id" v-if="os.i.account.twitter">Twitter ID: {{ os.i.account.twitter.user_id }}</p>
+	<p class="id" v-if="os.i.account.twitter">Twitter ID: {{ os.i.account.twitter.userId }}</p>
 </div>
 </template>
 
diff --git a/src/web/app/common/views/components/uploader.vue b/src/web/app/common/views/components/uploader.vue
index 73006b16e..c74a1edb4 100644
--- a/src/web/app/common/views/components/uploader.vue
+++ b/src/web/app/common/views/components/uploader.vue
@@ -53,7 +53,7 @@ export default Vue.extend({
 			data.append('i', (this as any).os.i.account.token);
 			data.append('file', file);
 
-			if (folder) data.append('folder_id', folder);
+			if (folder) data.append('folderId', folder);
 
 			const xhr = new XMLHttpRequest();
 			xhr.open('POST', apiUrl + '/drive/files/create', true);
diff --git a/src/web/app/common/views/components/welcome-timeline.vue b/src/web/app/common/views/components/welcome-timeline.vue
index 7586e9264..c61cae0f7 100644
--- a/src/web/app/common/views/components/welcome-timeline.vue
+++ b/src/web/app/common/views/components/welcome-timeline.vue
@@ -10,7 +10,7 @@
 				<span class="username">@{{ getAcct(post.user) }}</span>
 				<div class="info">
 					<router-link class="created-at" :to="`/@${getAcct(post.user)}/${post.id}`">
-						<mk-time :time="post.created_at"/>
+						<mk-time :time="post.createdAt"/>
 					</router-link>
 				</div>
 			</header>
diff --git a/src/web/app/common/views/widgets/slideshow.vue b/src/web/app/common/views/widgets/slideshow.vue
index e9451663e..ad32299f3 100644
--- a/src/web/app/common/views/widgets/slideshow.vue
+++ b/src/web/app/common/views/widgets/slideshow.vue
@@ -97,7 +97,7 @@ export default define({
 			this.fetching = true;
 
 			(this as any).api('drive/files', {
-				folder_id: this.props.folder,
+				folderId: this.props.folder,
 				type: 'image/*',
 				limit: 100
 			}).then(images => {
diff --git a/src/web/app/desktop/api/update-avatar.ts b/src/web/app/desktop/api/update-avatar.ts
index 8f748d853..445ef7d74 100644
--- a/src/web/app/desktop/api/update-avatar.ts
+++ b/src/web/app/desktop/api/update-avatar.ts
@@ -49,7 +49,7 @@ export default (os: OS) => (cb, file = null) => {
 		}).$mount();
 		document.body.appendChild(dialog.$el);
 
-		if (folder) data.append('folder_id', folder.id);
+		if (folder) data.append('folderId', folder.id);
 
 		const xhr = new XMLHttpRequest();
 		xhr.open('POST', apiUrl + '/drive/files/create', true);
@@ -68,9 +68,9 @@ export default (os: OS) => (cb, file = null) => {
 
 	const set = file => {
 		os.api('i/update', {
-			avatar_id: file.id
+			avatarId: file.id
 		}).then(i => {
-			os.i.avatar_id = i.avatar_id;
+			os.i.avatarId = i.avatarId;
 			os.i.avatar_url = i.avatar_url;
 
 			os.apis.dialog({
diff --git a/src/web/app/desktop/api/update-banner.ts b/src/web/app/desktop/api/update-banner.ts
index 9ed48b267..002efce8c 100644
--- a/src/web/app/desktop/api/update-banner.ts
+++ b/src/web/app/desktop/api/update-banner.ts
@@ -49,7 +49,7 @@ export default (os: OS) => (cb, file = null) => {
 		}).$mount();
 		document.body.appendChild(dialog.$el);
 
-		if (folder) data.append('folder_id', folder.id);
+		if (folder) data.append('folderId', folder.id);
 
 		const xhr = new XMLHttpRequest();
 		xhr.open('POST', apiUrl + '/drive/files/create', true);
@@ -68,9 +68,9 @@ export default (os: OS) => (cb, file = null) => {
 
 	const set = file => {
 		os.api('i/update', {
-			banner_id: file.id
+			bannerId: file.id
 		}).then(i => {
-			os.i.banner_id = i.banner_id;
+			os.i.bannerId = i.bannerId;
 			os.i.banner_url = i.banner_url;
 
 			os.apis.dialog({
diff --git a/src/web/app/desktop/views/components/activity.vue b/src/web/app/desktop/views/components/activity.vue
index 33b53eb70..480b956ec 100644
--- a/src/web/app/desktop/views/components/activity.vue
+++ b/src/web/app/desktop/views/components/activity.vue
@@ -43,7 +43,7 @@ export default Vue.extend({
 	},
 	mounted() {
 		(this as any).api('aggregation/users/activity', {
-			user_id: this.user.id,
+			userId: this.user.id,
 			limit: 20 * 7
 		}).then(activity => {
 			this.activity = activity;
diff --git a/src/web/app/desktop/views/components/drive.file.vue b/src/web/app/desktop/views/components/drive.file.vue
index 924ff7052..85f8361c9 100644
--- a/src/web/app/desktop/views/components/drive.file.vue
+++ b/src/web/app/desktop/views/components/drive.file.vue
@@ -9,10 +9,10 @@
 	@contextmenu.prevent.stop="onContextmenu"
 	:title="title"
 >
-	<div class="label" v-if="os.i.avatar_id == file.id"><img src="/assets/label.svg"/>
+	<div class="label" v-if="os.i.avatarId == file.id"><img src="/assets/label.svg"/>
 		<p>%i18n:desktop.tags.mk-drive-browser-file.avatar%</p>
 	</div>
-	<div class="label" v-if="os.i.banner_id == file.id"><img src="/assets/label.svg"/>
+	<div class="label" v-if="os.i.bannerId == 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: ${ background }`">
@@ -50,8 +50,8 @@ export default Vue.extend({
 			return `${this.file.name}\n${this.file.type} ${Vue.filter('bytes')(this.file.datasize)}`;
 		},
 		background(): string {
-			return this.file.properties.average_color
-				? `rgb(${this.file.properties.average_color.join(',')})`
+			return this.file.properties.avgColor
+				? `rgb(${this.file.properties.avgColor.join(',')})`
 				: 'transparent';
 		}
 	},
@@ -129,10 +129,10 @@ export default Vue.extend({
 		},
 
 		onThumbnailLoaded() {
-			if (this.file.properties.average_color) {
+			if (this.file.properties.avgColor) {
 				anime({
 					targets: this.$refs.thumbnail,
-					backgroundColor: `rgba(${this.file.properties.average_color.join(',')}, 0)`,
+					backgroundColor: `rgba(${this.file.properties.avgColor.join(',')}, 0)`,
 					duration: 100,
 					easing: 'linear'
 				});
@@ -147,7 +147,7 @@ export default Vue.extend({
 				allowEmpty: false
 			}).then(name => {
 				(this as any).api('drive/files/update', {
-					file_id: this.file.id,
+					fileId: this.file.id,
 					name: name
 				})
 			});
diff --git a/src/web/app/desktop/views/components/drive.folder.vue b/src/web/app/desktop/views/components/drive.folder.vue
index a8a9a0137..a926bf47b 100644
--- a/src/web/app/desktop/views/components/drive.folder.vue
+++ b/src/web/app/desktop/views/components/drive.folder.vue
@@ -135,8 +135,8 @@ export default Vue.extend({
 				const file = JSON.parse(driveFile);
 				this.browser.removeFile(file.id);
 				(this as any).api('drive/files/update', {
-					file_id: file.id,
-					folder_id: this.folder.id
+					fileId: file.id,
+					folderId: this.folder.id
 				});
 			}
 			//#endregion
@@ -151,8 +151,8 @@ export default Vue.extend({
 
 				this.browser.removeFolder(folder.id);
 				(this as any).api('drive/folders/update', {
-					folder_id: folder.id,
-					parent_id: this.folder.id
+					folderId: folder.id,
+					parentId: this.folder.id
 				}).then(() => {
 					// noop
 				}).catch(err => {
@@ -204,7 +204,7 @@ export default Vue.extend({
 				default: this.folder.name
 			}).then(name => {
 				(this as any).api('drive/folders/update', {
-					folder_id: this.folder.id,
+					folderId: this.folder.id,
 					name: name
 				});
 			});
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 dfbf116bf..d885a72f7 100644
--- a/src/web/app/desktop/views/components/drive.nav-folder.vue
+++ b/src/web/app/desktop/views/components/drive.nav-folder.vue
@@ -78,8 +78,8 @@ export default Vue.extend({
 				const file = JSON.parse(driveFile);
 				this.browser.removeFile(file.id);
 				(this as any).api('drive/files/update', {
-					file_id: file.id,
-					folder_id: this.folder ? this.folder.id : null
+					fileId: file.id,
+					folderId: this.folder ? this.folder.id : null
 				});
 			}
 			//#endregion
@@ -92,8 +92,8 @@ export default Vue.extend({
 				if (this.folder && folder.id == this.folder.id) return;
 				this.browser.removeFolder(folder.id);
 				(this as any).api('drive/folders/update', {
-					folder_id: folder.id,
-					parent_id: this.folder ? this.folder.id : null
+					folderId: folder.id,
+					parentId: this.folder ? this.folder.id : null
 				});
 			}
 			//#endregion
diff --git a/src/web/app/desktop/views/components/drive.vue b/src/web/app/desktop/views/components/drive.vue
index 0fafa8cf2..c766dfec1 100644
--- a/src/web/app/desktop/views/components/drive.vue
+++ b/src/web/app/desktop/views/components/drive.vue
@@ -160,7 +160,7 @@ export default Vue.extend({
 
 		onStreamDriveFileUpdated(file) {
 			const current = this.folder ? this.folder.id : null;
-			if (current != file.folder_id) {
+			if (current != file.folderId) {
 				this.removeFile(file);
 			} else {
 				this.addFile(file, true);
@@ -173,7 +173,7 @@ export default Vue.extend({
 
 		onStreamDriveFolderUpdated(folder) {
 			const current = this.folder ? this.folder.id : null;
-			if (current != folder.parent_id) {
+			if (current != folder.parentId) {
 				this.removeFolder(folder);
 			} else {
 				this.addFolder(folder, true);
@@ -282,8 +282,8 @@ export default Vue.extend({
 				if (this.files.some(f => f.id == file.id)) return;
 				this.removeFile(file.id);
 				(this as any).api('drive/files/update', {
-					file_id: file.id,
-					folder_id: this.folder ? this.folder.id : null
+					fileId: file.id,
+					folderId: this.folder ? this.folder.id : null
 				});
 			}
 			//#endregion
@@ -298,8 +298,8 @@ export default Vue.extend({
 				if (this.folders.some(f => f.id == folder.id)) return false;
 				this.removeFolder(folder.id);
 				(this as any).api('drive/folders/update', {
-					folder_id: folder.id,
-					parent_id: this.folder ? this.folder.id : null
+					folderId: folder.id,
+					parentId: this.folder ? this.folder.id : null
 				}).then(() => {
 					// noop
 				}).catch(err => {
@@ -332,7 +332,7 @@ export default Vue.extend({
 			}).then(url => {
 				(this as any).api('drive/files/upload_from_url', {
 					url: url,
-					folder_id: this.folder ? this.folder.id : undefined
+					folderId: this.folder ? this.folder.id : undefined
 				});
 
 				(this as any).apis.dialog({
@@ -352,7 +352,7 @@ export default Vue.extend({
 			}).then(name => {
 				(this as any).api('drive/folders/create', {
 					name: name,
-					folder_id: this.folder ? this.folder.id : undefined
+					folderId: this.folder ? this.folder.id : undefined
 				}).then(folder => {
 					this.addFolder(folder, true);
 				});
@@ -412,7 +412,7 @@ export default Vue.extend({
 			this.fetching = true;
 
 			(this as any).api('drive/folders/show', {
-				folder_id: target
+				folderId: target
 			}).then(folder => {
 				this.folder = folder;
 				this.hierarchyFolders = [];
@@ -431,7 +431,7 @@ export default Vue.extend({
 
 		addFolder(folder, unshift = false) {
 			const current = this.folder ? this.folder.id : null;
-			if (current != folder.parent_id) return;
+			if (current != folder.parentId) return;
 
 			if (this.folders.some(f => f.id == folder.id)) {
 				const exist = this.folders.map(f => f.id).indexOf(folder.id);
@@ -448,7 +448,7 @@ export default Vue.extend({
 
 		addFile(file, unshift = false) {
 			const current = this.folder ? this.folder.id : null;
-			if (current != file.folder_id) return;
+			if (current != file.folderId) return;
 
 			if (this.files.some(f => f.id == file.id)) {
 				const exist = this.files.map(f => f.id).indexOf(file.id);
@@ -514,7 +514,7 @@ export default Vue.extend({
 
 			// フォルダ一覧取得
 			(this as any).api('drive/folders', {
-				folder_id: this.folder ? this.folder.id : null,
+				folderId: this.folder ? this.folder.id : null,
 				limit: foldersMax + 1
 			}).then(folders => {
 				if (folders.length == foldersMax + 1) {
@@ -527,7 +527,7 @@ export default Vue.extend({
 
 			// ファイル一覧取得
 			(this as any).api('drive/files', {
-				folder_id: this.folder ? this.folder.id : null,
+				folderId: this.folder ? this.folder.id : null,
 				limit: filesMax + 1
 			}).then(files => {
 				if (files.length == filesMax + 1) {
@@ -557,7 +557,7 @@ export default Vue.extend({
 
 			// ファイル一覧取得
 			(this as any).api('drive/files', {
-				folder_id: this.folder ? this.folder.id : null,
+				folderId: this.folder ? this.folder.id : null,
 				limit: max + 1
 			}).then(files => {
 				if (files.length == max + 1) {
diff --git a/src/web/app/desktop/views/components/follow-button.vue b/src/web/app/desktop/views/components/follow-button.vue
index fc4f87188..01b7e2aef 100644
--- a/src/web/app/desktop/views/components/follow-button.vue
+++ b/src/web/app/desktop/views/components/follow-button.vue
@@ -67,7 +67,7 @@ export default Vue.extend({
 			this.wait = true;
 			if (this.user.is_following) {
 				(this as any).api('following/delete', {
-					user_id: this.user.id
+					userId: this.user.id
 				}).then(() => {
 					this.user.is_following = false;
 				}).catch(err => {
@@ -77,7 +77,7 @@ export default Vue.extend({
 				});
 			} else {
 				(this as any).api('following/create', {
-					user_id: this.user.id
+					userId: this.user.id
 				}).then(() => {
 					this.user.is_following = true;
 				}).catch(err => {
diff --git a/src/web/app/desktop/views/components/followers.vue b/src/web/app/desktop/views/components/followers.vue
index 4541a0007..e8330289c 100644
--- a/src/web/app/desktop/views/components/followers.vue
+++ b/src/web/app/desktop/views/components/followers.vue
@@ -1,7 +1,7 @@
 <template>
 <mk-users-list
 	:fetch="fetch"
-	:count="user.followers_count"
+	:count="user.followersCount"
 	:you-know-count="user.followers_you_know_count"
 >
 	フォロワーはいないようです。
@@ -15,7 +15,7 @@ export default Vue.extend({
 	methods: {
 		fetch(iknow, limit, cursor, cb) {
 			(this as any).api('users/followers', {
-				user_id: this.user.id,
+				userId: this.user.id,
 				iknow: iknow,
 				limit: limit,
 				cursor: cursor ? cursor : undefined
diff --git a/src/web/app/desktop/views/components/following.vue b/src/web/app/desktop/views/components/following.vue
index e0b9f1169..0dab6ac7b 100644
--- a/src/web/app/desktop/views/components/following.vue
+++ b/src/web/app/desktop/views/components/following.vue
@@ -1,7 +1,7 @@
 <template>
 <mk-users-list
 	:fetch="fetch"
-	:count="user.following_count"
+	:count="user.followingCount"
 	:you-know-count="user.following_you_know_count"
 >
 	フォロー中のユーザーはいないようです。
@@ -15,7 +15,7 @@ export default Vue.extend({
 	methods: {
 		fetch(iknow, limit, cursor, cb) {
 			(this as any).api('users/following', {
-				user_id: this.user.id,
+				userId: this.user.id,
 				iknow: iknow,
 				limit: limit,
 				cursor: cursor ? cursor : undefined
diff --git a/src/web/app/desktop/views/components/home.vue b/src/web/app/desktop/views/components/home.vue
index a4ce1ef94..7145ddce0 100644
--- a/src/web/app/desktop/views/components/home.vue
+++ b/src/web/app/desktop/views/components/home.vue
@@ -53,7 +53,7 @@
 			<div class="main">
 				<a @click="hint">カスタマイズのヒント</a>
 				<div>
-					<mk-post-form v-if="os.i.account.client_settings.showPostFormOnTopOfTl"/>
+					<mk-post-form v-if="os.i.account.clientSettings.showPostFormOnTopOfTl"/>
 					<mk-timeline ref="tl" @loaded="onTlLoaded"/>
 				</div>
 			</div>
@@ -63,7 +63,7 @@
 				<component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" @chosen="warp"/>
 			</div>
 			<div class="main">
-				<mk-post-form v-if="os.i.account.client_settings.showPostFormOnTopOfTl"/>
+				<mk-post-form v-if="os.i.account.clientSettings.showPostFormOnTopOfTl"/>
 				<mk-timeline ref="tl" @loaded="onTlLoaded" v-if="mode == 'timeline'"/>
 				<mk-mentions @loaded="onTlLoaded" v-if="mode == 'mentions'"/>
 			</div>
@@ -104,16 +104,16 @@ export default Vue.extend({
 		home: {
 			get(): any[] {
 				//#region 互換性のため
-				(this as any).os.i.account.client_settings.home.forEach(w => {
+				(this as any).os.i.account.clientSettings.home.forEach(w => {
 					if (w.name == 'rss-reader') w.name = 'rss';
 					if (w.name == 'user-recommendation') w.name = 'users';
 					if (w.name == 'recommended-polls') w.name = 'polls';
 				});
 				//#endregion
-				return (this as any).os.i.account.client_settings.home;
+				return (this as any).os.i.account.clientSettings.home;
 			},
 			set(value) {
-				(this as any).os.i.account.client_settings.home = value;
+				(this as any).os.i.account.clientSettings.home = value;
 			}
 		},
 		left(): any[] {
@@ -126,7 +126,7 @@ export default Vue.extend({
 	created() {
 		this.widgets.left = this.left;
 		this.widgets.right = this.right;
-		this.$watch('os.i.account.client_settings', i => {
+		this.$watch('os.i.account.clientSettings', i => {
 			this.widgets.left = this.left;
 			this.widgets.right = this.right;
 		}, {
@@ -161,17 +161,17 @@ export default Vue.extend({
 		},
 		onHomeUpdated(data) {
 			if (data.home) {
-				(this as any).os.i.account.client_settings.home = data.home;
+				(this as any).os.i.account.clientSettings.home = data.home;
 				this.widgets.left = data.home.filter(w => w.place == 'left');
 				this.widgets.right = data.home.filter(w => w.place == 'right');
 			} else {
-				const w = (this as any).os.i.account.client_settings.home.find(w => w.id == data.id);
+				const w = (this as any).os.i.account.clientSettings.home.find(w => w.id == data.id);
 				if (w != null) {
 					w.data = data.data;
 					this.$refs[w.id][0].preventSave = true;
 					this.$refs[w.id][0].props = w.data;
-					this.widgets.left = (this as any).os.i.account.client_settings.home.filter(w => w.place == 'left');
-					this.widgets.right = (this as any).os.i.account.client_settings.home.filter(w => w.place == 'right');
+					this.widgets.left = (this as any).os.i.account.clientSettings.home.filter(w => w.place == 'left');
+					this.widgets.right = (this as any).os.i.account.clientSettings.home.filter(w => w.place == 'right');
 				}
 			}
 		},
diff --git a/src/web/app/desktop/views/components/media-image.vue b/src/web/app/desktop/views/components/media-image.vue
index bc02d0f9b..51309a057 100644
--- a/src/web/app/desktop/views/components/media-image.vue
+++ b/src/web/app/desktop/views/components/media-image.vue
@@ -18,7 +18,7 @@ export default Vue.extend({
 	computed: {
 		style(): any {
 			return {
-				'background-color': this.image.properties.average_color ? `rgb(${this.image.properties.average_color.join(',')})` : 'transparent',
+				'background-color': this.image.properties.avgColor ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
 				'background-image': `url(${this.image.url}?thumbnail&size=512)`
 			};
 		}
diff --git a/src/web/app/desktop/views/components/notifications.vue b/src/web/app/desktop/views/components/notifications.vue
index b48ffc174..62593cf97 100644
--- a/src/web/app/desktop/views/components/notifications.vue
+++ b/src/web/app/desktop/views/components/notifications.vue
@@ -3,7 +3,7 @@
 	<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"/>
+				<mk-time :time="notification.createdAt"/>
 				<template v-if="notification.type == 'reaction'">
 					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">
 						<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
@@ -19,12 +19,12 @@
 					</div>
 				</template>
 				<template v-if="notification.type == 'repost'">
-					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.user_id">
+					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">
 						<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:retweet%
-							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</router-link>
+							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ notification.post.user.name }}</router-link>
 						</p>
 						<router-link class="post-ref" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">
 							%fa:quote-left%{{ getPostSummary(notification.post.repost) }}%fa:quote-right%
@@ -32,12 +32,12 @@
 					</div>
 				</template>
 				<template v-if="notification.type == 'quote'">
-					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.user_id">
+					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">
 						<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:quote-left%
-							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</router-link>
+							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ notification.post.user.name }}</router-link>
 						</p>
 						<router-link class="post-preview" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link>
 					</div>
@@ -53,23 +53,23 @@
 					</div>
 				</template>
 				<template v-if="notification.type == 'reply'">
-					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.user_id">
+					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">
 						<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:reply%
-							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</router-link>
+							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ notification.post.user.name }}</router-link>
 						</p>
 						<router-link class="post-preview" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link>
 					</div>
 				</template>
 				<template v-if="notification.type == 'mention'">
-					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.user_id">
+					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">
 						<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:at%
-							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</router-link>
+							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ notification.post.user.name }}</router-link>
 						</p>
 						<a class="post-preview" :href="`/@${getAcct(notification.post.user)}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</a>
 					</div>
@@ -120,8 +120,8 @@ export default Vue.extend({
 	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;
+				const date = new Date(notification.createdAt).getDate();
+				const month = new Date(notification.createdAt).getMonth() + 1;
 				notification._date = date;
 				notification._datetext = `${month}月 ${date}日`;
 				return notification;
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 59d8db04c..7453a8bfc 100644
--- a/src/web/app/desktop/views/components/post-detail.sub.vue
+++ b/src/web/app/desktop/views/components/post-detail.sub.vue
@@ -1,17 +1,17 @@
 <template>
 <div class="sub" :title="title">
 	<router-link class="avatar-anchor" :to="`/@${acct}`">
-		<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.userId"/>
 	</router-link>
 	<div class="main">
 		<header>
 			<div class="left">
-				<router-link class="name" :to="`/@${acct}`" v-user-preview="post.user_id">{{ post.user.name }}</router-link>
+				<router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ post.user.name }}</router-link>
 				<span class="username">@{{ acct }}</span>
 			</div>
 			<div class="right">
 				<router-link class="time" :to="`/@${acct}/${post.id}`">
-					<mk-time :time="post.created_at"/>
+					<mk-time :time="post.createdAt"/>
 				</router-link>
 			</div>
 		</header>
@@ -37,7 +37,7 @@ export default Vue.extend({
 			return getAcct(this.post.user);
 		},
 		title(): string {
-			return dateStringify(this.post.created_at);
+			return dateStringify(this.post.createdAt);
 		}
 	}
 });
diff --git a/src/web/app/desktop/views/components/post-detail.vue b/src/web/app/desktop/views/components/post-detail.vue
index f09bf4cbd..1ab751aaf 100644
--- a/src/web/app/desktop/views/components/post-detail.vue
+++ b/src/web/app/desktop/views/components/post-detail.vue
@@ -2,7 +2,7 @@
 <div class="mk-post-detail" :title="title">
 	<button
 		class="read-more"
-		v-if="p.reply && p.reply.reply_id && context == null"
+		v-if="p.reply && p.reply.replyId && context == null"
 		title="会話をもっと読み込む"
 		@click="fetchContext"
 		:disabled="contextFetching"
@@ -18,7 +18,7 @@
 	</div>
 	<div class="repost" v-if="isRepost">
 		<p>
-			<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="post.user_id">
+			<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="post.userId">
 				<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=32`" alt="avatar"/>
 			</router-link>
 			%fa:retweet%
@@ -34,7 +34,7 @@
 			<router-link class="name" :to="`/@${acct}`" v-user-preview="p.user.id">{{ p.user.name }}</router-link>
 			<span class="username">@{{ acct }}</span>
 			<router-link class="time" :to="`/@${acct}/${p.id}`">
-				<mk-time :time="p.created_at"/>
+				<mk-time :time="p.createdAt"/>
 			</router-link>
 		</header>
 		<div class="body">
@@ -115,7 +115,7 @@ export default Vue.extend({
 		isRepost(): boolean {
 			return (this.post.repost &&
 				this.post.text == null &&
-				this.post.media_ids == null &&
+				this.post.mediaIds == null &&
 				this.post.poll == null);
 		},
 		p(): any {
@@ -129,7 +129,7 @@ export default Vue.extend({
 				: 0;
 		},
 		title(): string {
-			return dateStringify(this.p.created_at);
+			return dateStringify(this.p.createdAt);
 		},
 		urls(): string[] {
 			if (this.p.ast) {
@@ -145,7 +145,7 @@ export default Vue.extend({
 		// Get replies
 		if (!this.compact) {
 			(this as any).api('posts/replies', {
-				post_id: this.p.id,
+				postId: this.p.id,
 				limit: 8
 			}).then(replies => {
 				this.replies = replies;
@@ -154,7 +154,7 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.client_settings.showMaps : true;
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
 					const uluru = new maps.LatLng(this.p.geo.latitude, this.p.geo.longitude);
@@ -176,7 +176,7 @@ export default Vue.extend({
 
 			// Fetch context
 			(this as any).api('posts/context', {
-				post_id: this.p.reply_id
+				postId: this.p.replyId
 			}).then(context => {
 				this.contextFetching = false;
 				this.context = context.reverse();
diff --git a/src/web/app/desktop/views/components/post-form.vue b/src/web/app/desktop/views/components/post-form.vue
index 78f6d445a..11028ceb5 100644
--- a/src/web/app/desktop/views/components/post-form.vue
+++ b/src/web/app/desktop/views/components/post-form.vue
@@ -219,9 +219,9 @@ export default Vue.extend({
 
 			(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,
-				repost_id: this.repost ? this.repost.id : undefined,
+				mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
+				replyId: this.reply ? this.reply.id : undefined,
+				repostId: this.repost ? this.repost.id : undefined,
 				poll: this.poll ? (this.$refs.poll as any).get() : undefined,
 				geo: this.geo ? {
 					latitude: this.geo.latitude,
@@ -255,7 +255,7 @@ export default Vue.extend({
 			const data = JSON.parse(localStorage.getItem('drafts') || '{}');
 
 			data[this.draftId] = {
-				updated_at: new Date(),
+				updatedAt: new Date(),
 				data: {
 					text: this.text,
 					files: this.files,
diff --git a/src/web/app/desktop/views/components/post-preview.vue b/src/web/app/desktop/views/components/post-preview.vue
index 808220c0e..450632656 100644
--- a/src/web/app/desktop/views/components/post-preview.vue
+++ b/src/web/app/desktop/views/components/post-preview.vue
@@ -1,14 +1,14 @@
 <template>
 <div class="mk-post-preview" :title="title">
 	<router-link class="avatar-anchor" :to="`/@${acct}`">
-		<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.userId"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`" v-user-preview="post.user_id">{{ post.user.name }}</router-link>
+			<router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ post.user.name }}</router-link>
 			<span class="username">@{{ acct }}</span>
 			<router-link class="time" :to="`/@${acct}/${post.id}`">
-				<mk-time :time="post.created_at"/>
+				<mk-time :time="post.createdAt"/>
 			</router-link>
 		</header>
 		<div class="body">
@@ -30,7 +30,7 @@ export default Vue.extend({
 			return getAcct(this.post.user);
 		},
 		title(): string {
-			return dateStringify(this.post.created_at);
+			return dateStringify(this.post.createdAt);
 		}
 	}
 });
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 120700877..7d2695d6b 100644
--- a/src/web/app/desktop/views/components/posts.post.sub.vue
+++ b/src/web/app/desktop/views/components/posts.post.sub.vue
@@ -1,14 +1,14 @@
 <template>
 <div class="sub" :title="title">
 	<router-link class="avatar-anchor" :to="`/@${acct}`">
-		<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.userId"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`" v-user-preview="post.user_id">{{ post.user.name }}</router-link>
+			<router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ post.user.name }}</router-link>
 			<span class="username">@{{ acct }}</span>
 			<router-link class="created-at" :to="`/@${acct}/${post.id}`">
-				<mk-time :time="post.created_at"/>
+				<mk-time :time="post.createdAt"/>
 			</router-link>
 		</header>
 		<div class="body">
@@ -30,7 +30,7 @@ export default Vue.extend({
 			return getAcct(this.post.user);
 		},
 		title(): string {
-			return dateStringify(this.post.created_at);
+			return dateStringify(this.post.createdAt);
 		}
 	}
 });
diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index 6b4d3d278..185a621aa 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -5,15 +5,15 @@
 	</div>
 	<div class="repost" v-if="isRepost">
 		<p>
-			<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="post.user_id">
+			<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="post.userId">
 				<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=32`" alt="avatar"/>
 			</router-link>
 			%fa:retweet%
 			<span>{{ '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('{')) }}</span>
-			<a class="name" :href="`/@${acct}`" v-user-preview="post.user_id">{{ post.user.name }}</a>
+			<a class="name" :href="`/@${acct}`" v-user-preview="post.userId">{{ post.user.name }}</a>
 			<span>{{ '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1) }}</span>
 		</p>
-		<mk-time :time="post.created_at"/>
+		<mk-time :time="post.createdAt"/>
 	</div>
 	<article>
 		<router-link class="avatar-anchor" :to="`/@${acct}`">
@@ -22,13 +22,13 @@
 		<div class="main">
 			<header>
 				<router-link class="name" :to="`/@${acct}`" v-user-preview="p.user.id">{{ acct }}</router-link>
-				<span class="is-bot" v-if="p.user.host === null && p.user.account.is_bot">bot</span>
+				<span class="is-bot" v-if="p.user.host === null && p.user.account.isBot">bot</span>
 				<span class="username">@{{ acct }}</span>
 				<div class="info">
 					<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
-					<span class="mobile" v-if="p.via_mobile">%fa:mobile-alt%</span>
+					<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
 					<router-link class="created-at" :to="url">
-						<mk-time :time="p.created_at"/>
+						<mk-time :time="p.createdAt"/>
 					</router-link>
 				</div>
 			</header>
@@ -122,7 +122,7 @@ export default Vue.extend({
 		isRepost(): boolean {
 			return (this.post.repost &&
 				this.post.text == null &&
-				this.post.media_ids == null &&
+				this.post.mediaIds == null &&
 				this.post.poll == null);
 		},
 		p(): any {
@@ -136,7 +136,7 @@ export default Vue.extend({
 				: 0;
 		},
 		title(): string {
-			return dateStringify(this.p.created_at);
+			return dateStringify(this.p.createdAt);
 		},
 		url(): string {
 			return `/@${this.acct}/${this.p.id}`;
@@ -166,7 +166,7 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.client_settings.showMaps : true;
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
 					const uluru = new maps.LatLng(this.p.geo.latitude, this.p.geo.longitude);
@@ -216,7 +216,7 @@ export default Vue.extend({
 			const post = data.post;
 			if (post.id == this.post.id) {
 				this.$emit('update:post', post);
-			} else if (post.id == this.post.repost_id) {
+			} else if (post.id == this.post.repostId) {
 				this.post.repost = post;
 			}
 		},
diff --git a/src/web/app/desktop/views/components/posts.vue b/src/web/app/desktop/views/components/posts.vue
index ffceff876..5031667c7 100644
--- a/src/web/app/desktop/views/components/posts.vue
+++ b/src/web/app/desktop/views/components/posts.vue
@@ -30,8 +30,8 @@ export default Vue.extend({
 	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;
+				const date = new Date(post.createdAt).getDate();
+				const month = new Date(post.createdAt).getMonth() + 1;
 				post._date = date;
 				post._datetext = `${month}月 ${date}日`;
 				return post;
diff --git a/src/web/app/desktop/views/components/repost-form.vue b/src/web/app/desktop/views/components/repost-form.vue
index f2774b817..3a5e3a7c5 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({
 		ok() {
 			this.wait = true;
 			(this as any).api('posts/create', {
-				repost_id: this.post.id
+				repostId: this.post.id
 			}).then(data => {
 				this.$emit('posted');
 				(this as any).apis.notify('%i18n:desktop.tags.mk-repost-form.success%');
diff --git a/src/web/app/desktop/views/components/settings.2fa.vue b/src/web/app/desktop/views/components/settings.2fa.vue
index 85f2d6ba5..b8dd1dfd9 100644
--- a/src/web/app/desktop/views/components/settings.2fa.vue
+++ b/src/web/app/desktop/views/components/settings.2fa.vue
@@ -2,8 +2,8 @@
 <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 && !os.i.account.two_factor_enabled"><button @click="register" class="ui primary">%i18n:desktop.tags.mk-2fa-setting.register%</button></p>
-	<template v-if="os.i.account.two_factor_enabled">
+	<p v-if="!data && !os.i.account.twoFactorEnabled"><button @click="register" class="ui primary">%i18n:desktop.tags.mk-2fa-setting.register%</button></p>
+	<template v-if="os.i.account.twoFactorEnabled">
 		<p>%i18n:desktop.tags.mk-2fa-setting.already-registered%</p>
 		<button @click="unregister" class="ui">%i18n:desktop.tags.mk-2fa-setting.unregister%</button>
 	</template>
@@ -54,7 +54,7 @@ export default Vue.extend({
 					password: password
 				}).then(() => {
 					(this as any).apis.notify('%i18n:desktop.tags.mk-2fa-setting.unregistered%');
-					(this as any).os.i.account.two_factor_enabled = false;
+					(this as any).os.i.account.twoFactorEnabled = false;
 				});
 			});
 		},
@@ -64,7 +64,7 @@ export default Vue.extend({
 				token: this.token
 			}).then(() => {
 				(this as any).apis.notify('%i18n:desktop.tags.mk-2fa-setting.success%');
-				(this as any).os.i.account.two_factor_enabled = true;
+				(this as any).os.i.account.twoFactorEnabled = true;
 			}).catch(() => {
 				(this as any).apis.notify('%i18n:desktop.tags.mk-2fa-setting.failed%');
 			});
diff --git a/src/web/app/desktop/views/components/settings.profile.vue b/src/web/app/desktop/views/components/settings.profile.vue
index 67a211c79..f34d8ff00 100644
--- a/src/web/app/desktop/views/components/settings.profile.vue
+++ b/src/web/app/desktop/views/components/settings.profile.vue
@@ -24,7 +24,7 @@
 	<button class="ui primary" @click="save">%i18n:desktop.tags.mk-profile-setting.save%</button>
 	<section>
 		<h2>その他</h2>
-		<mk-switch v-model="os.i.account.is_bot" @change="onChangeIsBot" text="このアカウントはbotです"/>
+		<mk-switch v-model="os.i.account.isBot" @change="onChangeIsBot" text="このアカウントはbotです"/>
 	</section>
 </div>
 </template>
@@ -63,7 +63,7 @@ export default Vue.extend({
 		},
 		onChangeIsBot() {
 			(this as any).api('i/update', {
-				is_bot: (this as any).os.i.account.is_bot
+				isBot: (this as any).os.i.account.isBot
 			});
 		}
 	}
diff --git a/src/web/app/desktop/views/components/settings.signins.vue b/src/web/app/desktop/views/components/settings.signins.vue
index ddc567f06..a414c95c2 100644
--- a/src/web/app/desktop/views/components/settings.signins.vue
+++ b/src/web/app/desktop/views/components/settings.signins.vue
@@ -6,7 +6,7 @@
 			<template v-if="signin.success">%fa:check%</template>
 			<template v-else>%fa:times%</template>
 			<span class="ip">{{ signin.ip }}</span>
-			<mk-time :time="signin.created_at"/>
+			<mk-time :time="signin.createdAt"/>
 		</header>
 		<div class="headers" v-show="signin._show">
 			<tree-view :data="signin.headers"/>
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index 3e6a477ce..cf75e52be 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -20,7 +20,7 @@
 
 		<section class="web" v-show="page == 'web'">
 			<h1>動作</h1>
-			<mk-switch v-model="os.i.account.client_settings.fetchOnScroll" @change="onChangeFetchOnScroll" text="スクロールで自動読み込み">
+			<mk-switch v-model="os.i.account.clientSettings.fetchOnScroll" @change="onChangeFetchOnScroll" text="スクロールで自動読み込み">
 				<span>ページを下までスクロールしたときに自動で追加のコンテンツを読み込みます。</span>
 			</mk-switch>
 			<mk-switch v-model="autoPopout" text="ウィンドウの自動ポップアウト">
@@ -33,11 +33,11 @@
 			<div class="div">
 				<button class="ui button" @click="customizeHome">ホームをカスタマイズ</button>
 			</div>
-			<mk-switch v-model="os.i.account.client_settings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="タイムライン上部に投稿フォームを表示する"/>
-			<mk-switch v-model="os.i.account.client_settings.showMaps" @change="onChangeShowMaps" text="マップの自動展開">
+			<mk-switch v-model="os.i.account.clientSettings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="タイムライン上部に投稿フォームを表示する"/>
+			<mk-switch v-model="os.i.account.clientSettings.showMaps" @change="onChangeShowMaps" text="マップの自動展開">
 				<span>位置情報が添付された投稿のマップを自動的に展開します。</span>
 			</mk-switch>
-			<mk-switch v-model="os.i.account.client_settings.gradientWindowHeader" @change="onChangeGradientWindowHeader" text="ウィンドウのタイトルバーにグラデーションを使用"/>
+			<mk-switch v-model="os.i.account.clientSettings.gradientWindowHeader" @change="onChangeGradientWindowHeader" text="ウィンドウのタイトルバーにグラデーションを使用"/>
 		</section>
 
 		<section class="web" v-show="page == 'web'">
@@ -57,7 +57,7 @@
 
 		<section class="web" v-show="page == 'web'">
 			<h1>モバイル</h1>
-			<mk-switch v-model="os.i.account.client_settings.disableViaMobile" @change="onChangeDisableViaMobile" text="「モバイルからの投稿」フラグを付けない"/>
+			<mk-switch v-model="os.i.account.clientSettings.disableViaMobile" @change="onChangeDisableViaMobile" text="「モバイルからの投稿」フラグを付けない"/>
 		</section>
 
 		<section class="web" v-show="page == 'web'">
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 8c8f42c80..f13822331 100644
--- a/src/web/app/desktop/views/components/sub-post-content.vue
+++ b/src/web/app/desktop/views/components/sub-post-content.vue
@@ -1,9 +1,9 @@
 <template>
 <div class="mk-sub-post-content">
 	<div class="body">
-		<a class="reply" v-if="post.reply_id">%fa:reply%</a>
+		<a class="reply" v-if="post.replyId">%fa:reply%</a>
 		<mk-post-html :ast="post.ast" :i="os.i"/>
-		<a class="rp" v-if="post.repost_id" :href="`/post:${post.repost_id}`">RP: ...</a>
+		<a class="rp" v-if="post.repostId" :href="`/post:${post.repostId}`">RP: ...</a>
 		<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 	</div>
 	<details v-if="post.media">
diff --git a/src/web/app/desktop/views/components/timeline.vue b/src/web/app/desktop/views/components/timeline.vue
index 47a9688b6..c0eae2cd9 100644
--- a/src/web/app/desktop/views/components/timeline.vue
+++ b/src/web/app/desktop/views/components/timeline.vue
@@ -34,7 +34,7 @@ export default Vue.extend({
 	},
 	computed: {
 		alone(): boolean {
-			return (this as any).os.i.following_count == 0;
+			return (this as any).os.i.followingCount == 0;
 		}
 	},
 	mounted() {
@@ -107,7 +107,7 @@ export default Vue.extend({
 			this.fetch();
 		},
 		onScroll() {
-			if ((this as any).os.i.account.client_settings.fetchOnScroll !== false) {
+			if ((this as any).os.i.account.clientSettings.fetchOnScroll !== false) {
 				const current = window.scrollY + window.innerHeight;
 				if (current > document.body.offsetHeight - 8) this.more();
 			}
diff --git a/src/web/app/desktop/views/components/ui.header.vue b/src/web/app/desktop/views/components/ui.header.vue
index 8af0e2fbe..7e337d2ae 100644
--- a/src/web/app/desktop/views/components/ui.header.vue
+++ b/src/web/app/desktop/views/components/ui.header.vue
@@ -44,9 +44,9 @@ export default Vue.extend({
 	},
 	mounted() {
 		if ((this as any).os.isSignedIn) {
-			const ago = (new Date().getTime() - new Date((this as any).os.i.account.last_used_at).getTime()) / 1000
+			const ago = (new Date().getTime() - new Date((this as any).os.i.account.lastUsedAt).getTime()) / 1000
 			const isHisasiburi = ago >= 3600;
-			(this as any).os.i.account.last_used_at = new Date();
+			(this as any).os.i.account.lastUsedAt = new Date();
 			if (isHisasiburi) {
 				(this.$refs.welcomeback as any).style.display = 'block';
 				(this.$refs.main as any).style.overflow = 'hidden';
diff --git a/src/web/app/desktop/views/components/user-preview.vue b/src/web/app/desktop/views/components/user-preview.vue
index 24d613f12..4535ad890 100644
--- a/src/web/app/desktop/views/components/user-preview.vue
+++ b/src/web/app/desktop/views/components/user-preview.vue
@@ -12,13 +12,13 @@
 		<div class="description">{{ u.description }}</div>
 		<div class="status">
 			<div>
-				<p>投稿</p><a>{{ u.posts_count }}</a>
+				<p>投稿</p><a>{{ u.postsCount }}</a>
 			</div>
 			<div>
-				<p>フォロー</p><a>{{ u.following_count }}</a>
+				<p>フォロー</p><a>{{ u.followingCount }}</a>
 			</div>
 			<div>
-				<p>フォロワー</p><a>{{ u.followers_count }}</a>
+				<p>フォロワー</p><a>{{ u.followersCount }}</a>
 			</div>
 		</div>
 		<mk-follow-button v-if="os.isSignedIn && user.id != os.i.id" :user="u"/>
@@ -58,7 +58,7 @@ export default Vue.extend({
 		} else {
 			const query = this.user[0] == '@' ?
 				parseAcct(this.user[0].substr(1)) :
-				{ user_id: this.user[0] };
+				{ userId: this.user[0] };
 
 			(this as any).api('users/show', query).then(user => {
 				this.u = user;
diff --git a/src/web/app/desktop/views/components/widget-container.vue b/src/web/app/desktop/views/components/widget-container.vue
index dd42be63b..68c5bcb8d 100644
--- a/src/web/app/desktop/views/components/widget-container.vue
+++ b/src/web/app/desktop/views/components/widget-container.vue
@@ -24,8 +24,8 @@ export default Vue.extend({
 	computed: {
 		withGradient(): boolean {
 			return (this as any).os.isSignedIn
-				? (this as any).os.i.account.client_settings.gradientWindowHeader != null
-					? (this as any).os.i.account.client_settings.gradientWindowHeader
+				? (this as any).os.i.account.clientSettings.gradientWindowHeader != null
+					? (this as any).os.i.account.clientSettings.gradientWindowHeader
 					: false
 				: false;
 		}
diff --git a/src/web/app/desktop/views/components/window.vue b/src/web/app/desktop/views/components/window.vue
index 75f725d4b..48dc46feb 100644
--- a/src/web/app/desktop/views/components/window.vue
+++ b/src/web/app/desktop/views/components/window.vue
@@ -92,8 +92,8 @@ export default Vue.extend({
 		},
 		withGradient(): boolean {
 			return (this as any).os.isSignedIn
-				? (this as any).os.i.account.client_settings.gradientWindowHeader != null
-					? (this as any).os.i.account.client_settings.gradientWindowHeader
+				? (this as any).os.i.account.clientSettings.gradientWindowHeader != null
+					? (this as any).os.i.account.clientSettings.gradientWindowHeader
 					: false
 				: false;
 		}
diff --git a/src/web/app/desktop/views/pages/home.vue b/src/web/app/desktop/views/pages/home.vue
index e1464bab1..69e134f79 100644
--- a/src/web/app/desktop/views/pages/home.vue
+++ b/src/web/app/desktop/views/pages/home.vue
@@ -45,7 +45,7 @@ export default Vue.extend({
 		},
 
 		onStreamPost(post) {
-			if (document.hidden && post.user_id != (this as any).os.i.id) {
+			if (document.hidden && post.userId != (this as any).os.i.id) {
 				this.unreadCount++;
 				document.title = `(${this.unreadCount}) ${getPostSummary(post)}`;
 			}
diff --git a/src/web/app/desktop/views/pages/post.vue b/src/web/app/desktop/views/pages/post.vue
index c7b8729b7..dbd707e04 100644
--- a/src/web/app/desktop/views/pages/post.vue
+++ b/src/web/app/desktop/views/pages/post.vue
@@ -31,7 +31,7 @@ export default Vue.extend({
 			this.fetching = true;
 
 			(this as any).api('posts/show', {
-				post_id: this.$route.params.post
+				postId: this.$route.params.post
 			}).then(post => {
 				this.post = post;
 				this.fetching = false;
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 80b38e8ac..9f67f5cf7 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
@@ -28,7 +28,7 @@ export default Vue.extend({
 	},
 	mounted() {
 		(this as any).api('users/followers', {
-			user_id: this.user.id,
+			userId: this.user.id,
 			iknow: true,
 			limit: 16
 		}).then(x => {
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 57e6def27..cc0bcef25 100644
--- a/src/web/app/desktop/views/pages/user/user.friends.vue
+++ b/src/web/app/desktop/views/pages/user/user.friends.vue
@@ -35,7 +35,7 @@ export default Vue.extend({
 	},
 	mounted() {
 		(this as any).api('users/get_frequently_replied_users', {
-			user_id: this.user.id,
+			userId: this.user.id,
 			limit: 4
 		}).then(docs => {
 			this.users = docs.map(doc => doc.user);
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 2483a6c72..071c9bb61 100644
--- a/src/web/app/desktop/views/pages/user/user.home.vue
+++ b/src/web/app/desktop/views/pages/user/user.home.vue
@@ -5,16 +5,16 @@
 			<x-profile :user="user"/>
 			<x-photos :user="user"/>
 			<x-followers-you-know v-if="os.isSignedIn && os.i.id != user.id" :user="user"/>
-			<p v-if="user.host === null">%i18n:desktop.tags.mk-user.last-used-at%: <b><mk-time :time="user.account.last_used_at"/></b></p>
+			<p v-if="user.host === null">%i18n:desktop.tags.mk-user.last-used-at%: <b><mk-time :time="user.account.lastUsedAt"/></b></p>
 		</div>
 	</div>
 	<main>
-		<mk-post-detail v-if="user.pinned_post" :post="user.pinned_post" :compact="true"/>
+		<mk-post-detail v-if="user.pinnedPost" :post="user.pinnedPost" :compact="true"/>
 		<x-timeline class="timeline" ref="tl" :user="user"/>
 	</main>
 	<div>
 		<div ref="right">
-			<mk-calendar @chosen="warp" :start="new Date(user.created_at)"/>
+			<mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/>
 			<mk-activity :user="user"/>
 			<x-friends :user="user"/>
 			<div class="nav"><mk-nav/></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 db29a9945..2baf042bc 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 as any).api('users/posts', {
-			user_id: this.user.id,
+			userId: this.user.id,
 			with_media: true,
 			limit: 9
 		}).then(posts => {
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 b51aae18f..0d91df2a5 100644
--- a/src/web/app/desktop/views/pages/user/user.profile.vue
+++ b/src/web/app/desktop/views/pages/user/user.profile.vue
@@ -11,12 +11,12 @@
 		<p>%fa:birthday-cake%{{ user.account.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳)</p>
 	</div>
 	<div class="twitter" v-if="user.host === null && user.account.twitter">
-		<p>%fa:B twitter%<a :href="`https://twitter.com/${user.account.twitter.screen_name}`" target="_blank">@{{ user.account.twitter.screen_name }}</a></p>
+		<p>%fa:B twitter%<a :href="`https://twitter.com/${user.account.twitter.screenName}`" target="_blank">@{{ user.account.twitter.screenName }}</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>
+		<p class="posts-count">%fa:angle-right%<a>{{ user.postsCount }}</a><b>投稿</b></p>
+		<p class="following">%fa:angle-right%<a @click="showFollowing">{{ user.followingCount }}</a>人を<b>フォロー</b></p>
+		<p class="followers">%fa:angle-right%<a @click="showFollowers">{{ user.followersCount }}</a>人の<b>フォロワー</b></p>
 	</div>
 </div>
 </template>
@@ -49,7 +49,7 @@ export default Vue.extend({
 
 		mute() {
 			(this as any).api('mute/create', {
-				user_id: this.user.id
+				userId: this.user.id
 			}).then(() => {
 				this.user.is_muted = true;
 			}, () => {
@@ -59,7 +59,7 @@ export default Vue.extend({
 
 		unmute() {
 			(this as any).api('mute/delete', {
-				user_id: this.user.id
+				userId: this.user.id
 			}).then(() => {
 				this.user.is_muted = false;
 			}, () => {
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 60eef8951..1f0d0b198 100644
--- a/src/web/app/desktop/views/pages/user/user.timeline.vue
+++ b/src/web/app/desktop/views/pages/user/user.timeline.vue
@@ -61,7 +61,7 @@ export default Vue.extend({
 		},
 		fetch(cb?) {
 			(this as any).api('users/posts', {
-				user_id: this.user.id,
+				userId: this.user.id,
 				until_date: this.date ? this.date.getTime() : undefined,
 				with_replies: this.mode == 'with-replies'
 			}).then(posts => {
@@ -74,7 +74,7 @@ export default Vue.extend({
 			if (this.moreFetching || this.fetching || this.posts.length == 0) return;
 			this.moreFetching = true;
 			(this as any).api('users/posts', {
-				user_id: this.user.id,
+				userId: this.user.id,
 				with_replies: this.mode == 'with-replies',
 				until_id: this.posts[this.posts.length - 1].id
 			}).then(posts => {
diff --git a/src/web/app/desktop/views/widgets/channel.channel.form.vue b/src/web/app/desktop/views/widgets/channel.channel.form.vue
index 392ba5924..aaf327f1e 100644
--- a/src/web/app/desktop/views/widgets/channel.channel.form.vue
+++ b/src/web/app/desktop/views/widgets/channel.channel.form.vue
@@ -30,8 +30,8 @@ export default Vue.extend({
 
 			(this as any).api('posts/create', {
 				text: this.text,
-				reply_id: reply ? reply.id : undefined,
-				channel_id: (this.$parent as any).channel.id
+				replyId: reply ? reply.id : undefined,
+				channelId: (this.$parent as any).channel.id
 			}).then(data => {
 				this.text = '';
 			}).catch(err => {
diff --git a/src/web/app/desktop/views/widgets/channel.channel.vue b/src/web/app/desktop/views/widgets/channel.channel.vue
index de5885bfc..e9fb9e3fd 100644
--- a/src/web/app/desktop/views/widgets/channel.channel.vue
+++ b/src/web/app/desktop/views/widgets/channel.channel.vue
@@ -44,7 +44,7 @@ export default Vue.extend({
 			this.fetching = true;
 
 			(this as any).api('channels/posts', {
-				channel_id: this.channel.id
+				channelId: this.channel.id
 			}).then(posts => {
 				this.posts = posts;
 				this.fetching = false;
diff --git a/src/web/app/desktop/views/widgets/channel.vue b/src/web/app/desktop/views/widgets/channel.vue
index fc143bb1d..c9b62dfea 100644
--- a/src/web/app/desktop/views/widgets/channel.vue
+++ b/src/web/app/desktop/views/widgets/channel.vue
@@ -48,7 +48,7 @@ export default define({
 			this.fetching = true;
 
 			(this as any).api('channels/show', {
-				channel_id: this.props.channel
+				channelId: this.props.channel
 			}).then(channel => {
 				this.channel = channel;
 				this.fetching = false;
diff --git a/src/web/app/dev/views/app.vue b/src/web/app/dev/views/app.vue
index 2c2a3c83c..a35b032b7 100644
--- a/src/web/app/dev/views/app.vue
+++ b/src/web/app/dev/views/app.vue
@@ -28,7 +28,7 @@ export default Vue.extend({
 		fetch() {
 			this.fetching = true;
 			(this as any).api('app/show', {
-				app_id: this.$route.params.id
+				appId: this.$route.params.id
 			}).then(app => {
 				this.app = app;
 				this.fetching = false;
diff --git a/src/web/app/dev/views/new-app.vue b/src/web/app/dev/views/new-app.vue
index 1a796299c..cd07cc4d4 100644
--- a/src/web/app/dev/views/new-app.vue
+++ b/src/web/app/dev/views/new-app.vue
@@ -77,8 +77,8 @@ export default Vue.extend({
 
 			this.nidState = 'wait';
 
-			(this as any).api('app/name_id/available', {
-				name_id: this.nid
+			(this as any).api('app/nameId/available', {
+				nameId: this.nid
 			}).then(result => {
 				this.nidState = result.available ? 'ok' : 'unavailable';
 			}).catch(err => {
@@ -90,7 +90,7 @@ export default Vue.extend({
 		onSubmit() {
 			(this as any).api('app/create', {
 				name: this.name,
-				name_id: this.nid,
+				nameId: this.nid,
 				description: this.description,
 				callback_url: this.cb,
 				permission: this.permission
diff --git a/src/web/app/mobile/api/post.ts b/src/web/app/mobile/api/post.ts
index 9b78ce10c..841103fee 100644
--- a/src/web/app/mobile/api/post.ts
+++ b/src/web/app/mobile/api/post.ts
@@ -18,7 +18,7 @@ export default (os) => (opts) => {
 		const text = window.prompt(`「${getPostSummary(o.repost)}」をRepost`);
 		if (text == null) return;
 		os.api('posts/create', {
-			repost_id: o.repost.id,
+			repostId: o.repost.id,
 			text: text == '' ? undefined : text
 		});
 	} else {
diff --git a/src/web/app/mobile/views/components/activity.vue b/src/web/app/mobile/views/components/activity.vue
index b50044b3d..2e44017e7 100644
--- a/src/web/app/mobile/views/components/activity.vue
+++ b/src/web/app/mobile/views/components/activity.vue
@@ -29,7 +29,7 @@ export default Vue.extend({
 	},
 	mounted() {
 		(this as any).api('aggregation/users/activity', {
-			user_id: this.user.id,
+			userId: this.user.id,
 			limit: 30
 		}).then(data => {
 			data.forEach(d => d.total = d.posts + d.replies + d.reposts);
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 e41ebbb45..f3274f677 100644
--- a/src/web/app/mobile/views/components/drive.file-detail.vue
+++ b/src/web/app/mobile/views/components/drive.file-detail.vue
@@ -29,7 +29,7 @@
 			<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>
+			<span class="created-at" @click="showCreatedAt">%fa:R clock%<mk-time :time="file.createdAt"/></span>
 		</div>
 	</div>
 	<div class="menu">
@@ -86,8 +86,8 @@ export default Vue.extend({
 			return this.file.type.split('/')[0];
 		},
 		style(): any {
-			return this.file.properties.average_color ? {
-				'background-color': `rgb(${ this.file.properties.average_color.join(',') })`
+			return this.file.properties.avgColor ? {
+				'background-color': `rgb(${ this.file.properties.avgColor.join(',') })`
 			} : {};
 		}
 	},
@@ -96,7 +96,7 @@ export default Vue.extend({
 			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,
+				fileId: this.file.id,
 				name: name
 			}).then(() => {
 				this.browser.cf(this.file, true);
@@ -105,15 +105,15 @@ export default Vue.extend({
 		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
+					fileId: this.file.id,
+					folderId: folder == null ? null : folder.id
 				}).then(() => {
 					this.browser.cf(this.file, true);
 				});
 			});
 		},
 		showCreatedAt() {
-			alert(new Date(this.file.created_at).toLocaleString());
+			alert(new Date(this.file.createdAt).toLocaleString());
 		},
 		onImageLoaded() {
 			const self = this;
diff --git a/src/web/app/mobile/views/components/drive.file.vue b/src/web/app/mobile/views/components/drive.file.vue
index db7381628..7d1957042 100644
--- a/src/web/app/mobile/views/components/drive.file.vue
+++ b/src/web/app/mobile/views/components/drive.file.vue
@@ -19,7 +19,7 @@
 				<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"/>
+					%fa:R clock%<mk-time :time="file.createdAt"/>
 				</p>
 			</footer>
 		</div>
@@ -42,7 +42,7 @@ export default Vue.extend({
 		},
 		thumbnail(): any {
 			return {
-				'background-color': this.file.properties.average_color ? `rgb(${this.file.properties.average_color.join(',')})` : 'transparent',
+				'background-color': this.file.properties.avgColor ? `rgb(${this.file.properties.avgColor.join(',')})` : 'transparent',
 				'background-image': `url(${this.file.url}?thumbnail&size=128)`
 			};
 		}
diff --git a/src/web/app/mobile/views/components/drive.vue b/src/web/app/mobile/views/components/drive.vue
index 696c63e2a..dd4d97e96 100644
--- a/src/web/app/mobile/views/components/drive.vue
+++ b/src/web/app/mobile/views/components/drive.vue
@@ -129,7 +129,7 @@ export default Vue.extend({
 
 		onStreamDriveFileUpdated(file) {
 			const current = this.folder ? this.folder.id : null;
-			if (current != file.folder_id) {
+			if (current != file.folderId) {
 				this.removeFile(file);
 			} else {
 				this.addFile(file, true);
@@ -142,7 +142,7 @@ export default Vue.extend({
 
 		onStreamDriveFolderUpdated(folder) {
 			const current = this.folder ? this.folder.id : null;
-			if (current != folder.parent_id) {
+			if (current != folder.parentId) {
 				this.removeFolder(folder);
 			} else {
 				this.addFolder(folder, true);
@@ -167,7 +167,7 @@ export default Vue.extend({
 			this.fetching = true;
 
 			(this as any).api('drive/folders/show', {
-				folder_id: target
+				folderId: target
 			}).then(folder => {
 				this.folder = folder;
 				this.hierarchyFolders = [];
@@ -182,7 +182,7 @@ export default Vue.extend({
 		addFolder(folder, unshift = false) {
 			const current = this.folder ? this.folder.id : null;
 			// 追加しようとしているフォルダが、今居る階層とは違う階層のものだったら中断
-			if (current != folder.parent_id) return;
+			if (current != folder.parentId) return;
 
 			// 追加しようとしているフォルダを既に所有してたら中断
 			if (this.folders.some(f => f.id == folder.id)) return;
@@ -197,7 +197,7 @@ export default Vue.extend({
 		addFile(file, unshift = false) {
 			const current = this.folder ? this.folder.id : null;
 			// 追加しようとしているファイルが、今居る階層とは違う階層のものだったら中断
-			if (current != file.folder_id) return;
+			if (current != file.folderId) return;
 
 			if (this.files.some(f => f.id == file.id)) {
 				const exist = this.files.map(f => f.id).indexOf(file.id);
@@ -262,7 +262,7 @@ export default Vue.extend({
 
 			// フォルダ一覧取得
 			(this as any).api('drive/folders', {
-				folder_id: this.folder ? this.folder.id : null,
+				folderId: this.folder ? this.folder.id : null,
 				limit: foldersMax + 1
 			}).then(folders => {
 				if (folders.length == foldersMax + 1) {
@@ -275,7 +275,7 @@ export default Vue.extend({
 
 			// ファイル一覧取得
 			(this as any).api('drive/files', {
-				folder_id: this.folder ? this.folder.id : null,
+				folderId: this.folder ? this.folder.id : null,
 				limit: filesMax + 1
 			}).then(files => {
 				if (files.length == filesMax + 1) {
@@ -318,7 +318,7 @@ export default Vue.extend({
 
 			// ファイル一覧取得
 			(this as any).api('drive/files', {
-				folder_id: this.folder ? this.folder.id : null,
+				folderId: this.folder ? this.folder.id : null,
 				limit: max + 1,
 				until_id: this.files[this.files.length - 1].id
 			}).then(files => {
@@ -357,7 +357,7 @@ export default Vue.extend({
 			this.fetching = true;
 
 			(this as any).api('drive/files/show', {
-				file_id: file
+				fileId: file
 			}).then(file => {
 				this.file = file;
 				this.folder = null;
@@ -405,7 +405,7 @@ export default Vue.extend({
 			if (name == null || name == '') return;
 			(this as any).api('drive/folders/create', {
 				name: name,
-				parent_id: this.folder ? this.folder.id : undefined
+				parentId: this.folder ? this.folder.id : undefined
 			}).then(folder => {
 				this.addFolder(folder, true);
 			});
@@ -420,7 +420,7 @@ export default Vue.extend({
 			if (name == null || name == '') return;
 			(this as any).api('drive/folders/update', {
 				name: name,
-				folder_id: this.folder.id
+				folderId: this.folder.id
 			}).then(folder => {
 				this.cd(folder);
 			});
@@ -433,8 +433,8 @@ export default Vue.extend({
 			}
 			(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
+					parentId: folder ? folder.id : null,
+					folderId: this.folder.id
 				}).then(folder => {
 					this.cd(folder);
 				});
@@ -446,7 +446,7 @@ export default Vue.extend({
 			if (url == null || url == '') return;
 			(this as any).api('drive/files/upload_from_url', {
 				url: url,
-				folder_id: this.folder ? this.folder.id : undefined
+				folderId: this.folder ? this.folder.id : undefined
 			});
 			alert('アップロードをリクエストしました。アップロードが完了するまで時間がかかる場合があります。');
 		},
diff --git a/src/web/app/mobile/views/components/follow-button.vue b/src/web/app/mobile/views/components/follow-button.vue
index fb6eaa39c..838ea404e 100644
--- a/src/web/app/mobile/views/components/follow-button.vue
+++ b/src/web/app/mobile/views/components/follow-button.vue
@@ -57,7 +57,7 @@ export default Vue.extend({
 			this.wait = true;
 			if (this.user.is_following) {
 				(this as any).api('following/delete', {
-					user_id: this.user.id
+					userId: this.user.id
 				}).then(() => {
 					this.user.is_following = false;
 				}).catch(err => {
@@ -67,7 +67,7 @@ export default Vue.extend({
 				});
 			} else {
 				(this as any).api('following/create', {
-					user_id: this.user.id
+					userId: this.user.id
 				}).then(() => {
 					this.user.is_following = true;
 				}).catch(err => {
diff --git a/src/web/app/mobile/views/components/media-image.vue b/src/web/app/mobile/views/components/media-image.vue
index faf8bad48..cfc213498 100644
--- a/src/web/app/mobile/views/components/media-image.vue
+++ b/src/web/app/mobile/views/components/media-image.vue
@@ -10,7 +10,7 @@ export default Vue.extend({
 	computed: {
 		style(): any {
 			return {
-				'background-color': this.image.properties.average_color ? `rgb(${this.image.properties.average_color.join(',')})` : 'transparent',
+				'background-color': this.image.properties.avgColor ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
 				'background-image': `url(${this.image.url}?thumbnail&size=512)`
 			};
 		}
diff --git a/src/web/app/mobile/views/components/notification.vue b/src/web/app/mobile/views/components/notification.vue
index 150ac0fd8..aac8f8290 100644
--- a/src/web/app/mobile/views/components/notification.vue
+++ b/src/web/app/mobile/views/components/notification.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mk-notification">
 	<div class="notification reaction" v-if="notification.type == 'reaction'">
-		<mk-time :time="notification.created_at"/>
+		<mk-time :time="notification.createdAt"/>
 		<router-link class="avatar-anchor" :to="`/@${acct}`">
 			<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
@@ -18,7 +18,7 @@
 	</div>
 
 	<div class="notification repost" v-if="notification.type == 'repost'">
-		<mk-time :time="notification.created_at"/>
+		<mk-time :time="notification.createdAt"/>
 		<router-link class="avatar-anchor" :to="`/@${acct}`">
 			<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
@@ -38,7 +38,7 @@
 	</template>
 
 	<div class="notification follow" v-if="notification.type == 'follow'">
-		<mk-time :time="notification.created_at"/>
+		<mk-time :time="notification.createdAt"/>
 		<router-link class="avatar-anchor" :to="`/@${acct}`">
 			<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
@@ -59,7 +59,7 @@
 	</template>
 
 	<div class="notification poll_vote" v-if="notification.type == 'poll_vote'">
-		<mk-time :time="notification.created_at"/>
+		<mk-time :time="notification.createdAt"/>
 		<router-link class="avatar-anchor" :to="`/@${acct}`">
 			<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
diff --git a/src/web/app/mobile/views/components/notifications.vue b/src/web/app/mobile/views/components/notifications.vue
index 1cd6e2bc1..4365198f1 100644
--- a/src/web/app/mobile/views/components/notifications.vue
+++ b/src/web/app/mobile/views/components/notifications.vue
@@ -34,8 +34,8 @@ export default Vue.extend({
 	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;
+				const date = new Date(notification.createdAt).getDate();
+				const month = new Date(notification.createdAt).getMonth() + 1;
 				notification._date = date;
 				notification._datetext = `${month}月 ${date}日`;
 				return notification;
diff --git a/src/web/app/mobile/views/components/post-card.vue b/src/web/app/mobile/views/components/post-card.vue
index 8ca7550c2..10dfd9241 100644
--- a/src/web/app/mobile/views/components/post-card.vue
+++ b/src/web/app/mobile/views/components/post-card.vue
@@ -7,7 +7,7 @@
 		<div>
 			{{ text }}
 		</div>
-		<mk-time :time="post.created_at"/>
+		<mk-time :time="post.createdAt"/>
 	</a>
 </div>
 </template>
diff --git a/src/web/app/mobile/views/components/post-detail.sub.vue b/src/web/app/mobile/views/components/post-detail.sub.vue
index 6906cf570..427e054fd 100644
--- a/src/web/app/mobile/views/components/post-detail.sub.vue
+++ b/src/web/app/mobile/views/components/post-detail.sub.vue
@@ -8,7 +8,7 @@
 			<router-link class="name" :to="`/@${acct}`">{{ post.user.name }}</router-link>
 			<span class="username">@{{ acct }}</span>
 			<router-link class="time" :to="`/@${acct}/${post.id}`">
-				<mk-time :time="post.created_at"/>
+				<mk-time :time="post.createdAt"/>
 			</router-link>
 		</header>
 		<div class="body">
diff --git a/src/web/app/mobile/views/components/post-detail.vue b/src/web/app/mobile/views/components/post-detail.vue
index b5c915830..9a8c33a80 100644
--- a/src/web/app/mobile/views/components/post-detail.vue
+++ b/src/web/app/mobile/views/components/post-detail.vue
@@ -2,7 +2,7 @@
 <div class="mk-post-detail">
 	<button
 		class="more"
-		v-if="p.reply && p.reply.reply_id && context == null"
+		v-if="p.reply && p.reply.replyId && context == null"
 		@click="fetchContext"
 		:disabled="fetchingContext"
 	>
@@ -54,7 +54,7 @@
 			</div>
 		</div>
 		<router-link class="time" :to="`/@${pAcct}/${p.id}`">
-			<mk-time :time="p.created_at" mode="detail"/>
+			<mk-time :time="p.createdAt" mode="detail"/>
 		</router-link>
 		<footer>
 			<mk-reactions-viewer :post="p"/>
@@ -115,7 +115,7 @@ export default Vue.extend({
 		isRepost(): boolean {
 			return (this.post.repost &&
 				this.post.text == null &&
-				this.post.media_ids == null &&
+				this.post.mediaIds == null &&
 				this.post.poll == null);
 		},
 		p(): any {
@@ -142,7 +142,7 @@ export default Vue.extend({
 		// Get replies
 		if (!this.compact) {
 			(this as any).api('posts/replies', {
-				post_id: this.p.id,
+				postId: this.p.id,
 				limit: 8
 			}).then(replies => {
 				this.replies = replies;
@@ -151,7 +151,7 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.client_settings.showMaps : true;
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
 					const uluru = new maps.LatLng(this.p.geo.latitude, this.p.geo.longitude);
@@ -173,7 +173,7 @@ export default Vue.extend({
 
 			// Fetch context
 			(this as any).api('posts/context', {
-				post_id: this.p.reply_id
+				postId: this.p.replyId
 			}).then(context => {
 				this.contextFetching = false;
 				this.context = context.reverse();
diff --git a/src/web/app/mobile/views/components/post-form.vue b/src/web/app/mobile/views/components/post-form.vue
index 2aa3c6f6c..929dc5933 100644
--- a/src/web/app/mobile/views/components/post-form.vue
+++ b/src/web/app/mobile/views/components/post-form.vue
@@ -111,11 +111,11 @@ export default Vue.extend({
 		},
 		post() {
 			this.posting = true;
-			const viaMobile = (this as any).os.i.account.client_settings.disableViaMobile !== true;
+			const viaMobile = (this as any).os.i.account.clientSettings.disableViaMobile !== 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,
+				mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
+				replyId: this.reply ? this.reply.id : undefined,
 				poll: this.poll ? (this.$refs.poll as any).get() : undefined,
 				geo: this.geo ? {
 					latitude: this.geo.latitude,
@@ -126,7 +126,7 @@ export default Vue.extend({
 					heading: isNaN(this.geo.heading) ? null : this.geo.heading,
 					speed: this.geo.speed,
 				} : null,
-				via_mobile: viaMobile
+				viaMobile: viaMobile
 			}).then(data => {
 				this.$emit('post');
 				this.$destroy();
diff --git a/src/web/app/mobile/views/components/post-preview.vue b/src/web/app/mobile/views/components/post-preview.vue
index 0bd0a355b..e64084341 100644
--- a/src/web/app/mobile/views/components/post-preview.vue
+++ b/src/web/app/mobile/views/components/post-preview.vue
@@ -8,7 +8,7 @@
 			<router-link class="name" :to="`/@${acct}`">{{ post.user.name }}</router-link>
 			<span class="username">@{{ acct }}</span>
 			<router-link class="time" :to="`/@${acct}/${post.id}`">
-				<mk-time :time="post.created_at"/>
+				<mk-time :time="post.createdAt"/>
 			</router-link>
 		</header>
 		<div class="body">
diff --git a/src/web/app/mobile/views/components/post.sub.vue b/src/web/app/mobile/views/components/post.sub.vue
index b6ee7c1e0..8a11239da 100644
--- a/src/web/app/mobile/views/components/post.sub.vue
+++ b/src/web/app/mobile/views/components/post.sub.vue
@@ -8,7 +8,7 @@
 			<router-link class="name" :to="`/@${acct}`">{{ post.user.name }}</router-link>
 			<span class="username">@{{ acct }}</span>
 			<router-link class="created-at" :to="`/@${acct}/${post.id}`">
-				<mk-time :time="post.created_at"/>
+				<mk-time :time="post.createdAt"/>
 			</router-link>
 		</header>
 		<div class="body">
diff --git a/src/web/app/mobile/views/components/post.vue b/src/web/app/mobile/views/components/post.vue
index e5bc96479..243e7d9c2 100644
--- a/src/web/app/mobile/views/components/post.vue
+++ b/src/web/app/mobile/views/components/post.vue
@@ -13,7 +13,7 @@
 			<router-link class="name" :to="`/@${acct}`">{{ post.user.name }}</router-link>
 			<span>{{ '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr('%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1) }}</span>
 		</p>
-		<mk-time :time="post.created_at"/>
+		<mk-time :time="post.createdAt"/>
 	</div>
 	<article>
 		<router-link class="avatar-anchor" :to="`/@${pAcct}`">
@@ -22,12 +22,12 @@
 		<div class="main">
 			<header>
 				<router-link class="name" :to="`/@${pAcct}`">{{ p.user.name }}</router-link>
-				<span class="is-bot" v-if="p.user.host === null && p.user.account.is_bot">bot</span>
+				<span class="is-bot" v-if="p.user.host === null && p.user.account.isBot">bot</span>
 				<span class="username">@{{ pAcct }}</span>
 				<div class="info">
-					<span class="mobile" v-if="p.via_mobile">%fa:mobile-alt%</span>
+					<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
 					<router-link class="created-at" :to="url">
-						<mk-time :time="p.created_at"/>
+						<mk-time :time="p.createdAt"/>
 					</router-link>
 				</div>
 			</header>
@@ -103,7 +103,7 @@ export default Vue.extend({
 		isRepost(): boolean {
 			return (this.post.repost &&
 				this.post.text == null &&
-				this.post.media_ids == null &&
+				this.post.mediaIds == null &&
 				this.post.poll == null);
 		},
 		p(): any {
@@ -144,7 +144,7 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.client_settings.showMaps : true;
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
 					const uluru = new maps.LatLng(this.p.geo.latitude, this.p.geo.longitude);
@@ -194,7 +194,7 @@ export default Vue.extend({
 			const post = data.post;
 			if (post.id == this.post.id) {
 				this.$emit('update:post', post);
-			} else if (post.id == this.post.repost_id) {
+			} else if (post.id == this.post.repostId) {
 				this.post.repost = post;
 			}
 		},
diff --git a/src/web/app/mobile/views/components/posts.vue b/src/web/app/mobile/views/components/posts.vue
index 7e71fa098..4695f1bea 100644
--- a/src/web/app/mobile/views/components/posts.vue
+++ b/src/web/app/mobile/views/components/posts.vue
@@ -28,8 +28,8 @@ export default Vue.extend({
 	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;
+				const date = new Date(post.createdAt).getDate();
+				const month = new Date(post.createdAt).getMonth() + 1;
 				post._date = date;
 				post._datetext = `${month}月 ${date}日`;
 				return post;
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 389fc420e..b95883de7 100644
--- a/src/web/app/mobile/views/components/sub-post-content.vue
+++ b/src/web/app/mobile/views/components/sub-post-content.vue
@@ -1,9 +1,9 @@
 <template>
 <div class="mk-sub-post-content">
 	<div class="body">
-		<a class="reply" v-if="post.reply_id">%fa:reply%</a>
+		<a class="reply" v-if="post.replyId">%fa:reply%</a>
 		<mk-post-html v-if="post.ast" :ast="post.ast" :i="os.i"/>
-		<a class="rp" v-if="post.repost_id">RP: ...</a>
+		<a class="rp" v-if="post.repostId">RP: ...</a>
 	</div>
 	<details v-if="post.media">
 		<summary>({{ post.media.length }}個のメディア)</summary>
diff --git a/src/web/app/mobile/views/components/timeline.vue b/src/web/app/mobile/views/components/timeline.vue
index c0e766523..999f4a1f1 100644
--- a/src/web/app/mobile/views/components/timeline.vue
+++ b/src/web/app/mobile/views/components/timeline.vue
@@ -41,7 +41,7 @@ export default Vue.extend({
 	},
 	computed: {
 		alone(): boolean {
-			return (this as any).os.i.following_count == 0;
+			return (this as any).os.i.followingCount == 0;
 		}
 	},
 	mounted() {
diff --git a/src/web/app/mobile/views/components/ui.header.vue b/src/web/app/mobile/views/components/ui.header.vue
index 66e10a0f8..2bf47a90a 100644
--- a/src/web/app/mobile/views/components/ui.header.vue
+++ b/src/web/app/mobile/views/components/ui.header.vue
@@ -57,9 +57,9 @@ export default Vue.extend({
 				}
 			});
 
-			const ago = (new Date().getTime() - new Date((this as any).os.i.account.last_used_at).getTime()) / 1000
+			const ago = (new Date().getTime() - new Date((this as any).os.i.account.lastUsedAt).getTime()) / 1000
 			const isHisasiburi = ago >= 3600;
-			(this as any).os.i.account.last_used_at = new Date();
+			(this as any).os.i.account.lastUsedAt = new Date();
 			if (isHisasiburi) {
 				(this.$refs.welcomeback as any).style.display = 'block';
 				(this.$refs.main as any).style.overflow = 'hidden';
diff --git a/src/web/app/mobile/views/components/user-timeline.vue b/src/web/app/mobile/views/components/user-timeline.vue
index 39f959187..73ff440dc 100644
--- a/src/web/app/mobile/views/components/user-timeline.vue
+++ b/src/web/app/mobile/views/components/user-timeline.vue
@@ -33,7 +33,7 @@ export default Vue.extend({
 	},
 	mounted() {
 		(this as any).api('users/posts', {
-			user_id: this.user.id,
+			userId: this.user.id,
 			with_media: this.withMedia,
 			limit: limit + 1
 		}).then(posts => {
@@ -50,7 +50,7 @@ export default Vue.extend({
 		more() {
 			this.moreFetching = true;
 			(this as any).api('users/posts', {
-				user_id: this.user.id,
+				userId: this.user.id,
 				with_media: this.withMedia,
 				limit: limit + 1,
 				until_id: this.posts[this.posts.length - 1].id
diff --git a/src/web/app/mobile/views/pages/followers.vue b/src/web/app/mobile/views/pages/followers.vue
index 1edf4e38a..b5267bebf 100644
--- a/src/web/app/mobile/views/pages/followers.vue
+++ b/src/web/app/mobile/views/pages/followers.vue
@@ -7,7 +7,7 @@
 	<mk-users-list
 		v-if="!fetching"
 		:fetch="fetchUsers"
-		:count="user.followers_count"
+		:count="user.followersCount"
 		:you-know-count="user.followers_you_know_count"
 		@loaded="onLoaded"
 	>
@@ -54,7 +54,7 @@ export default Vue.extend({
 		},
 		fetchUsers(iknow, limit, cursor, cb) {
 			(this as any).api('users/followers', {
-				user_id: this.user.id,
+				userId: this.user.id,
 				iknow: iknow,
 				limit: limit,
 				cursor: cursor ? cursor : undefined
diff --git a/src/web/app/mobile/views/pages/following.vue b/src/web/app/mobile/views/pages/following.vue
index 0dd171cce..d8c31c9f0 100644
--- a/src/web/app/mobile/views/pages/following.vue
+++ b/src/web/app/mobile/views/pages/following.vue
@@ -7,7 +7,7 @@
 	<mk-users-list
 		v-if="!fetching"
 		:fetch="fetchUsers"
-		:count="user.following_count"
+		:count="user.followingCount"
 		:you-know-count="user.following_you_know_count"
 		@loaded="onLoaded"
 	>
@@ -54,7 +54,7 @@ export default Vue.extend({
 		},
 		fetchUsers(iknow, limit, cursor, cb) {
 			(this as any).api('users/following', {
-				user_id: this.user.id,
+				userId: this.user.id,
 				iknow: iknow,
 				limit: limit,
 				cursor: cursor ? cursor : undefined
diff --git a/src/web/app/mobile/views/pages/home.vue b/src/web/app/mobile/views/pages/home.vue
index b110fc409..be9101aa7 100644
--- a/src/web/app/mobile/views/pages/home.vue
+++ b/src/web/app/mobile/views/pages/home.vue
@@ -82,8 +82,8 @@ export default Vue.extend({
 		};
 	},
 	created() {
-		if ((this as any).os.i.account.client_settings.mobile_home == null) {
-			Vue.set((this as any).os.i.account.client_settings, 'mobile_home', [{
+		if ((this as any).os.i.account.clientSettings.mobile_home == null) {
+			Vue.set((this as any).os.i.account.clientSettings, 'mobile_home', [{
 				name: 'calendar',
 				id: 'a', data: {}
 			}, {
@@ -105,14 +105,14 @@ export default Vue.extend({
 				name: 'version',
 				id: 'g', data: {}
 			}]);
-			this.widgets = (this as any).os.i.account.client_settings.mobile_home;
+			this.widgets = (this as any).os.i.account.clientSettings.mobile_home;
 			this.saveHome();
 		} else {
-			this.widgets = (this as any).os.i.account.client_settings.mobile_home;
+			this.widgets = (this as any).os.i.account.clientSettings.mobile_home;
 		}
 
-		this.$watch('os.i.account.client_settings', i => {
-			this.widgets = (this as any).os.i.account.client_settings.mobile_home;
+		this.$watch('os.i.account.clientSettings', i => {
+			this.widgets = (this as any).os.i.account.clientSettings.mobile_home;
 		}, {
 			deep: true
 		});
@@ -144,7 +144,7 @@ export default Vue.extend({
 			Progress.done();
 		},
 		onStreamPost(post) {
-			if (document.hidden && post.user_id !== (this as any).os.i.id) {
+			if (document.hidden && post.userId !== (this as any).os.i.id) {
 				this.unreadCount++;
 				document.title = `(${this.unreadCount}) ${getPostSummary(post)}`;
 			}
@@ -157,15 +157,15 @@ export default Vue.extend({
 		},
 		onHomeUpdated(data) {
 			if (data.home) {
-				(this as any).os.i.account.client_settings.mobile_home = data.home;
+				(this as any).os.i.account.clientSettings.mobile_home = data.home;
 				this.widgets = data.home;
 			} else {
-				const w = (this as any).os.i.account.client_settings.mobile_home.find(w => w.id == data.id);
+				const w = (this as any).os.i.account.clientSettings.mobile_home.find(w => w.id == data.id);
 				if (w != null) {
 					w.data = data.data;
 					this.$refs[w.id][0].preventSave = true;
 					this.$refs[w.id][0].props = w.data;
-					this.widgets = (this as any).os.i.account.client_settings.mobile_home;
+					this.widgets = (this as any).os.i.account.clientSettings.mobile_home;
 				}
 			}
 		},
@@ -194,7 +194,7 @@ export default Vue.extend({
 			this.saveHome();
 		},
 		saveHome() {
-			(this as any).os.i.account.client_settings.mobile_home = this.widgets;
+			(this as any).os.i.account.clientSettings.mobile_home = this.widgets;
 			(this as any).api('i/update_mobile_home', {
 				home: this.widgets
 			});
diff --git a/src/web/app/mobile/views/pages/post.vue b/src/web/app/mobile/views/pages/post.vue
index 2ed2ebfcf..49a4bfd9d 100644
--- a/src/web/app/mobile/views/pages/post.vue
+++ b/src/web/app/mobile/views/pages/post.vue
@@ -38,7 +38,7 @@ export default Vue.extend({
 			this.fetching = true;
 
 			(this as any).api('posts/show', {
-				post_id: this.$route.params.post
+				postId: this.$route.params.post
 			}).then(post => {
 				this.post = post;
 				this.fetching = false;
diff --git a/src/web/app/mobile/views/pages/profile-setting.vue b/src/web/app/mobile/views/pages/profile-setting.vue
index 941165c99..d4bb25487 100644
--- a/src/web/app/mobile/views/pages/profile-setting.vue
+++ b/src/web/app/mobile/views/pages/profile-setting.vue
@@ -69,7 +69,7 @@ export default Vue.extend({
 				this.avatarSaving = true;
 
 				(this as any).api('i/update', {
-					avatar_id: file.id
+					avatarId: file.id
 				}).then(() => {
 					this.avatarSaving = false;
 					alert('%i18n:mobile.tags.mk-profile-setting.avatar-saved%');
@@ -83,7 +83,7 @@ export default Vue.extend({
 				this.bannerSaving = true;
 
 				(this as any).api('i/update', {
-					banner_id: file.id
+					bannerId: file.id
 				}).then(() => {
 					this.bannerSaving = false;
 					alert('%i18n:mobile.tags.mk-profile-setting.banner-saved%');
diff --git a/src/web/app/mobile/views/pages/user.vue b/src/web/app/mobile/views/pages/user.vue
index 7ff897e42..c4d6b67e6 100644
--- a/src/web/app/mobile/views/pages/user.vue
+++ b/src/web/app/mobile/views/pages/user.vue
@@ -27,15 +27,15 @@
 				</div>
 				<div class="status">
 					<a>
-						<b>{{ user.posts_count | number }}</b>
+						<b>{{ user.postsCount | number }}</b>
 						<i>%i18n:mobile.tags.mk-user.posts%</i>
 					</a>
 					<a :href="`@${acct}/following`">
-						<b>{{ user.following_count | number }}</b>
+						<b>{{ user.followingCount | number }}</b>
 						<i>%i18n:mobile.tags.mk-user.following%</i>
 					</a>
 					<a :href="`@${acct}/followers`">
-						<b>{{ user.followers_count | number }}</b>
+						<b>{{ user.followersCount | number }}</b>
 						<i>%i18n:mobile.tags.mk-user.followers%</i>
 					</a>
 				</div>
diff --git a/src/web/app/mobile/views/pages/user/home.followers-you-know.vue b/src/web/app/mobile/views/pages/user/home.followers-you-know.vue
index 1a2b8f708..508ab4b4a 100644
--- a/src/web/app/mobile/views/pages/user/home.followers-you-know.vue
+++ b/src/web/app/mobile/views/pages/user/home.followers-you-know.vue
@@ -27,7 +27,7 @@ export default Vue.extend({
 	},
 	mounted() {
 		(this as any).api('users/followers', {
-			user_id: this.user.id,
+			userId: this.user.id,
 			iknow: true,
 			limit: 30
 		}).then(res => {
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 b37f1a2fe..469781abb 100644
--- a/src/web/app/mobile/views/pages/user/home.friends.vue
+++ b/src/web/app/mobile/views/pages/user/home.friends.vue
@@ -20,7 +20,7 @@ export default Vue.extend({
 	},
 	mounted() {
 		(this as any).api('users/get_frequently_replied_users', {
-			user_id: this.user.id
+			userId: this.user.id
 		}).then(res => {
 			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 f12f59a40..94b5af553 100644
--- a/src/web/app/mobile/views/pages/user/home.photos.vue
+++ b/src/web/app/mobile/views/pages/user/home.photos.vue
@@ -29,7 +29,7 @@ export default Vue.extend({
 	},
 	mounted() {
 		(this as any).api('users/posts', {
-			user_id: this.user.id,
+			userId: this.user.id,
 			with_media: true,
 			limit: 6
 		}).then(posts => {
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 70b20ce94..654f7f63e 100644
--- a/src/web/app/mobile/views/pages/user/home.posts.vue
+++ b/src/web/app/mobile/views/pages/user/home.posts.vue
@@ -20,7 +20,7 @@ export default Vue.extend({
 	},
 	mounted() {
 		(this as any).api('users/posts', {
-			user_id: this.user.id
+			userId: this.user.id
 		}).then(posts => {
 			this.posts = 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 e3def6151..1afcd1f5b 100644
--- a/src/web/app/mobile/views/pages/user/home.vue
+++ b/src/web/app/mobile/views/pages/user/home.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="root home">
-	<mk-post-detail v-if="user.pinned_post" :post="user.pinned_post" :compact="true"/>
+	<mk-post-detail v-if="user.pinnedPost" :post="user.pinnedPost" :compact="true"/>
 	<section class="recent-posts">
 		<h2>%fa:R comments%%i18n:mobile.tags.mk-user-overview.recent-posts%</h2>
 		<div>
@@ -31,7 +31,7 @@
 			<x-followers-you-know :user="user"/>
 		</div>
 	</section>
-	<p v-if="user.host === null">%i18n:mobile.tags.mk-user-overview.last-used-at%: <b><mk-time :time="user.account.last_used_at"/></b></p>
+	<p v-if="user.host === null">%i18n:mobile.tags.mk-user-overview.last-used-at%: <b><mk-time :time="user.account.lastUsedAt"/></b></p>
 </div>
 </template>
 
diff --git a/src/web/app/mobile/views/pages/welcome.vue b/src/web/app/mobile/views/pages/welcome.vue
index 855744834..7a809702c 100644
--- a/src/web/app/mobile/views/pages/welcome.vue
+++ b/src/web/app/mobile/views/pages/welcome.vue
@@ -8,7 +8,7 @@
 			<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.account.two_factor_enabled" v-model="token" type="number" placeholder="トークン" required/>
+				<input v-if="user && user.account.twoFactorEnabled" v-model="token" type="number" placeholder="トークン" required/>
 				<button type="submit" :disabled="signing">{{ signing ? 'ログインしています' : 'ログイン' }}</button>
 			</form>
 			<div>
@@ -70,7 +70,7 @@ export default Vue.extend({
 			(this as any).api('signin', {
 				username: this.username,
 				password: this.password,
-				token: this.user && this.user.account.two_factor_enabled ? this.token : undefined
+				token: this.user && this.user.account.twoFactorEnabled ? this.token : undefined
 			}).then(() => {
 				location.reload();
 			}).catch(() => {
diff --git a/src/web/app/stats/tags/index.tag b/src/web/app/stats/tags/index.tag
index 4b167ccbc..bf08c38c3 100644
--- a/src/web/app/stats/tags/index.tag
+++ b/src/web/app/stats/tags/index.tag
@@ -57,7 +57,7 @@
 </mk-index>
 
 <mk-posts>
-	<h2>%i18n:stats.posts-count% <b>{ stats.posts_count }</b></h2>
+	<h2>%i18n:stats.posts-count% <b>{ stats.postsCount }</b></h2>
 	<mk-posts-chart v-if="!initializing" data={ data }/>
 	<style lang="stylus" scoped>
 		:scope
diff --git a/src/web/docs/api/endpoints/posts/create.yaml b/src/web/docs/api/endpoints/posts/create.yaml
index 5e2307dab..70d35008c 100644
--- a/src/web/docs/api/endpoints/posts/create.yaml
+++ b/src/web/docs/api/endpoints/posts/create.yaml
@@ -11,19 +11,19 @@ params:
     desc:
       ja: "投稿の本文"
       en: "The text of your post"
-  - name: "media_ids"
+  - name: "mediaIds"
     type: "id(DriveFile)[]"
     optional: true
     desc:
       ja: "添付するメディア(1~4つ)"
       en: "Media you want to attach (1~4)"
-  - name: "reply_id"
+  - name: "replyId"
     type: "id(Post)"
     optional: true
     desc:
       ja: "返信する投稿"
       en: "The post you want to reply"
-  - name: "repost_id"
+  - name: "repostId"
     type: "id(Post)"
     optional: true
     desc:
diff --git a/src/web/docs/api/entities/drive-file.yaml b/src/web/docs/api/entities/drive-file.yaml
index 2ebbb089a..02ab0d608 100644
--- a/src/web/docs/api/entities/drive-file.yaml
+++ b/src/web/docs/api/entities/drive-file.yaml
@@ -11,13 +11,13 @@ props:
     desc:
       ja: "ファイルID"
       en: "The ID of this file"
-  - name: "created_at"
+  - name: "createdAt"
     type: "date"
     optional: false
     desc:
       ja: "アップロード日時"
       en: "The upload date of this file"
-  - name: "user_id"
+  - name: "userId"
     type: "id(User)"
     optional: false
     desc:
@@ -59,7 +59,7 @@ props:
     desc:
       ja: "ファイルのURL"
       en: "The URL of this file"
-  - name: "folder_id"
+  - name: "folderId"
     type: "id(DriveFolder)"
     optional: true
     desc:
diff --git a/src/web/docs/api/entities/post.yaml b/src/web/docs/api/entities/post.yaml
index f78026314..8a616f088 100644
--- a/src/web/docs/api/entities/post.yaml
+++ b/src/web/docs/api/entities/post.yaml
@@ -11,13 +11,13 @@ props:
     desc:
       ja: "投稿ID"
       en: "The ID of this post"
-  - name: "created_at"
+  - name: "createdAt"
     type: "date"
     optional: false
     desc:
       ja: "投稿日時"
       en: "The posted date of this post"
-  - name: "via_mobile"
+  - name: "viaMobile"
     type: "boolean"
     optional: true
     desc:
@@ -29,7 +29,7 @@ props:
     desc:
       ja: "投稿の本文"
       en: "The text of this post"
-  - name: "media_ids"
+  - name: "mediaIds"
     type: "id(DriveFile)[]"
     optional: true
     desc:
@@ -41,7 +41,7 @@ props:
     desc:
       ja: "添付されているメディア"
       en: "The attached media"
-  - name: "user_id"
+  - name: "userId"
     type: "id(User)"
     optional: false
     desc:
@@ -64,7 +64,7 @@ props:
     optional: false
     desc:
       ja: "<a href='/docs/api/reactions'>リアクション</a>をキーとし、この投稿に対するそのリアクションの数を値としたオブジェクト"
-  - name: "reply_id"
+  - name: "replyId"
     type: "id(Post)"
     optional: true
     desc:
@@ -76,7 +76,7 @@ props:
     desc:
       ja: "返信した投稿"
       en: "The replyed post"
-  - name: "repost_id"
+  - name: "repostId"
     type: "id(Post)"
     optional: true
     desc:
diff --git a/src/web/docs/api/entities/user.yaml b/src/web/docs/api/entities/user.yaml
index a451a4080..60f2c8608 100644
--- a/src/web/docs/api/entities/user.yaml
+++ b/src/web/docs/api/entities/user.yaml
@@ -11,7 +11,7 @@ props:
     desc:
       ja: "ユーザーID"
       en: "The ID of this user"
-  - name: "created_at"
+  - name: "createdAt"
     type: "date"
     optional: false
     desc:
@@ -29,7 +29,7 @@ props:
     desc:
       ja: "アカウントの説明(自己紹介)"
       en: "The description of this user"
-  - name: "avatar_id"
+  - name: "avatarId"
     type: "id(DriveFile)"
     optional: true
     desc:
@@ -41,7 +41,7 @@ props:
     desc:
       ja: "アバターのURL"
       en: "The URL of the avatar of this user"
-  - name: "banner_id"
+  - name: "bannerId"
     type: "id(DriveFile)"
     optional: true
     desc:
@@ -53,13 +53,13 @@ props:
     desc:
       ja: "バナーのURL"
       en: "The URL of the banner of this user"
-  - name: "followers_count"
+  - name: "followersCount"
     type: "number"
     optional: false
     desc:
       ja: "フォロワーの数"
       en: "The number of the followers for this user"
-  - name: "following_count"
+  - name: "followingCount"
     type: "number"
     optional: false
     desc:
@@ -81,25 +81,25 @@ props:
     desc:
       ja: "自分がこのユーザーをミュートしているか"
       en: "Whether you muted this user"
-  - name: "posts_count"
+  - name: "postsCount"
     type: "number"
     optional: false
     desc:
       ja: "投稿の数"
       en: "The number of the posts of this user"
-  - name: "pinned_post"
+  - name: "pinnedPost"
     type: "entity(Post)"
     optional: true
     desc:
       ja: "ピン留めされた投稿"
       en: "The pinned post of this user"
-  - name: "pinned_post_id"
+  - name: "pinnedPostId"
     type: "id(Post)"
     optional: true
     desc:
       ja: "ピン留めされた投稿のID"
       en: "The ID of the pinned post of this user"
-  - name: "drive_capacity"
+  - name: "driveCapacity"
     type: "number"
     optional: false
     desc:
@@ -119,13 +119,13 @@ props:
       en: "The account of this user on this server"
     defName: "account"
     def:
-      - name: "last_used_at"
+      - name: "lastUsedAt"
         type: "date"
         optional: false
         desc:
           ja: "最終利用日時"
           en: "The last used date of this user"
-      - name: "is_bot"
+      - name: "isBot"
         type: "boolean"
         optional: true
         desc:
@@ -139,13 +139,13 @@ props:
           en: "The info of the connected twitter account of this user"
         defName: "twitter"
         def:
-          - name: "user_id"
+          - name: "userId"
             type: "string"
             optional: false
             desc:
               ja: "ユーザーID"
               en: "The user ID"
-          - name: "screen_name"
+          - name: "screenName"
             type: "string"
             optional: false
             desc:

From d8b756b977436e558f148c91d3a24831279fad2e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 16:46:11 +0900
Subject: [PATCH 0900/1250] wip

---
 src/api/bot/core.ts                           |  4 +--
 src/api/bot/interfaces/line.ts                |  2 +-
 src/api/endpoints.ts                          |  2 +-
 src/api/endpoints/app/create.ts               | 10 +++----
 src/api/endpoints/auth/session/generate.ts    |  8 +++---
 src/api/endpoints/auth/session/userkey.ts     |  8 +++---
 src/api/endpoints/channels.ts                 | 16 +++++------
 src/api/endpoints/channels/posts.ts           | 16 +++++------
 src/api/endpoints/drive/files.ts              | 16 +++++------
 src/api/endpoints/drive/folders.ts            | 16 +++++------
 src/api/endpoints/drive/stream.ts             | 16 +++++------
 src/api/endpoints/i/change_password.ts        | 12 ++++----
 src/api/endpoints/i/notifications.ts          | 22 +++++++--------
 src/api/endpoints/i/signin_history.ts         | 16 +++++------
 src/api/endpoints/i/update.ts                 |  8 +++---
 src/api/endpoints/messaging/messages.ts       | 22 +++++++--------
 src/api/endpoints/othello/games.ts            | 16 +++++------
 src/api/endpoints/posts.ts                    | 16 +++++------
 src/api/endpoints/posts/create.ts             |  2 +-
 src/api/endpoints/posts/mentions.ts           | 16 +++++------
 src/api/endpoints/posts/polls/vote.ts         |  2 +-
 src/api/endpoints/posts/reactions/create.ts   |  2 +-
 src/api/endpoints/posts/reposts.ts            | 16 +++++------
 src/api/endpoints/posts/search.ts             | 12 ++++----
 src/api/endpoints/posts/timeline.ts           | 28 +++++++++----------
 src/api/endpoints/users/posts.ts              | 28 +++++++++----------
 src/api/models/user.ts                        |  4 +--
 src/api/private/signup.ts                     |  2 +-
 src/web/app/auth/views/index.vue              |  8 +++---
 .../common/scripts/compose-notification.ts    | 12 ++++----
 .../common/views/components/autocomplete.vue  |  2 +-
 .../components/messaging-room.message.vue     |  2 +-
 .../views/components/messaging-room.vue       |  2 +-
 .../app/common/views/components/messaging.vue |  4 +--
 .../common/views/components/othello.game.vue  |  4 +--
 .../app/common/views/components/othello.vue   | 10 +++----
 .../views/components/welcome-timeline.vue     |  2 +-
 src/web/app/desktop/api/update-avatar.ts      |  2 +-
 src/web/app/desktop/api/update-banner.ts      |  2 +-
 .../views/components/followers-window.vue     |  2 +-
 .../views/components/following-window.vue     |  2 +-
 .../views/components/friends-maker.vue        |  2 +-
 .../app/desktop/views/components/mentions.vue |  2 +-
 .../views/components/notifications.vue        | 16 +++++------
 .../views/components/post-detail.sub.vue      |  2 +-
 .../desktop/views/components/post-detail.vue  |  4 +--
 .../desktop/views/components/post-preview.vue |  2 +-
 .../views/components/posts.post.sub.vue       |  2 +-
 .../desktop/views/components/posts.post.vue   |  4 +--
 .../views/components/settings.password.vue    |  4 +--
 .../views/components/settings.profile.vue     |  2 +-
 .../app/desktop/views/components/settings.vue |  4 +--
 .../app/desktop/views/components/timeline.vue |  4 +--
 .../views/components/ui.header.account.vue    |  2 +-
 .../desktop/views/components/user-preview.vue |  4 +--
 .../views/components/users-list.item.vue      |  2 +-
 .../pages/user/user.followers-you-know.vue    |  2 +-
 .../desktop/views/pages/user/user.friends.vue |  2 +-
 .../desktop/views/pages/user/user.header.vue  | 10 +++----
 .../views/pages/user/user.timeline.vue        |  4 +--
 src/web/app/desktop/views/pages/welcome.vue   |  2 +-
 src/web/app/desktop/views/widgets/profile.vue |  4 +--
 src/web/app/desktop/views/widgets/users.vue   |  2 +-
 src/web/app/dev/views/new-app.vue             |  2 +-
 src/web/app/mobile/views/components/drive.vue |  2 +-
 .../views/components/notification-preview.vue | 14 +++++-----
 .../mobile/views/components/notification.vue  |  8 +++---
 .../mobile/views/components/notifications.vue |  2 +-
 .../views/components/post-detail.sub.vue      |  2 +-
 .../mobile/views/components/post-detail.vue   |  4 +--
 .../mobile/views/components/post-preview.vue  |  2 +-
 .../app/mobile/views/components/post.sub.vue  |  2 +-
 src/web/app/mobile/views/components/post.vue  |  4 +--
 .../app/mobile/views/components/timeline.vue  |  4 +--
 .../app/mobile/views/components/ui.nav.vue    |  2 +-
 .../app/mobile/views/components/user-card.vue |  4 +--
 .../mobile/views/components/user-preview.vue  |  2 +-
 .../mobile/views/components/user-timeline.vue |  2 +-
 src/web/app/mobile/views/pages/followers.vue  |  2 +-
 src/web/app/mobile/views/pages/following.vue  |  2 +-
 .../app/mobile/views/pages/notifications.vue  |  2 +-
 .../mobile/views/pages/profile-setting.vue    |  4 +--
 src/web/app/mobile/views/pages/user.vue       |  4 +--
 .../pages/user/home.followers-you-know.vue    |  2 +-
 src/web/app/mobile/views/pages/welcome.vue    |  2 +-
 src/web/app/mobile/views/widgets/profile.vue  |  4 +--
 src/web/docs/api.ja.pug                       |  4 +--
 .../docs/api/endpoints/posts/timeline.yaml    |  8 +++---
 src/web/docs/api/entities/user.yaml           |  4 +--
 swagger.js                                    |  4 +--
 90 files changed, 286 insertions(+), 286 deletions(-)

diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
index 9e699572d..ec7c935f9 100644
--- a/src/api/bot/core.ts
+++ b/src/api/bot/core.ts
@@ -297,7 +297,7 @@ class TlContext extends Context {
 	private async getTl() {
 		const tl = await require('../endpoints/posts/timeline')({
 			limit: 5,
-			until_id: this.next ? this.next : undefined
+			untilId: this.next ? this.next : undefined
 		}, this.bot.user);
 
 		if (tl.length > 0) {
@@ -349,7 +349,7 @@ class NotificationsContext extends Context {
 	private async getNotifications() {
 		const notifications = await require('../endpoints/i/notifications')({
 			limit: 5,
-			until_id: this.next ? this.next : undefined
+			untilId: this.next ? this.next : undefined
 		}, this.bot.user);
 
 		if (notifications.length > 0) {
diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts
index dc600125c..296b42a28 100644
--- a/src/api/bot/interfaces/line.ts
+++ b/src/api/bot/interfaces/line.ts
@@ -130,7 +130,7 @@ class LineBot extends BotCore {
 			altText: await super.showUserCommand(q),
 			template: {
 				type: 'buttons',
-				thumbnailImageUrl: `${user.avatar_url}?thumbnail&size=1024`,
+				thumbnailImageUrl: `${user.avatarUrl}?thumbnail&size=1024`,
 				title: `${user.name} (@${acct})`,
 				text: user.description || '(no description)',
 				actions: actions
diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts
index c7100bd03..979d8ac29 100644
--- a/src/api/endpoints.ts
+++ b/src/api/endpoints.ts
@@ -289,7 +289,7 @@ const endpoints: Endpoint[] = [
 		kind: 'notification-write'
 	},
 	{
-		name: 'notifications/mark_as_read_all',
+		name: 'notifications/markAsRead_all',
 		withCredential: true,
 		kind: 'notification-write'
 	},
diff --git a/src/api/endpoints/app/create.ts b/src/api/endpoints/app/create.ts
index cde4846e0..713078463 100644
--- a/src/api/endpoints/app/create.ts
+++ b/src/api/endpoints/app/create.ts
@@ -40,7 +40,7 @@ import App, { isValidNameId, pack } from '../../models/app';
  *           type: string
  *           collectionFormat: csv
  *       -
- *         name: callback_url
+ *         name: callbackUrl
  *         description: URL called back after authentication
  *         in: formData
  *         required: false
@@ -82,10 +82,10 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	const [permission, permissionErr] = $(params.permission).array('string').unique().$;
 	if (permissionErr) return rej('invalid permission param');
 
-	// Get 'callback_url' parameter
+	// Get 'callbackUrl' parameter
 	// TODO: Check it is valid url
-	const [callbackUrl = null, callbackUrlErr] = $(params.callback_url).optional.nullable.string().$;
-	if (callbackUrlErr) return rej('invalid callback_url param');
+	const [callbackUrl = null, callbackUrlErr] = $(params.callbackUrl).optional.nullable.string().$;
+	if (callbackUrlErr) return rej('invalid callbackUrl param');
 
 	// Generate secret
 	const secret = rndstr('a-zA-Z0-9', 32);
@@ -99,7 +99,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 		nameIdLower: nameId.toLowerCase(),
 		description: description,
 		permission: permission,
-		callback_url: callbackUrl,
+		callbackUrl: callbackUrl,
 		secret: secret
 	});
 
diff --git a/src/api/endpoints/auth/session/generate.ts b/src/api/endpoints/auth/session/generate.ts
index 81db188ee..a087b46d8 100644
--- a/src/api/endpoints/auth/session/generate.ts
+++ b/src/api/endpoints/auth/session/generate.ts
@@ -14,7 +14,7 @@ import config from '../../../../conf';
  *     summary: Generate a session
  *     parameters:
  *       -
- *         name: app_secret
+ *         name: appSecret
  *         description: App Secret
  *         in: formData
  *         required: true
@@ -45,9 +45,9 @@ import config from '../../../../conf';
  * @return {Promise<any>}
  */
 module.exports = (params) => new Promise(async (res, rej) => {
-	// Get 'app_secret' parameter
-	const [appSecret, appSecretErr] = $(params.app_secret).string().$;
-	if (appSecretErr) return rej('invalid app_secret param');
+	// Get 'appSecret' parameter
+	const [appSecret, appSecretErr] = $(params.appSecret).string().$;
+	if (appSecretErr) return rej('invalid appSecret param');
 
 	// Lookup app
 	const app = await App.findOne({
diff --git a/src/api/endpoints/auth/session/userkey.ts b/src/api/endpoints/auth/session/userkey.ts
index 74f025c8a..5d9983af6 100644
--- a/src/api/endpoints/auth/session/userkey.ts
+++ b/src/api/endpoints/auth/session/userkey.ts
@@ -14,7 +14,7 @@ import { pack } from '../../../models/user';
  *     summary: Get an access token(userkey)
  *     parameters:
  *       -
- *         name: app_secret
+ *         name: appSecret
  *         description: App Secret
  *         in: formData
  *         required: true
@@ -50,9 +50,9 @@ import { pack } from '../../../models/user';
  * @return {Promise<any>}
  */
 module.exports = (params) => new Promise(async (res, rej) => {
-	// Get 'app_secret' parameter
-	const [appSecret, appSecretErr] = $(params.app_secret).string().$;
-	if (appSecretErr) return rej('invalid app_secret param');
+	// Get 'appSecret' parameter
+	const [appSecret, appSecretErr] = $(params.appSecret).string().$;
+	if (appSecretErr) return rej('invalid appSecret param');
 
 	// Lookup app
 	const app = await App.findOne({
diff --git a/src/api/endpoints/channels.ts b/src/api/endpoints/channels.ts
index b9a7d1b78..a4acc0660 100644
--- a/src/api/endpoints/channels.ts
+++ b/src/api/endpoints/channels.ts
@@ -16,17 +16,17 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) return rej('invalid limit param');
 
-	// Get 'since_id' parameter
-	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) return rej('invalid since_id param');
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) return rej('invalid sinceId param');
 
-	// Get 'until_id' parameter
-	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
-	if (untilIdErr) return rej('invalid until_id param');
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) return rej('invalid untilId param');
 
-	// Check if both of since_id and until_id is specified
+	// Check if both of sinceId and untilId is specified
 	if (sinceId && untilId) {
-		return rej('cannot set since_id and until_id');
+		return rej('cannot set sinceId and untilId');
 	}
 
 	// Construct query
diff --git a/src/api/endpoints/channels/posts.ts b/src/api/endpoints/channels/posts.ts
index 753666405..348dbb108 100644
--- a/src/api/endpoints/channels/posts.ts
+++ b/src/api/endpoints/channels/posts.ts
@@ -17,17 +17,17 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [limit = 1000, limitErr] = $(params.limit).optional.number().range(1, 1000).$;
 	if (limitErr) return rej('invalid limit param');
 
-	// Get 'since_id' parameter
-	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) return rej('invalid since_id param');
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) return rej('invalid sinceId param');
 
-	// Get 'until_id' parameter
-	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
-	if (untilIdErr) return rej('invalid until_id param');
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) return rej('invalid untilId param');
 
-	// Check if both of since_id and until_id is specified
+	// Check if both of sinceId and untilId is specified
 	if (sinceId && untilId) {
-		return rej('cannot set since_id and until_id');
+		return rej('cannot set sinceId and untilId');
 	}
 
 	// Get 'channelId' parameter
diff --git a/src/api/endpoints/drive/files.ts b/src/api/endpoints/drive/files.ts
index 1ce855932..f982ef62e 100644
--- a/src/api/endpoints/drive/files.ts
+++ b/src/api/endpoints/drive/files.ts
@@ -17,17 +17,17 @@ module.exports = async (params, user, app) => {
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) throw 'invalid limit param';
 
-	// Get 'since_id' parameter
-	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) throw 'invalid since_id param';
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) throw 'invalid sinceId param';
 
-	// Get 'until_id' parameter
-	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
-	if (untilIdErr) throw 'invalid until_id param';
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) throw 'invalid untilId param';
 
-	// Check if both of since_id and until_id is specified
+	// Check if both of sinceId and untilId is specified
 	if (sinceId && untilId) {
-		throw 'cannot set since_id and until_id';
+		throw 'cannot set sinceId and untilId';
 	}
 
 	// Get 'folderId' parameter
diff --git a/src/api/endpoints/drive/folders.ts b/src/api/endpoints/drive/folders.ts
index c25964635..c314837f7 100644
--- a/src/api/endpoints/drive/folders.ts
+++ b/src/api/endpoints/drive/folders.ts
@@ -17,17 +17,17 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) return rej('invalid limit param');
 
-	// Get 'since_id' parameter
-	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) return rej('invalid since_id param');
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) return rej('invalid sinceId param');
 
-	// Get 'until_id' parameter
-	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
-	if (untilIdErr) return rej('invalid until_id param');
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) return rej('invalid untilId param');
 
-	// Check if both of since_id and until_id is specified
+	// Check if both of sinceId and untilId is specified
 	if (sinceId && untilId) {
-		return rej('cannot set since_id and until_id');
+		return rej('cannot set sinceId and untilId');
 	}
 
 	// Get 'folderId' parameter
diff --git a/src/api/endpoints/drive/stream.ts b/src/api/endpoints/drive/stream.ts
index 0f9cea9f1..71db38f3b 100644
--- a/src/api/endpoints/drive/stream.ts
+++ b/src/api/endpoints/drive/stream.ts
@@ -16,17 +16,17 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) return rej('invalid limit param');
 
-	// Get 'since_id' parameter
-	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) return rej('invalid since_id param');
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) return rej('invalid sinceId param');
 
-	// Get 'until_id' parameter
-	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
-	if (untilIdErr) return rej('invalid until_id param');
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) return rej('invalid untilId param');
 
-	// Check if both of since_id and until_id is specified
+	// Check if both of sinceId and untilId is specified
 	if (sinceId && untilId) {
-		return rej('cannot set since_id and until_id');
+		return rej('cannot set sinceId and untilId');
 	}
 
 	// Get 'type' parameter
diff --git a/src/api/endpoints/i/change_password.ts b/src/api/endpoints/i/change_password.ts
index 88fb36b1f..e3b0127e7 100644
--- a/src/api/endpoints/i/change_password.ts
+++ b/src/api/endpoints/i/change_password.ts
@@ -13,13 +13,13 @@ import User from '../../models/user';
  * @return {Promise<any>}
  */
 module.exports = async (params, user) => new Promise(async (res, rej) => {
-	// Get 'current_password' parameter
-	const [currentPassword, currentPasswordErr] = $(params.current_password).string().$;
-	if (currentPasswordErr) return rej('invalid current_password param');
+	// Get 'currentPasword' parameter
+	const [currentPassword, currentPasswordErr] = $(params.currentPasword).string().$;
+	if (currentPasswordErr) return rej('invalid currentPasword param');
 
-	// Get 'new_password' parameter
-	const [newPassword, newPasswordErr] = $(params.new_password).string().$;
-	if (newPasswordErr) return rej('invalid new_password param');
+	// Get 'newPassword' parameter
+	const [newPassword, newPasswordErr] = $(params.newPassword).string().$;
+	if (newPasswordErr) return rej('invalid newPassword param');
 
 	// Compare password
 	const same = await bcrypt.compare(currentPassword, user.account.password);
diff --git a/src/api/endpoints/i/notifications.ts b/src/api/endpoints/i/notifications.ts
index e3447c17e..7119bf6ea 100644
--- a/src/api/endpoints/i/notifications.ts
+++ b/src/api/endpoints/i/notifications.ts
@@ -21,9 +21,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		$(params.following).optional.boolean().$;
 	if (followingError) return rej('invalid following param');
 
-	// Get 'mark_as_read' parameter
-	const [markAsRead = true, markAsReadErr] = $(params.mark_as_read).optional.boolean().$;
-	if (markAsReadErr) return rej('invalid mark_as_read param');
+	// Get 'markAsRead' parameter
+	const [markAsRead = true, markAsReadErr] = $(params.markAsRead).optional.boolean().$;
+	if (markAsReadErr) return rej('invalid markAsRead param');
 
 	// Get 'type' parameter
 	const [type, typeErr] = $(params.type).optional.array('string').unique().$;
@@ -33,17 +33,17 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) return rej('invalid limit param');
 
-	// Get 'since_id' parameter
-	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) return rej('invalid since_id param');
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) return rej('invalid sinceId param');
 
-	// Get 'until_id' parameter
-	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
-	if (untilIdErr) return rej('invalid until_id param');
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) return rej('invalid untilId param');
 
-	// Check if both of since_id and until_id is specified
+	// Check if both of sinceId and untilId is specified
 	if (sinceId && untilId) {
-		return rej('cannot set since_id and until_id');
+		return rej('cannot set sinceId and untilId');
 	}
 
 	const mute = await Mute.find({
diff --git a/src/api/endpoints/i/signin_history.ts b/src/api/endpoints/i/signin_history.ts
index 5b794d0a4..a4ba22790 100644
--- a/src/api/endpoints/i/signin_history.ts
+++ b/src/api/endpoints/i/signin_history.ts
@@ -16,17 +16,17 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) return rej('invalid limit param');
 
-	// Get 'since_id' parameter
-	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) return rej('invalid since_id param');
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) return rej('invalid sinceId param');
 
-	// Get 'until_id' parameter
-	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
-	if (untilIdErr) return rej('invalid until_id param');
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) return rej('invalid untilId param');
 
-	// Check if both of since_id and until_id is specified
+	// Check if both of sinceId and untilId is specified
 	if (sinceId && untilId) {
-		return rej('cannot set since_id and until_id');
+		return rej('cannot set sinceId and untilId');
 	}
 
 	const query = {
diff --git a/src/api/endpoints/i/update.ts b/src/api/endpoints/i/update.ts
index 664575187..45dfddcfe 100644
--- a/src/api/endpoints/i/update.ts
+++ b/src/api/endpoints/i/update.ts
@@ -51,10 +51,10 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re
 	if (isBotErr) return rej('invalid isBot param');
 	if (isBot != null) user.account.isBot = isBot;
 
-	// Get 'auto_watch' parameter
-	const [autoWatch, autoWatchErr] = $(params.auto_watch).optional.boolean().$;
-	if (autoWatchErr) return rej('invalid auto_watch param');
-	if (autoWatch != null) user.account.settings.auto_watch = autoWatch;
+	// Get 'autoWatch' parameter
+	const [autoWatch, autoWatchErr] = $(params.autoWatch).optional.boolean().$;
+	if (autoWatchErr) return rej('invalid autoWatch param');
+	if (autoWatch != null) user.account.settings.autoWatch = autoWatch;
 
 	await User.update(user._id, {
 		$set: {
diff --git a/src/api/endpoints/messaging/messages.ts b/src/api/endpoints/messaging/messages.ts
index ba8ca7d1c..dd80e41d0 100644
--- a/src/api/endpoints/messaging/messages.ts
+++ b/src/api/endpoints/messaging/messages.ts
@@ -32,25 +32,25 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		return rej('user not found');
 	}
 
-	// Get 'mark_as_read' parameter
-	const [markAsRead = true, markAsReadErr] = $(params.mark_as_read).optional.boolean().$;
-	if (markAsReadErr) return rej('invalid mark_as_read param');
+	// Get 'markAsRead' parameter
+	const [markAsRead = true, markAsReadErr] = $(params.markAsRead).optional.boolean().$;
+	if (markAsReadErr) return rej('invalid markAsRead param');
 
 	// Get 'limit' parameter
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) return rej('invalid limit param');
 
-	// Get 'since_id' parameter
-	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) return rej('invalid since_id param');
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) return rej('invalid sinceId param');
 
-	// Get 'until_id' parameter
-	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
-	if (untilIdErr) return rej('invalid until_id param');
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) return rej('invalid untilId param');
 
-	// Check if both of since_id and until_id is specified
+	// Check if both of sinceId and untilId is specified
 	if (sinceId && untilId) {
-		return rej('cannot set since_id and until_id');
+		return rej('cannot set sinceId and untilId');
 	}
 
 	const query = {
diff --git a/src/api/endpoints/othello/games.ts b/src/api/endpoints/othello/games.ts
index 5c71f9882..37fa38418 100644
--- a/src/api/endpoints/othello/games.ts
+++ b/src/api/endpoints/othello/games.ts
@@ -10,17 +10,17 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) return rej('invalid limit param');
 
-	// Get 'since_id' parameter
-	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) return rej('invalid since_id param');
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) return rej('invalid sinceId param');
 
-	// Get 'until_id' parameter
-	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
-	if (untilIdErr) return rej('invalid until_id param');
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) return rej('invalid untilId param');
 
-	// Check if both of since_id and until_id is specified
+	// Check if both of sinceId and untilId is specified
 	if (sinceId && untilId) {
-		return rej('cannot set since_id and until_id');
+		return rej('cannot set sinceId and untilId');
 	}
 
 	const q: any = my ? {
diff --git a/src/api/endpoints/posts.ts b/src/api/endpoints/posts.ts
index 7e9ff3ad7..bee1de02d 100644
--- a/src/api/endpoints/posts.ts
+++ b/src/api/endpoints/posts.ts
@@ -35,17 +35,17 @@ module.exports = (params) => new Promise(async (res, rej) => {
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) return rej('invalid limit param');
 
-	// Get 'since_id' parameter
-	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) return rej('invalid since_id param');
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) return rej('invalid sinceId param');
 
-	// Get 'until_id' parameter
-	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
-	if (untilIdErr) return rej('invalid until_id param');
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) return rej('invalid untilId param');
 
-	// Check if both of since_id and until_id is specified
+	// Check if both of sinceId and untilId is specified
 	if (sinceId && untilId) {
-		return rej('cannot set since_id and until_id');
+		return rej('cannot set sinceId and untilId');
 	}
 
 	// Construct query
diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index 2a3d974fe..b99d1fbbc 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -392,7 +392,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 			});
 
 		// この投稿をWatchする
-		if ((user.account as ILocalAccount).settings.auto_watch !== false) {
+		if ((user.account as ILocalAccount).settings.autoWatch !== false) {
 			watch(user._id, reply);
 		}
 
diff --git a/src/api/endpoints/posts/mentions.ts b/src/api/endpoints/posts/mentions.ts
index da90583bb..1b342e8de 100644
--- a/src/api/endpoints/posts/mentions.ts
+++ b/src/api/endpoints/posts/mentions.ts
@@ -23,17 +23,17 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) return rej('invalid limit param');
 
-	// Get 'since_id' parameter
-	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) return rej('invalid since_id param');
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) return rej('invalid sinceId param');
 
-	// Get 'until_id' parameter
-	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
-	if (untilIdErr) return rej('invalid until_id param');
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) return rej('invalid untilId param');
 
-	// Check if both of since_id and until_id is specified
+	// Check if both of sinceId and untilId is specified
 	if (sinceId && untilId) {
-		return rej('cannot set since_id and until_id');
+		return rej('cannot set sinceId and untilId');
 	}
 
 	// Construct query
diff --git a/src/api/endpoints/posts/polls/vote.ts b/src/api/endpoints/posts/polls/vote.ts
index e87474ae6..734a3a3c4 100644
--- a/src/api/endpoints/posts/polls/vote.ts
+++ b/src/api/endpoints/posts/polls/vote.ts
@@ -100,7 +100,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		});
 
 	// この投稿をWatchする
-	if (user.account.settings.auto_watch !== false) {
+	if (user.account.settings.autoWatch !== false) {
 		watch(user._id, post);
 	}
 });
diff --git a/src/api/endpoints/posts/reactions/create.ts b/src/api/endpoints/posts/reactions/create.ts
index 7031d28e5..6f75a923c 100644
--- a/src/api/endpoints/posts/reactions/create.ts
+++ b/src/api/endpoints/posts/reactions/create.ts
@@ -116,7 +116,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		});
 
 	// この投稿をWatchする
-	if (user.account.settings.auto_watch !== false) {
+	if (user.account.settings.autoWatch !== false) {
 		watch(user._id, post);
 	}
 });
diff --git a/src/api/endpoints/posts/reposts.ts b/src/api/endpoints/posts/reposts.ts
index c1645117f..51af41f52 100644
--- a/src/api/endpoints/posts/reposts.ts
+++ b/src/api/endpoints/posts/reposts.ts
@@ -20,17 +20,17 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) return rej('invalid limit param');
 
-	// Get 'since_id' parameter
-	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) return rej('invalid since_id param');
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) return rej('invalid sinceId param');
 
-	// Get 'until_id' parameter
-	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
-	if (untilIdErr) return rej('invalid until_id param');
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) return rej('invalid untilId param');
 
-	// Check if both of since_id and until_id is specified
+	// Check if both of sinceId and untilId is specified
 	if (sinceId && untilId) {
-		return rej('cannot set since_id and until_id');
+		return rej('cannot set sinceId and untilId');
 	}
 
 	// Lookup post
diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index e7906c95c..f90b9aa0d 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -61,13 +61,13 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [poll = null, pollErr] = $(params.poll).optional.nullable.boolean().$;
 	if (pollErr) return rej('invalid poll param');
 
-	// Get 'since_date' parameter
-	const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$;
-	if (sinceDateErr) throw 'invalid since_date param';
+	// Get 'sinceDate' parameter
+	const [sinceDate, sinceDateErr] = $(params.sinceDate).optional.number().$;
+	if (sinceDateErr) throw 'invalid sinceDate param';
 
-	// Get 'until_date' parameter
-	const [untilDate, untilDateErr] = $(params.until_date).optional.number().$;
-	if (untilDateErr) throw 'invalid until_date param';
+	// Get 'untilDate' parameter
+	const [untilDate, untilDateErr] = $(params.untilDate).optional.number().$;
+	if (untilDateErr) throw 'invalid untilDate param';
 
 	// Get 'offset' parameter
 	const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$;
diff --git a/src/api/endpoints/posts/timeline.ts b/src/api/endpoints/posts/timeline.ts
index c7cb8032e..a3e915f16 100644
--- a/src/api/endpoints/posts/timeline.ts
+++ b/src/api/endpoints/posts/timeline.ts
@@ -22,25 +22,25 @@ module.exports = async (params, user, app) => {
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) throw 'invalid limit param';
 
-	// Get 'since_id' parameter
-	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) throw 'invalid since_id param';
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) throw 'invalid sinceId param';
 
-	// Get 'until_id' parameter
-	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
-	if (untilIdErr) throw 'invalid until_id param';
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) throw 'invalid untilId param';
 
-	// Get 'since_date' parameter
-	const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$;
-	if (sinceDateErr) throw 'invalid since_date param';
+	// Get 'sinceDate' parameter
+	const [sinceDate, sinceDateErr] = $(params.sinceDate).optional.number().$;
+	if (sinceDateErr) throw 'invalid sinceDate param';
 
-	// Get 'until_date' parameter
-	const [untilDate, untilDateErr] = $(params.until_date).optional.number().$;
-	if (untilDateErr) throw 'invalid until_date param';
+	// Get 'untilDate' parameter
+	const [untilDate, untilDateErr] = $(params.untilDate).optional.number().$;
+	if (untilDateErr) throw 'invalid untilDate param';
 
-	// Check if only one of since_id, until_id, since_date, until_date specified
+	// Check if only one of sinceId, untilId, sinceDate, untilDate specified
 	if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) {
-		throw 'only one of since_id, until_id, since_date, until_date can be specified';
+		throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified';
 	}
 
 	const { followingIds, watchingChannelIds, mutedUserIds } = await rap({
diff --git a/src/api/endpoints/users/posts.ts b/src/api/endpoints/users/posts.ts
index f08be91c4..9ece429b6 100644
--- a/src/api/endpoints/users/posts.ts
+++ b/src/api/endpoints/users/posts.ts
@@ -46,25 +46,25 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) return rej('invalid limit param');
 
-	// Get 'since_id' parameter
-	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) return rej('invalid since_id param');
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) return rej('invalid sinceId param');
 
-	// Get 'until_id' parameter
-	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
-	if (untilIdErr) return rej('invalid until_id param');
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) return rej('invalid untilId param');
 
-	// Get 'since_date' parameter
-	const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$;
-	if (sinceDateErr) throw 'invalid since_date param';
+	// Get 'sinceDate' parameter
+	const [sinceDate, sinceDateErr] = $(params.sinceDate).optional.number().$;
+	if (sinceDateErr) throw 'invalid sinceDate param';
 
-	// Get 'until_date' parameter
-	const [untilDate, untilDateErr] = $(params.until_date).optional.number().$;
-	if (untilDateErr) throw 'invalid until_date param';
+	// Get 'untilDate' parameter
+	const [untilDate, untilDateErr] = $(params.untilDate).optional.number().$;
+	if (untilDateErr) throw 'invalid untilDate param';
 
-	// Check if only one of since_id, until_id, since_date, until_date specified
+	// Check if only one of sinceId, untilId, sinceDate, untilDate specified
 	if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) {
-		throw 'only one of since_id, until_id, since_date, until_date can be specified';
+		throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified';
 	}
 
 	const q = userId !== undefined
diff --git a/src/api/models/user.ts b/src/api/models/user.ts
index 8c68b06df..9ee413e0d 100644
--- a/src/api/models/user.ts
+++ b/src/api/models/user.ts
@@ -195,11 +195,11 @@ export const pack = (
 		}
 	}
 
-	_user.avatar_url = _user.avatarId != null
+	_user.avatarUrl = _user.avatarId != null
 		? `${config.drive_url}/${_user.avatarId}`
 		: `${config.drive_url}/default-avatar.jpg`;
 
-	_user.banner_url = _user.bannerId != null
+	_user.bannerUrl = _user.bannerId != null
 		? `${config.drive_url}/${_user.bannerId}`
 		: null;
 
diff --git a/src/api/private/signup.ts b/src/api/private/signup.ts
index 1304ccb54..9178c0eb8 100644
--- a/src/api/private/signup.ts
+++ b/src/api/private/signup.ts
@@ -137,7 +137,7 @@ export default async (req: express.Request, res: express.Response) => {
 				weight: null
 			},
 			settings: {
-				auto_watch: true
+				autoWatch: true
 			},
 			clientSettings: {
 				home: homeData
diff --git a/src/web/app/auth/views/index.vue b/src/web/app/auth/views/index.vue
index 17e5cc610..690cc4f28 100644
--- a/src/web/app/auth/views/index.vue
+++ b/src/web/app/auth/views/index.vue
@@ -15,8 +15,8 @@
 		</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>
+			<p v-if="session.app.callbackUrl">アプリケーションに戻っています<mk-ellipsis/></p>
+			<p v-if="!session.app.callbackUrl">アプリケーションに戻って、やっていってください。</p>
 		</div>
 		<div class="error" v-if="state == 'fetch-session-error'">
 			<p>セッションが存在しません。</p>
@@ -77,8 +77,8 @@ export default Vue.extend({
 	methods: {
 		accepted() {
 			this.state = 'accepted';
-			if (this.session.app.callback_url) {
-				location.href = this.session.app.callback_url + '?token=' + this.session.token;
+			if (this.session.app.callbackUrl) {
+				location.href = this.session.app.callbackUrl + '?token=' + this.session.token;
 			}
 		}
 	}
diff --git a/src/web/app/common/scripts/compose-notification.ts b/src/web/app/common/scripts/compose-notification.ts
index e1dbd3bc1..273579cbc 100644
--- a/src/web/app/common/scripts/compose-notification.ts
+++ b/src/web/app/common/scripts/compose-notification.ts
@@ -23,42 +23,42 @@ export default function(type, data): Notification {
 			return {
 				title: `${data.user.name}さんから:`,
 				body: getPostSummary(data),
-				icon: data.user.avatar_url + '?thumbnail&size=64'
+				icon: data.user.avatarUrl + '?thumbnail&size=64'
 			};
 
 		case 'reply':
 			return {
 				title: `${data.user.name}さんから返信:`,
 				body: getPostSummary(data),
-				icon: data.user.avatar_url + '?thumbnail&size=64'
+				icon: data.user.avatarUrl + '?thumbnail&size=64'
 			};
 
 		case 'quote':
 			return {
 				title: `${data.user.name}さんが引用:`,
 				body: getPostSummary(data),
-				icon: data.user.avatar_url + '?thumbnail&size=64'
+				icon: data.user.avatarUrl + '?thumbnail&size=64'
 			};
 
 		case 'reaction':
 			return {
 				title: `${data.user.name}: ${getReactionEmoji(data.reaction)}:`,
 				body: getPostSummary(data.post),
-				icon: data.user.avatar_url + '?thumbnail&size=64'
+				icon: data.user.avatarUrl + '?thumbnail&size=64'
 			};
 
 		case 'unread_messaging_message':
 			return {
 				title: `${data.user.name}さんからメッセージ:`,
 				body: data.text, // TODO: getMessagingMessageSummary(data),
-				icon: data.user.avatar_url + '?thumbnail&size=64'
+				icon: data.user.avatarUrl + '?thumbnail&size=64'
 			};
 
 		case 'othello_invited':
 			return {
 				title: '対局への招待があります',
 				body: `${data.parent.name}さんから`,
-				icon: data.parent.avatar_url + '?thumbnail&size=64'
+				icon: data.parent.avatarUrl + '?thumbnail&size=64'
 			};
 
 		default:
diff --git a/src/web/app/common/views/components/autocomplete.vue b/src/web/app/common/views/components/autocomplete.vue
index 8afa291e3..79bd2ba02 100644
--- a/src/web/app/common/views/components/autocomplete.vue
+++ b/src/web/app/common/views/components/autocomplete.vue
@@ -2,7 +2,7 @@
 <div class="mk-autocomplete" @contextmenu.prevent="() => {}">
 	<ol class="users" ref="suggests" v-if="users.length > 0">
 		<li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1">
-			<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=32`" alt=""/>
+			<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=32`" alt=""/>
 			<span class="name">{{ user.name }}</span>
 			<span class="username">@{{ getAcct(user) }}</span>
 		</li>
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 d21cce1a0..8d35b5039 100644
--- a/src/web/app/common/views/components/messaging-room.message.vue
+++ b/src/web/app/common/views/components/messaging-room.message.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="message" :data-is-me="isMe">
 	<router-link class="avatar-anchor" :to="`/@${acct}`" :title="acct" target="_blank">
-		<img class="avatar" :src="`${message.user.avatar_url}?thumbnail&size=80`" alt=""/>
+		<img class="avatar" :src="`${message.user.avatarUrl}?thumbnail&size=80`" alt=""/>
 	</router-link>
 	<div class="content">
 		<div class="balloon" :data-no-text="message.text == null">
diff --git a/src/web/app/common/views/components/messaging-room.vue b/src/web/app/common/views/components/messaging-room.vue
index 36c53fd3f..d30c64d74 100644
--- a/src/web/app/common/views/components/messaging-room.vue
+++ b/src/web/app/common/views/components/messaging-room.vue
@@ -125,7 +125,7 @@ export default Vue.extend({
 				(this as any).api('messaging/messages', {
 					userId: this.user.id,
 					limit: max + 1,
-					until_id: this.existMoreMessages ? this.messages[0].id : undefined
+					untilId: this.existMoreMessages ? this.messages[0].id : undefined
 				}).then(messages => {
 					if (messages.length == max + 1) {
 						this.existMoreMessages = true;
diff --git a/src/web/app/common/views/components/messaging.vue b/src/web/app/common/views/components/messaging.vue
index 0272c6707..8317c3738 100644
--- a/src/web/app/common/views/components/messaging.vue
+++ b/src/web/app/common/views/components/messaging.vue
@@ -13,7 +13,7 @@
 					@click="navigate(user)"
 					tabindex="-1"
 				>
-					<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=32`" alt=""/>
+					<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=32`" alt=""/>
 					<span class="name">{{ user.name }}</span>
 					<span class="username">@{{ getAcct(user) }}</span>
 				</li>
@@ -31,7 +31,7 @@
 				:key="message.id"
 			>
 				<div>
-					<img class="avatar" :src="`${isMe(message) ? message.recipient.avatar_url : message.user.avatar_url}?thumbnail&size=64`" alt=""/>
+					<img class="avatar" :src="`${isMe(message) ? message.recipient.avatarUrl : message.user.avatarUrl}?thumbnail&size=64`" alt=""/>
 					<header>
 						<span class="name">{{ isMe(message) ? message.recipient.name : message.user.name }}</span>
 						<span class="username">@{{ getAcct(isMe(message) ? message.recipient : message.user) }}</span>
diff --git a/src/web/app/common/views/components/othello.game.vue b/src/web/app/common/views/components/othello.game.vue
index 8150fe07f..f08742ad1 100644
--- a/src/web/app/common/views/components/othello.game.vue
+++ b/src/web/app/common/views/components/othello.game.vue
@@ -19,8 +19,8 @@
 			@click="set(i)"
 			:title="'[' + (o.transformPosToXy(i)[0] + 1) + ', ' + (o.transformPosToXy(i)[1] + 1) + '] (' + i + ')'"
 		>
-			<img v-if="stone === true" :src="`${blackUser.avatar_url}?thumbnail&size=128`" alt="">
-			<img v-if="stone === false" :src="`${whiteUser.avatar_url}?thumbnail&size=128`" alt="">
+			<img v-if="stone === true" :src="`${blackUser.avatarUrl}?thumbnail&size=128`" alt="">
+			<img v-if="stone === false" :src="`${whiteUser.avatarUrl}?thumbnail&size=128`" alt="">
 		</div>
 	</div>
 
diff --git a/src/web/app/common/views/components/othello.vue b/src/web/app/common/views/components/othello.vue
index 7bdb47100..7737d74de 100644
--- a/src/web/app/common/views/components/othello.vue
+++ b/src/web/app/common/views/components/othello.vue
@@ -31,7 +31,7 @@
 		<section v-if="invitations.length > 0">
 			<h2>対局の招待があります!:</h2>
 			<div class="invitation" v-for="i in invitations" tabindex="-1" @click="accept(i)">
-				<img :src="`${i.parent.avatar_url}?thumbnail&size=32`" alt="">
+				<img :src="`${i.parent.avatarUrl}?thumbnail&size=32`" alt="">
 				<span class="name"><b>{{ i.parent.name }}</b></span>
 				<span class="username">@{{ i.parent.username }}</span>
 				<mk-time :time="i.createdAt"/>
@@ -40,8 +40,8 @@
 		<section v-if="myGames.length > 0">
 			<h2>自分の対局</h2>
 			<a class="game" v-for="g in myGames" tabindex="-1" @click.prevent="go(g)" :href="`/othello/${g.id}`">
-				<img :src="`${g.user1.avatar_url}?thumbnail&size=32`" alt="">
-				<img :src="`${g.user2.avatar_url}?thumbnail&size=32`" alt="">
+				<img :src="`${g.user1.avatarUrl}?thumbnail&size=32`" alt="">
+				<img :src="`${g.user2.avatarUrl}?thumbnail&size=32`" alt="">
 				<span><b>{{ g.user1.name }}</b> vs <b>{{ g.user2.name }}</b></span>
 				<span class="state">{{ g.isEnded ? '終了' : '進行中' }}</span>
 			</a>
@@ -49,8 +49,8 @@
 		<section v-if="games.length > 0">
 			<h2>みんなの対局</h2>
 			<a class="game" v-for="g in games" tabindex="-1" @click.prevent="go(g)" :href="`/othello/${g.id}`">
-				<img :src="`${g.user1.avatar_url}?thumbnail&size=32`" alt="">
-				<img :src="`${g.user2.avatar_url}?thumbnail&size=32`" alt="">
+				<img :src="`${g.user1.avatarUrl}?thumbnail&size=32`" alt="">
+				<img :src="`${g.user2.avatarUrl}?thumbnail&size=32`" alt="">
 				<span><b>{{ g.user1.name }}</b> vs <b>{{ g.user2.name }}</b></span>
 				<span class="state">{{ g.isEnded ? '終了' : '進行中' }}</span>
 			</a>
diff --git a/src/web/app/common/views/components/welcome-timeline.vue b/src/web/app/common/views/components/welcome-timeline.vue
index c61cae0f7..8f6199732 100644
--- a/src/web/app/common/views/components/welcome-timeline.vue
+++ b/src/web/app/common/views/components/welcome-timeline.vue
@@ -2,7 +2,7 @@
 <div class="mk-welcome-timeline">
 	<div v-for="post in posts">
 		<router-link class="avatar-anchor" :to="`/@${getAcct(post.user)}`" v-user-preview="post.user.id">
-			<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=96`" alt="avatar"/>
+			<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
 		</router-link>
 		<div class="body">
 			<header>
diff --git a/src/web/app/desktop/api/update-avatar.ts b/src/web/app/desktop/api/update-avatar.ts
index 445ef7d74..36a2ffe91 100644
--- a/src/web/app/desktop/api/update-avatar.ts
+++ b/src/web/app/desktop/api/update-avatar.ts
@@ -71,7 +71,7 @@ export default (os: OS) => (cb, file = null) => {
 			avatarId: file.id
 		}).then(i => {
 			os.i.avatarId = i.avatarId;
-			os.i.avatar_url = i.avatar_url;
+			os.i.avatarUrl = i.avatarUrl;
 
 			os.apis.dialog({
 				title: '%fa:info-circle%アバターを更新しました',
diff --git a/src/web/app/desktop/api/update-banner.ts b/src/web/app/desktop/api/update-banner.ts
index 002efce8c..e66dbf016 100644
--- a/src/web/app/desktop/api/update-banner.ts
+++ b/src/web/app/desktop/api/update-banner.ts
@@ -71,7 +71,7 @@ export default (os: OS) => (cb, file = null) => {
 			bannerId: file.id
 		}).then(i => {
 			os.i.bannerId = i.bannerId;
-			os.i.banner_url = i.banner_url;
+			os.i.bannerUrl = i.bannerUrl;
 
 			os.apis.dialog({
 				title: '%fa:info-circle%バナーを更新しました',
diff --git a/src/web/app/desktop/views/components/followers-window.vue b/src/web/app/desktop/views/components/followers-window.vue
index d41d356f9..623971fa3 100644
--- a/src/web/app/desktop/views/components/followers-window.vue
+++ b/src/web/app/desktop/views/components/followers-window.vue
@@ -1,7 +1,7 @@
 <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 }}のフォロワー
+		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ user.name }}のフォロワー
 	</span>
 	<mk-followers :user="user"/>
 </mk-window>
diff --git a/src/web/app/desktop/views/components/following-window.vue b/src/web/app/desktop/views/components/following-window.vue
index c516b3b17..612847b38 100644
--- a/src/web/app/desktop/views/components/following-window.vue
+++ b/src/web/app/desktop/views/components/following-window.vue
@@ -1,7 +1,7 @@
 <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 }}のフォロー
+		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ user.name }}のフォロー
 	</span>
 	<mk-following :user="user"/>
 </mk-window>
diff --git a/src/web/app/desktop/views/components/friends-maker.vue b/src/web/app/desktop/views/components/friends-maker.vue
index eed15e077..fd9914b15 100644
--- a/src/web/app/desktop/views/components/friends-maker.vue
+++ b/src/web/app/desktop/views/components/friends-maker.vue
@@ -4,7 +4,7 @@
 	<div class="users" v-if="!fetching && users.length > 0">
 		<div class="user" v-for="user in users" :key="user.id">
 			<router-link class="avatar-anchor" :to="`/@${getAcct(user)}`">
-				<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=42`" alt="" v-user-preview="user.id"/>
+				<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="user.id"/>
 			</router-link>
 			<div class="body">
 				<router-link class="name" :to="`/@${getAcct(user)}`" v-user-preview="user.id">{{ user.name }}</router-link>
diff --git a/src/web/app/desktop/views/components/mentions.vue b/src/web/app/desktop/views/components/mentions.vue
index 47066e813..90a92495b 100644
--- a/src/web/app/desktop/views/components/mentions.vue
+++ b/src/web/app/desktop/views/components/mentions.vue
@@ -70,7 +70,7 @@ export default Vue.extend({
 			this.moreFetching = true;
 			(this as any).api('posts/mentions', {
 				following: this.mode == 'following',
-				until_id: this.posts[this.posts.length - 1].id
+				untilId: this.posts[this.posts.length - 1].id
 			}).then(posts => {
 				this.posts = this.posts.concat(posts);
 				this.moreFetching = false;
diff --git a/src/web/app/desktop/views/components/notifications.vue b/src/web/app/desktop/views/components/notifications.vue
index 62593cf97..5e6db08c1 100644
--- a/src/web/app/desktop/views/components/notifications.vue
+++ b/src/web/app/desktop/views/components/notifications.vue
@@ -6,7 +6,7 @@
 				<mk-time :time="notification.createdAt"/>
 				<template v-if="notification.type == 'reaction'">
 					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">
-						<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
+						<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>
@@ -20,7 +20,7 @@
 				</template>
 				<template v-if="notification.type == 'repost'">
 					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">
-						<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
+						<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:retweet%
@@ -33,7 +33,7 @@
 				</template>
 				<template v-if="notification.type == 'quote'">
 					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">
-						<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
+						<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:quote-left%
@@ -44,7 +44,7 @@
 				</template>
 				<template v-if="notification.type == 'follow'">
 					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">
-						<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
+						<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:user-plus%
@@ -54,7 +54,7 @@
 				</template>
 				<template v-if="notification.type == 'reply'">
 					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">
-						<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
+						<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:reply%
@@ -65,7 +65,7 @@
 				</template>
 				<template v-if="notification.type == 'mention'">
 					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">
-						<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
+						<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:at%
@@ -76,7 +76,7 @@
 				</template>
 				<template v-if="notification.type == 'poll_vote'">
 					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">
-						<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
+						<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:chart-pie%<a :href="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ notification.user.name }}</a></p>
@@ -161,7 +161,7 @@ export default Vue.extend({
 
 			(this as any).api('i/notifications', {
 				limit: max + 1,
-				until_id: this.notifications[this.notifications.length - 1].id
+				untilId: this.notifications[this.notifications.length - 1].id
 			}).then(notifications => {
 				if (notifications.length == max + 1) {
 					this.moreNotifications = true;
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 7453a8bfc..35377e7c2 100644
--- a/src/web/app/desktop/views/components/post-detail.sub.vue
+++ b/src/web/app/desktop/views/components/post-detail.sub.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="sub" :title="title">
 	<router-link class="avatar-anchor" :to="`/@${acct}`">
-		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar" v-user-preview="post.userId"/>
+		<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="post.userId"/>
 	</router-link>
 	<div class="main">
 		<header>
diff --git a/src/web/app/desktop/views/components/post-detail.vue b/src/web/app/desktop/views/components/post-detail.vue
index 1ab751aaf..611660f52 100644
--- a/src/web/app/desktop/views/components/post-detail.vue
+++ b/src/web/app/desktop/views/components/post-detail.vue
@@ -19,7 +19,7 @@
 	<div class="repost" v-if="isRepost">
 		<p>
 			<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="post.userId">
-				<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=32`" alt="avatar"/>
+				<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
 			</router-link>
 			%fa:retweet%
 			<router-link class="name" :href="`/@${acct}`">{{ post.user.name }}</router-link>
@@ -28,7 +28,7 @@
 	</div>
 	<article>
 		<router-link class="avatar-anchor" :to="`/@${acct}`">
-			<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.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
 		</router-link>
 		<header>
 			<router-link class="name" :to="`/@${acct}`" v-user-preview="p.user.id">{{ p.user.name }}</router-link>
diff --git a/src/web/app/desktop/views/components/post-preview.vue b/src/web/app/desktop/views/components/post-preview.vue
index 450632656..0ac3223be 100644
--- a/src/web/app/desktop/views/components/post-preview.vue
+++ b/src/web/app/desktop/views/components/post-preview.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mk-post-preview" :title="title">
 	<router-link class="avatar-anchor" :to="`/@${acct}`">
-		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar" v-user-preview="post.userId"/>
+		<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="post.userId"/>
 	</router-link>
 	<div class="main">
 		<header>
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 7d2695d6b..65d3017d3 100644
--- a/src/web/app/desktop/views/components/posts.post.sub.vue
+++ b/src/web/app/desktop/views/components/posts.post.sub.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="sub" :title="title">
 	<router-link class="avatar-anchor" :to="`/@${acct}`">
-		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar" v-user-preview="post.userId"/>
+		<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="post.userId"/>
 	</router-link>
 	<div class="main">
 		<header>
diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index 185a621aa..e6dff2ccd 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -6,7 +6,7 @@
 	<div class="repost" v-if="isRepost">
 		<p>
 			<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="post.userId">
-				<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=32`" alt="avatar"/>
+				<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
 			</router-link>
 			%fa:retweet%
 			<span>{{ '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('{')) }}</span>
@@ -17,7 +17,7 @@
 	</div>
 	<article>
 		<router-link class="avatar-anchor" :to="`/@${acct}`">
-			<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.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
 		</router-link>
 		<div class="main">
 			<header>
diff --git a/src/web/app/desktop/views/components/settings.password.vue b/src/web/app/desktop/views/components/settings.password.vue
index be3f0370d..f883b5406 100644
--- a/src/web/app/desktop/views/components/settings.password.vue
+++ b/src/web/app/desktop/views/components/settings.password.vue
@@ -33,8 +33,8 @@ export default Vue.extend({
 							return;
 						}
 						(this as any).api('i/change_password', {
-							current_password: currentPassword,
-							new_password: newPassword
+							currentPasword: currentPassword,
+							newPassword: newPassword
 						}).then(() => {
 							(this as any).apis.notify('%i18n:desktop.tags.mk-password-setting.changed%');
 						});
diff --git a/src/web/app/desktop/views/components/settings.profile.vue b/src/web/app/desktop/views/components/settings.profile.vue
index f34d8ff00..ba86286f8 100644
--- a/src/web/app/desktop/views/components/settings.profile.vue
+++ b/src/web/app/desktop/views/components/settings.profile.vue
@@ -2,7 +2,7 @@
 <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"/>
+		<img class="avatar" :src="`${os.i.avatarUrl}?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">
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index cf75e52be..fd82c171c 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -86,7 +86,7 @@
 
 		<section class="notification" v-show="page == 'notification'">
 			<h1>通知</h1>
-			<mk-switch v-model="os.i.account.settings.auto_watch" @change="onChangeAutoWatch" text="投稿の自動ウォッチ">
+			<mk-switch v-model="os.i.account.settings.autoWatch" @change="onChangeAutoWatch" text="投稿の自動ウォッチ">
 				<span>リアクションしたり返信したりした投稿に関する通知を自動的に受け取るようにします。</span>
 			</mk-switch>
 		</section>
@@ -283,7 +283,7 @@ export default Vue.extend({
 		},
 		onChangeAutoWatch(v) {
 			(this as any).api('i/update', {
-				auto_watch: v
+				autoWatch: v
 			});
 		},
 		onChangeShowPostFormOnTopOfTl(v) {
diff --git a/src/web/app/desktop/views/components/timeline.vue b/src/web/app/desktop/views/components/timeline.vue
index c0eae2cd9..65b4bd1c7 100644
--- a/src/web/app/desktop/views/components/timeline.vue
+++ b/src/web/app/desktop/views/components/timeline.vue
@@ -65,7 +65,7 @@ export default Vue.extend({
 
 			(this as any).api('posts/timeline', {
 				limit: 11,
-				until_date: this.date ? this.date.getTime() : undefined
+				untilDate: this.date ? this.date.getTime() : undefined
 			}).then(posts => {
 				if (posts.length == 11) {
 					posts.pop();
@@ -82,7 +82,7 @@ export default Vue.extend({
 			this.moreFetching = true;
 			(this as any).api('posts/timeline', {
 				limit: 11,
-				until_id: this.posts[this.posts.length - 1].id
+				untilId: this.posts[this.posts.length - 1].id
 			}).then(posts => {
 				if (posts.length == 11) {
 					posts.pop();
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 19b9d7779..ec4635f33 100644
--- a/src/web/app/desktop/views/components/ui.header.account.vue
+++ b/src/web/app/desktop/views/components/ui.header.account.vue
@@ -2,7 +2,7 @@
 <div class="account">
 	<button class="header" :data-active="isOpen" @click="toggle">
 		<span class="username">{{ os.i.username }}<template v-if="!isOpen">%fa:angle-down%</template><template v-if="isOpen">%fa:angle-up%</template></span>
-		<img class="avatar" :src="`${ os.i.avatar_url }?thumbnail&size=64`" alt="avatar"/>
+		<img class="avatar" :src="`${ os.i.avatarUrl }?thumbnail&size=64`" alt="avatar"/>
 	</button>
 	<transition name="zoom-in-top">
 		<div class="menu" v-if="isOpen">
diff --git a/src/web/app/desktop/views/components/user-preview.vue b/src/web/app/desktop/views/components/user-preview.vue
index 4535ad890..8c86b2efe 100644
--- a/src/web/app/desktop/views/components/user-preview.vue
+++ b/src/web/app/desktop/views/components/user-preview.vue
@@ -1,9 +1,9 @@
 <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>
+		<div class="banner" :style="u.bannerUrl ? `background-image: url(${u.bannerUrl}?thumbnail&size=512)` : ''"></div>
 		<router-link class="avatar" :to="`/@${acct}`">
-			<img :src="`${u.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+			<img :src="`${u.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="title">
 			<router-link class="name" :to="`/@${acct}`">{{ u.name }}</router-link>
diff --git a/src/web/app/desktop/views/components/users-list.item.vue b/src/web/app/desktop/views/components/users-list.item.vue
index e02d1311d..c2f30cf38 100644
--- a/src/web/app/desktop/views/components/users-list.item.vue
+++ b/src/web/app/desktop/views/components/users-list.item.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="root item">
 	<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="user.id">
-		<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 	</router-link>
 	<div class="main">
 		<header>
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 9f67f5cf7..d0dab6c3d 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
@@ -4,7 +4,7 @@
 	<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="`/@${getAcct(user)}`" :key="user.id">
-		<img :src="`${user.avatar_url}?thumbnail&size=64`" :alt="user.name" v-user-preview="user.id"/>
+		<img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="user.name" v-user-preview="user.id"/>
 	</router-link>
 	</div>
 	<p class="empty" v-if="!fetching && users.length == 0">%i18n:desktop.tags.mk-user.followers-you-know.no-users%</p>
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 cc0bcef25..3ec30fb43 100644
--- a/src/web/app/desktop/views/pages/user/user.friends.vue
+++ b/src/web/app/desktop/views/pages/user/user.friends.vue
@@ -5,7 +5,7 @@
 	<template v-if="!fetching && users.length != 0">
 		<div class="user" v-for="friend in users">
 			<router-link class="avatar-anchor" :to="`/@${getAcct(friend)}`">
-				<img class="avatar" :src="`${friend.avatar_url}?thumbnail&size=42`" alt="" v-user-preview="friend.id"/>
+				<img class="avatar" :src="`${friend.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="friend.id"/>
 			</router-link>
 			<div class="body">
 				<router-link class="name" :to="`/@${getAcct(friend)}`" v-user-preview="friend.id">{{ friend.name }}</router-link>
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 3522e76bd..54f431fd2 100644
--- a/src/web/app/desktop/views/pages/user/user.header.vue
+++ b/src/web/app/desktop/views/pages/user/user.header.vue
@@ -1,11 +1,11 @@
 <template>
-<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 class="header" :data-is-dark-background="user.bannerUrl != null">
+	<div class="banner-container" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=2048)` : ''">
+		<div class="banner" ref="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?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"/>
+		<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=150`" alt="avatar"/>
 		<div class="title">
 			<p class="name">{{ user.name }}</p>
 			<p class="username">@{{ acct }}</p>
@@ -59,7 +59,7 @@ export default Vue.extend({
 			if (!(this as any).os.isSignedIn || (this as any).os.i.id != this.user.id) return;
 
 			(this as any).apis.updateBanner((this as any).os.i, i => {
-				this.user.banner_url = i.banner_url;
+				this.user.bannerUrl = i.bannerUrl;
 			});
 		}
 	}
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 1f0d0b198..134ad423c 100644
--- a/src/web/app/desktop/views/pages/user/user.timeline.vue
+++ b/src/web/app/desktop/views/pages/user/user.timeline.vue
@@ -62,7 +62,7 @@ export default Vue.extend({
 		fetch(cb?) {
 			(this as any).api('users/posts', {
 				userId: this.user.id,
-				until_date: this.date ? this.date.getTime() : undefined,
+				untilDate: this.date ? this.date.getTime() : undefined,
 				with_replies: this.mode == 'with-replies'
 			}).then(posts => {
 				this.posts = posts;
@@ -76,7 +76,7 @@ export default Vue.extend({
 			(this as any).api('users/posts', {
 				userId: this.user.id,
 				with_replies: this.mode == 'with-replies',
-				until_id: this.posts[this.posts.length - 1].id
+				untilId: this.posts[this.posts.length - 1].id
 			}).then(posts => {
 				this.moreFetching = false;
 				this.posts = this.posts.concat(posts);
diff --git a/src/web/app/desktop/views/pages/welcome.vue b/src/web/app/desktop/views/pages/welcome.vue
index 927ddf575..34c28854b 100644
--- a/src/web/app/desktop/views/pages/welcome.vue
+++ b/src/web/app/desktop/views/pages/welcome.vue
@@ -9,7 +9,7 @@
 					<p><button class="signup" @click="signup">はじめる</button><button class="signin" @click="signin">ログイン</button></p>
 					<div class="users">
 						<router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="`/@${getAcct(user)}`" v-user-preview="user.id">
-							<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+							<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 						</router-link>
 					</div>
 				</div>
diff --git a/src/web/app/desktop/views/widgets/profile.vue b/src/web/app/desktop/views/widgets/profile.vue
index 394010619..83cd67b50 100644
--- a/src/web/app/desktop/views/widgets/profile.vue
+++ b/src/web/app/desktop/views/widgets/profile.vue
@@ -4,12 +4,12 @@
 	:data-melt="props.design == 2"
 >
 	<div class="banner"
-		:style="os.i.banner_url ? `background-image: url(${os.i.banner_url}?thumbnail&size=256)` : ''"
+		:style="os.i.bannerUrl ? `background-image: url(${os.i.bannerUrl}?thumbnail&size=256)` : ''"
 		title="クリックでバナー編集"
 		@click="os.apis.updateBanner"
 	></div>
 	<img class="avatar"
-		:src="`${os.i.avatar_url}?thumbnail&size=96`"
+		:src="`${os.i.avatarUrl}?thumbnail&size=96`"
 		@click="os.apis.updateAvatar"
 		alt="avatar"
 		title="クリックでアバター編集"
diff --git a/src/web/app/desktop/views/widgets/users.vue b/src/web/app/desktop/views/widgets/users.vue
index 10e3c529e..7b8944112 100644
--- a/src/web/app/desktop/views/widgets/users.vue
+++ b/src/web/app/desktop/views/widgets/users.vue
@@ -8,7 +8,7 @@
 	<template v-else-if="users.length != 0">
 		<div class="user" v-for="_user in users">
 			<router-link class="avatar-anchor" :to="`/@${getAcct(_user)}`">
-				<img class="avatar" :src="`${_user.avatar_url}?thumbnail&size=42`" alt="" v-user-preview="_user.id"/>
+				<img class="avatar" :src="`${_user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="_user.id"/>
 			</router-link>
 			<div class="body">
 				<router-link class="name" :to="`/@${getAcct(_user)}`" v-user-preview="_user.id">{{ _user.name }}</router-link>
diff --git a/src/web/app/dev/views/new-app.vue b/src/web/app/dev/views/new-app.vue
index cd07cc4d4..e407ca00d 100644
--- a/src/web/app/dev/views/new-app.vue
+++ b/src/web/app/dev/views/new-app.vue
@@ -92,7 +92,7 @@ export default Vue.extend({
 				name: this.name,
 				nameId: this.nid,
 				description: this.description,
-				callback_url: this.cb,
+				callbackUrl: this.cb,
 				permission: this.permission
 			}).then(() => {
 				location.href = '/apps';
diff --git a/src/web/app/mobile/views/components/drive.vue b/src/web/app/mobile/views/components/drive.vue
index dd4d97e96..5affbdaf1 100644
--- a/src/web/app/mobile/views/components/drive.vue
+++ b/src/web/app/mobile/views/components/drive.vue
@@ -320,7 +320,7 @@ export default Vue.extend({
 			(this as any).api('drive/files', {
 				folderId: this.folder ? this.folder.id : null,
 				limit: max + 1,
-				until_id: this.files[this.files.length - 1].id
+				untilId: this.files[this.files.length - 1].id
 			}).then(files => {
 				if (files.length == max + 1) {
 					this.moreFiles = true;
diff --git a/src/web/app/mobile/views/components/notification-preview.vue b/src/web/app/mobile/views/components/notification-preview.vue
index 47df626fa..fce9ed82f 100644
--- a/src/web/app/mobile/views/components/notification-preview.vue
+++ b/src/web/app/mobile/views/components/notification-preview.vue
@@ -1,7 +1,7 @@
 <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"/>
+		<img class="avatar" :src="`${notification.user.avatarUrl}?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>
@@ -9,7 +9,7 @@
 	</template>
 
 	<template v-if="notification.type == 'repost'">
-		<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		<img class="avatar" :src="`${notification.post.user.avatarUrl}?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>
@@ -17,7 +17,7 @@
 	</template>
 
 	<template v-if="notification.type == 'quote'">
-		<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		<img class="avatar" :src="`${notification.post.user.avatarUrl}?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>
@@ -25,14 +25,14 @@
 	</template>
 
 	<template v-if="notification.type == 'follow'">
-		<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		<img class="avatar" :src="`${notification.user.avatarUrl}?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"/>
+		<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
 			<p>%fa:reply%{{ notification.post.user.name }}</p>
 			<p class="post-preview">{{ getPostSummary(notification.post) }}</p>
@@ -40,7 +40,7 @@
 	</template>
 
 	<template v-if="notification.type == 'mention'">
-		<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
 			<p>%fa:at%{{ notification.post.user.name }}</p>
 			<p class="post-preview">{{ getPostSummary(notification.post) }}</p>
@@ -48,7 +48,7 @@
 	</template>
 
 	<template v-if="notification.type == 'poll_vote'">
-		<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		<img class="avatar" :src="`${notification.user.avatarUrl}?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>
diff --git a/src/web/app/mobile/views/components/notification.vue b/src/web/app/mobile/views/components/notification.vue
index aac8f8290..e221fb3ac 100644
--- a/src/web/app/mobile/views/components/notification.vue
+++ b/src/web/app/mobile/views/components/notification.vue
@@ -3,7 +3,7 @@
 	<div class="notification reaction" v-if="notification.type == 'reaction'">
 		<mk-time :time="notification.createdAt"/>
 		<router-link class="avatar-anchor" :to="`/@${acct}`">
-			<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+			<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="text">
 			<p>
@@ -20,7 +20,7 @@
 	<div class="notification repost" v-if="notification.type == 'repost'">
 		<mk-time :time="notification.createdAt"/>
 		<router-link class="avatar-anchor" :to="`/@${acct}`">
-			<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+			<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="text">
 			<p>
@@ -40,7 +40,7 @@
 	<div class="notification follow" v-if="notification.type == 'follow'">
 		<mk-time :time="notification.createdAt"/>
 		<router-link class="avatar-anchor" :to="`/@${acct}`">
-			<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+			<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="text">
 			<p>
@@ -61,7 +61,7 @@
 	<div class="notification poll_vote" v-if="notification.type == 'poll_vote'">
 		<mk-time :time="notification.createdAt"/>
 		<router-link class="avatar-anchor" :to="`/@${acct}`">
-			<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+			<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="text">
 			<p>
diff --git a/src/web/app/mobile/views/components/notifications.vue b/src/web/app/mobile/views/components/notifications.vue
index 4365198f1..d68b990df 100644
--- a/src/web/app/mobile/views/components/notifications.vue
+++ b/src/web/app/mobile/views/components/notifications.vue
@@ -75,7 +75,7 @@ export default Vue.extend({
 
 			(this as any).api('i/notifications', {
 				limit: max + 1,
-				until_id: this.notifications[this.notifications.length - 1].id
+				untilId: this.notifications[this.notifications.length - 1].id
 			}).then(notifications => {
 				if (notifications.length == max + 1) {
 					this.moreNotifications = true;
diff --git a/src/web/app/mobile/views/components/post-detail.sub.vue b/src/web/app/mobile/views/components/post-detail.sub.vue
index 427e054fd..db7567834 100644
--- a/src/web/app/mobile/views/components/post-detail.sub.vue
+++ b/src/web/app/mobile/views/components/post-detail.sub.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="root sub">
 	<router-link class="avatar-anchor" :to="`/@${acct}`">
-		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 	</router-link>
 	<div class="main">
 		<header>
diff --git a/src/web/app/mobile/views/components/post-detail.vue b/src/web/app/mobile/views/components/post-detail.vue
index 9a8c33a80..241782aa5 100644
--- a/src/web/app/mobile/views/components/post-detail.vue
+++ b/src/web/app/mobile/views/components/post-detail.vue
@@ -18,7 +18,7 @@
 	<div class="repost" v-if="isRepost">
 		<p>
 			<router-link class="avatar-anchor" :to="`/@${acct}`">
-				<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=32`" alt="avatar"/>
+				<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
 			</router-link>
 			%fa:retweet%
 			<router-link class="name" :to="`/@${acct}`">
@@ -30,7 +30,7 @@
 	<article>
 		<header>
 			<router-link class="avatar-anchor" :to="`/@${pAcct}`">
-				<img class="avatar" :src="`${p.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+				<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 			</router-link>
 			<div>
 				<router-link class="name" :to="`/@${pAcct}`">{{ p.user.name }}</router-link>
diff --git a/src/web/app/mobile/views/components/post-preview.vue b/src/web/app/mobile/views/components/post-preview.vue
index e64084341..a6141dc8e 100644
--- a/src/web/app/mobile/views/components/post-preview.vue
+++ b/src/web/app/mobile/views/components/post-preview.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mk-post-preview">
 	<router-link class="avatar-anchor" :to="`/@${acct}`">
-		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 	</router-link>
 	<div class="main">
 		<header>
diff --git a/src/web/app/mobile/views/components/post.sub.vue b/src/web/app/mobile/views/components/post.sub.vue
index 8a11239da..adf444a2d 100644
--- a/src/web/app/mobile/views/components/post.sub.vue
+++ b/src/web/app/mobile/views/components/post.sub.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="sub">
 	<router-link class="avatar-anchor" :to="`/@${acct}`">
-		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=96`" alt="avatar"/>
+		<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
 	</router-link>
 	<div class="main">
 		<header>
diff --git a/src/web/app/mobile/views/components/post.vue b/src/web/app/mobile/views/components/post.vue
index 243e7d9c2..0c6522db8 100644
--- a/src/web/app/mobile/views/components/post.vue
+++ b/src/web/app/mobile/views/components/post.vue
@@ -6,7 +6,7 @@
 	<div class="repost" v-if="isRepost">
 		<p>
 			<router-link class="avatar-anchor" :to="`/@${acct}`">
-				<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+				<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 			</router-link>
 			%fa:retweet%
 			<span>{{ '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('{')) }}</span>
@@ -17,7 +17,7 @@
 	</div>
 	<article>
 		<router-link class="avatar-anchor" :to="`/@${pAcct}`">
-			<img class="avatar" :src="`${p.user.avatar_url}?thumbnail&size=96`" alt="avatar"/>
+			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
 		</router-link>
 		<div class="main">
 			<header>
diff --git a/src/web/app/mobile/views/components/timeline.vue b/src/web/app/mobile/views/components/timeline.vue
index 999f4a1f1..7b5948faf 100644
--- a/src/web/app/mobile/views/components/timeline.vue
+++ b/src/web/app/mobile/views/components/timeline.vue
@@ -65,7 +65,7 @@ export default Vue.extend({
 			this.fetching = true;
 			(this as any).api('posts/timeline', {
 				limit: limit + 1,
-				until_date: this.date ? (this.date as any).getTime() : undefined
+				untilDate: this.date ? (this.date as any).getTime() : undefined
 			}).then(posts => {
 				if (posts.length == limit + 1) {
 					posts.pop();
@@ -81,7 +81,7 @@ export default Vue.extend({
 			this.moreFetching = true;
 			(this as any).api('posts/timeline', {
 				limit: limit + 1,
-				until_id: this.posts[this.posts.length - 1].id
+				untilId: this.posts[this.posts.length - 1].id
 			}).then(posts => {
 				if (posts.length == limit + 1) {
 					posts.pop();
diff --git a/src/web/app/mobile/views/components/ui.nav.vue b/src/web/app/mobile/views/components/ui.nav.vue
index 760a5b518..a923774a7 100644
--- a/src/web/app/mobile/views/components/ui.nav.vue
+++ b/src/web/app/mobile/views/components/ui.nav.vue
@@ -10,7 +10,7 @@
 	<transition name="nav">
 		<div class="body" v-if="isOpen">
 			<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"/>
+				<img class="avatar" :src="`${os.i.avatarUrl}?thumbnail&size=128`" alt="avatar"/>
 				<p class="name">{{ os.i.name }}</p>
 			</router-link>
 			<div class="links">
diff --git a/src/web/app/mobile/views/components/user-card.vue b/src/web/app/mobile/views/components/user-card.vue
index 5a7309cfd..ffa110051 100644
--- a/src/web/app/mobile/views/components/user-card.vue
+++ b/src/web/app/mobile/views/components/user-card.vue
@@ -1,8 +1,8 @@
 <template>
 <div class="mk-user-card">
-	<header :style="user.banner_url ? `background-image: url(${user.banner_url}?thumbnail&size=1024)` : ''">
+	<header :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=1024)` : ''">
 		<a :href="`/@${acct}`">
-			<img :src="`${user.avatar_url}?thumbnail&size=200`" alt="avatar"/>
+			<img :src="`${user.avatarUrl}?thumbnail&size=200`" alt="avatar"/>
 		</a>
 	</header>
 	<a class="name" :href="`/@${acct}`" target="_blank">{{ user.name }}</a>
diff --git a/src/web/app/mobile/views/components/user-preview.vue b/src/web/app/mobile/views/components/user-preview.vue
index be80582ca..e51e4353d 100644
--- a/src/web/app/mobile/views/components/user-preview.vue
+++ b/src/web/app/mobile/views/components/user-preview.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mk-user-preview">
 	<router-link class="avatar-anchor" :to="`/@${acct}`">
-		<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 	</router-link>
 	<div class="main">
 		<header>
diff --git a/src/web/app/mobile/views/components/user-timeline.vue b/src/web/app/mobile/views/components/user-timeline.vue
index 73ff440dc..d1f771f81 100644
--- a/src/web/app/mobile/views/components/user-timeline.vue
+++ b/src/web/app/mobile/views/components/user-timeline.vue
@@ -53,7 +53,7 @@ export default Vue.extend({
 				userId: this.user.id,
 				with_media: this.withMedia,
 				limit: limit + 1,
-				until_id: this.posts[this.posts.length - 1].id
+				untilId: this.posts[this.posts.length - 1].id
 			}).then(posts => {
 				if (posts.length == limit + 1) {
 					posts.pop();
diff --git a/src/web/app/mobile/views/pages/followers.vue b/src/web/app/mobile/views/pages/followers.vue
index b5267bebf..08a15f945 100644
--- a/src/web/app/mobile/views/pages/followers.vue
+++ b/src/web/app/mobile/views/pages/followers.vue
@@ -1,7 +1,7 @@
 <template>
 <mk-ui>
 	<template slot="header" v-if="!fetching">
-		<img :src="`${user.avatar_url}?thumbnail&size=64`" alt="">
+		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt="">
 		{{ '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name) }}
 	</template>
 	<mk-users-list
diff --git a/src/web/app/mobile/views/pages/following.vue b/src/web/app/mobile/views/pages/following.vue
index d8c31c9f0..ecdaa5a58 100644
--- a/src/web/app/mobile/views/pages/following.vue
+++ b/src/web/app/mobile/views/pages/following.vue
@@ -1,7 +1,7 @@
 <template>
 <mk-ui>
 	<template slot="header" v-if="!fetching">
-		<img :src="`${user.avatar_url}?thumbnail&size=64`" alt="">
+		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt="">
 		{{ '%i18n:mobile.tags.mk-user-following-page.following-of%'.replace('{}', user.name) }}
 	</template>
 	<mk-users-list
diff --git a/src/web/app/mobile/views/pages/notifications.vue b/src/web/app/mobile/views/pages/notifications.vue
index 3dcfb2f38..6d45e22a9 100644
--- a/src/web/app/mobile/views/pages/notifications.vue
+++ b/src/web/app/mobile/views/pages/notifications.vue
@@ -22,7 +22,7 @@ export default Vue.extend({
 			const ok = window.confirm('%i18n:mobile.tags.mk-notifications-page.read-all%');
 			if (!ok) return;
 
-			(this as any).api('notifications/mark_as_read_all');
+			(this as any).api('notifications/markAsRead_all');
 		},
 		onFetched() {
 			Progress.done();
diff --git a/src/web/app/mobile/views/pages/profile-setting.vue b/src/web/app/mobile/views/pages/profile-setting.vue
index d4bb25487..15f9bc9b6 100644
--- a/src/web/app/mobile/views/pages/profile-setting.vue
+++ b/src/web/app/mobile/views/pages/profile-setting.vue
@@ -4,8 +4,8 @@
 	<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 :style="os.i.bannerUrl ? `background-image: url(${os.i.bannerUrl}?thumbnail&size=1024)` : ''" @click="setBanner">
+				<img :src="`${os.i.avatarUrl}?thumbnail&size=200`" alt="avatar" @click="setAvatar"/>
 			</div>
 			<label>
 				<p>%i18n:mobile.tags.mk-profile-setting.name%</p>
diff --git a/src/web/app/mobile/views/pages/user.vue b/src/web/app/mobile/views/pages/user.vue
index c4d6b67e6..f5bbd4162 100644
--- a/src/web/app/mobile/views/pages/user.vue
+++ b/src/web/app/mobile/views/pages/user.vue
@@ -3,11 +3,11 @@
 	<span slot="header" v-if="!fetching">%fa:user% {{ user.name }}</span>
 	<main v-if="!fetching">
 		<header>
-			<div class="banner" :style="user.banner_url ? `background-image: url(${user.banner_url}?thumbnail&size=1024)` : ''"></div>
+			<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=1024)` : ''"></div>
 			<div class="body">
 				<div class="top">
 					<a class="avatar">
-						<img :src="`${user.avatar_url}?thumbnail&size=200`" alt="avatar"/>
+						<img :src="`${user.avatarUrl}?thumbnail&size=200`" alt="avatar"/>
 					</a>
 					<mk-follow-button v-if="os.isSignedIn && os.i.id != user.id" :user="user"/>
 				</div>
diff --git a/src/web/app/mobile/views/pages/user/home.followers-you-know.vue b/src/web/app/mobile/views/pages/user/home.followers-you-know.vue
index 508ab4b4a..8c84d2dbb 100644
--- a/src/web/app/mobile/views/pages/user/home.followers-you-know.vue
+++ b/src/web/app/mobile/views/pages/user/home.followers-you-know.vue
@@ -3,7 +3,7 @@
 	<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="`/@${getAcct(user)}`">
-			<img :src="`${user.avatar_url}?thumbnail&size=64`" :alt="user.name"/>
+			<img :src="`${user.avatarUrl}?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>
diff --git a/src/web/app/mobile/views/pages/welcome.vue b/src/web/app/mobile/views/pages/welcome.vue
index 7a809702c..17cdf9306 100644
--- a/src/web/app/mobile/views/pages/welcome.vue
+++ b/src/web/app/mobile/views/pages/welcome.vue
@@ -22,7 +22,7 @@
 	</div>
 	<div class="users">
 		<router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="`/@${user.username}`">
-			<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+			<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 	</div>
 	<footer>
diff --git a/src/web/app/mobile/views/widgets/profile.vue b/src/web/app/mobile/views/widgets/profile.vue
index 1c9d038b4..f1d283e45 100644
--- a/src/web/app/mobile/views/widgets/profile.vue
+++ b/src/web/app/mobile/views/widgets/profile.vue
@@ -2,10 +2,10 @@
 <div class="mkw-profile">
 	<mk-widget-container>
 		<div :class="$style.banner"
-			:style="os.i.banner_url ? `background-image: url(${os.i.banner_url}?thumbnail&size=256)` : ''"
+			:style="os.i.bannerUrl ? `background-image: url(${os.i.bannerUrl}?thumbnail&size=256)` : ''"
 		></div>
 		<img :class="$style.avatar"
-			:src="`${os.i.avatar_url}?thumbnail&size=96`"
+			:src="`${os.i.avatarUrl}?thumbnail&size=96`"
 			alt="avatar"
 		/>
 		<router-link :class="$style.name" :to="`/@${os.i.username}`">{{ os.i.name }}</router-link>
diff --git a/src/web/docs/api.ja.pug b/src/web/docs/api.ja.pug
index 2bb08f7f3..665cfdc4b 100644
--- a/src/web/docs/api.ja.pug
+++ b/src/web/docs/api.ja.pug
@@ -54,7 +54,7 @@ section
 		h3 2.ユーザーに認証させる
 		p あなたのアプリを使ってもらうには、ユーザーにアカウントへのアクセスの許可をもらう必要があります。
 		p
-			| 認証セッションを開始するには、#{common.config.api_url}/auth/session/generate へパラメータに app_secret としてシークレットキーを含めたリクエストを送信します。
+			| 認証セッションを開始するには、#{common.config.api_url}/auth/session/generate へパラメータに appSecret としてシークレットキーを含めたリクエストを送信します。
 			| リクエスト形式はJSONで、メソッドはPOSTです。
 			| レスポンスとして認証セッションのトークンや認証フォームのURLが取得できるので、認証フォームのURLをブラウザで表示し、ユーザーにフォームを提示してください。
 
@@ -76,7 +76,7 @@ section
 					th 説明
 			tbody
 				tr
-					td app_secret
+					td appSecret
 					td string
 					td あなたのアプリのシークレットキー
 				tr
diff --git a/src/web/docs/api/endpoints/posts/timeline.yaml b/src/web/docs/api/endpoints/posts/timeline.yaml
index 01976b061..9c44dd736 100644
--- a/src/web/docs/api/endpoints/posts/timeline.yaml
+++ b/src/web/docs/api/endpoints/posts/timeline.yaml
@@ -10,22 +10,22 @@ params:
     optional: true
     desc:
       ja: "取得する最大の数"
-  - name: "since_id"
+  - name: "sinceId"
     type: "id(Post)"
     optional: true
     desc:
       ja: "指定すると、この投稿を基点としてより新しい投稿を取得します"
-  - name: "until_id"
+  - name: "untilId"
     type: "id(Post)"
     optional: true
     desc:
       ja: "指定すると、この投稿を基点としてより古い投稿を取得します"
-  - name: "since_date"
+  - name: "sinceDate"
     type: "number"
     optional: true
     desc:
       ja: "指定した時間を基点としてより新しい投稿を取得します。数値は、1970 年 1 月 1 日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。"
-  - name: "until_date"
+  - name: "untilDate"
     type: "number"
     optional: true
     desc:
diff --git a/src/web/docs/api/entities/user.yaml b/src/web/docs/api/entities/user.yaml
index 60f2c8608..f45455e73 100644
--- a/src/web/docs/api/entities/user.yaml
+++ b/src/web/docs/api/entities/user.yaml
@@ -35,7 +35,7 @@ props:
     desc:
       ja: "アバターのID"
       en: "The ID of the avatar of this user"
-  - name: "avatar_url"
+  - name: "avatarUrl"
     type: "string"
     optional: false
     desc:
@@ -47,7 +47,7 @@ props:
     desc:
       ja: "バナーのID"
       en: "The ID of the banner of this user"
-  - name: "banner_url"
+  - name: "bannerUrl"
     type: "string"
     optional: false
     desc:
diff --git a/swagger.js b/swagger.js
index f6c2ca7fd..ebd7a356e 100644
--- a/swagger.js
+++ b/swagger.js
@@ -83,7 +83,7 @@ const defaultSwagger = {
           "type": "string",
           "description": "アバターに設定しているドライブのファイルのID"
         },
-        "avatar_url": {
+        "avatarUrl": {
           "type": "string",
           "description": "アバターURL"
         },
@@ -91,7 +91,7 @@ const defaultSwagger = {
           "type": "string",
           "description": "バナーに設定しているドライブのファイルのID"
         },
-        "banner_url": {
+        "bannerUrl": {
           "type": "string",
           "description": "バナーURL"
         },

From 9d0b581e0655d211a21eb268760259c975ef8de3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 16:59:11 +0900
Subject: [PATCH 0901/1250] wip

---
 src/api/endpoints/meta.ts                      |  1 -
 src/api/endpoints/othello/games/show.ts        |  6 +++---
 src/api/endpoints/posts/create.ts              | 16 ++++++++--------
 src/api/endpoints/posts/reactions/create.ts    |  2 +-
 src/api/endpoints/posts/reactions/delete.ts    |  2 +-
 src/api/endpoints/posts/search.ts              | 18 +++++++++---------
 src/api/endpoints/posts/trend.ts               |  4 ++--
 src/api/endpoints/stats.ts                     |  4 ++--
 src/api/endpoints/users/posts.ts               | 12 ++++++------
 src/api/models/app.ts                          |  2 +-
 src/api/models/channel.ts                      |  2 +-
 src/api/models/drive-folder.ts                 |  4 ++--
 src/api/models/post.ts                         |  6 +++++-
 src/api/models/user.ts                         | 14 +++++++-------
 src/web/app/auth/views/index.vue               |  4 ++--
 src/web/app/ch/tags/channel.tag                |  8 ++++----
 .../app/common/scripts/parse-search-query.ts   |  4 ++--
 .../components/messaging-room.message.vue      |  4 ++--
 .../app/common/views/components/othello.vue    |  2 +-
 src/web/app/common/views/components/poll.vue   |  8 ++++----
 .../views/components/reactions-viewer.vue      |  2 +-
 .../desktop/views/components/follow-button.vue | 18 +++++++++---------
 .../app/desktop/views/components/followers.vue |  2 +-
 .../app/desktop/views/components/following.vue |  2 +-
 .../desktop/views/components/post-detail.vue   | 10 +++++-----
 .../desktop/views/components/posts.post.vue    | 10 +++++-----
 .../views/components/users-list.item.vue       |  2 +-
 src/web/app/desktop/views/pages/othello.vue    |  2 +-
 .../desktop/views/pages/user/user.photos.vue   |  2 +-
 .../desktop/views/pages/user/user.profile.vue  | 10 +++++-----
 src/web/app/mobile/views/components/drive.vue  |  8 ++++----
 .../mobile/views/components/follow-button.vue  | 18 +++++++++---------
 .../mobile/views/components/post-detail.vue    | 10 +++++-----
 src/web/app/mobile/views/components/post.vue   | 10 +++++-----
 .../mobile/views/components/user-timeline.vue  |  4 ++--
 src/web/app/mobile/views/pages/followers.vue   |  2 +-
 src/web/app/mobile/views/pages/following.vue   |  2 +-
 src/web/app/mobile/views/pages/othello.vue     |  2 +-
 src/web/app/mobile/views/pages/user.vue        |  2 +-
 .../mobile/views/pages/user/home.photos.vue    |  2 +-
 src/web/app/stats/tags/index.tag               |  2 +-
 src/web/docs/api/entities/post.yaml            |  4 ++--
 src/web/docs/api/entities/user.yaml            |  6 +++---
 tools/migration/shell.camel-case.js            |  3 +++
 44 files changed, 132 insertions(+), 126 deletions(-)

diff --git a/src/api/endpoints/meta.ts b/src/api/endpoints/meta.ts
index 1370ead3c..80a3725eb 100644
--- a/src/api/endpoints/meta.ts
+++ b/src/api/endpoints/meta.ts
@@ -53,7 +53,6 @@ module.exports = (params) => new Promise(async (res, rej) => {
 			model: os.cpus()[0].model,
 			cores: os.cpus().length
 		},
-		top_image: meta.top_image,
 		broadcasts: meta.broadcasts
 	});
 });
diff --git a/src/api/endpoints/othello/games/show.ts b/src/api/endpoints/othello/games/show.ts
index 19f5d0fef..f9084682f 100644
--- a/src/api/endpoints/othello/games/show.ts
+++ b/src/api/endpoints/othello/games/show.ts
@@ -3,9 +3,9 @@ import OthelloGame, { pack } from '../../../models/othello-game';
 import Othello from '../../../../common/othello/core';
 
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'game_id' parameter
-	const [gameId, gameIdErr] = $(params.game_id).id().$;
-	if (gameIdErr) return rej('invalid game_id param');
+	// Get 'gameId' parameter
+	const [gameId, gameIdErr] = $(params.gameId).id().$;
+	if (gameIdErr) return rej('invalid gameId param');
 
 	const game = await OthelloGame.findOne({ _id: gameId });
 
diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index b99d1fbbc..281737454 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -211,12 +211,12 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 
 	// 直近の投稿と重複してたらエラー
 	// TODO: 直近の投稿が一日前くらいなら重複とは見なさない
-	if (user.latest_post) {
+	if (user.latestPost) {
 		if (deepEqual({
-			text: user.latest_post.text,
-			reply: user.latest_post.replyId ? user.latest_post.replyId.toString() : null,
-			repost: user.latest_post.repostId ? user.latest_post.repostId.toString() : null,
-			mediaIds: (user.latest_post.mediaIds || []).map(id => id.toString())
+			text: user.latestPost.text,
+			reply: user.latestPost.replyId ? user.latestPost.replyId.toString() : null,
+			repost: user.latestPost.repostId ? user.latestPost.repostId.toString() : null,
+			mediaIds: (user.latestPost.mediaIds || []).map(id => id.toString())
 		}, {
 			text: text,
 			reply: reply ? reply._id.toString() : null,
@@ -277,7 +277,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 
 	User.update({ _id: user._id }, {
 		$set: {
-			latest_post: post
+			latestPost: post
 		}
 	});
 
@@ -362,7 +362,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		// Increment replies count
 		Post.update({ _id: reply._id }, {
 			$inc: {
-				replies_count: 1
+				repliesCount: 1
 			}
 		});
 
@@ -457,7 +457,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 			// Update repostee status
 			Post.update({ _id: repost._id }, {
 				$inc: {
-					repost_count: 1
+					repostCount: 1
 				}
 			});
 		}
diff --git a/src/api/endpoints/posts/reactions/create.ts b/src/api/endpoints/posts/reactions/create.ts
index 6f75a923c..a1e677980 100644
--- a/src/api/endpoints/posts/reactions/create.ts
+++ b/src/api/endpoints/posts/reactions/create.ts
@@ -73,7 +73,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	res();
 
 	const inc = {};
-	inc[`reaction_counts.${reaction}`] = 1;
+	inc[`reactionCounts.${reaction}`] = 1;
 
 	// Increment reactions count
 	await Post.update({ _id: post._id }, {
diff --git a/src/api/endpoints/posts/reactions/delete.ts b/src/api/endpoints/posts/reactions/delete.ts
index 18fdabcdc..b09bcbb4b 100644
--- a/src/api/endpoints/posts/reactions/delete.ts
+++ b/src/api/endpoints/posts/reactions/delete.ts
@@ -51,7 +51,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	res();
 
 	const dec = {};
-	dec[`reaction_counts.${exist.reaction}`] = -1;
+	dec[`reactionCounts.${exist.reaction}`] = -1;
 
 	// Decrement reactions count
 	Post.update({ _id: post._id }, {
diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index f90b9aa0d..5c324bfe9 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -21,21 +21,21 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [text, textError] = $(params.text).optional.string().$;
 	if (textError) return rej('invalid text param');
 
-	// Get 'include_userIds' parameter
-	const [includeUserIds = [], includeUserIdsErr] = $(params.include_userIds).optional.array('id').$;
-	if (includeUserIdsErr) return rej('invalid include_userIds param');
+	// Get 'includeUserIds' parameter
+	const [includeUserIds = [], includeUserIdsErr] = $(params.includeUserIds).optional.array('id').$;
+	if (includeUserIdsErr) return rej('invalid includeUserIds param');
 
 	// Get 'exclude_userIds' parameter
 	const [excludeUserIds = [], excludeUserIdsErr] = $(params.exclude_userIds).optional.array('id').$;
 	if (excludeUserIdsErr) return rej('invalid exclude_userIds param');
 
-	// Get 'include_user_usernames' parameter
-	const [includeUserUsernames = [], includeUserUsernamesErr] = $(params.include_user_usernames).optional.array('string').$;
-	if (includeUserUsernamesErr) return rej('invalid include_user_usernames param');
+	// Get 'includeUserUsernames' parameter
+	const [includeUserUsernames = [], includeUserUsernamesErr] = $(params.includeUserUsernames).optional.array('string').$;
+	if (includeUserUsernamesErr) return rej('invalid includeUserUsernames param');
 
-	// Get 'exclude_user_usernames' parameter
-	const [excludeUserUsernames = [], excludeUserUsernamesErr] = $(params.exclude_user_usernames).optional.array('string').$;
-	if (excludeUserUsernamesErr) return rej('invalid exclude_user_usernames param');
+	// Get 'exclude_userUsernames' parameter
+	const [excludeUserUsernames = [], excludeUserUsernamesErr] = $(params.exclude_userUsernames).optional.array('string').$;
+	if (excludeUserUsernamesErr) return rej('invalid exclude_userUsernames param');
 
 	// Get 'following' parameter
 	const [following = null, followingErr] = $(params.following).optional.nullable.boolean().$;
diff --git a/src/api/endpoints/posts/trend.ts b/src/api/endpoints/posts/trend.ts
index 3f92f0616..bc0c47fbc 100644
--- a/src/api/endpoints/posts/trend.ts
+++ b/src/api/endpoints/posts/trend.ts
@@ -41,7 +41,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		createdAt: {
 			$gte: new Date(Date.now() - ms('1days'))
 		},
-		repost_count: {
+		repostCount: {
 			$gt: 0
 		}
 	} as any;
@@ -68,7 +68,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 			limit: limit,
 			skip: offset,
 			sort: {
-				repost_count: -1,
+				repostCount: -1,
 				_id: -1
 			}
 		});
diff --git a/src/api/endpoints/stats.ts b/src/api/endpoints/stats.ts
index eee6f4870..719792d40 100644
--- a/src/api/endpoints/stats.ts
+++ b/src/api/endpoints/stats.ts
@@ -18,7 +18,7 @@ import User from '../models/user';
  *             postsCount:
  *               description: count of all posts of misskey
  *               type: number
- *             users_count:
+ *             usersCount:
  *               description: count of all users of misskey
  *               type: number
  *
@@ -43,6 +43,6 @@ module.exports = params => new Promise(async (res, rej) => {
 
 	res({
 		postsCount: postsCount,
-		users_count: usersCount
+		usersCount: usersCount
 	});
 });
diff --git a/src/api/endpoints/users/posts.ts b/src/api/endpoints/users/posts.ts
index 9ece429b6..934690749 100644
--- a/src/api/endpoints/users/posts.ts
+++ b/src/api/endpoints/users/posts.ts
@@ -34,13 +34,13 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 		return rej('userId or pair of username and host is required');
 	}
 
-	// Get 'include_replies' parameter
-	const [includeReplies = true, includeRepliesErr] = $(params.include_replies).optional.boolean().$;
-	if (includeRepliesErr) return rej('invalid include_replies param');
+	// Get 'includeReplies' parameter
+	const [includeReplies = true, includeRepliesErr] = $(params.includeReplies).optional.boolean().$;
+	if (includeRepliesErr) return rej('invalid includeReplies param');
 
-	// Get 'with_media' parameter
-	const [withMedia = false, withMediaErr] = $(params.with_media).optional.boolean().$;
-	if (withMediaErr) return rej('invalid with_media param');
+	// Get 'withMedia' parameter
+	const [withMedia = false, withMediaErr] = $(params.withMedia).optional.boolean().$;
+	if (withMediaErr) return rej('invalid withMedia param');
 
 	// Get 'limit' parameter
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
diff --git a/src/api/models/app.ts b/src/api/models/app.ts
index 20af049b2..528ab156f 100644
--- a/src/api/models/app.ts
+++ b/src/api/models/app.ts
@@ -96,7 +96,7 @@ export const pack = (
 				limit: 1
 			});
 
-		_app.is_authorized = exist === 1;
+		_app.isAuthorized = exist === 1;
 	}
 
 	resolve(_app);
diff --git a/src/api/models/channel.ts b/src/api/models/channel.ts
index aab21db07..1c7c52a34 100644
--- a/src/api/models/channel.ts
+++ b/src/api/models/channel.ts
@@ -67,7 +67,7 @@ export const pack = (
 			deletedAt: { $exists: false }
 		});
 
-		_channel.is_watching = watch !== null;
+		_channel.isWatching = watch !== null;
 		//#endregion
 	}
 
diff --git a/src/api/models/drive-folder.ts b/src/api/models/drive-folder.ts
index 52f784e06..958e3fb9e 100644
--- a/src/api/models/drive-folder.ts
+++ b/src/api/models/drive-folder.ts
@@ -62,8 +62,8 @@ export const pack = (
 			'metadata.folderId': _folder.id
 		});
 
-		_folder.folders_count = childFoldersCount;
-		_folder.files_count = childFilesCount;
+		_folder.foldersCount = childFoldersCount;
+		_folder.filesCount = childFilesCount;
 	}
 
 	if (opts.detail && _folder.parentId) {
diff --git a/src/api/models/post.ts b/src/api/models/post.ts
index 4ab840b5e..4f7729fbe 100644
--- a/src/api/models/post.ts
+++ b/src/api/models/post.ts
@@ -30,6 +30,10 @@ export type IPost = {
 	userId: mongo.ObjectID;
 	appId: mongo.ObjectID;
 	viaMobile: boolean;
+	repostCount: number;
+	repliesCount: number;
+	reactionCounts: any;
+	mentions: mongo.ObjectID[];
 	geo: {
 		latitude: number;
 		longitude: number;
@@ -184,7 +188,7 @@ export const pack = async (
 					const myChoice = poll.choices
 						.filter(c => c.id == vote.choice)[0];
 
-					myChoice.is_voted = true;
+					myChoice.isVoted = true;
 				}
 
 				return poll;
diff --git a/src/api/models/user.ts b/src/api/models/user.ts
index 9ee413e0d..0cf0fe0bd 100644
--- a/src/api/models/user.ts
+++ b/src/api/models/user.ts
@@ -88,7 +88,7 @@ export type IUser = {
 	bannerId: mongo.ObjectID;
 	data: any;
 	description: string;
-	latest_post: IPost;
+	latestPost: IPost;
 	pinnedPostId: mongo.ObjectID;
 	isSuspended: boolean;
 	keywords: string[];
@@ -167,7 +167,7 @@ export const pack = (
 	delete _user._id;
 
 	// Remove needless properties
-	delete _user.latest_post;
+	delete _user.latestPost;
 
 	if (!_user.host) {
 		// Remove private properties
@@ -212,7 +212,7 @@ export const pack = (
 
 	if (meId && !meId.equals(_user.id)) {
 		// Whether the user is following
-		_user.is_following = (async () => {
+		_user.isFollowing = (async () => {
 			const follow = await Following.findOne({
 				followerId: meId,
 				followeeId: _user.id,
@@ -222,7 +222,7 @@ export const pack = (
 		})();
 
 		// Whether the user is followed
-		_user.is_followed = (async () => {
+		_user.isFollowed = (async () => {
 			const follow2 = await Following.findOne({
 				followerId: _user.id,
 				followeeId: meId,
@@ -232,7 +232,7 @@ export const pack = (
 		})();
 
 		// Whether the user is muted
-		_user.is_muted = (async () => {
+		_user.isMuted = (async () => {
 			const mute = await Mute.findOne({
 				muterId: meId,
 				muteeId: _user.id,
@@ -254,14 +254,14 @@ export const pack = (
 			const myFollowingIds = await getFriends(meId);
 
 			// Get following you know count
-			_user.following_you_know_count = Following.count({
+			_user.followingYouKnowCount = Following.count({
 				followeeId: { $in: myFollowingIds },
 				followerId: _user.id,
 				deletedAt: { $exists: false }
 			});
 
 			// Get followers you know count
-			_user.followers_you_know_count = Following.count({
+			_user.followersYouKnowCount = Following.count({
 				followeeId: _user.id,
 				followerId: { $in: myFollowingIds },
 				deletedAt: { $exists: false }
diff --git a/src/web/app/auth/views/index.vue b/src/web/app/auth/views/index.vue
index 690cc4f28..e1e1b265e 100644
--- a/src/web/app/auth/views/index.vue
+++ b/src/web/app/auth/views/index.vue
@@ -14,7 +14,7 @@
 			<p>このアプリがあなたのアカウントにアクセスすることはありません。</p>
 		</div>
 		<div class="accepted" v-if="state == 'accepted'">
-			<h1>{{ session.app.is_authorized ? 'このアプリは既に連携済みです' : 'アプリケーションの連携を許可しました'}}</h1>
+			<h1>{{ session.app.isAuthorized ? 'このアプリは既に連携済みです' : 'アプリケーションの連携を許可しました' }}</h1>
 			<p v-if="session.app.callbackUrl">アプリケーションに戻っています<mk-ellipsis/></p>
 			<p v-if="!session.app.callbackUrl">アプリケーションに戻って、やっていってください。</p>
 		</div>
@@ -61,7 +61,7 @@ export default Vue.extend({
 			this.fetching = false;
 
 			// 既に連携していた場合
-			if (this.session.app.is_authorized) {
+			if (this.session.app.isAuthorized) {
 				this.$root.$data.os.api('auth/accept', {
 					token: this.session.token
 				}).then(() => {
diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index 225129088..2abfb106a 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 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>
+			<p v-if="channel.isWatching">このチャンネルをウォッチしています <a @click="unwatch">ウォッチ解除</a></p>
+			<p v-if="!channel.isWatching"><a @click="watch">このチャンネルをウォッチする</a></p>
 		</div>
 
 		<div class="share">
@@ -142,7 +142,7 @@
 			this.$root.$data.os.api('channels/watch', {
 				channelId: this.id
 			}).then(() => {
-				this.channel.is_watching = true;
+				this.channel.isWatching = true;
 				this.update();
 			}, e => {
 				alert('error');
@@ -153,7 +153,7 @@
 			this.$root.$data.os.api('channels/unwatch', {
 				channelId: this.id
 			}).then(() => {
-				this.channel.is_watching = false;
+				this.channel.isWatching = false;
 				this.update();
 			}, e => {
 				alert('error');
diff --git a/src/web/app/common/scripts/parse-search-query.ts b/src/web/app/common/scripts/parse-search-query.ts
index 512791ecb..81444c8b0 100644
--- a/src/web/app/common/scripts/parse-search-query.ts
+++ b/src/web/app/common/scripts/parse-search-query.ts
@@ -8,10 +8,10 @@ export default function(qs: string) {
 			const [key, value] = x.split(':');
 			switch (key) {
 				case 'user':
-					q['include_user_usernames'] = value.split(',');
+					q['includeUserUsernames'] = value.split(',');
 					break;
 				case 'exclude_user':
-					q['exclude_user_usernames'] = value.split(',');
+					q['exclude_userUsernames'] = value.split(',');
 					break;
 				case 'follow':
 					q['following'] = value == 'null' ? null : value == 'true';
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 8d35b5039..94f87fd70 100644
--- a/src/web/app/common/views/components/messaging-room.message.vue
+++ b/src/web/app/common/views/components/messaging-room.message.vue
@@ -9,7 +9,7 @@
 			<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">
+			<div class="content" v-if="!message.isDeleted">
 				<mk-post-html class="text" v-if="message.ast" :ast="message.ast" :i="os.i"/>
 				<div class="file" v-if="message.file">
 					<a :href="message.file.url" target="_blank" :title="message.file.name">
@@ -18,7 +18,7 @@
 					</a>
 				</div>
 			</div>
-			<div class="content" v-if="message.is_deleted">
+			<div class="content" v-if="message.isDeleted">
 				<p class="is-deleted">%i18n:common.tags.mk-messaging-message.deleted%</p>
 			</div>
 		</div>
diff --git a/src/web/app/common/views/components/othello.vue b/src/web/app/common/views/components/othello.vue
index 7737d74de..8f7d9dfd6 100644
--- a/src/web/app/common/views/components/othello.vue
+++ b/src/web/app/common/views/components/othello.vue
@@ -133,7 +133,7 @@ export default Vue.extend({
 	methods: {
 		go(game) {
 			(this as any).api('othello/games/show', {
-				game_id: game.id
+				gameId: game.id
 			}).then(game => {
 				this.matching = null;
 				this.game = game;
diff --git a/src/web/app/common/views/components/poll.vue b/src/web/app/common/views/components/poll.vue
index e46e89f55..711d89720 100644
--- a/src/web/app/common/views/components/poll.vue
+++ b/src/web/app/common/views/components/poll.vue
@@ -4,7 +4,7 @@
 		<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>
+				<template v-if="choice.isVoted">%fa:check%</template>
 				<span>{{ choice.text }}</span>
 				<span class="votes" v-if="showResult">({{ '%i18n:common.tags.mk-poll.vote-count%'.replace('{}', choice.votes) }})</span>
 			</span>
@@ -36,7 +36,7 @@ export default Vue.extend({
 			return this.poll.choices.reduce((a, b) => a + b.votes, 0);
 		},
 		isVoted(): boolean {
-			return this.poll.choices.some(c => c.is_voted);
+			return this.poll.choices.some(c => c.isVoted);
 		}
 	},
 	created() {
@@ -47,7 +47,7 @@ export default Vue.extend({
 			this.showResult = !this.showResult;
 		},
 		vote(id) {
-			if (this.poll.choices.some(c => c.is_voted)) return;
+			if (this.poll.choices.some(c => c.isVoted)) return;
 			(this as any).api('posts/polls/vote', {
 				postId: this.post.id,
 				choice: id
@@ -55,7 +55,7 @@ export default Vue.extend({
 				this.poll.choices.forEach(c => {
 					if (c.id == id) {
 						c.votes++;
-						Vue.set(c, 'is_voted', true);
+						Vue.set(c, 'isVoted', true);
 					}
 				});
 				this.showResult = true;
diff --git a/src/web/app/common/views/components/reactions-viewer.vue b/src/web/app/common/views/components/reactions-viewer.vue
index f6a27d913..246451008 100644
--- a/src/web/app/common/views/components/reactions-viewer.vue
+++ b/src/web/app/common/views/components/reactions-viewer.vue
@@ -20,7 +20,7 @@ export default Vue.extend({
 	props: ['post'],
 	computed: {
 		reactions(): number {
-			return this.post.reaction_counts;
+			return this.post.reactionCounts;
 		}
 	}
 });
diff --git a/src/web/app/desktop/views/components/follow-button.vue b/src/web/app/desktop/views/components/follow-button.vue
index 01b7e2aef..9eb22b0fb 100644
--- a/src/web/app/desktop/views/components/follow-button.vue
+++ b/src/web/app/desktop/views/components/follow-button.vue
@@ -1,15 +1,15 @@
 <template>
 <button class="mk-follow-button"
-	:class="{ wait, follow: !user.is_following, unfollow: user.is_following, big: size == 'big' }"
+	:class="{ wait, follow: !user.isFollowing, unfollow: user.isFollowing, big: size == 'big' }"
 	@click="onClick"
 	:disabled="wait"
-	:title="user.is_following ? 'フォロー解除' : 'フォローする'"
+	:title="user.isFollowing ? 'フォロー解除' : 'フォローする'"
 >
-	<template v-if="!wait && user.is_following">
+	<template v-if="!wait && user.isFollowing">
 		<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="!wait && !user.isFollowing">
 		<template v-if="size == 'compact'">%fa:plus%</template>
 		<template v-if="size == 'big'">%fa:plus%フォロー</template>
 	</template>
@@ -53,23 +53,23 @@ export default Vue.extend({
 
 		onFollow(user) {
 			if (user.id == this.user.id) {
-				this.user.is_following = user.is_following;
+				this.user.isFollowing = user.isFollowing;
 			}
 		},
 
 		onUnfollow(user) {
 			if (user.id == this.user.id) {
-				this.user.is_following = user.is_following;
+				this.user.isFollowing = user.isFollowing;
 			}
 		},
 
 		onClick() {
 			this.wait = true;
-			if (this.user.is_following) {
+			if (this.user.isFollowing) {
 				(this as any).api('following/delete', {
 					userId: this.user.id
 				}).then(() => {
-					this.user.is_following = false;
+					this.user.isFollowing = false;
 				}).catch(err => {
 					console.error(err);
 				}).then(() => {
@@ -79,7 +79,7 @@ export default Vue.extend({
 				(this as any).api('following/create', {
 					userId: this.user.id
 				}).then(() => {
-					this.user.is_following = true;
+					this.user.isFollowing = true;
 				}).catch(err => {
 					console.error(err);
 				}).then(() => {
diff --git a/src/web/app/desktop/views/components/followers.vue b/src/web/app/desktop/views/components/followers.vue
index e8330289c..a1b98995d 100644
--- a/src/web/app/desktop/views/components/followers.vue
+++ b/src/web/app/desktop/views/components/followers.vue
@@ -2,7 +2,7 @@
 <mk-users-list
 	:fetch="fetch"
 	:count="user.followersCount"
-	:you-know-count="user.followers_you_know_count"
+	:you-know-count="user.followersYouKnowCount"
 >
 	フォロワーはいないようです。
 </mk-users-list>
diff --git a/src/web/app/desktop/views/components/following.vue b/src/web/app/desktop/views/components/following.vue
index 0dab6ac7b..b7aedda84 100644
--- a/src/web/app/desktop/views/components/following.vue
+++ b/src/web/app/desktop/views/components/following.vue
@@ -2,7 +2,7 @@
 <mk-users-list
 	:fetch="fetch"
 	:count="user.followingCount"
-	:you-know-count="user.following_you_know_count"
+	:you-know-count="user.followingYouKnowCount"
 >
 	フォロー中のユーザーはいないようです。
 </mk-users-list>
diff --git a/src/web/app/desktop/views/components/post-detail.vue b/src/web/app/desktop/views/components/post-detail.vue
index 611660f52..3f62c0d59 100644
--- a/src/web/app/desktop/views/components/post-detail.vue
+++ b/src/web/app/desktop/views/components/post-detail.vue
@@ -56,10 +56,10 @@
 		<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>
+				%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</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.repostCount > 0">{{ p.repostCount }}</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>
@@ -122,9 +122,9 @@ 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])
+			return this.p.reactionCounts
+				? Object.keys(this.p.reactionCounts)
+					.map(key => this.p.reactionCounts[key])
 					.reduce((a, b) => a + b)
 				: 0;
 		},
diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index e6dff2ccd..a9b4d9eea 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -58,10 +58,10 @@
 			<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>
+					%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</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>
+					%fa:retweet%<p class="count" v-if="p.repostCount > 0">{{ p.repostCount }}</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>
@@ -129,9 +129,9 @@ 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])
+			return this.p.reactionCounts
+				? Object.keys(this.p.reactionCounts)
+					.map(key => this.p.reactionCounts[key])
 					.reduce((a, b) => a + b)
 				: 0;
 		},
diff --git a/src/web/app/desktop/views/components/users-list.item.vue b/src/web/app/desktop/views/components/users-list.item.vue
index c2f30cf38..d2bfc117d 100644
--- a/src/web/app/desktop/views/components/users-list.item.vue
+++ b/src/web/app/desktop/views/components/users-list.item.vue
@@ -9,7 +9,7 @@
 			<span class="username">@{{ acct }}</span>
 		</header>
 		<div class="body">
-			<p class="followed" v-if="user.is_followed">フォローされています</p>
+			<p class="followed" v-if="user.isFollowed">フォローされています</p>
 			<div class="description">{{ user.description }}</div>
 		</div>
 	</div>
diff --git a/src/web/app/desktop/views/pages/othello.vue b/src/web/app/desktop/views/pages/othello.vue
index 160dd9a35..0d8e987dd 100644
--- a/src/web/app/desktop/views/pages/othello.vue
+++ b/src/web/app/desktop/views/pages/othello.vue
@@ -34,7 +34,7 @@ export default Vue.extend({
 			this.fetching = true;
 
 			(this as any).api('othello/games/show', {
-				game_id: this.$route.params.game
+				gameId: this.$route.params.game
 			}).then(game => {
 				this.game = game;
 				this.fetching = false;
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 2baf042bc..1ff79b4ae 100644
--- a/src/web/app/desktop/views/pages/user/user.photos.vue
+++ b/src/web/app/desktop/views/pages/user/user.photos.vue
@@ -24,7 +24,7 @@ export default Vue.extend({
 	mounted() {
 		(this as any).api('users/posts', {
 			userId: this.user.id,
-			with_media: true,
+			withMedia: true,
 			limit: 9
 		}).then(posts => {
 			posts.forEach(post => {
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 0d91df2a5..f5562d091 100644
--- a/src/web/app/desktop/views/pages/user/user.profile.vue
+++ b/src/web/app/desktop/views/pages/user/user.profile.vue
@@ -2,9 +2,9 @@
 <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>
-		<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>
+		<p class="followed" v-if="user.isFollowed">%i18n:desktop.tags.mk-user.follows-you%</p>
+		<p v-if="user.isMuted">%i18n:desktop.tags.mk-user.muted% <a @click="unmute">%i18n:desktop.tags.mk-user.unmute%</a></p>
+		<p v-if="!user.isMuted"><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.host === null && user.account.profile.birthday">
@@ -51,7 +51,7 @@ export default Vue.extend({
 			(this as any).api('mute/create', {
 				userId: this.user.id
 			}).then(() => {
-				this.user.is_muted = true;
+				this.user.isMuted = true;
 			}, () => {
 				alert('error');
 			});
@@ -61,7 +61,7 @@ export default Vue.extend({
 			(this as any).api('mute/delete', {
 				userId: this.user.id
 			}).then(() => {
-				this.user.is_muted = false;
+				this.user.isMuted = false;
 			}, () => {
 				alert('error');
 			});
diff --git a/src/web/app/mobile/views/components/drive.vue b/src/web/app/mobile/views/components/drive.vue
index 5affbdaf1..ff5366a0a 100644
--- a/src/web/app/mobile/views/components/drive.vue
+++ b/src/web/app/mobile/views/components/drive.vue
@@ -19,10 +19,10 @@
 	<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 && (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.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 v-if="folder != null && (folder.foldersCount > 0 || folder.filesCount > 0)">
+				<template v-if="folder.foldersCount > 0">{{ folder.foldersCount }} %i18n:mobile.tags.mk-drive.folder-count%</template>
+				<template v-if="folder.foldersCount > 0 && folder.filesCount > 0">%i18n:mobile.tags.mk-drive.count-separator%</template>
+				<template v-if="folder.filesCount > 0">{{ folder.filesCount }} %i18n:mobile.tags.mk-drive.file-count%</template>
 			</p>
 		</div>
 		<div class="folders" v-if="folders.length > 0">
diff --git a/src/web/app/mobile/views/components/follow-button.vue b/src/web/app/mobile/views/components/follow-button.vue
index 838ea404e..43c69d4e0 100644
--- a/src/web/app/mobile/views/components/follow-button.vue
+++ b/src/web/app/mobile/views/components/follow-button.vue
@@ -1,13 +1,13 @@
 <template>
 <button class="mk-follow-button"
-	:class="{ wait: wait, follow: !user.is_following, unfollow: user.is_following }"
+	:class="{ wait: wait, follow: !user.isFollowing, unfollow: user.isFollowing }"
 	@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 && user.isFollowing">%fa:minus%</template>
+	<template v-if="!wait && !user.isFollowing">%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%' }}
+	{{ user.isFollowing ? '%i18n:mobile.tags.mk-follow-button.unfollow%' : '%i18n:mobile.tags.mk-follow-button.follow%' }}
 </button>
 </template>
 
@@ -43,23 +43,23 @@ export default Vue.extend({
 
 		onFollow(user) {
 			if (user.id == this.user.id) {
-				this.user.is_following = user.is_following;
+				this.user.isFollowing = user.isFollowing;
 			}
 		},
 
 		onUnfollow(user) {
 			if (user.id == this.user.id) {
-				this.user.is_following = user.is_following;
+				this.user.isFollowing = user.isFollowing;
 			}
 		},
 
 		onClick() {
 			this.wait = true;
-			if (this.user.is_following) {
+			if (this.user.isFollowing) {
 				(this as any).api('following/delete', {
 					userId: this.user.id
 				}).then(() => {
-					this.user.is_following = false;
+					this.user.isFollowing = false;
 				}).catch(err => {
 					console.error(err);
 				}).then(() => {
@@ -69,7 +69,7 @@ export default Vue.extend({
 				(this as any).api('following/create', {
 					userId: this.user.id
 				}).then(() => {
-					this.user.is_following = true;
+					this.user.isFollowing = true;
 				}).catch(err => {
 					console.error(err);
 				}).then(() => {
diff --git a/src/web/app/mobile/views/components/post-detail.vue b/src/web/app/mobile/views/components/post-detail.vue
index 241782aa5..cf51696c4 100644
--- a/src/web/app/mobile/views/components/post-detail.vue
+++ b/src/web/app/mobile/views/components/post-detail.vue
@@ -59,10 +59,10 @@
 		<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>
+				%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</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.repostCount > 0">{{ p.repostCount }}</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>
@@ -122,9 +122,9 @@ 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])
+			return this.p.reactionCounts
+				? Object.keys(this.p.reactionCounts)
+					.map(key => this.p.reactionCounts[key])
 					.reduce((a, b) => a + b)
 				: 0;
 		},
diff --git a/src/web/app/mobile/views/components/post.vue b/src/web/app/mobile/views/components/post.vue
index 0c6522db8..77ca45a7f 100644
--- a/src/web/app/mobile/views/components/post.vue
+++ b/src/web/app/mobile/views/components/post.vue
@@ -58,10 +58,10 @@
 			<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>
+					%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</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.repostCount > 0">{{ p.repostCount }}</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>
@@ -110,9 +110,9 @@ 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])
+			return this.p.reactionCounts
+				? Object.keys(this.p.reactionCounts)
+					.map(key => this.p.reactionCounts[key])
 					.reduce((a, b) => a + b)
 				: 0;
 		},
diff --git a/src/web/app/mobile/views/components/user-timeline.vue b/src/web/app/mobile/views/components/user-timeline.vue
index d1f771f81..bd3e3d0c8 100644
--- a/src/web/app/mobile/views/components/user-timeline.vue
+++ b/src/web/app/mobile/views/components/user-timeline.vue
@@ -34,7 +34,7 @@ export default Vue.extend({
 	mounted() {
 		(this as any).api('users/posts', {
 			userId: this.user.id,
-			with_media: this.withMedia,
+			withMedia: this.withMedia,
 			limit: limit + 1
 		}).then(posts => {
 			if (posts.length == limit + 1) {
@@ -51,7 +51,7 @@ export default Vue.extend({
 			this.moreFetching = true;
 			(this as any).api('users/posts', {
 				userId: this.user.id,
-				with_media: this.withMedia,
+				withMedia: this.withMedia,
 				limit: limit + 1,
 				untilId: this.posts[this.posts.length - 1].id
 			}).then(posts => {
diff --git a/src/web/app/mobile/views/pages/followers.vue b/src/web/app/mobile/views/pages/followers.vue
index 08a15f945..8c058eb4e 100644
--- a/src/web/app/mobile/views/pages/followers.vue
+++ b/src/web/app/mobile/views/pages/followers.vue
@@ -8,7 +8,7 @@
 		v-if="!fetching"
 		:fetch="fetchUsers"
 		:count="user.followersCount"
-		:you-know-count="user.followers_you_know_count"
+		:you-know-count="user.followersYouKnowCount"
 		@loaded="onLoaded"
 	>
 		%i18n:mobile.tags.mk-user-followers.no-users%
diff --git a/src/web/app/mobile/views/pages/following.vue b/src/web/app/mobile/views/pages/following.vue
index ecdaa5a58..a73c9d171 100644
--- a/src/web/app/mobile/views/pages/following.vue
+++ b/src/web/app/mobile/views/pages/following.vue
@@ -8,7 +8,7 @@
 		v-if="!fetching"
 		:fetch="fetchUsers"
 		:count="user.followingCount"
-		:you-know-count="user.following_you_know_count"
+		:you-know-count="user.followingYouKnowCount"
 		@loaded="onLoaded"
 	>
 		%i18n:mobile.tags.mk-user-following.no-users%
diff --git a/src/web/app/mobile/views/pages/othello.vue b/src/web/app/mobile/views/pages/othello.vue
index b110bf309..e04e583c2 100644
--- a/src/web/app/mobile/views/pages/othello.vue
+++ b/src/web/app/mobile/views/pages/othello.vue
@@ -34,7 +34,7 @@ export default Vue.extend({
 			this.fetching = true;
 
 			(this as any).api('othello/games/show', {
-				game_id: this.$route.params.game
+				gameId: this.$route.params.game
 			}).then(game => {
 				this.game = game;
 				this.fetching = false;
diff --git a/src/web/app/mobile/views/pages/user.vue b/src/web/app/mobile/views/pages/user.vue
index f5bbd4162..114decb8e 100644
--- a/src/web/app/mobile/views/pages/user.vue
+++ b/src/web/app/mobile/views/pages/user.vue
@@ -14,7 +14,7 @@
 				<div class="title">
 					<h1>{{ user.name }}</h1>
 					<span class="username">@{{ acct }}</span>
-					<span class="followed" v-if="user.is_followed">%i18n:mobile.tags.mk-user.follows-you%</span>
+					<span class="followed" v-if="user.isFollowed">%i18n:mobile.tags.mk-user.follows-you%</span>
 				</div>
 				<div class="description">{{ user.description }}</div>
 				<div class="info">
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 94b5af553..f703f8a74 100644
--- a/src/web/app/mobile/views/pages/user/home.photos.vue
+++ b/src/web/app/mobile/views/pages/user/home.photos.vue
@@ -30,7 +30,7 @@ export default Vue.extend({
 	mounted() {
 		(this as any).api('users/posts', {
 			userId: this.user.id,
-			with_media: true,
+			withMedia: true,
 			limit: 6
 		}).then(posts => {
 			posts.forEach(post => {
diff --git a/src/web/app/stats/tags/index.tag b/src/web/app/stats/tags/index.tag
index bf08c38c3..63fdd2404 100644
--- a/src/web/app/stats/tags/index.tag
+++ b/src/web/app/stats/tags/index.tag
@@ -83,7 +83,7 @@
 </mk-posts>
 
 <mk-users>
-	<h2>%i18n:stats.users-count% <b>{ stats.users_count }</b></h2>
+	<h2>%i18n:stats.users-count% <b>{ stats.usersCount }</b></h2>
 	<mk-users-chart v-if="!initializing" data={ data }/>
 	<style lang="stylus" scoped>
 		:scope
diff --git a/src/web/docs/api/entities/post.yaml b/src/web/docs/api/entities/post.yaml
index 8a616f088..0a0730846 100644
--- a/src/web/docs/api/entities/post.yaml
+++ b/src/web/docs/api/entities/post.yaml
@@ -59,7 +59,7 @@ props:
     desc:
       ja: "この投稿に対する自分の<a href='/docs/api/reactions'>リアクション</a>"
       en: "The your <a href='/docs/api/reactions'>reaction</a> of this post"
-  - name: "reaction_counts"
+  - name: "reactionCounts"
     type: "object"
     optional: false
     desc:
@@ -110,7 +110,7 @@ props:
             desc:
               ja: "選択肢ID"
               en: "The ID of this choice"
-          - name: "is_voted"
+          - name: "isVoted"
             type: "boolean"
             optional: true
             desc:
diff --git a/src/web/docs/api/entities/user.yaml b/src/web/docs/api/entities/user.yaml
index f45455e73..a1fae1482 100644
--- a/src/web/docs/api/entities/user.yaml
+++ b/src/web/docs/api/entities/user.yaml
@@ -65,17 +65,17 @@ props:
     desc:
       ja: "フォローしているユーザーの数"
       en: "The number of the following users for this user"
-  - name: "is_following"
+  - name: "isFollowing"
     type: "boolean"
     optional: true
     desc:
       ja: "自分がこのユーザーをフォローしているか"
-  - name: "is_followed"
+  - name: "isFollowed"
     type: "boolean"
     optional: true
     desc:
       ja: "自分がこのユーザーにフォローされているか"
-  - name: "is_muted"
+  - name: "isMuted"
     type: "boolean"
     optional: true
     desc:
diff --git a/tools/migration/shell.camel-case.js b/tools/migration/shell.camel-case.js
index afe831e5b..8d07140ba 100644
--- a/tools/migration/shell.camel-case.js
+++ b/tools/migration/shell.camel-case.js
@@ -176,6 +176,9 @@ db.posts.update({}, {
 		reply_id: 'replyId',
 		repost_id: 'repostId',
 		via_mobile: 'viaMobile',
+		reaction_counts: 'reactionCounts',
+		replies_count: 'repliesCount',
+		repost_count: 'repostCount',
 		'_reply.user_id': '_reply.userId',
 		'_repost.user_id': '_repost.userId',
 	}

From 47ea9fe25475c02a86bc11359dd57da3a418a68c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 17:00:28 +0900
Subject: [PATCH 0902/1250] wip

---
 src/api/endpoints/posts/create.ts            | 2 +-
 src/api/models/app.ts                        | 2 +-
 src/common/othello/ai/back.ts                | 2 +-
 src/web/docs/api/endpoints/posts/create.yaml | 2 +-
 4 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index 281737454..34f3eea02 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -270,7 +270,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 
 	// Reponse
 	res({
-		created_post: postObj
+		createdPost: postObj
 	});
 
 	//#region Post processes
diff --git a/src/api/models/app.ts b/src/api/models/app.ts
index 528ab156f..6759b52cd 100644
--- a/src/api/models/app.ts
+++ b/src/api/models/app.ts
@@ -83,7 +83,7 @@ export const pack = (
 		delete _app.secret;
 	}
 
-	_app.icon_url = _app.icon != null
+	_app.iconUrl = _app.icon != null
 		? `${config.drive_url}/${_app.icon}`
 		: `${config.drive_url}/app-default.jpg`;
 
diff --git a/src/common/othello/ai/back.ts b/src/common/othello/ai/back.ts
index 52f01ff97..0950adaa9 100644
--- a/src/common/othello/ai/back.ts
+++ b/src/common/othello/ai/back.ts
@@ -56,7 +56,7 @@ process.on('message', async msg => {
 			}
 		});
 
-		post = res.created_post;
+		post = res.createdPost;
 		//#endregion
 	}
 
diff --git a/src/web/docs/api/endpoints/posts/create.yaml b/src/web/docs/api/endpoints/posts/create.yaml
index 70d35008c..11d9f40c5 100644
--- a/src/web/docs/api/endpoints/posts/create.yaml
+++ b/src/web/docs/api/endpoints/posts/create.yaml
@@ -45,7 +45,7 @@ params:
           en: "Choices of a poll"
 
 res:
-  - name: "created_post"
+  - name: "createdPost"
     type: "entity(Post)"
     optional: false
     desc:

From 14676d60568fc6ae9f064c40284b96d88447fe9f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 17:02:02 +0900
Subject: [PATCH 0903/1250] wip

---
 src/api/endpoints/posts/search.ts                    | 12 ++++++------
 src/api/models/post.ts                               |  2 +-
 src/web/app/common/scripts/parse-search-query.ts     |  2 +-
 src/web/app/desktop/views/components/post-detail.vue |  2 +-
 src/web/app/desktop/views/components/posts.post.vue  |  2 +-
 src/web/app/mobile/views/components/post-detail.vue  |  2 +-
 src/web/app/mobile/views/components/post.vue         |  2 +-
 src/web/docs/api/entities/post.yaml                  |  2 +-
 8 files changed, 13 insertions(+), 13 deletions(-)

diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index 5c324bfe9..bb5c43892 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -25,17 +25,17 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [includeUserIds = [], includeUserIdsErr] = $(params.includeUserIds).optional.array('id').$;
 	if (includeUserIdsErr) return rej('invalid includeUserIds param');
 
-	// Get 'exclude_userIds' parameter
-	const [excludeUserIds = [], excludeUserIdsErr] = $(params.exclude_userIds).optional.array('id').$;
-	if (excludeUserIdsErr) return rej('invalid exclude_userIds param');
+	// Get 'excludeUserIds' parameter
+	const [excludeUserIds = [], excludeUserIdsErr] = $(params.excludeUserIds).optional.array('id').$;
+	if (excludeUserIdsErr) return rej('invalid excludeUserIds param');
 
 	// Get 'includeUserUsernames' parameter
 	const [includeUserUsernames = [], includeUserUsernamesErr] = $(params.includeUserUsernames).optional.array('string').$;
 	if (includeUserUsernamesErr) return rej('invalid includeUserUsernames param');
 
-	// Get 'exclude_userUsernames' parameter
-	const [excludeUserUsernames = [], excludeUserUsernamesErr] = $(params.exclude_userUsernames).optional.array('string').$;
-	if (excludeUserUsernamesErr) return rej('invalid exclude_userUsernames param');
+	// Get 'excludeUserUsernames' parameter
+	const [excludeUserUsernames = [], excludeUserUsernamesErr] = $(params.excludeUserUsernames).optional.array('string').$;
+	if (excludeUserUsernamesErr) return rej('invalid excludeUserUsernames param');
 
 	// Get 'following' parameter
 	const [following = null, followingErr] = $(params.following).optional.nullable.boolean().$;
diff --git a/src/api/models/post.ts b/src/api/models/post.ts
index 4f7729fbe..7a93753f7 100644
--- a/src/api/models/post.ts
+++ b/src/api/models/post.ts
@@ -197,7 +197,7 @@ export const pack = async (
 
 		// Fetch my reaction
 		if (meId) {
-			_post.my_reaction = (async () => {
+			_post.myReaction = (async () => {
 				const reaction = await Reaction
 					.findOne({
 						userId: meId,
diff --git a/src/web/app/common/scripts/parse-search-query.ts b/src/web/app/common/scripts/parse-search-query.ts
index 81444c8b0..4f09d2b93 100644
--- a/src/web/app/common/scripts/parse-search-query.ts
+++ b/src/web/app/common/scripts/parse-search-query.ts
@@ -11,7 +11,7 @@ export default function(qs: string) {
 					q['includeUserUsernames'] = value.split(',');
 					break;
 				case 'exclude_user':
-					q['exclude_userUsernames'] = value.split(',');
+					q['excludeUserUsernames'] = value.split(',');
 					break;
 				case 'follow':
 					q['following'] = value == 'null' ? null : value == 'true';
diff --git a/src/web/app/desktop/views/components/post-detail.vue b/src/web/app/desktop/views/components/post-detail.vue
index 3f62c0d59..7783ec62c 100644
--- a/src/web/app/desktop/views/components/post-detail.vue
+++ b/src/web/app/desktop/views/components/post-detail.vue
@@ -61,7 +61,7 @@
 			<button @click="repost" title="Repost">
 				%fa:retweet%<p class="count" v-if="p.repostCount > 0">{{ p.repostCount }}</p>
 			</button>
-			<button :class="{ reacted: p.my_reaction != null }" @click="react" ref="reactButton" title="リアクション">
+			<button :class="{ reacted: p.myReaction != 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/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index a9b4d9eea..c70e01911 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -63,7 +63,7 @@
 				<button @click="repost" title="%i18n:desktop.tags.mk-timeline-post.repost%">
 					%fa:retweet%<p class="count" v-if="p.repostCount > 0">{{ p.repostCount }}</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.myReaction != 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/views/components/post-detail.vue b/src/web/app/mobile/views/components/post-detail.vue
index cf51696c4..29993c79e 100644
--- a/src/web/app/mobile/views/components/post-detail.vue
+++ b/src/web/app/mobile/views/components/post-detail.vue
@@ -64,7 +64,7 @@
 			<button @click="repost" title="Repost">
 				%fa:retweet%<p class="count" v-if="p.repostCount > 0">{{ p.repostCount }}</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.myReaction != 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/views/components/post.vue b/src/web/app/mobile/views/components/post.vue
index 77ca45a7f..66c595f4e 100644
--- a/src/web/app/mobile/views/components/post.vue
+++ b/src/web/app/mobile/views/components/post.vue
@@ -63,7 +63,7 @@
 				<button @click="repost" title="Repost">
 					%fa:retweet%<p class="count" v-if="p.repostCount > 0">{{ p.repostCount }}</p>
 				</button>
-				<button :class="{ reacted: p.my_reaction != null }" @click="react" ref="reactButton">
+				<button :class="{ reacted: p.myReaction != 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">
diff --git a/src/web/docs/api/entities/post.yaml b/src/web/docs/api/entities/post.yaml
index 0a0730846..71b6a6412 100644
--- a/src/web/docs/api/entities/post.yaml
+++ b/src/web/docs/api/entities/post.yaml
@@ -53,7 +53,7 @@ props:
     desc:
       ja: "投稿者"
       en: "The author of this post"
-  - name: "my_reaction"
+  - name: "myReaction"
     type: "string"
     optional: true
     desc:

From e2cec2c1e1ba99e1cf6981340ea7ec121dd05468 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 17:03:32 +0900
Subject: [PATCH 0904/1250] oops

---
 src/api/common/add-file-to-drive.ts | 306 ----------------------------
 1 file changed, 306 deletions(-)
 delete mode 100644 src/api/common/add-file-to-drive.ts

diff --git a/src/api/common/add-file-to-drive.ts b/src/api/common/add-file-to-drive.ts
deleted file mode 100644
index 6bf5fcbc0..000000000
--- a/src/api/common/add-file-to-drive.ts
+++ /dev/null
@@ -1,306 +0,0 @@
-import { Buffer } from 'buffer';
-import * as fs from 'fs';
-import * as tmp from 'tmp';
-import * as stream from 'stream';
-
-import * as mongodb from 'mongodb';
-import * as crypto from 'crypto';
-import * as _gm from 'gm';
-import * as debug from 'debug';
-import fileType = require('file-type');
-import prominence = require('prominence');
-
-import DriveFile, { getGridFSBucket } from '../models/drive-file';
-import DriveFolder from '../models/drive-folder';
-import { pack } from '../models/drive-file';
-import event, { publishDriveStream } from '../event';
-import config from '../../conf';
-
-const gm = _gm.subClass({
-	imageMagick: true
-});
-
-const log = debug('misskey:register-drive-file');
-
-const tmpFile = (): Promise<string> => new Promise((resolve, reject) => {
-	tmp.file((e, path) => {
-		if (e) return reject(e);
-		resolve(path);
-	});
-});
-
-const addToGridFS = (name: string, readable: stream.Readable, type: string, metadata: any): Promise<any> =>
-	getGridFSBucket()
-		.then(bucket => new Promise((resolve, reject) => {
-			const writeStream = bucket.openUploadStream(name, { contentType: type, metadata });
-			writeStream.once('finish', (doc) => { resolve(doc); });
-			writeStream.on('error', reject);
-			readable.pipe(writeStream);
-		}));
-
-const addFile = async (
-	user: any,
-	path: string,
-	name: string = null,
-	comment: string = null,
-	folderId: mongodb.ObjectID = null,
-	force: boolean = false
-) => {
-	log(`registering ${name} (user: ${user.username}, path: ${path})`);
-
-	// Calculate hash, get content type and get file size
-	const [hash, [mime, ext], size] = await Promise.all([
-		// hash
-		((): Promise<string> => new Promise((res, rej) => {
-			const readable = fs.createReadStream(path);
-			const hash = crypto.createHash('md5');
-			const chunks = [];
-			readable
-				.on('error', rej)
-				.pipe(hash)
-				.on('error', rej)
-				.on('data', (chunk) => chunks.push(chunk))
-				.on('end', () => {
-					const buffer = Buffer.concat(chunks);
-					res(buffer.toString('hex'));
-				});
-		}))(),
-		// mime
-		((): Promise<[string, string | null]> => new Promise((res, rej) => {
-			const readable = fs.createReadStream(path);
-			readable
-				.on('error', rej)
-				.once('data', (buffer: Buffer) => {
-					readable.destroy();
-					const type = fileType(buffer);
-					if (type) {
-						return res([type.mime, type.ext]);
-					} else {
-						// 種類が同定できなかったら application/octet-stream にする
-						return res(['application/octet-stream', null]);
-					}
-				});
-		}))(),
-		// size
-		((): Promise<number> => new Promise((res, rej) => {
-			fs.stat(path, (err, stats) => {
-				if (err) return rej(err);
-				res(stats.size);
-			});
-		}))()
-	]);
-
-	log(`hash: ${hash}, mime: ${mime}, ext: ${ext}, size: ${size}`);
-
-	// detect name
-	const detectedName: string = name || (ext ? `untitled.${ext}` : 'untitled');
-
-	if (!force) {
-		// Check if there is a file with the same hash
-		const much = await DriveFile.findOne({
-			md5: hash,
-			'metadata.userId': user._id
-		});
-
-		if (much !== null) {
-			log('file with same hash is found');
-			return much;
-		} else {
-			log('file with same hash is not found');
-		}
-	}
-
-	const [wh, avgColor, folder] = await Promise.all([
-		// Width and height (when image)
-		(async () => {
-			// 画像かどうか
-			if (!/^image\/.*$/.test(mime)) {
-				return null;
-			}
-
-			const imageType = mime.split('/')[1];
-
-			// 画像でもPNGかJPEGかGIFでないならスキップ
-			if (imageType != 'png' && imageType != 'jpeg' && imageType != 'gif') {
-				return null;
-			}
-
-			log('calculate image width and height...');
-
-			// Calculate width and height
-			const g = gm(fs.createReadStream(path), name);
-			const size = await prominence(g).size();
-
-			log(`image width and height is calculated: ${size.width}, ${size.height}`);
-
-			return [size.width, size.height];
-		})(),
-		// average color (when image)
-		(async () => {
-			// 画像かどうか
-			if (!/^image\/.*$/.test(mime)) {
-				return null;
-			}
-
-			const imageType = mime.split('/')[1];
-
-			// 画像でもPNGかJPEGでないならスキップ
-			if (imageType != 'png' && imageType != 'jpeg') {
-				return null;
-			}
-
-			log('calculate average color...');
-
-			const buffer = await prominence(gm(fs.createReadStream(path), name)
-				.setFormat('ppm')
-				.resize(1, 1)) // 1pxのサイズに縮小して平均色を取得するというハック
-				.toBuffer();
-
-			const r = buffer.readUInt8(buffer.length - 3);
-			const g = buffer.readUInt8(buffer.length - 2);
-			const b = buffer.readUInt8(buffer.length - 1);
-
-			log(`average color is calculated: ${r}, ${g}, ${b}`);
-
-			return [r, g, b];
-		})(),
-		// folder
-		(async () => {
-			if (!folderId) {
-				return null;
-			}
-			const driveFolder = await DriveFolder.findOne({
-				_id: folderId,
-				userId: user._id
-			});
-			if (!driveFolder) {
-				throw 'folder-not-found';
-			}
-			return driveFolder;
-		})(),
-		// usage checker
-		(async () => {
-			// Calculate drive usage
-			const usage = await DriveFile
-				.aggregate([{
-					$match: { 'metadata.userId': user._id }
-				}, {
-					$project: {
-						length: true
-					}
-				}, {
-					$group: {
-						_id: null,
-						usage: { $sum: '$length' }
-					}
-				}])
-				.then((aggregates: any[]) => {
-					if (aggregates.length > 0) {
-						return aggregates[0].usage;
-					}
-					return 0;
-				});
-
-			log(`drive usage is ${usage}`);
-
-			// If usage limit exceeded
-			if (usage + size > user.driveCapacity) {
-				throw 'no-free-space';
-			}
-		})()
-	]);
-
-	const readable = fs.createReadStream(path);
-
-	const properties = {};
-
-	if (wh) {
-		properties['width'] = wh[0];
-		properties['height'] = wh[1];
-	}
-
-	if (avgColor) {
-		properties['avgColor'] = avgColor;
-	}
-
-	return addToGridFS(detectedName, readable, mime, {
-		userId: user._id,
-		folderId: folder !== null ? folder._id : null,
-		comment: comment,
-		properties: properties
-	});
-};
-
-/**
- * Add file to drive
- *
- * @param user User who wish to add file
- * @param file File path or readableStream
- * @param comment Comment
- * @param type File type
- * @param folderId Folder ID
- * @param force If set to true, forcibly upload the file even if there is a file with the same hash.
- * @return Object that represents added file
- */
-export default (user: any, file: string | stream.Readable, ...args) => new Promise<any>((resolve, reject) => {
-	// Get file path
-	new Promise((res: (v: [string, boolean]) => void, rej) => {
-		if (typeof file === 'string') {
-			res([file, false]);
-			return;
-		}
-		if (typeof file === 'object' && typeof file.read === 'function') {
-			tmpFile()
-				.then(path => {
-					const readable: stream.Readable = file;
-					const writable = fs.createWriteStream(path);
-					readable
-						.on('error', rej)
-						.on('end', () => {
-							res([path, true]);
-						})
-						.pipe(writable)
-						.on('error', rej);
-				})
-				.catch(rej);
-		}
-		rej(new Error('un-compatible file.'));
-	})
-	.then(([path, shouldCleanup]): Promise<any> => new Promise((res, rej) => {
-		addFile(user, path, ...args)
-			.then(file => {
-				res(file);
-				if (shouldCleanup) {
-					fs.unlink(path, (e) => {
-						if (e) log(e.stack);
-					});
-				}
-			})
-			.catch(rej);
-	}))
-	.then(file => {
-		log(`drive file has been created ${file._id}`);
-		resolve(file);
-
-		pack(file).then(serializedFile => {
-			// Publish drive_file_created event
-			event(user._id, 'drive_file_created', serializedFile);
-			publishDriveStream(user._id, 'file_created', serializedFile);
-
-			// Register to search database
-			if (config.elasticsearch.enable) {
-				const es = require('../../db/elasticsearch');
-				es.index({
-					index: 'misskey',
-					type: 'drive_file',
-					id: file._id.toString(),
-					body: {
-						name: file.name,
-						userId: user._id.toString()
-					}
-				});
-			}
-		});
-	})
-	.catch(reject);
-});

From 5957946f6ae3bee5bb345f22c88d24bf94895031 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 18:43:49 +0900
Subject: [PATCH 0905/1250] =?UTF-8?q?=E8=89=AF=E3=81=84=E6=84=9F=E3=81=98?=
 =?UTF-8?q?=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../1.js}                                     |  3 +-
 .../2.js}                                     |  3 +-
 tools/migration/nighthike/3.js                | 73 +++++++++++++++++++
 .../{shell.camel-case.js => nighthike/4.js}   |  0
 .../node.1522066477.user-account-keypair.js   | 39 ----------
 .../shell.1522038492.user-account.js          | 41 -----------
 tools/migration/shell.1522116709.user-host.js |  1 -
 .../shell.1522116710.user-host_lower.js       |  1 -
 8 files changed, 75 insertions(+), 86 deletions(-)
 rename tools/migration/{node.2018-03-28.username.js => nighthike/1.js} (86%)
 rename tools/migration/{node.2018-03-28.appname.js => nighthike/2.js} (86%)
 create mode 100644 tools/migration/nighthike/3.js
 rename tools/migration/{shell.camel-case.js => nighthike/4.js} (100%)
 delete mode 100644 tools/migration/node.1522066477.user-account-keypair.js
 delete mode 100644 tools/migration/shell.1522038492.user-account.js
 delete mode 100644 tools/migration/shell.1522116709.user-host.js
 delete mode 100644 tools/migration/shell.1522116710.user-host_lower.js

diff --git a/tools/migration/node.2018-03-28.username.js b/tools/migration/nighthike/1.js
similarity index 86%
rename from tools/migration/node.2018-03-28.username.js
rename to tools/migration/nighthike/1.js
index 222215210..d7e011c5b 100644
--- a/tools/migration/node.2018-03-28.username.js
+++ b/tools/migration/nighthike/1.js
@@ -1,7 +1,6 @@
 // for Node.js interpret
 
-const { default: User } = require('../../built/api/models/user');
-const { generate } = require('../../built/crypto_key');
+const { default: User } = require('../../../built/api/models/user');
 const { default: zip } = require('@prezzemolo/zip')
 
 const migrate = async (user) => {
diff --git a/tools/migration/node.2018-03-28.appname.js b/tools/migration/nighthike/2.js
similarity index 86%
rename from tools/migration/node.2018-03-28.appname.js
rename to tools/migration/nighthike/2.js
index 9f16e4720..8fb5bbb08 100644
--- a/tools/migration/node.2018-03-28.appname.js
+++ b/tools/migration/nighthike/2.js
@@ -1,7 +1,6 @@
 // for Node.js interpret
 
-const { default: App } = require('../../built/api/models/app');
-const { generate } = require('../../built/crypto_key');
+const { default: App } = require('../../../built/api/models/app');
 const { default: zip } = require('@prezzemolo/zip')
 
 const migrate = async (app) => {
diff --git a/tools/migration/nighthike/3.js b/tools/migration/nighthike/3.js
new file mode 100644
index 000000000..cc0603d9e
--- /dev/null
+++ b/tools/migration/nighthike/3.js
@@ -0,0 +1,73 @@
+// for Node.js interpret
+
+const { default: User } = require('../../../built/api/models/user');
+const { generate } = require('../../../built/crypto_key');
+const { default: zip } = require('@prezzemolo/zip')
+
+const migrate = async (user) => {
+	const result = await User.update(user._id, {
+		$unset: {
+			email: '',
+			links: '',
+			password: '',
+			token: '',
+			twitter: '',
+			line: '',
+			profile: '',
+			last_used_at: '',
+			is_bot: '',
+			is_pro: '',
+			two_factor_secret: '',
+			two_factor_enabled: '',
+			client_settings: '',
+			settings: ''
+		},
+		$set: {
+			host: null,
+			host_lower: null,
+			account: {
+				email: user.email,
+				links: user.links,
+				password: user.password,
+				token: user.token,
+				twitter: user.twitter,
+				line: user.line,
+				profile: user.profile,
+				last_used_at: user.last_used_at,
+				is_bot: user.is_bot,
+				is_pro: user.is_pro,
+				two_factor_secret: user.two_factor_secret,
+				two_factor_enabled: user.two_factor_enabled,
+				client_settings: user.client_settings,
+				settings: user.settings,
+				keypair: generate()
+			}
+		}
+	});
+	return result.ok === 1;
+}
+
+async function main() {
+	const count = await User.count({});
+
+	const dop = Number.parseInt(process.argv[2]) || 5
+	const idop = ((count - (count % dop)) / dop) + 1
+
+	return zip(
+		1,
+		async (time) => {
+			console.log(`${time} / ${idop}`)
+			const doc = await User.find({}, {
+				limit: dop, skip: time * dop
+			})
+			return Promise.all(doc.map(migrate))
+		},
+		idop
+	).then(a => {
+		const rv = []
+		a.forEach(e => rv.push(...e))
+		return rv
+	})
+}
+
+main().then(console.dir).catch(console.error)
diff --git a/tools/migration/shell.camel-case.js b/tools/migration/nighthike/4.js
similarity index 100%
rename from tools/migration/shell.camel-case.js
rename to tools/migration/nighthike/4.js
diff --git a/tools/migration/node.1522066477.user-account-keypair.js b/tools/migration/node.1522066477.user-account-keypair.js
deleted file mode 100644
index c413e3db1..000000000
--- a/tools/migration/node.1522066477.user-account-keypair.js
+++ /dev/null
@@ -1,39 +0,0 @@
-// for Node.js interpret
-
-const { default: User } = require('../../built/api/models/user');
-const { generate } = require('../../built/crypto_key');
-const { default: zip } = require('@prezzemolo/zip')
-
-const migrate = async (user) => {
-	const result = await User.update(user._id, {
-		$set: {
-			'account.keypair': generate()
-		}
-	});
-	return result.ok === 1;
-}
-
-async function main() {
-	const count = await User.count({});
-
-	const dop = Number.parseInt(process.argv[2]) || 5
-	const idop = ((count - (count % dop)) / dop) + 1
-
-	return zip(
-		1,
-		async (time) => {
-			console.log(`${time} / ${idop}`)
-			const doc = await User.find({}, {
-				limit: dop, skip: time * dop
-			})
-			return Promise.all(doc.map(migrate))
-		},
-		idop
-	).then(a => {
-		const rv = []
-		a.forEach(e => rv.push(...e))
-		return rv
-	})
-}
-
-main().then(console.dir).catch(console.error)
diff --git a/tools/migration/shell.1522038492.user-account.js b/tools/migration/shell.1522038492.user-account.js
deleted file mode 100644
index 056c29e8e..000000000
--- a/tools/migration/shell.1522038492.user-account.js
+++ /dev/null
@@ -1,41 +0,0 @@
-db.users.dropIndex({ token: 1 });
-
-db.users.find({}).forEach(function(user) {
-	print(user._id);
-	db.users.update({ _id: user._id }, {
-		$unset: {
-			email: '',
-			links: '',
-			password: '',
-			token: '',
-			twitter: '',
-			line: '',
-			profile: '',
-			last_used_at: '',
-			is_bot: '',
-			is_pro: '',
-			two_factor_secret: '',
-			two_factor_enabled: '',
-			client_settings: '',
-			settings: ''
-		},
-		$set: {
-			account: {
-				email: user.email,
-				links: user.links,
-				password: user.password,
-				token: user.token,
-				twitter: user.twitter,
-				line: user.line,
-				profile: user.profile,
-				last_used_at: user.last_used_at,
-				is_bot: user.is_bot,
-				is_pro: user.is_pro,
-				two_factor_secret: user.two_factor_secret,
-				two_factor_enabled: user.two_factor_enabled,
-				client_settings: user.client_settings,
-				settings: user.settings
-			}
-		}
-	}, false, false);
-});
diff --git a/tools/migration/shell.1522116709.user-host.js b/tools/migration/shell.1522116709.user-host.js
deleted file mode 100644
index b354709a6..000000000
--- a/tools/migration/shell.1522116709.user-host.js
+++ /dev/null
@@ -1 +0,0 @@
-db.users.update({ }, { $set: { host: null } }, { multi: true });
diff --git a/tools/migration/shell.1522116710.user-host_lower.js b/tools/migration/shell.1522116710.user-host_lower.js
deleted file mode 100644
index 31ec6c468..000000000
--- a/tools/migration/shell.1522116710.user-host_lower.js
+++ /dev/null
@@ -1 +0,0 @@
-db.users.update({ }, { $set: { host_lower: null } }, { multi: true });

From 12db5cde5f6f659028dd7c96a25b6cfa8cf368f2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 19:09:13 +0900
Subject: [PATCH 0906/1250] oops

---
 src/api/endpoints/i/favorites.ts | 2 +-
 src/api/models/favorite.ts       | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/api/endpoints/i/favorites.ts b/src/api/endpoints/i/favorites.ts
index 22a439954..9f8becf21 100644
--- a/src/api/endpoints/i/favorites.ts
+++ b/src/api/endpoints/i/favorites.ts
@@ -39,6 +39,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(favorites.map(async favorite =>
-		await pack(favorite.post)
+		await pack(favorite.postId)
 	)));
 });
diff --git a/src/api/models/favorite.ts b/src/api/models/favorite.ts
index 5ba55c4c9..a21c276ff 100644
--- a/src/api/models/favorite.ts
+++ b/src/api/models/favorite.ts
@@ -1,10 +1,10 @@
 import * as mongo from 'mongodb';
 import db from '../../db/mongodb';
 
-const Favorites = db.get<IFavorites>('favorites');
+const Favorites = db.get<IFavorite>('favorites');
 export default Favorites;
 
-export type IFavorites = {
+export type IFavorite = {
 	_id: mongo.ObjectID;
 	createdAt: Date;
 	userId: mongo.ObjectID;

From d05bf07b3f4165fc46bf6e2aa80e2f3448bac84f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 19:09:42 +0900
Subject: [PATCH 0907/1250] Fix bug

---
 src/api/endpoints/posts/favorites/delete.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/api/endpoints/posts/favorites/delete.ts b/src/api/endpoints/posts/favorites/delete.ts
index b1b4fcebc..db52036ec 100644
--- a/src/api/endpoints/posts/favorites/delete.ts
+++ b/src/api/endpoints/posts/favorites/delete.ts
@@ -37,7 +37,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}
 
 	// Delete favorite
-	await Favorite.deleteOne({
+	await Favorite.remove({
 		_id: exist._id
 	});
 

From eca2bde48f97acede3cc495431ebe36b6c9c6829 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 19:22:53 +0900
Subject: [PATCH 0908/1250] Fix bug

---
 tools/migration/nighthike/4.js | 24 ++++++++++++------------
 1 file changed, 12 insertions(+), 12 deletions(-)

diff --git a/tools/migration/nighthike/4.js b/tools/migration/nighthike/4.js
index 8d07140ba..2e252b7f4 100644
--- a/tools/migration/nighthike/4.js
+++ b/tools/migration/nighthike/4.js
@@ -212,21 +212,21 @@ db.users.update({}, {
 		pinned_post_id: 'pinnedPostId',
 		is_suspended: 'isSuspended',
 		host_lower: 'hostLower',
-		'twitter.access_token': 'twitter.accessToken',
-		'twitter.access_token_secret': 'twitter.accessTokenSecret',
-		'twitter.user_id': 'twitter.userId',
-		'twitter.screen_name': 'twitter.screenName',
-		'line.user_id': 'line.userId',
-		last_used_at: 'lastUsedAt',
-		is_bot: 'isBot',
-		is_pro: 'isPro',
-		two_factor_secret: 'twoFactorSecret',
-		two_factor_enabled: 'twoFactorEnabled',
-		client_settings: 'clientSettings'
+		'account.last_used_at': 'account.lastUsedAt',
+		'account.is_bot': 'account.isBot',
+		'account.is_pro': 'account.isPro',
+		'account.two_factor_secret': 'account.twoFactorSecret',
+		'account.two_factor_enabled': 'account.twoFactorEnabled',
+		'account.client_settings': 'account.clientSettings'
 	},
 	$unset: {
 		likes_count: '',
 		liked_count: '',
-		latest_post: ''
+		latest_post: '',
+		'account.twitter.access_token': '',
+		'account.twitter.access_token_secret': '',
+		'account.twitter.user_id': '',
+		'account.twitter.screen_name': '',
+		'account.line.user_id': ''
 	}
 }, false, true);

From 2bbbf481ac7e0bb71a6a806cb27058cbf79da3a5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 22:07:11 +0900
Subject: [PATCH 0909/1250] :v:

---
 CHANGELOG.md | 532 ---------------------------------------------------
 README.md    |   4 -
 2 files changed, 536 deletions(-)
 delete mode 100644 CHANGELOG.md

diff --git a/CHANGELOG.md b/CHANGELOG.md
deleted file mode 100644
index 6e69a7319..000000000
--- a/CHANGELOG.md
+++ /dev/null
@@ -1,532 +0,0 @@
-ChangeLog (Release Notes)
-=========================
-主に notable な changes を書いていきます
-
-3493 (2018/01/01)
------------------
-* なんか
-
-3460 (2017/12/23)
------------------
-* 検索で複数のユーザーを指定できるように
-* 検索でユーザーを除外できるように
-* など
-
-3451 (2017/12/22)
------------------
-* ミュート機能
-
-3430 (2017/12/21)
------------------
-* oops
-
-3428 (2017/12/21)
------------------
-* バグ修正
-
-3426 (2017/12/21)
------------------
-* 検索にpoll追加
-
-3424 (2017/12/21)
------------------
-* 検索にrepost追加
-* など
-
-3422 (2017/12/21)
------------------
-* 検索にfollow追加 #1023
-
-3420 (2017/12/21)
------------------
-* 検索機能を大幅に強化
-
-3415 (2017/12/19)
------------------
-* デザインの調整
-
-3404 (2017/12/17)
------------------
-* なんか
-
-3400 (2017/12/17)
------------------
-* なんか
-
-3392 (2017/12/17)
------------------
-* ドキュメントなど
-
-3390 (2017/12/16)
------------------
-* ドキュメントなど
-
-3347 (2017/12/11)
------------------
-* バグ修正
-
-3342 (2017/12/11)
------------------
-* なんか
-
-3339 (2017/12/11)
------------------
-* なんか
-
-3334 (2017/12/10)
------------------
-* いい感じにした
-
-3322 (2017/12/10)
------------------
-* :art:
-
-3320 (2017/12/10)
------------------
-* なんか
-
-3310 (2017/12/09)
------------------
-* i18nなど
-
-3308 (2017/12/09)
------------------
-* :art:
-
-3294 (2017/12/09)
------------------
-* バグ修正
-
-3292 (2017/12/09)
------------------
-* ユーザビリティの向上
-
-3281 (2017/12/08)
------------------
-* 二段階認証の実装 (#967)
-
-3278 (2017/12/08)
------------------
-* :v:
-
-3272 (2017/12/08)
------------------
-* Fix bug
-
-3268 (2017/12/08)
------------------
-* :v:
-
-3263 (2017/12/08)
------------------
-* FontAwesome5に移行
-
-3230 (2017/11/28)
------------------
-* :v:
-
-3219 (2017/11/28)
------------------
-* なんか
-
-3212 (2017/11/27)
------------------
-* なんか
-
-3201 (2017/11/23)
------------------
-* Twitterログインを実装 (#939)
-
-3196 (2017/11/23)
------------------
-* バグ修正
-
-3194 (2017/11/23)
------------------
-* バグ修正
-
-3191 (2017/11/23)
------------------
-* :v:
-
-3188 (2017/11/22)
------------------
-* バグ修正
-
-3180 (2017/11/21)
------------------
-* バグ修正
-
-3177 (2017/11/21)
------------------
-* ServiceWorker support
-  * Misskeyを開いていないときでも通知を受け取れるように(Chromeのみ)
-
-3165 (2017/11/20)
------------------
-* デスクトップ版でも通知バッジを表示 (#918)
-* デザインの調整
-* バグ修正
-
-3155 (2017/11/20)
------------------
-* デスクトップ版でユーザーの投稿グラフを見れるように
-
-3142 (2017/11/18)
------------------
-* バグ修正
-
-3140 (2017/11/18)
------------------
-* ウィジェットをスクロールに追従させるように
-
-3136 (2017/11/17)
------------------
-* バグ修正
-* 通信の最適化
-
-3131 (2017/11/17)
------------------
-* バグ修正
-* 通信の最適化
-
-3124 (2017/11/16)
------------------
-* バグ修正
-
-3121 (2017/11/16)
------------------
-* ブロードキャストウィジェットの強化
-* デザインのグリッチの修正
-* 通信の最適化
-
-3113 (2017/11/15)
------------------
-* アクティビティのレンダリングの問題の修正など
-
-3110 (2017/11/15)
------------------
-* デザインの調整など
-
-3107 (2017/11/14)
------------------
-* デザインの調整
-
-3104 (2017/11/14)
------------------
-* デスクトップ版ユーザーページのデザインの改良
-* バグ修正
-
-3099 (2017/11/14)
------------------
-* デスクトップ版ユーザーページの強化
-* バグ修正
-
-3093 (2017/11/14)
------------------
-* やった
-
-3089 (2017/11/14)
------------------
-* なんか
-
-3069 (2017/11/14)
------------------
-* ドライブウィンドウもポップアウトできるように
-* デザインの調整
-
-3066 (2017/11/14)
------------------
-* メッセージウィジェット追加
-* アクセスログウィジェット追加
-
-3057 (2017/11/13)
------------------
-* グリッチ修正
-
-3055 (2017/11/13)
------------------
-* メッセージのウィンドウのポップアウト (#911)
-
-3050 (2017/11/13)
------------------
-* 通信の最適化
-  * これで例えばサーバー情報ウィジェットを5000兆個設置しても利用するコネクションは一つだけになりウィジェットを1つ設置したときと(ネットワーク的な)負荷は変わらなくなる
-* デザインの調整
-* ユーザビリティの向上
-
-3040 (2017/11/12)
------------------
-* バグ修正
-
-3038 (2017/11/12)
------------------
-* 投稿フォームウィジェットの追加
-* タイムライン上部にもウィジェットを配置できるように
-
-3035 (2017/11/12)
------------------
-* ウィジェットの強化
-
-3033 (2017/11/12)
------------------
-* デザインの調整
-
-3031 (2017/11/12)
------------------
-* ウィジェットの強化
-
-3028 (2017/11/12)
------------------
-* ウィジェットの表示をコンパクトにできるように
-
-3026 (2017/11/12)
------------------
-* バグ修正
-
-3024 (2017/11/12)
------------------
-* いい感じにするなど
-
-3020 (2017/11/12)
------------------
-* 通信の最適化
-
-3017 (2017/11/11)
------------------
-* 誤字修正など
-
-3012 (2017/11/11)
------------------
-* デザインの調整
-
-3010 (2017/11/11)
------------------
-* デザインの調整
-
-3008 (2017/11/11)
------------------
-* カレンダー(タイムマシン)ウィジェットの追加
-
-3006 (2017/11/11)
------------------
-* デザインの調整
-* など
-
-2996 (2017/11/10)
------------------
-* デザインの調整
-* など
-
-2991 (2017/11/09)
------------------
-* デザインの調整
-
-2988 (2017/11/09)
------------------
-* チャンネルウィジェットを追加
-
-2984 (2017/11/09)
------------------
-* スライドショーウィジェットを追加
-
-2974 (2017/11/08)
------------------
-* ホームのカスタマイズを実装するなど
-
-2971 (2017/11/08)
------------------
-* バグ修正
-* デザインの調整
-* i18n
-
-2944 (2017/11/07)
------------------
-* パフォーマンスの向上
-  * GirdFSになるなどした
-* 依存関係の更新
-
-2807 (2017/11/02)
------------------
-* いい感じに
-
-2805 (2017/11/02)
------------------
-* いい感じに
-
-2801 (2017/11/01)
------------------
-* チャンネルのWatch実装
-
-2799 (2017/11/01)
------------------
-* いい感じに
-
-2795 (2017/11/01)
------------------
-* いい感じに
-
-2793 (2017/11/01)
------------------
-* なんか
-
-2783 (2017/11/01)
------------------
-* なんか
-
-2777 (2017/11/01)
------------------
-* 細かいブラッシュアップ
-
-2775 (2017/11/01)
------------------
-* Fix: バグ修正
-
-2769 (2017/11/01)
------------------
-* New: チャンネルシステム
-
-2752 (2017/10/30)
------------------
-* New: 未読の通知がある場合アイコンを表示するように
-
-2747 (2017/10/25)
------------------
-* Fix: 非ログイン状態ですべてのページが致命的な問題を発生させる (#89)
-
-2742 (2017/10/25)
------------------
-* New: トラブルシューティングを実装するなど
-
-2735 (2017/10/22)
------------------
-* New: モバイル版からでもクライアントバージョンを確認できるように
-
-2732 (2017/10/22)
------------------
-* 依存関係の更新など
-
-2584 (2017/09/08)
------------------
-* New: ユーザーページによく使うドメインを表示 (#771)
-* New: よくリプライするユーザーをユーザーページに表示 (#770)
-
-2566 (2017/09/07)
------------------
-* New: 投稿することの多いキーワードをユーザーページに表示する (#768)
-* l10n
-* デザインの修正
-
-2544 (2017/09/06)
------------------
-* 投稿のカテゴリに関する実験的な実装
-* l10n
-* ユーザビリティの向上
-
-2520 (2017/08/30)
------------------
-* デザインの調整
-
-2518 (2017/08/30)
------------------
-* Fix: モバイル版のタイムラインからリアクションやメニューを開けない
-* デザインの調整
-
-2515 (2017/08/30)
------------------
-* New: 投稿のピン留め (#746)
-* New: モバイル版のユーザーページに知り合いのフォロワーを表示するように
-* New: ホームストリームにメッセージを流すことでlast_used_atを更新できるようにする (#745)
-* その他細かな修正
-
-2508 (2017/08/30)
------------------
-* New: モバイル版のユーザーページのアクティビティチャートを変更
-* New: モバイル版のユーザーページに最終ログイン日時を表示するように
-* デザインの調整
-
-2503 (2017/08/30)
------------------
-* デザインの調整
-
-2502 (2017/08/30)
------------------
-* デザインの修正・調整
-
-2501 (2017/08/30)
------------------
-* New: モバイルのユーザーページを刷新
-
-2498 (2017/08/29)
------------------
-* Fix: repostのborder-radiusが効いていない (#743)
-* テーマカラーを赤に戻してみた
-* ユーザビリティの向上
-* デザインの調整
-
-2493-2 (2017/08/29)
--------------------
-* デザインの修正
-
-2493 (2017/08/29)
------------------
-* デザインの変更など
-
-2491 (2017/08/29)
------------------
-* デザインの修正と調整
-
-2489 (2017/08/29)
------------------
-* ユーザビリティの向上
-* デザインの調整
-
-2487 (2017/08/29)
------------------
-* New: パスワードを変更する際に新しいパスワードを二度入力させる (#739)
-* New: ドナーを表示する (#738)
-* Fix: 投稿のリンクが機能していない問題を修正
-* Fix: アカウント作成フォームのユーザーページURLプレビューが正しく機能していなかった問題を修正
-* l10n
-* デザインの調整
-
-2470 (2017/08/29)
------------------
-* New: トークンを再生成できるように (#497)
-* New: パスワードを変更する機能 (#364)
-
-2461 (2017/08/28)
------------------
-* Fix: モバイル版からアバターとバナーの設定を行えなかった問題を修正
-* デザインの修正
-
-2458 (2017/08/28)
------------------
-* New: モバイル版からプロフィールを設定できるように
-* New: モバイル版からサインアウトを行えるように
-* New: 投稿ページに次の投稿/前の投稿リンクを作成 (#734)
-* New: タイムラインの投稿をダブルクリックすることで詳細な情報が見れるように
-* Fix: モバイル版でおすすめユーザーをフォローしてもタイムラインが更新されない (#736)
-* Fix: モバイル版で設定にアクセスできない
-* デザインの調整
-* 依存関係の更新
-
-2380
-----
-アプリケーションが作れない問題を修正
-
-2367
-----
-Statsのユーザー数グラフに「アカウントが作成された**回数**」(その日時点での「アカウント数」**ではなく**)グラフも併記するようにした
-
-2364
-----
-デザインの微調整
-
-2361
-----
-Statsを実装するなど
-
-2357
-----
-Statusを実装するなど
diff --git a/README.md b/README.md
index cb8821b1e..05a90ad23 100644
--- a/README.md
+++ b/README.md
@@ -32,10 +32,6 @@ Contribution
 ----------------------------------------------------------------
 Please see [Contribution guide](./CONTRIBUTING.md).
 
-Release Notes
-----------------------------------------------------------------
-Please see [ChangeLog](./CHANGELOG.md).
-
 Sponsors & Backers
 ----------------------------------------------------------------
 Misskey has no 100+ GitHub stars currently. However, a donation is always welcome!

From dbf38541525834b943d65c9d32131b8345a71136 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 22:30:52 +0900
Subject: [PATCH 0910/1250] :v:

---
 README.md | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/README.md b/README.md
index 05a90ad23..703400755 100644
--- a/README.md
+++ b/README.md
@@ -39,11 +39,11 @@ If you want to donate to Misskey, please get in touch with [@syuilo][syuilo-link
 
 **Note:** When you donate to Misskey, your name will be listed in [donors](./DONORS.md).
 
-Collaborators
+Notable contributors
 ----------------------------------------------------------------
-| ![syuilo][syuilo-icon] | ![Morisawa Aya][ayamorisawa-icon] | ![otofune][otofune-icon]        |
-|------------------------|-----------------------------------|---------------------------------|
-| [syuilo][syuilo-link]  | [Aya Morisawa][ayamorisawa-link]  | [otofune][otofune-link] |
+| ![syuilo][syuilo-icon] | ![Morisawa Aya][ayamorisawa-icon] | ![otofune][otofune-icon] | ![akihikodaki][akihikodaki-icon] |
+|:-:|:-:|:-:|:-:|
+| [syuilo][syuilo-link]<br>Owner | [Aya Morisawa][ayamorisawa-link]<br>Collaborator | [otofune][otofune-link]<br>Collaborator | [akihikodaki][akihikodaki-link] |
 
 [List of all contributors](https://github.com/syuilo/misskey/graphs/contributors)
 
@@ -65,10 +65,12 @@ license is applied.) See Git log to identify them.
 [himawari-badge]:     https://img.shields.io/badge/%E5%8F%A4%E8%B0%B7-%E5%90%91%E6%97%A5%E8%91%B5-1684c5.svg?style=flat-square
 [sakurako-badge]:     https://img.shields.io/badge/%E5%A4%A7%E5%AE%A4-%E6%AB%BB%E5%AD%90-efb02a.svg?style=flat-square
 
-<!-- Collaborators Info -->
+<!-- Contributors Info -->
 [syuilo-link]:      https://syuilo.com
 [syuilo-icon]:      https://avatars2.githubusercontent.com/u/4439005?v=3&s=70
 [ayamorisawa-link]: https://github.com/ayamorisawa
 [ayamorisawa-icon]: https://avatars0.githubusercontent.com/u/10798641?v=3&s=70
 [otofune-link]:     https://github.com/otofune
 [otofune-icon]:     https://avatars0.githubusercontent.com/u/15062473?v=3&s=70
+[akihikodaki-link]: https://github.com/akihikodaki
+[akihikodaki-icon]: https://avatars2.githubusercontent.com/u/17036990?s=70&v=4

From 7d0763ab8ac5afc75a097b19d0497901c4e47caf Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 22:37:09 +0900
Subject: [PATCH 0911/1250] :art:

---
 README.md | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/README.md b/README.md
index 703400755..df6056993 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,6 @@
-![Misskey](./assets/title.png)
+<img src="https://github.com/syuilo/misskey/blob/b3f42e62af698a67c2250533c437569559f1fdf9/src/himasaku/resources/himasaku.png?raw=true" align="right" width="320px"/>
+
+Misskey
 ================================================================
 
 [![][travis-badge]][travis-link]
@@ -10,8 +12,6 @@
 [Misskey](https://misskey.xyz) is a completely open source,
 ultimately sophisticated new type of mini-blog based SNS.
 
-![ss](./assets/ss.jpg)
-
 Key features
 ----------------------------------------------------------------
 * Automatically updated timeline

From 696d668aa6e224cbdb7cca6cb4060d0e8b686640 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 22:44:30 +0900
Subject: [PATCH 0912/1250] :v:

---
 README.md | 11 +++--------
 1 file changed, 3 insertions(+), 8 deletions(-)

diff --git a/README.md b/README.md
index df6056993..69a9221c8 100644
--- a/README.md
+++ b/README.md
@@ -28,16 +28,11 @@ Setup and Installation
 If you want to run your own instance of Misskey,
 please see [Setup and installation guide](./docs/setup.en.md).
 
-Contribution
+Donation
 ----------------------------------------------------------------
-Please see [Contribution guide](./CONTRIBUTING.md).
+If you want to donate to Misskey, please see [this][./docs/donate.ja.md].
 
-Sponsors & Backers
-----------------------------------------------------------------
-Misskey has no 100+ GitHub stars currently. However, a donation is always welcome!
-If you want to donate to Misskey, please get in touch with [@syuilo][syuilo-link].
-
-**Note:** When you donate to Misskey, your name will be listed in [donors](./DONORS.md).
+[List of all donors](./DONORS.md)
 
 Notable contributors
 ----------------------------------------------------------------

From 7d2d7a9904a5df1c24d1a1014529882f56ccbc9d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 22:56:28 +0900
Subject: [PATCH 0913/1250] Use AGPLv3

---
 LICENSE          | 674 +++++++++++++++++++++++++++++++++++++++++++++--
 LICENSE_AGPL-3.0 | 661 ----------------------------------------------
 README.md        |   6 +-
 3 files changed, 658 insertions(+), 683 deletions(-)
 delete mode 100644 LICENSE_AGPL-3.0

diff --git a/LICENSE b/LICENSE
index 0b6e30e45..dba13ed2d 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,21 +1,661 @@
-The MIT License (MIT)
+                    GNU AFFERO GENERAL PUBLIC LICENSE
+                       Version 3, 19 November 2007
 
-Copyright (c) 2014-2018 syuilo
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
 
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+                            Preamble
 
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
+  The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
 
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+  A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate.  Many developers of free software are heartened and
+encouraged by the resulting cooperation.  However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+  The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community.  It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server.  Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+  An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals.  This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU Affero General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Remote Network Interaction; Use with the GNU General Public License.
+
+  Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software.  This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time.  Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source.  For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code.  There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<http://www.gnu.org/licenses/>.
diff --git a/LICENSE_AGPL-3.0 b/LICENSE_AGPL-3.0
deleted file mode 100644
index dba13ed2d..000000000
--- a/LICENSE_AGPL-3.0
+++ /dev/null
@@ -1,661 +0,0 @@
-                    GNU AFFERO GENERAL PUBLIC LICENSE
-                       Version 3, 19 November 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
-                            Preamble
-
-  The GNU Affero General Public License is a free, copyleft license for
-software and other kinds of works, specifically designed to ensure
-cooperation with the community in the case of network server software.
-
-  The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works.  By contrast,
-our General Public Licenses are intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users.
-
-  When we speak of free software, we are referring to freedom, not
-price.  Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
-  Developers that use our General Public Licenses protect your rights
-with two steps: (1) assert copyright on the software, and (2) offer
-you this License which gives you legal permission to copy, distribute
-and/or modify the software.
-
-  A secondary benefit of defending all users' freedom is that
-improvements made in alternate versions of the program, if they
-receive widespread use, become available for other developers to
-incorporate.  Many developers of free software are heartened and
-encouraged by the resulting cooperation.  However, in the case of
-software used on network servers, this result may fail to come about.
-The GNU General Public License permits making a modified version and
-letting the public access it on a server without ever releasing its
-source code to the public.
-
-  The GNU Affero General Public License is designed specifically to
-ensure that, in such cases, the modified source code becomes available
-to the community.  It requires the operator of a network server to
-provide the source code of the modified version running there to the
-users of that server.  Therefore, public use of a modified version, on
-a publicly accessible server, gives the public access to the source
-code of the modified version.
-
-  An older license, called the Affero General Public License and
-published by Affero, was designed to accomplish similar goals.  This is
-a different license, not a version of the Affero GPL, but Affero has
-released a new version of the Affero GPL which permits relicensing under
-this license.
-
-  The precise terms and conditions for copying, distribution and
-modification follow.
-
-                       TERMS AND CONDITIONS
-
-  0. Definitions.
-
-  "This License" refers to version 3 of the GNU Affero General Public License.
-
-  "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
-  "The Program" refers to any copyrightable work licensed under this
-License.  Each licensee is addressed as "you".  "Licensees" and
-"recipients" may be individuals or organizations.
-
-  To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy.  The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
-  A "covered work" means either the unmodified Program or a work based
-on the Program.
-
-  To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy.  Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
-  To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies.  Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
-  An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License.  If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
-  1. Source Code.
-
-  The "source code" for a work means the preferred form of the work
-for making modifications to it.  "Object code" means any non-source
-form of a work.
-
-  A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
-  The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form.  A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
-  The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities.  However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work.  For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
-  The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
-  The Corresponding Source for a work in source code form is that
-same work.
-
-  2. Basic Permissions.
-
-  All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met.  This License explicitly affirms your unlimited
-permission to run the unmodified Program.  The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work.  This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
-  You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force.  You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright.  Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
-  Conveying under any other circumstances is permitted solely under
-the conditions stated below.  Sublicensing is not allowed; section 10
-makes it unnecessary.
-
-  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
-  No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
-  When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
-  4. Conveying Verbatim Copies.
-
-  You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
-  You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
-  5. Conveying Modified Source Versions.
-
-  You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
-    a) The work must carry prominent notices stating that you modified
-    it, and giving a relevant date.
-
-    b) The work must carry prominent notices stating that it is
-    released under this License and any conditions added under section
-    7.  This requirement modifies the requirement in section 4 to
-    "keep intact all notices".
-
-    c) You must license the entire work, as a whole, under this
-    License to anyone who comes into possession of a copy.  This
-    License will therefore apply, along with any applicable section 7
-    additional terms, to the whole of the work, and all its parts,
-    regardless of how they are packaged.  This License gives no
-    permission to license the work in any other way, but it does not
-    invalidate such permission if you have separately received it.
-
-    d) If the work has interactive user interfaces, each must display
-    Appropriate Legal Notices; however, if the Program has interactive
-    interfaces that do not display Appropriate Legal Notices, your
-    work need not make them do so.
-
-  A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit.  Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
-  6. Conveying Non-Source Forms.
-
-  You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
-    a) Convey the object code in, or embodied in, a physical product
-    (including a physical distribution medium), accompanied by the
-    Corresponding Source fixed on a durable physical medium
-    customarily used for software interchange.
-
-    b) Convey the object code in, or embodied in, a physical product
-    (including a physical distribution medium), accompanied by a
-    written offer, valid for at least three years and valid for as
-    long as you offer spare parts or customer support for that product
-    model, to give anyone who possesses the object code either (1) a
-    copy of the Corresponding Source for all the software in the
-    product that is covered by this License, on a durable physical
-    medium customarily used for software interchange, for a price no
-    more than your reasonable cost of physically performing this
-    conveying of source, or (2) access to copy the
-    Corresponding Source from a network server at no charge.
-
-    c) Convey individual copies of the object code with a copy of the
-    written offer to provide the Corresponding Source.  This
-    alternative is allowed only occasionally and noncommercially, and
-    only if you received the object code with such an offer, in accord
-    with subsection 6b.
-
-    d) Convey the object code by offering access from a designated
-    place (gratis or for a charge), and offer equivalent access to the
-    Corresponding Source in the same way through the same place at no
-    further charge.  You need not require recipients to copy the
-    Corresponding Source along with the object code.  If the place to
-    copy the object code is a network server, the Corresponding Source
-    may be on a different server (operated by you or a third party)
-    that supports equivalent copying facilities, provided you maintain
-    clear directions next to the object code saying where to find the
-    Corresponding Source.  Regardless of what server hosts the
-    Corresponding Source, you remain obligated to ensure that it is
-    available for as long as needed to satisfy these requirements.
-
-    e) Convey the object code using peer-to-peer transmission, provided
-    you inform other peers where the object code and Corresponding
-    Source of the work are being offered to the general public at no
-    charge under subsection 6d.
-
-  A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
-  A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling.  In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage.  For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product.  A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
-  "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source.  The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
-  If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information.  But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
-  The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed.  Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
-  Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
-  7. Additional Terms.
-
-  "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law.  If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
-  When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it.  (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.)  You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
-  Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
-    a) Disclaiming warranty or limiting liability differently from the
-    terms of sections 15 and 16 of this License; or
-
-    b) Requiring preservation of specified reasonable legal notices or
-    author attributions in that material or in the Appropriate Legal
-    Notices displayed by works containing it; or
-
-    c) Prohibiting misrepresentation of the origin of that material, or
-    requiring that modified versions of such material be marked in
-    reasonable ways as different from the original version; or
-
-    d) Limiting the use for publicity purposes of names of licensors or
-    authors of the material; or
-
-    e) Declining to grant rights under trademark law for use of some
-    trade names, trademarks, or service marks; or
-
-    f) Requiring indemnification of licensors and authors of that
-    material by anyone who conveys the material (or modified versions of
-    it) with contractual assumptions of liability to the recipient, for
-    any liability that these contractual assumptions directly impose on
-    those licensors and authors.
-
-  All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10.  If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term.  If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
-  If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
-  Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
-  8. Termination.
-
-  You may not propagate or modify a covered work except as expressly
-provided under this License.  Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
-  However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
-  Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
-  Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License.  If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
-  9. Acceptance Not Required for Having Copies.
-
-  You are not required to accept this License in order to receive or
-run a copy of the Program.  Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance.  However,
-nothing other than this License grants you permission to propagate or
-modify any covered work.  These actions infringe copyright if you do
-not accept this License.  Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
-  10. Automatic Licensing of Downstream Recipients.
-
-  Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License.  You are not responsible
-for enforcing compliance by third parties with this License.
-
-  An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations.  If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
-  You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License.  For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
-  11. Patents.
-
-  A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based.  The
-work thus licensed is called the contributor's "contributor version".
-
-  A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version.  For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
-  Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
-  In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement).  To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
-  If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients.  "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
-  If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
-  A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License.  You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
-  Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
-  12. No Surrender of Others' Freedom.
-
-  If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License.  If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all.  For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
-  13. Remote Network Interaction; Use with the GNU General Public License.
-
-  Notwithstanding any other provision of this License, if you modify the
-Program, your modified version must prominently offer all users
-interacting with it remotely through a computer network (if your version
-supports such interaction) an opportunity to receive the Corresponding
-Source of your version by providing access to the Corresponding Source
-from a network server at no charge, through some standard or customary
-means of facilitating copying of software.  This Corresponding Source
-shall include the Corresponding Source for any work covered by version 3
-of the GNU General Public License that is incorporated pursuant to the
-following paragraph.
-
-  Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU General Public License into a single
-combined work, and to convey the resulting work.  The terms of this
-License will continue to apply to the part which is the covered work,
-but the work with which it is combined will remain governed by version
-3 of the GNU General Public License.
-
-  14. Revised Versions of this License.
-
-  The Free Software Foundation may publish revised and/or new versions of
-the GNU Affero General Public License from time to time.  Such new versions
-will be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
-  Each version is given a distinguishing version number.  If the
-Program specifies that a certain numbered version of the GNU Affero General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation.  If the Program does not specify a version number of the
-GNU Affero General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
-  If the Program specifies that a proxy can decide which future
-versions of the GNU Affero General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
-  Later license versions may give you additional or different
-permissions.  However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
-  15. Disclaimer of Warranty.
-
-  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
-  16. Limitation of Liability.
-
-  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
-  17. Interpretation of Sections 15 and 16.
-
-  If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
-                     END OF TERMS AND CONDITIONS
-
-            How to Apply These Terms to Your New Programs
-
-  If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
-  To do so, attach the following notices to the program.  It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-    <one line to give the program's name and a brief idea of what it does.>
-    Copyright (C) <year>  <name of author>
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as published by
-    the Free Software Foundation, either version 3 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-Also add information on how to contact you by electronic and paper mail.
-
-  If your software can interact with users remotely through a computer
-network, you should also make sure that it provides a way for users to
-get its source.  For example, if your program is a web application, its
-interface could display a "Source" link that leads users to an archive
-of the code.  There are many ways you could offer source, and different
-solutions will be better for different programs; see section 13 for the
-specific requirements.
-
-  You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU AGPL, see
-<http://www.gnu.org/licenses/>.
diff --git a/README.md b/README.md
index 69a9221c8..430237109 100644
--- a/README.md
+++ b/README.md
@@ -44,11 +44,7 @@ Notable contributors
 
 Copyright
 ----------------------------------------------------------------
-Misskey is an open-source software licensed under [The MIT License](LICENSE).
-
-The portions of Misskey contributed by Akihiko Odaki <nekomanma@pixiv.co.jp> is
-licensed under GNU Affero General Public License (only version 3.0 of the
-license is applied.) See Git log to identify them.
+Misskey is an open-source software licensed under [GNU AGPLv3](LICENSE).
 
 [agpl-3.0]:           https://www.gnu.org/licenses/agpl-3.0.en.html
 [agpl-3.0-badge]:     https://img.shields.io/badge/license-AGPL--3.0-444444.svg?style=flat-square

From 6cfd9ad042f0d24a5261bfd24c748bbb19e5dcba Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 23:01:42 +0900
Subject: [PATCH 0914/1250] :v:

---
 README.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/README.md b/README.md
index 430237109..800cad0f9 100644
--- a/README.md
+++ b/README.md
@@ -44,6 +44,8 @@ Notable contributors
 
 Copyright
 ----------------------------------------------------------------
+> Copyright (c) 2014-2018 syuilo
+
 Misskey is an open-source software licensed under [GNU AGPLv3](LICENSE).
 
 [agpl-3.0]:           https://www.gnu.org/licenses/agpl-3.0.en.html

From 91609fd4aa67c0ec8bbd40cce4f41442d5aa25a7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 28 Mar 2018 23:04:06 +0900
Subject: [PATCH 0915/1250] :v:

---
 README.md | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 800cad0f9..6984f52f1 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,9 @@ Misskey
 [![][sakurako-badge]][himasaku]
 [![][agpl-3.0-badge]][AGPL-3.0]
 
-[Misskey](https://misskey.xyz) is a completely open source,
+> Lead Maintainer: [syuilo][syuilo-link]
+
+**[Misskey](https://misskey.xyz)** is a completely open source,
 ultimately sophisticated new type of mini-blog based SNS.
 
 Key features

From 660cdf05c4397b574d7428259596f79ad796125b Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Thu, 29 Mar 2018 01:20:40 +0900
Subject: [PATCH 0916/1250] Introduce processor

---
 gulpfile.ts                                   | 28 +++++++-------
 package.json                                  |  2 +
 src/{common => }/build/fa.ts                  |  0
 src/{common => }/build/i18n.ts                |  2 +-
 src/{common => }/build/license.ts             |  2 +-
 src/index.ts                                  | 38 ++++++++++++++-----
 src/parse-opt.ts                              | 17 +++++++++
 src/processor/index.ts                        |  4 ++
 src/processor/report-github-failure.ts        | 29 ++++++++++++++
 src/queue.ts                                  | 10 +++++
 src/{ => server}/api/api-handler.ts           |  0
 src/{ => server}/api/authenticate.ts          |  0
 src/{ => server}/api/bot/core.ts              |  0
 src/{ => server}/api/bot/interfaces/line.ts   |  4 +-
 src/{ => server}/api/common/drive/add-file.ts |  2 +-
 .../api/common/drive/upload_from_url.ts       |  0
 .../api/common/generate-native-user-token.ts  |  0
 src/{ => server}/api/common/get-friends.ts    |  0
 src/{ => server}/api/common/get-host-lower.ts |  0
 .../api/common/is-native-token.ts             |  0
 src/{ => server}/api/common/notify.ts         |  0
 src/{ => server}/api/common/push-sw.ts        |  2 +-
 .../api/common/read-messaging-message.ts      |  0
 .../api/common/read-notification.ts           |  0
 src/{ => server}/api/common/signin.ts         |  2 +-
 .../common/text/core/syntax-highlighter.ts    |  0
 .../api/common/text/elements/bold.ts          |  0
 .../api/common/text/elements/code.ts          |  0
 .../api/common/text/elements/emoji.ts         |  0
 .../api/common/text/elements/hashtag.ts       |  0
 .../api/common/text/elements/inline-code.ts   |  0
 .../api/common/text/elements/link.ts          |  0
 .../api/common/text/elements/mention.ts       |  0
 .../api/common/text/elements/quote.ts         |  0
 .../api/common/text/elements/url.ts           |  0
 src/{ => server}/api/common/text/index.ts     |  0
 src/{ => server}/api/common/watch-post.ts     |  0
 src/{ => server}/api/endpoints.ts             |  0
 .../api/endpoints/aggregation/posts.ts        |  0
 .../endpoints/aggregation/posts/reaction.ts   |  0
 .../endpoints/aggregation/posts/reactions.ts  |  0
 .../api/endpoints/aggregation/posts/reply.ts  |  0
 .../api/endpoints/aggregation/posts/repost.ts |  0
 .../api/endpoints/aggregation/users.ts        |  0
 .../endpoints/aggregation/users/activity.ts   |  0
 .../endpoints/aggregation/users/followers.ts  |  0
 .../endpoints/aggregation/users/following.ts  |  0
 .../api/endpoints/aggregation/users/post.ts   |  0
 .../endpoints/aggregation/users/reaction.ts   |  0
 src/{ => server}/api/endpoints/app/create.ts  |  0
 .../api/endpoints/app/name_id/available.ts    |  0
 src/{ => server}/api/endpoints/app/show.ts    |  0
 src/{ => server}/api/endpoints/auth/accept.ts |  0
 .../api/endpoints/auth/session/generate.ts    |  2 +-
 .../api/endpoints/auth/session/show.ts        |  0
 .../api/endpoints/auth/session/userkey.ts     |  0
 src/{ => server}/api/endpoints/channels.ts    |  0
 .../api/endpoints/channels/create.ts          |  0
 .../api/endpoints/channels/posts.ts           |  0
 .../api/endpoints/channels/show.ts            |  0
 .../api/endpoints/channels/unwatch.ts         |  0
 .../api/endpoints/channels/watch.ts           |  0
 src/{ => server}/api/endpoints/drive.ts       |  0
 src/{ => server}/api/endpoints/drive/files.ts |  0
 .../api/endpoints/drive/files/create.ts       |  0
 .../api/endpoints/drive/files/find.ts         |  0
 .../api/endpoints/drive/files/show.ts         |  0
 .../api/endpoints/drive/files/update.ts       |  0
 .../endpoints/drive/files/upload_from_url.ts  |  0
 .../api/endpoints/drive/folders.ts            |  0
 .../api/endpoints/drive/folders/create.ts     |  0
 .../api/endpoints/drive/folders/find.ts       |  0
 .../api/endpoints/drive/folders/show.ts       |  0
 .../api/endpoints/drive/folders/update.ts     |  0
 .../api/endpoints/drive/stream.ts             |  0
 .../api/endpoints/following/create.ts         |  0
 .../api/endpoints/following/delete.ts         |  0
 src/{ => server}/api/endpoints/i.ts           |  0
 src/{ => server}/api/endpoints/i/2fa/done.ts  |  0
 .../api/endpoints/i/2fa/register.ts           |  2 +-
 .../api/endpoints/i/2fa/unregister.ts         |  0
 .../api/endpoints/i/appdata/get.ts            |  0
 .../api/endpoints/i/appdata/set.ts            |  0
 .../api/endpoints/i/authorized_apps.ts        |  0
 .../api/endpoints/i/change_password.ts        |  0
 src/{ => server}/api/endpoints/i/favorites.ts |  0
 .../api/endpoints/i/notifications.ts          |  0
 src/{ => server}/api/endpoints/i/pin.ts       |  0
 .../api/endpoints/i/regenerate_token.ts       |  0
 .../api/endpoints/i/signin_history.ts         |  0
 src/{ => server}/api/endpoints/i/update.ts    |  2 +-
 .../api/endpoints/i/update_client_setting.ts  |  0
 .../api/endpoints/i/update_home.ts            |  0
 .../api/endpoints/i/update_mobile_home.ts     |  0
 .../api/endpoints/messaging/history.ts        |  0
 .../api/endpoints/messaging/messages.ts       |  0
 .../endpoints/messaging/messages/create.ts    |  2 +-
 .../api/endpoints/messaging/unread.ts         |  0
 src/{ => server}/api/endpoints/meta.ts        |  4 +-
 src/{ => server}/api/endpoints/mute/create.ts |  0
 src/{ => server}/api/endpoints/mute/delete.ts |  0
 src/{ => server}/api/endpoints/mute/list.ts   |  0
 src/{ => server}/api/endpoints/my/apps.ts     |  0
 .../notifications/get_unread_count.ts         |  0
 .../notifications/mark_as_read_all.ts         |  0
 .../api/endpoints/othello/games.ts            |  0
 .../api/endpoints/othello/games/show.ts       |  0
 .../api/endpoints/othello/invitations.ts      |  0
 .../api/endpoints/othello/match.ts            |  0
 .../api/endpoints/othello/match/cancel.ts     |  0
 src/{ => server}/api/endpoints/posts.ts       |  0
 .../api/endpoints/posts/categorize.ts         |  0
 .../api/endpoints/posts/context.ts            |  0
 .../api/endpoints/posts/create.ts             |  2 +-
 .../api/endpoints/posts/favorites/create.ts   |  0
 .../api/endpoints/posts/favorites/delete.ts   |  0
 .../api/endpoints/posts/mentions.ts           |  0
 .../endpoints/posts/polls/recommendation.ts   |  0
 .../api/endpoints/posts/polls/vote.ts         |  0
 .../api/endpoints/posts/reactions.ts          |  0
 .../api/endpoints/posts/reactions/create.ts   |  0
 .../api/endpoints/posts/reactions/delete.ts   |  0
 .../api/endpoints/posts/replies.ts            |  0
 .../api/endpoints/posts/reposts.ts            |  0
 .../api/endpoints/posts/search.ts             |  0
 src/{ => server}/api/endpoints/posts/show.ts  |  0
 .../api/endpoints/posts/timeline.ts           |  0
 src/{ => server}/api/endpoints/posts/trend.ts |  0
 src/{ => server}/api/endpoints/stats.ts       |  0
 src/{ => server}/api/endpoints/sw/register.ts |  0
 .../api/endpoints/username/available.ts       |  0
 src/{ => server}/api/endpoints/users.ts       |  0
 .../api/endpoints/users/followers.ts          |  0
 .../api/endpoints/users/following.ts          |  0
 .../users/get_frequently_replied_users.ts     |  0
 src/{ => server}/api/endpoints/users/posts.ts |  0
 .../api/endpoints/users/recommendation.ts     |  0
 .../api/endpoints/users/search.ts             |  2 +-
 .../api/endpoints/users/search_by_username.ts |  0
 src/{ => server}/api/endpoints/users/show.ts  |  0
 src/{ => server}/api/event.ts                 |  2 +-
 src/{ => server}/api/limitter.ts              |  2 +-
 src/{ => server}/api/models/access-token.ts   |  2 +-
 src/{ => server}/api/models/app.ts            |  4 +-
 src/{ => server}/api/models/appdata.ts        |  2 +-
 src/{ => server}/api/models/auth-session.ts   |  2 +-
 .../api/models/channel-watching.ts            |  2 +-
 src/{ => server}/api/models/channel.ts        |  2 +-
 src/{ => server}/api/models/drive-file.ts     |  4 +-
 src/{ => server}/api/models/drive-folder.ts   |  2 +-
 src/{ => server}/api/models/drive-tag.ts      |  2 +-
 src/{ => server}/api/models/favorite.ts       |  2 +-
 src/{ => server}/api/models/following.ts      |  2 +-
 .../api/models/messaging-history.ts           |  2 +-
 .../api/models/messaging-message.ts           |  2 +-
 src/{ => server}/api/models/meta.ts           |  2 +-
 src/{ => server}/api/models/mute.ts           |  2 +-
 src/{ => server}/api/models/notification.ts   |  2 +-
 src/{ => server}/api/models/othello-game.ts   |  2 +-
 .../api/models/othello-matching.ts            |  2 +-
 src/{ => server}/api/models/poll-vote.ts      |  2 +-
 src/{ => server}/api/models/post-reaction.ts  |  2 +-
 src/{ => server}/api/models/post-watching.ts  |  2 +-
 src/{ => server}/api/models/post.ts           |  2 +-
 src/{ => server}/api/models/signin.ts         |  2 +-
 .../api/models/sw-subscription.ts             |  2 +-
 src/{ => server}/api/models/user.ts           |  4 +-
 src/{ => server}/api/private/signin.ts        |  2 +-
 src/{ => server}/api/private/signup.ts        |  4 +-
 src/{ => server}/api/reply.ts                 |  0
 src/{ => server}/api/server.ts                |  0
 src/{ => server}/api/service/github.ts        | 38 ++++++-------------
 src/{ => server}/api/service/twitter.ts       |  4 +-
 src/{ => server}/api/stream/channel.ts        |  0
 src/{ => server}/api/stream/drive.ts          |  0
 src/{ => server}/api/stream/home.ts           |  0
 .../api/stream/messaging-index.ts             |  0
 src/{ => server}/api/stream/messaging.ts      |  0
 src/{ => server}/api/stream/othello-game.ts   |  0
 src/{ => server}/api/stream/othello.ts        |  0
 src/{ => server}/api/stream/requests.ts       |  0
 src/{ => server}/api/stream/server.ts         |  0
 src/{ => server}/api/streaming.ts             |  2 +-
 .../common/get-notification-summary.ts        |  0
 src/{ => server}/common/get-post-summary.ts   |  0
 src/{ => server}/common/get-reaction-emoji.ts |  0
 src/{ => server}/common/othello/ai/back.ts    |  2 +-
 src/{ => server}/common/othello/ai/front.ts   |  2 +-
 src/{ => server}/common/othello/ai/index.ts   |  0
 src/{ => server}/common/othello/core.ts       |  0
 src/{ => server}/common/othello/maps.ts       |  0
 src/{ => server}/common/user/get-acct.ts      |  0
 src/{ => server}/common/user/get-summary.ts   |  0
 src/{ => server}/common/user/parse-acct.ts    |  0
 src/{ => server}/file/assets/avatar.jpg       |  0
 src/{ => server}/file/assets/bad-egg.png      |  0
 src/{ => server}/file/assets/dummy.png        |  0
 src/{ => server}/file/assets/not-an-image.png |  0
 .../file/assets/thumbnail-not-available.png   |  0
 src/{ => server}/file/server.ts               |  0
 src/{server.ts => server/index.ts}            | 38 +++++++------------
 src/{ => server}/log-request.ts               |  0
 src/{ => server}/web/app/animation.styl       |  0
 src/{ => server}/web/app/app.styl             |  0
 src/{ => server}/web/app/app.vue              |  0
 src/{ => server}/web/app/auth/assets/logo.svg |  0
 src/{ => server}/web/app/auth/script.ts       |  0
 src/{ => server}/web/app/auth/style.styl      |  0
 src/{ => server}/web/app/auth/views/form.vue  |  0
 src/{ => server}/web/app/auth/views/index.vue |  0
 src/{ => server}/web/app/base.pug             |  6 +--
 src/{ => server}/web/app/boot.js              |  0
 src/{ => server}/web/app/ch/script.ts         |  0
 src/{ => server}/web/app/ch/style.styl        |  0
 src/{ => server}/web/app/ch/tags/channel.tag  |  0
 src/{ => server}/web/app/ch/tags/header.tag   |  0
 src/{ => server}/web/app/ch/tags/index.tag    |  0
 src/{ => server}/web/app/ch/tags/index.ts     |  0
 .../web/app/common/define-widget.ts           |  0
 src/{ => server}/web/app/common/mios.ts       |  0
 .../app/common/scripts/check-for-update.ts    |  0
 .../common/scripts/compose-notification.ts    |  0
 .../web/app/common/scripts/contains.ts        |  0
 .../app/common/scripts/copy-to-clipboard.ts   |  0
 .../web/app/common/scripts/date-stringify.ts  |  0
 .../web/app/common/scripts/fuck-ad-block.ts   |  0
 .../web/app/common/scripts/gcd.ts             |  0
 .../web/app/common/scripts/get-kao.ts         |  0
 .../web/app/common/scripts/get-median.ts      |  0
 .../web/app/common/scripts/loading.ts         |  0
 .../app/common/scripts/parse-search-query.ts  |  0
 .../app/common/scripts/streaming/channel.ts   |  0
 .../web/app/common/scripts/streaming/drive.ts |  0
 .../web/app/common/scripts/streaming/home.ts  |  0
 .../scripts/streaming/messaging-index.ts      |  0
 .../app/common/scripts/streaming/messaging.ts |  0
 .../common/scripts/streaming/othello-game.ts  |  0
 .../app/common/scripts/streaming/othello.ts   |  0
 .../app/common/scripts/streaming/requests.ts  |  0
 .../app/common/scripts/streaming/server.ts    |  0
 .../scripts/streaming/stream-manager.ts       |  0
 .../app/common/scripts/streaming/stream.ts    |  0
 .../common/views/components/autocomplete.vue  |  0
 .../connect-failed.troubleshooter.vue         |  0
 .../views/components/connect-failed.vue       |  0
 .../app/common/views/components/ellipsis.vue  |  0
 .../views/components/file-type-icon.vue       |  0
 .../app/common/views/components/forkit.vue    |  0
 .../web/app/common/views/components/index.ts  |  0
 .../common/views/components/media-list.vue    |  0
 .../views/components/messaging-room.form.vue  |  0
 .../components/messaging-room.message.vue     |  0
 .../views/components/messaging-room.vue       |  0
 .../app/common/views/components/messaging.vue |  0
 .../web/app/common/views/components/nav.vue   |  0
 .../common/views/components/othello.game.vue  |  0
 .../views/components/othello.gameroom.vue     |  0
 .../common/views/components/othello.room.vue  |  0
 .../app/common/views/components/othello.vue   |  0
 .../common/views/components/poll-editor.vue   |  0
 .../web/app/common/views/components/poll.vue  |  0
 .../app/common/views/components/post-html.ts  |  0
 .../app/common/views/components/post-menu.vue |  0
 .../common/views/components/reaction-icon.vue |  0
 .../views/components/reaction-picker.vue      |  0
 .../views/components/reactions-viewer.vue     |  0
 .../app/common/views/components/signin.vue    |  0
 .../app/common/views/components/signup.vue    |  0
 .../views/components/special-message.vue      |  0
 .../views/components/stream-indicator.vue     |  0
 .../app/common/views/components/switch.vue    |  0
 .../web/app/common/views/components/time.vue  |  0
 .../web/app/common/views/components/timer.vue |  0
 .../views/components/twitter-setting.vue      |  0
 .../app/common/views/components/uploader.vue  |  0
 .../common/views/components/url-preview.vue   |  0
 .../web/app/common/views/components/url.vue   |  0
 .../views/components/welcome-timeline.vue     |  0
 .../common/views/directives/autocomplete.ts   |  0
 .../web/app/common/views/directives/index.ts  |  0
 .../web/app/common/views/filters/bytes.ts     |  0
 .../web/app/common/views/filters/index.ts     |  0
 .../web/app/common/views/filters/number.ts    |  0
 .../app/common/views/widgets/access-log.vue   |  0
 .../app/common/views/widgets/broadcast.vue    |  0
 .../web/app/common/views/widgets/calendar.vue |  0
 .../web/app/common/views/widgets/donation.vue |  0
 .../web/app/common/views/widgets/index.ts     |  0
 .../web/app/common/views/widgets/nav.vue      |  0
 .../app/common/views/widgets/photo-stream.vue |  0
 .../web/app/common/views/widgets/rss.vue      |  0
 .../views/widgets/server.cpu-memory.vue       |  0
 .../app/common/views/widgets/server.cpu.vue   |  0
 .../app/common/views/widgets/server.disk.vue  |  0
 .../app/common/views/widgets/server.info.vue  |  0
 .../common/views/widgets/server.memory.vue    |  0
 .../app/common/views/widgets/server.pie.vue   |  0
 .../common/views/widgets/server.uptimes.vue   |  0
 .../web/app/common/views/widgets/server.vue   |  0
 .../app/common/views/widgets/slideshow.vue    |  0
 .../web/app/common/views/widgets/tips.vue     |  0
 .../web/app/common/views/widgets/version.vue  |  0
 src/{ => server}/web/app/config.ts            |  0
 .../web/app/desktop/api/choose-drive-file.ts  |  0
 .../app/desktop/api/choose-drive-folder.ts    |  0
 .../web/app/desktop/api/contextmenu.ts        |  0
 .../web/app/desktop/api/dialog.ts             |  0
 src/{ => server}/web/app/desktop/api/input.ts |  0
 .../web/app/desktop/api/notify.ts             |  0
 src/{ => server}/web/app/desktop/api/post.ts  |  0
 .../web/app/desktop/api/update-avatar.ts      |  0
 .../web/app/desktop/api/update-banner.ts      |  0
 .../web/app/desktop/assets/grid.svg           |  0
 .../app/desktop/assets/header-logo-white.svg  |  0
 .../web/app/desktop/assets/header-logo.svg    |  0
 .../web/app/desktop/assets/index.jpg          |  0
 .../web/app/desktop/assets/remove.png         |  0
 src/{ => server}/web/app/desktop/script.ts    |  0
 src/{ => server}/web/app/desktop/style.styl   |  0
 src/{ => server}/web/app/desktop/ui.styl      |  0
 .../views/components/activity.calendar.vue    |  0
 .../views/components/activity.chart.vue       |  0
 .../app/desktop/views/components/activity.vue |  0
 .../desktop/views/components/analog-clock.vue |  0
 .../app/desktop/views/components/calendar.vue |  0
 .../choose-file-from-drive-window.vue         |  0
 .../choose-folder-from-drive-window.vue       |  0
 .../views/components/context-menu.menu.vue    |  0
 .../desktop/views/components/context-menu.vue |  0
 .../desktop/views/components/crop-window.vue  |  0
 .../app/desktop/views/components/dialog.vue   |  0
 .../desktop/views/components/drive-window.vue |  0
 .../desktop/views/components/drive.file.vue   |  0
 .../desktop/views/components/drive.folder.vue |  0
 .../views/components/drive.nav-folder.vue     |  0
 .../app/desktop/views/components/drive.vue    |  0
 .../views/components/ellipsis-icon.vue        |  0
 .../views/components/follow-button.vue        |  0
 .../views/components/followers-window.vue     |  0
 .../desktop/views/components/followers.vue    |  0
 .../views/components/following-window.vue     |  0
 .../desktop/views/components/following.vue    |  0
 .../views/components/friends-maker.vue        |  0
 .../desktop/views/components/game-window.vue  |  0
 .../web/app/desktop/views/components/home.vue |  0
 .../web/app/desktop/views/components/index.ts |  0
 .../desktop/views/components/input-dialog.vue |  0
 .../views/components/media-image-dialog.vue   |  0
 .../desktop/views/components/media-image.vue  |  0
 .../views/components/media-video-dialog.vue   |  0
 .../desktop/views/components/media-video.vue  |  0
 .../app/desktop/views/components/mentions.vue |  0
 .../components/messaging-room-window.vue      |  0
 .../views/components/messaging-window.vue     |  0
 .../views/components/notifications.vue        |  0
 .../views/components/post-detail.sub.vue      |  0
 .../desktop/views/components/post-detail.vue  |  0
 .../views/components/post-form-window.vue     |  0
 .../desktop/views/components/post-form.vue    |  0
 .../desktop/views/components/post-preview.vue |  0
 .../views/components/posts.post.sub.vue       |  0
 .../desktop/views/components/posts.post.vue   |  0
 .../app/desktop/views/components/posts.vue    |  0
 .../views/components/progress-dialog.vue      |  0
 .../views/components/repost-form-window.vue   |  0
 .../desktop/views/components/repost-form.vue  |  0
 .../views/components/settings-window.vue      |  0
 .../desktop/views/components/settings.2fa.vue |  0
 .../desktop/views/components/settings.api.vue |  0
 .../views/components/settings.apps.vue        |  0
 .../views/components/settings.drive.vue       |  0
 .../views/components/settings.mute.vue        |  0
 .../views/components/settings.password.vue    |  0
 .../views/components/settings.profile.vue     |  0
 .../views/components/settings.signins.vue     |  0
 .../app/desktop/views/components/settings.vue |  0
 .../views/components/sub-post-content.vue     |  0
 .../desktop/views/components/taskmanager.vue  |  0
 .../app/desktop/views/components/timeline.vue |  0
 .../views/components/ui-notification.vue      |  0
 .../views/components/ui.header.account.vue    |  0
 .../views/components/ui.header.clock.vue      |  0
 .../views/components/ui.header.nav.vue        |  0
 .../components/ui.header.notifications.vue    |  0
 .../views/components/ui.header.post.vue       |  0
 .../views/components/ui.header.search.vue     |  0
 .../desktop/views/components/ui.header.vue    |  0
 .../web/app/desktop/views/components/ui.vue   |  0
 .../desktop/views/components/user-preview.vue |  0
 .../views/components/users-list.item.vue      |  0
 .../desktop/views/components/users-list.vue   |  0
 .../views/components/widget-container.vue     |  0
 .../app/desktop/views/components/window.vue   |  0
 .../web/app/desktop/views/directives/index.ts |  0
 .../desktop/views/directives/user-preview.ts  |  0
 .../web/app/desktop/views/pages/drive.vue     |  0
 .../desktop/views/pages/home-customize.vue    |  0
 .../web/app/desktop/views/pages/home.vue      |  0
 .../web/app/desktop/views/pages/index.vue     |  0
 .../desktop/views/pages/messaging-room.vue    |  0
 .../web/app/desktop/views/pages/othello.vue   |  0
 .../web/app/desktop/views/pages/post.vue      |  0
 .../web/app/desktop/views/pages/search.vue    |  0
 .../app/desktop/views/pages/selectdrive.vue   |  0
 .../pages/user/user.followers-you-know.vue    |  0
 .../desktop/views/pages/user/user.friends.vue |  0
 .../desktop/views/pages/user/user.header.vue  |  0
 .../desktop/views/pages/user/user.home.vue    |  0
 .../desktop/views/pages/user/user.photos.vue  |  0
 .../desktop/views/pages/user/user.profile.vue |  0
 .../views/pages/user/user.timeline.vue        |  0
 .../web/app/desktop/views/pages/user/user.vue |  0
 .../web/app/desktop/views/pages/welcome.vue   |  0
 .../app/desktop/views/widgets/activity.vue    |  0
 .../views/widgets/channel.channel.form.vue    |  0
 .../views/widgets/channel.channel.post.vue    |  0
 .../desktop/views/widgets/channel.channel.vue |  0
 .../web/app/desktop/views/widgets/channel.vue |  0
 .../web/app/desktop/views/widgets/index.ts    |  0
 .../app/desktop/views/widgets/messaging.vue   |  0
 .../desktop/views/widgets/notifications.vue   |  0
 .../web/app/desktop/views/widgets/polls.vue   |  0
 .../app/desktop/views/widgets/post-form.vue   |  0
 .../web/app/desktop/views/widgets/profile.vue |  0
 .../app/desktop/views/widgets/timemachine.vue |  0
 .../web/app/desktop/views/widgets/trends.vue  |  0
 .../web/app/desktop/views/widgets/users.vue   |  0
 src/{ => server}/web/app/dev/script.ts        |  0
 src/{ => server}/web/app/dev/style.styl       |  0
 src/{ => server}/web/app/dev/views/app.vue    |  0
 src/{ => server}/web/app/dev/views/apps.vue   |  0
 src/{ => server}/web/app/dev/views/index.vue  |  0
 .../web/app/dev/views/new-app.vue             |  0
 src/{ => server}/web/app/dev/views/ui.vue     |  0
 src/{ => server}/web/app/init.css             |  0
 src/{ => server}/web/app/init.ts              |  0
 .../web/app/mobile/api/choose-drive-file.ts   |  0
 .../web/app/mobile/api/choose-drive-folder.ts |  0
 src/{ => server}/web/app/mobile/api/dialog.ts |  0
 src/{ => server}/web/app/mobile/api/input.ts  |  0
 src/{ => server}/web/app/mobile/api/notify.ts |  0
 src/{ => server}/web/app/mobile/api/post.ts   |  0
 src/{ => server}/web/app/mobile/script.ts     |  0
 src/{ => server}/web/app/mobile/style.styl    |  0
 .../app/mobile/views/components/activity.vue  |  0
 .../views/components/drive-file-chooser.vue   |  0
 .../views/components/drive-folder-chooser.vue |  0
 .../views/components/drive.file-detail.vue    |  0
 .../mobile/views/components/drive.file.vue    |  0
 .../mobile/views/components/drive.folder.vue  |  0
 .../web/app/mobile/views/components/drive.vue |  0
 .../mobile/views/components/follow-button.vue |  0
 .../mobile/views/components/friends-maker.vue |  0
 .../web/app/mobile/views/components/index.ts  |  0
 .../mobile/views/components/media-image.vue   |  0
 .../mobile/views/components/media-video.vue   |  0
 .../views/components/notification-preview.vue |  0
 .../mobile/views/components/notification.vue  |  0
 .../mobile/views/components/notifications.vue |  0
 .../app/mobile/views/components/notify.vue    |  0
 .../app/mobile/views/components/post-card.vue |  0
 .../views/components/post-detail.sub.vue      |  0
 .../mobile/views/components/post-detail.vue   |  0
 .../app/mobile/views/components/post-form.vue |  0
 .../mobile/views/components/post-preview.vue  |  0
 .../app/mobile/views/components/post.sub.vue  |  0
 .../web/app/mobile/views/components/post.vue  |  0
 .../web/app/mobile/views/components/posts.vue |  0
 .../views/components/sub-post-content.vue     |  0
 .../app/mobile/views/components/timeline.vue  |  0
 .../app/mobile/views/components/ui.header.vue |  0
 .../app/mobile/views/components/ui.nav.vue    |  0
 .../web/app/mobile/views/components/ui.vue    |  0
 .../app/mobile/views/components/user-card.vue |  0
 .../mobile/views/components/user-preview.vue  |  0
 .../mobile/views/components/user-timeline.vue |  0
 .../mobile/views/components/users-list.vue    |  0
 .../views/components/widget-container.vue     |  0
 .../web/app/mobile/views/directives/index.ts  |  0
 .../mobile/views/directives/user-preview.ts   |  0
 .../web/app/mobile/views/pages/drive.vue      |  0
 .../web/app/mobile/views/pages/followers.vue  |  0
 .../web/app/mobile/views/pages/following.vue  |  0
 .../web/app/mobile/views/pages/home.vue       |  0
 .../web/app/mobile/views/pages/index.vue      |  0
 .../app/mobile/views/pages/messaging-room.vue |  0
 .../web/app/mobile/views/pages/messaging.vue  |  0
 .../app/mobile/views/pages/notifications.vue  |  0
 .../web/app/mobile/views/pages/othello.vue    |  0
 .../web/app/mobile/views/pages/post.vue       |  0
 .../mobile/views/pages/profile-setting.vue    |  0
 .../web/app/mobile/views/pages/search.vue     |  0
 .../app/mobile/views/pages/selectdrive.vue    |  0
 .../web/app/mobile/views/pages/settings.vue   |  0
 .../web/app/mobile/views/pages/signup.vue     |  0
 .../web/app/mobile/views/pages/user.vue       |  0
 .../pages/user/home.followers-you-know.vue    |  0
 .../mobile/views/pages/user/home.friends.vue  |  0
 .../mobile/views/pages/user/home.photos.vue   |  0
 .../mobile/views/pages/user/home.posts.vue    |  0
 .../web/app/mobile/views/pages/user/home.vue  |  0
 .../web/app/mobile/views/pages/welcome.vue    |  0
 .../web/app/mobile/views/widgets/activity.vue |  0
 .../web/app/mobile/views/widgets/index.ts     |  0
 .../web/app/mobile/views/widgets/profile.vue  |  0
 src/{ => server}/web/app/reset.styl           |  0
 src/{ => server}/web/app/safe.js              |  0
 src/{ => server}/web/app/stats/style.styl     |  0
 src/{ => server}/web/app/stats/tags/index.tag |  0
 src/{ => server}/web/app/stats/tags/index.ts  |  0
 src/{ => server}/web/app/status/style.styl    |  0
 .../web/app/status/tags/index.tag             |  0
 src/{ => server}/web/app/status/tags/index.ts |  0
 src/{ => server}/web/app/sw.js                |  0
 src/{ => server}/web/app/tsconfig.json        |  0
 src/{ => server}/web/app/v.d.ts               |  0
 src/{ => server}/web/assets/404.js            |  0
 .../web/assets/code-highlight.css             |  0
 src/{ => server}/web/assets/error.jpg         |  0
 src/{ => server}/web/assets/favicon.ico       |  0
 src/{ => server}/web/assets/label.svg         |  0
 src/{ => server}/web/assets/manifest.json     |  0
 src/{ => server}/web/assets/message.mp3       |  0
 .../web/assets/othello-put-me.mp3             |  0
 .../web/assets/othello-put-you.mp3            |  0
 src/{ => server}/web/assets/post.mp3          |  0
 .../web/assets/reactions/angry.png            |  0
 .../web/assets/reactions/confused.png         |  0
 .../web/assets/reactions/congrats.png         |  0
 src/{ => server}/web/assets/reactions/hmm.png |  0
 .../web/assets/reactions/laugh.png            |  0
 .../web/assets/reactions/like.png             |  0
 .../web/assets/reactions/love.png             |  0
 .../web/assets/reactions/pudding.png          |  0
 .../web/assets/reactions/surprise.png         |  0
 src/{ => server}/web/assets/recover.html      |  0
 src/{ => server}/web/assets/title.svg         |  0
 src/{ => server}/web/assets/unread.svg        |  0
 src/{ => server}/web/assets/welcome-bg.svg    |  0
 src/{ => server}/web/assets/welcome-fg.svg    |  0
 src/{ => server}/web/const.styl               |  2 +-
 src/{ => server}/web/docs/about.en.pug        |  0
 src/{ => server}/web/docs/about.ja.pug        |  0
 src/{ => server}/web/docs/api.ja.pug          |  0
 .../web/docs/api/endpoints/posts/create.yaml  |  0
 .../docs/api/endpoints/posts/timeline.yaml    |  0
 .../web/docs/api/endpoints/style.styl         |  0
 .../web/docs/api/endpoints/view.pug           |  0
 .../web/docs/api/entities/drive-file.yaml     |  0
 .../web/docs/api/entities/post.yaml           |  0
 .../web/docs/api/entities/style.styl          |  0
 .../web/docs/api/entities/user.yaml           |  0
 .../web/docs/api/entities/view.pug            |  0
 src/{ => server}/web/docs/api/gulpfile.ts     | 24 ++++++------
 src/{ => server}/web/docs/api/mixins.pug      |  0
 src/{ => server}/web/docs/api/style.styl      |  0
 src/{ => server}/web/docs/gulpfile.ts         | 16 ++++----
 src/{ => server}/web/docs/index.en.pug        |  0
 src/{ => server}/web/docs/index.ja.pug        |  0
 src/{ => server}/web/docs/layout.pug          |  0
 src/{ => server}/web/docs/license.en.pug      |  0
 src/{ => server}/web/docs/license.ja.pug      |  0
 src/{ => server}/web/docs/mute.ja.pug         |  0
 src/{ => server}/web/docs/search.ja.pug       |  0
 src/{ => server}/web/docs/server.ts           |  0
 src/{ => server}/web/docs/style.styl          |  0
 src/{ => server}/web/docs/tou.ja.pug          |  0
 src/{ => server}/web/docs/ui.styl             |  0
 src/{ => server}/web/docs/vars.ts             | 16 ++++----
 src/{ => server}/web/element.scss             |  2 +-
 src/{ => server}/web/server.ts                |  0
 src/{ => server}/web/service/url-preview.ts   |  0
 src/{ => server}/web/style.styl               |  0
 src/tools/analysis/core.ts                    |  2 +-
 src/tools/analysis/extract-user-domains.ts    |  6 +--
 src/tools/analysis/extract-user-keywords.ts   |  6 +--
 .../analysis/predict-all-post-category.ts     |  2 +-
 src/tools/analysis/predict-user-interst.ts    |  4 +-
 test/api.js                                   |  2 +-
 test/text.js                                  |  4 +-
 tsconfig.json                                 |  2 +-
 webpack.config.ts                             | 30 +++++++--------
 582 files changed, 246 insertions(+), 188 deletions(-)
 rename src/{common => }/build/fa.ts (100%)
 rename src/{common => }/build/i18n.ts (96%)
 rename src/{common => }/build/license.ts (72%)
 create mode 100644 src/parse-opt.ts
 create mode 100644 src/processor/index.ts
 create mode 100644 src/processor/report-github-failure.ts
 create mode 100644 src/queue.ts
 rename src/{ => server}/api/api-handler.ts (100%)
 rename src/{ => server}/api/authenticate.ts (100%)
 rename src/{ => server}/api/bot/core.ts (100%)
 rename src/{ => server}/api/bot/interfaces/line.ts (98%)
 rename src/{ => server}/api/common/drive/add-file.ts (99%)
 rename src/{ => server}/api/common/drive/upload_from_url.ts (100%)
 rename src/{ => server}/api/common/generate-native-user-token.ts (100%)
 rename src/{ => server}/api/common/get-friends.ts (100%)
 rename src/{ => server}/api/common/get-host-lower.ts (100%)
 rename src/{ => server}/api/common/is-native-token.ts (100%)
 rename src/{ => server}/api/common/notify.ts (100%)
 rename src/{ => server}/api/common/push-sw.ts (97%)
 rename src/{ => server}/api/common/read-messaging-message.ts (100%)
 rename src/{ => server}/api/common/read-notification.ts (100%)
 rename src/{ => server}/api/common/signin.ts (92%)
 rename src/{ => server}/api/common/text/core/syntax-highlighter.ts (100%)
 rename src/{ => server}/api/common/text/elements/bold.ts (100%)
 rename src/{ => server}/api/common/text/elements/code.ts (100%)
 rename src/{ => server}/api/common/text/elements/emoji.ts (100%)
 rename src/{ => server}/api/common/text/elements/hashtag.ts (100%)
 rename src/{ => server}/api/common/text/elements/inline-code.ts (100%)
 rename src/{ => server}/api/common/text/elements/link.ts (100%)
 rename src/{ => server}/api/common/text/elements/mention.ts (100%)
 rename src/{ => server}/api/common/text/elements/quote.ts (100%)
 rename src/{ => server}/api/common/text/elements/url.ts (100%)
 rename src/{ => server}/api/common/text/index.ts (100%)
 rename src/{ => server}/api/common/watch-post.ts (100%)
 rename src/{ => server}/api/endpoints.ts (100%)
 rename src/{ => server}/api/endpoints/aggregation/posts.ts (100%)
 rename src/{ => server}/api/endpoints/aggregation/posts/reaction.ts (100%)
 rename src/{ => server}/api/endpoints/aggregation/posts/reactions.ts (100%)
 rename src/{ => server}/api/endpoints/aggregation/posts/reply.ts (100%)
 rename src/{ => server}/api/endpoints/aggregation/posts/repost.ts (100%)
 rename src/{ => server}/api/endpoints/aggregation/users.ts (100%)
 rename src/{ => server}/api/endpoints/aggregation/users/activity.ts (100%)
 rename src/{ => server}/api/endpoints/aggregation/users/followers.ts (100%)
 rename src/{ => server}/api/endpoints/aggregation/users/following.ts (100%)
 rename src/{ => server}/api/endpoints/aggregation/users/post.ts (100%)
 rename src/{ => server}/api/endpoints/aggregation/users/reaction.ts (100%)
 rename src/{ => server}/api/endpoints/app/create.ts (100%)
 rename src/{ => server}/api/endpoints/app/name_id/available.ts (100%)
 rename src/{ => server}/api/endpoints/app/show.ts (100%)
 rename src/{ => server}/api/endpoints/auth/accept.ts (100%)
 rename src/{ => server}/api/endpoints/auth/session/generate.ts (97%)
 rename src/{ => server}/api/endpoints/auth/session/show.ts (100%)
 rename src/{ => server}/api/endpoints/auth/session/userkey.ts (100%)
 rename src/{ => server}/api/endpoints/channels.ts (100%)
 rename src/{ => server}/api/endpoints/channels/create.ts (100%)
 rename src/{ => server}/api/endpoints/channels/posts.ts (100%)
 rename src/{ => server}/api/endpoints/channels/show.ts (100%)
 rename src/{ => server}/api/endpoints/channels/unwatch.ts (100%)
 rename src/{ => server}/api/endpoints/channels/watch.ts (100%)
 rename src/{ => server}/api/endpoints/drive.ts (100%)
 rename src/{ => server}/api/endpoints/drive/files.ts (100%)
 rename src/{ => server}/api/endpoints/drive/files/create.ts (100%)
 rename src/{ => server}/api/endpoints/drive/files/find.ts (100%)
 rename src/{ => server}/api/endpoints/drive/files/show.ts (100%)
 rename src/{ => server}/api/endpoints/drive/files/update.ts (100%)
 rename src/{ => server}/api/endpoints/drive/files/upload_from_url.ts (100%)
 rename src/{ => server}/api/endpoints/drive/folders.ts (100%)
 rename src/{ => server}/api/endpoints/drive/folders/create.ts (100%)
 rename src/{ => server}/api/endpoints/drive/folders/find.ts (100%)
 rename src/{ => server}/api/endpoints/drive/folders/show.ts (100%)
 rename src/{ => server}/api/endpoints/drive/folders/update.ts (100%)
 rename src/{ => server}/api/endpoints/drive/stream.ts (100%)
 rename src/{ => server}/api/endpoints/following/create.ts (100%)
 rename src/{ => server}/api/endpoints/following/delete.ts (100%)
 rename src/{ => server}/api/endpoints/i.ts (100%)
 rename src/{ => server}/api/endpoints/i/2fa/done.ts (100%)
 rename src/{ => server}/api/endpoints/i/2fa/register.ts (96%)
 rename src/{ => server}/api/endpoints/i/2fa/unregister.ts (100%)
 rename src/{ => server}/api/endpoints/i/appdata/get.ts (100%)
 rename src/{ => server}/api/endpoints/i/appdata/set.ts (100%)
 rename src/{ => server}/api/endpoints/i/authorized_apps.ts (100%)
 rename src/{ => server}/api/endpoints/i/change_password.ts (100%)
 rename src/{ => server}/api/endpoints/i/favorites.ts (100%)
 rename src/{ => server}/api/endpoints/i/notifications.ts (100%)
 rename src/{ => server}/api/endpoints/i/pin.ts (100%)
 rename src/{ => server}/api/endpoints/i/regenerate_token.ts (100%)
 rename src/{ => server}/api/endpoints/i/signin_history.ts (100%)
 rename src/{ => server}/api/endpoints/i/update.ts (98%)
 rename src/{ => server}/api/endpoints/i/update_client_setting.ts (100%)
 rename src/{ => server}/api/endpoints/i/update_home.ts (100%)
 rename src/{ => server}/api/endpoints/i/update_mobile_home.ts (100%)
 rename src/{ => server}/api/endpoints/messaging/history.ts (100%)
 rename src/{ => server}/api/endpoints/messaging/messages.ts (100%)
 rename src/{ => server}/api/endpoints/messaging/messages/create.ts (99%)
 rename src/{ => server}/api/endpoints/messaging/unread.ts (100%)
 rename src/{ => server}/api/endpoints/meta.ts (94%)
 rename src/{ => server}/api/endpoints/mute/create.ts (100%)
 rename src/{ => server}/api/endpoints/mute/delete.ts (100%)
 rename src/{ => server}/api/endpoints/mute/list.ts (100%)
 rename src/{ => server}/api/endpoints/my/apps.ts (100%)
 rename src/{ => server}/api/endpoints/notifications/get_unread_count.ts (100%)
 rename src/{ => server}/api/endpoints/notifications/mark_as_read_all.ts (100%)
 rename src/{ => server}/api/endpoints/othello/games.ts (100%)
 rename src/{ => server}/api/endpoints/othello/games/show.ts (100%)
 rename src/{ => server}/api/endpoints/othello/invitations.ts (100%)
 rename src/{ => server}/api/endpoints/othello/match.ts (100%)
 rename src/{ => server}/api/endpoints/othello/match/cancel.ts (100%)
 rename src/{ => server}/api/endpoints/posts.ts (100%)
 rename src/{ => server}/api/endpoints/posts/categorize.ts (100%)
 rename src/{ => server}/api/endpoints/posts/context.ts (100%)
 rename src/{ => server}/api/endpoints/posts/create.ts (99%)
 rename src/{ => server}/api/endpoints/posts/favorites/create.ts (100%)
 rename src/{ => server}/api/endpoints/posts/favorites/delete.ts (100%)
 rename src/{ => server}/api/endpoints/posts/mentions.ts (100%)
 rename src/{ => server}/api/endpoints/posts/polls/recommendation.ts (100%)
 rename src/{ => server}/api/endpoints/posts/polls/vote.ts (100%)
 rename src/{ => server}/api/endpoints/posts/reactions.ts (100%)
 rename src/{ => server}/api/endpoints/posts/reactions/create.ts (100%)
 rename src/{ => server}/api/endpoints/posts/reactions/delete.ts (100%)
 rename src/{ => server}/api/endpoints/posts/replies.ts (100%)
 rename src/{ => server}/api/endpoints/posts/reposts.ts (100%)
 rename src/{ => server}/api/endpoints/posts/search.ts (100%)
 rename src/{ => server}/api/endpoints/posts/show.ts (100%)
 rename src/{ => server}/api/endpoints/posts/timeline.ts (100%)
 rename src/{ => server}/api/endpoints/posts/trend.ts (100%)
 rename src/{ => server}/api/endpoints/stats.ts (100%)
 rename src/{ => server}/api/endpoints/sw/register.ts (100%)
 rename src/{ => server}/api/endpoints/username/available.ts (100%)
 rename src/{ => server}/api/endpoints/users.ts (100%)
 rename src/{ => server}/api/endpoints/users/followers.ts (100%)
 rename src/{ => server}/api/endpoints/users/following.ts (100%)
 rename src/{ => server}/api/endpoints/users/get_frequently_replied_users.ts (100%)
 rename src/{ => server}/api/endpoints/users/posts.ts (100%)
 rename src/{ => server}/api/endpoints/users/recommendation.ts (100%)
 rename src/{ => server}/api/endpoints/users/search.ts (98%)
 rename src/{ => server}/api/endpoints/users/search_by_username.ts (100%)
 rename src/{ => server}/api/endpoints/users/show.ts (100%)
 rename src/{ => server}/api/event.ts (98%)
 rename src/{ => server}/api/limitter.ts (97%)
 rename src/{ => server}/api/models/access-token.ts (86%)
 rename src/{ => server}/api/models/app.ts (96%)
 rename src/{ => server}/api/models/appdata.ts (63%)
 rename src/{ => server}/api/models/auth-session.ts (95%)
 rename src/{ => server}/api/models/channel-watching.ts (66%)
 rename src/{ => server}/api/models/channel.ts (97%)
 rename src/{ => server}/api/models/drive-file.ts (96%)
 rename src/{ => server}/api/models/drive-folder.ts (97%)
 rename src/{ => server}/api/models/drive-tag.ts (64%)
 rename src/{ => server}/api/models/favorite.ts (64%)
 rename src/{ => server}/api/models/following.ts (64%)
 rename src/{ => server}/api/models/messaging-history.ts (67%)
 rename src/{ => server}/api/models/messaging-message.ts (97%)
 rename src/{ => server}/api/models/meta.ts (74%)
 rename src/{ => server}/api/models/mute.ts (62%)
 rename src/{ => server}/api/models/notification.ts (98%)
 rename src/{ => server}/api/models/othello-game.ts (98%)
 rename src/{ => server}/api/models/othello-matching.ts (96%)
 rename src/{ => server}/api/models/poll-vote.ts (64%)
 rename src/{ => server}/api/models/post-reaction.ts (96%)
 rename src/{ => server}/api/models/post-watching.ts (65%)
 rename src/{ => server}/api/models/post.ts (99%)
 rename src/{ => server}/api/models/signin.ts (93%)
 rename src/{ => server}/api/models/sw-subscription.ts (66%)
 rename src/{ => server}/api/models/user.ts (99%)
 rename src/{ => server}/api/private/signin.ts (98%)
 rename src/{ => server}/api/private/signup.ts (96%)
 rename src/{ => server}/api/reply.ts (100%)
 rename src/{ => server}/api/server.ts (100%)
 rename src/{ => server}/api/service/github.ts (77%)
 rename src/{ => server}/api/service/twitter.ts (98%)
 rename src/{ => server}/api/stream/channel.ts (100%)
 rename src/{ => server}/api/stream/drive.ts (100%)
 rename src/{ => server}/api/stream/home.ts (100%)
 rename src/{ => server}/api/stream/messaging-index.ts (100%)
 rename src/{ => server}/api/stream/messaging.ts (100%)
 rename src/{ => server}/api/stream/othello-game.ts (100%)
 rename src/{ => server}/api/stream/othello.ts (100%)
 rename src/{ => server}/api/stream/requests.ts (100%)
 rename src/{ => server}/api/stream/server.ts (100%)
 rename src/{ => server}/api/streaming.ts (98%)
 rename src/{ => server}/common/get-notification-summary.ts (100%)
 rename src/{ => server}/common/get-post-summary.ts (100%)
 rename src/{ => server}/common/get-reaction-emoji.ts (100%)
 rename src/{ => server}/common/othello/ai/back.ts (99%)
 rename src/{ => server}/common/othello/ai/front.ts (99%)
 rename src/{ => server}/common/othello/ai/index.ts (100%)
 rename src/{ => server}/common/othello/core.ts (100%)
 rename src/{ => server}/common/othello/maps.ts (100%)
 rename src/{ => server}/common/user/get-acct.ts (100%)
 rename src/{ => server}/common/user/get-summary.ts (100%)
 rename src/{ => server}/common/user/parse-acct.ts (100%)
 rename src/{ => server}/file/assets/avatar.jpg (100%)
 rename src/{ => server}/file/assets/bad-egg.png (100%)
 rename src/{ => server}/file/assets/dummy.png (100%)
 rename src/{ => server}/file/assets/not-an-image.png (100%)
 rename src/{ => server}/file/assets/thumbnail-not-available.png (100%)
 rename src/{ => server}/file/server.ts (100%)
 rename src/{server.ts => server/index.ts} (77%)
 rename src/{ => server}/log-request.ts (100%)
 rename src/{ => server}/web/app/animation.styl (100%)
 rename src/{ => server}/web/app/app.styl (100%)
 rename src/{ => server}/web/app/app.vue (100%)
 rename src/{ => server}/web/app/auth/assets/logo.svg (100%)
 rename src/{ => server}/web/app/auth/script.ts (100%)
 rename src/{ => server}/web/app/auth/style.styl (100%)
 rename src/{ => server}/web/app/auth/views/form.vue (100%)
 rename src/{ => server}/web/app/auth/views/index.vue (100%)
 rename src/{ => server}/web/app/base.pug (76%)
 rename src/{ => server}/web/app/boot.js (100%)
 rename src/{ => server}/web/app/ch/script.ts (100%)
 rename src/{ => server}/web/app/ch/style.styl (100%)
 rename src/{ => server}/web/app/ch/tags/channel.tag (100%)
 rename src/{ => server}/web/app/ch/tags/header.tag (100%)
 rename src/{ => server}/web/app/ch/tags/index.tag (100%)
 rename src/{ => server}/web/app/ch/tags/index.ts (100%)
 rename src/{ => server}/web/app/common/define-widget.ts (100%)
 rename src/{ => server}/web/app/common/mios.ts (100%)
 rename src/{ => server}/web/app/common/scripts/check-for-update.ts (100%)
 rename src/{ => server}/web/app/common/scripts/compose-notification.ts (100%)
 rename src/{ => server}/web/app/common/scripts/contains.ts (100%)
 rename src/{ => server}/web/app/common/scripts/copy-to-clipboard.ts (100%)
 rename src/{ => server}/web/app/common/scripts/date-stringify.ts (100%)
 rename src/{ => server}/web/app/common/scripts/fuck-ad-block.ts (100%)
 rename src/{ => server}/web/app/common/scripts/gcd.ts (100%)
 rename src/{ => server}/web/app/common/scripts/get-kao.ts (100%)
 rename src/{ => server}/web/app/common/scripts/get-median.ts (100%)
 rename src/{ => server}/web/app/common/scripts/loading.ts (100%)
 rename src/{ => server}/web/app/common/scripts/parse-search-query.ts (100%)
 rename src/{ => server}/web/app/common/scripts/streaming/channel.ts (100%)
 rename src/{ => server}/web/app/common/scripts/streaming/drive.ts (100%)
 rename src/{ => server}/web/app/common/scripts/streaming/home.ts (100%)
 rename src/{ => server}/web/app/common/scripts/streaming/messaging-index.ts (100%)
 rename src/{ => server}/web/app/common/scripts/streaming/messaging.ts (100%)
 rename src/{ => server}/web/app/common/scripts/streaming/othello-game.ts (100%)
 rename src/{ => server}/web/app/common/scripts/streaming/othello.ts (100%)
 rename src/{ => server}/web/app/common/scripts/streaming/requests.ts (100%)
 rename src/{ => server}/web/app/common/scripts/streaming/server.ts (100%)
 rename src/{ => server}/web/app/common/scripts/streaming/stream-manager.ts (100%)
 rename src/{ => server}/web/app/common/scripts/streaming/stream.ts (100%)
 rename src/{ => server}/web/app/common/views/components/autocomplete.vue (100%)
 rename src/{ => server}/web/app/common/views/components/connect-failed.troubleshooter.vue (100%)
 rename src/{ => server}/web/app/common/views/components/connect-failed.vue (100%)
 rename src/{ => server}/web/app/common/views/components/ellipsis.vue (100%)
 rename src/{ => server}/web/app/common/views/components/file-type-icon.vue (100%)
 rename src/{ => server}/web/app/common/views/components/forkit.vue (100%)
 rename src/{ => server}/web/app/common/views/components/index.ts (100%)
 rename src/{ => server}/web/app/common/views/components/media-list.vue (100%)
 rename src/{ => server}/web/app/common/views/components/messaging-room.form.vue (100%)
 rename src/{ => server}/web/app/common/views/components/messaging-room.message.vue (100%)
 rename src/{ => server}/web/app/common/views/components/messaging-room.vue (100%)
 rename src/{ => server}/web/app/common/views/components/messaging.vue (100%)
 rename src/{ => server}/web/app/common/views/components/nav.vue (100%)
 rename src/{ => server}/web/app/common/views/components/othello.game.vue (100%)
 rename src/{ => server}/web/app/common/views/components/othello.gameroom.vue (100%)
 rename src/{ => server}/web/app/common/views/components/othello.room.vue (100%)
 rename src/{ => server}/web/app/common/views/components/othello.vue (100%)
 rename src/{ => server}/web/app/common/views/components/poll-editor.vue (100%)
 rename src/{ => server}/web/app/common/views/components/poll.vue (100%)
 rename src/{ => server}/web/app/common/views/components/post-html.ts (100%)
 rename src/{ => server}/web/app/common/views/components/post-menu.vue (100%)
 rename src/{ => server}/web/app/common/views/components/reaction-icon.vue (100%)
 rename src/{ => server}/web/app/common/views/components/reaction-picker.vue (100%)
 rename src/{ => server}/web/app/common/views/components/reactions-viewer.vue (100%)
 rename src/{ => server}/web/app/common/views/components/signin.vue (100%)
 rename src/{ => server}/web/app/common/views/components/signup.vue (100%)
 rename src/{ => server}/web/app/common/views/components/special-message.vue (100%)
 rename src/{ => server}/web/app/common/views/components/stream-indicator.vue (100%)
 rename src/{ => server}/web/app/common/views/components/switch.vue (100%)
 rename src/{ => server}/web/app/common/views/components/time.vue (100%)
 rename src/{ => server}/web/app/common/views/components/timer.vue (100%)
 rename src/{ => server}/web/app/common/views/components/twitter-setting.vue (100%)
 rename src/{ => server}/web/app/common/views/components/uploader.vue (100%)
 rename src/{ => server}/web/app/common/views/components/url-preview.vue (100%)
 rename src/{ => server}/web/app/common/views/components/url.vue (100%)
 rename src/{ => server}/web/app/common/views/components/welcome-timeline.vue (100%)
 rename src/{ => server}/web/app/common/views/directives/autocomplete.ts (100%)
 rename src/{ => server}/web/app/common/views/directives/index.ts (100%)
 rename src/{ => server}/web/app/common/views/filters/bytes.ts (100%)
 rename src/{ => server}/web/app/common/views/filters/index.ts (100%)
 rename src/{ => server}/web/app/common/views/filters/number.ts (100%)
 rename src/{ => server}/web/app/common/views/widgets/access-log.vue (100%)
 rename src/{ => server}/web/app/common/views/widgets/broadcast.vue (100%)
 rename src/{ => server}/web/app/common/views/widgets/calendar.vue (100%)
 rename src/{ => server}/web/app/common/views/widgets/donation.vue (100%)
 rename src/{ => server}/web/app/common/views/widgets/index.ts (100%)
 rename src/{ => server}/web/app/common/views/widgets/nav.vue (100%)
 rename src/{ => server}/web/app/common/views/widgets/photo-stream.vue (100%)
 rename src/{ => server}/web/app/common/views/widgets/rss.vue (100%)
 rename src/{ => server}/web/app/common/views/widgets/server.cpu-memory.vue (100%)
 rename src/{ => server}/web/app/common/views/widgets/server.cpu.vue (100%)
 rename src/{ => server}/web/app/common/views/widgets/server.disk.vue (100%)
 rename src/{ => server}/web/app/common/views/widgets/server.info.vue (100%)
 rename src/{ => server}/web/app/common/views/widgets/server.memory.vue (100%)
 rename src/{ => server}/web/app/common/views/widgets/server.pie.vue (100%)
 rename src/{ => server}/web/app/common/views/widgets/server.uptimes.vue (100%)
 rename src/{ => server}/web/app/common/views/widgets/server.vue (100%)
 rename src/{ => server}/web/app/common/views/widgets/slideshow.vue (100%)
 rename src/{ => server}/web/app/common/views/widgets/tips.vue (100%)
 rename src/{ => server}/web/app/common/views/widgets/version.vue (100%)
 rename src/{ => server}/web/app/config.ts (100%)
 rename src/{ => server}/web/app/desktop/api/choose-drive-file.ts (100%)
 rename src/{ => server}/web/app/desktop/api/choose-drive-folder.ts (100%)
 rename src/{ => server}/web/app/desktop/api/contextmenu.ts (100%)
 rename src/{ => server}/web/app/desktop/api/dialog.ts (100%)
 rename src/{ => server}/web/app/desktop/api/input.ts (100%)
 rename src/{ => server}/web/app/desktop/api/notify.ts (100%)
 rename src/{ => server}/web/app/desktop/api/post.ts (100%)
 rename src/{ => server}/web/app/desktop/api/update-avatar.ts (100%)
 rename src/{ => server}/web/app/desktop/api/update-banner.ts (100%)
 rename src/{ => server}/web/app/desktop/assets/grid.svg (100%)
 rename src/{ => server}/web/app/desktop/assets/header-logo-white.svg (100%)
 rename src/{ => server}/web/app/desktop/assets/header-logo.svg (100%)
 rename src/{ => server}/web/app/desktop/assets/index.jpg (100%)
 rename src/{ => server}/web/app/desktop/assets/remove.png (100%)
 rename src/{ => server}/web/app/desktop/script.ts (100%)
 rename src/{ => server}/web/app/desktop/style.styl (100%)
 rename src/{ => server}/web/app/desktop/ui.styl (100%)
 rename src/{ => server}/web/app/desktop/views/components/activity.calendar.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/activity.chart.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/activity.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/analog-clock.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/calendar.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/choose-file-from-drive-window.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/choose-folder-from-drive-window.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/context-menu.menu.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/context-menu.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/crop-window.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/dialog.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/drive-window.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/drive.file.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/drive.folder.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/drive.nav-folder.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/drive.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/ellipsis-icon.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/follow-button.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/followers-window.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/followers.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/following-window.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/following.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/friends-maker.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/game-window.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/home.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/index.ts (100%)
 rename src/{ => server}/web/app/desktop/views/components/input-dialog.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/media-image-dialog.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/media-image.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/media-video-dialog.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/media-video.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/mentions.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/messaging-room-window.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/messaging-window.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/notifications.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/post-detail.sub.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/post-detail.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/post-form-window.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/post-form.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/post-preview.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/posts.post.sub.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/posts.post.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/posts.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/progress-dialog.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/repost-form-window.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/repost-form.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/settings-window.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/settings.2fa.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/settings.api.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/settings.apps.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/settings.drive.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/settings.mute.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/settings.password.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/settings.profile.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/settings.signins.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/settings.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/sub-post-content.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/taskmanager.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/timeline.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/ui-notification.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/ui.header.account.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/ui.header.clock.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/ui.header.nav.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/ui.header.notifications.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/ui.header.post.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/ui.header.search.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/ui.header.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/ui.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/user-preview.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/users-list.item.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/users-list.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/widget-container.vue (100%)
 rename src/{ => server}/web/app/desktop/views/components/window.vue (100%)
 rename src/{ => server}/web/app/desktop/views/directives/index.ts (100%)
 rename src/{ => server}/web/app/desktop/views/directives/user-preview.ts (100%)
 rename src/{ => server}/web/app/desktop/views/pages/drive.vue (100%)
 rename src/{ => server}/web/app/desktop/views/pages/home-customize.vue (100%)
 rename src/{ => server}/web/app/desktop/views/pages/home.vue (100%)
 rename src/{ => server}/web/app/desktop/views/pages/index.vue (100%)
 rename src/{ => server}/web/app/desktop/views/pages/messaging-room.vue (100%)
 rename src/{ => server}/web/app/desktop/views/pages/othello.vue (100%)
 rename src/{ => server}/web/app/desktop/views/pages/post.vue (100%)
 rename src/{ => server}/web/app/desktop/views/pages/search.vue (100%)
 rename src/{ => server}/web/app/desktop/views/pages/selectdrive.vue (100%)
 rename src/{ => server}/web/app/desktop/views/pages/user/user.followers-you-know.vue (100%)
 rename src/{ => server}/web/app/desktop/views/pages/user/user.friends.vue (100%)
 rename src/{ => server}/web/app/desktop/views/pages/user/user.header.vue (100%)
 rename src/{ => server}/web/app/desktop/views/pages/user/user.home.vue (100%)
 rename src/{ => server}/web/app/desktop/views/pages/user/user.photos.vue (100%)
 rename src/{ => server}/web/app/desktop/views/pages/user/user.profile.vue (100%)
 rename src/{ => server}/web/app/desktop/views/pages/user/user.timeline.vue (100%)
 rename src/{ => server}/web/app/desktop/views/pages/user/user.vue (100%)
 rename src/{ => server}/web/app/desktop/views/pages/welcome.vue (100%)
 rename src/{ => server}/web/app/desktop/views/widgets/activity.vue (100%)
 rename src/{ => server}/web/app/desktop/views/widgets/channel.channel.form.vue (100%)
 rename src/{ => server}/web/app/desktop/views/widgets/channel.channel.post.vue (100%)
 rename src/{ => server}/web/app/desktop/views/widgets/channel.channel.vue (100%)
 rename src/{ => server}/web/app/desktop/views/widgets/channel.vue (100%)
 rename src/{ => server}/web/app/desktop/views/widgets/index.ts (100%)
 rename src/{ => server}/web/app/desktop/views/widgets/messaging.vue (100%)
 rename src/{ => server}/web/app/desktop/views/widgets/notifications.vue (100%)
 rename src/{ => server}/web/app/desktop/views/widgets/polls.vue (100%)
 rename src/{ => server}/web/app/desktop/views/widgets/post-form.vue (100%)
 rename src/{ => server}/web/app/desktop/views/widgets/profile.vue (100%)
 rename src/{ => server}/web/app/desktop/views/widgets/timemachine.vue (100%)
 rename src/{ => server}/web/app/desktop/views/widgets/trends.vue (100%)
 rename src/{ => server}/web/app/desktop/views/widgets/users.vue (100%)
 rename src/{ => server}/web/app/dev/script.ts (100%)
 rename src/{ => server}/web/app/dev/style.styl (100%)
 rename src/{ => server}/web/app/dev/views/app.vue (100%)
 rename src/{ => server}/web/app/dev/views/apps.vue (100%)
 rename src/{ => server}/web/app/dev/views/index.vue (100%)
 rename src/{ => server}/web/app/dev/views/new-app.vue (100%)
 rename src/{ => server}/web/app/dev/views/ui.vue (100%)
 rename src/{ => server}/web/app/init.css (100%)
 rename src/{ => server}/web/app/init.ts (100%)
 rename src/{ => server}/web/app/mobile/api/choose-drive-file.ts (100%)
 rename src/{ => server}/web/app/mobile/api/choose-drive-folder.ts (100%)
 rename src/{ => server}/web/app/mobile/api/dialog.ts (100%)
 rename src/{ => server}/web/app/mobile/api/input.ts (100%)
 rename src/{ => server}/web/app/mobile/api/notify.ts (100%)
 rename src/{ => server}/web/app/mobile/api/post.ts (100%)
 rename src/{ => server}/web/app/mobile/script.ts (100%)
 rename src/{ => server}/web/app/mobile/style.styl (100%)
 rename src/{ => server}/web/app/mobile/views/components/activity.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/drive-file-chooser.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/drive-folder-chooser.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/drive.file-detail.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/drive.file.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/drive.folder.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/drive.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/follow-button.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/friends-maker.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/index.ts (100%)
 rename src/{ => server}/web/app/mobile/views/components/media-image.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/media-video.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/notification-preview.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/notification.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/notifications.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/notify.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/post-card.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/post-detail.sub.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/post-detail.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/post-form.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/post-preview.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/post.sub.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/post.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/posts.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/sub-post-content.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/timeline.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/ui.header.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/ui.nav.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/ui.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/user-card.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/user-preview.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/user-timeline.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/users-list.vue (100%)
 rename src/{ => server}/web/app/mobile/views/components/widget-container.vue (100%)
 rename src/{ => server}/web/app/mobile/views/directives/index.ts (100%)
 rename src/{ => server}/web/app/mobile/views/directives/user-preview.ts (100%)
 rename src/{ => server}/web/app/mobile/views/pages/drive.vue (100%)
 rename src/{ => server}/web/app/mobile/views/pages/followers.vue (100%)
 rename src/{ => server}/web/app/mobile/views/pages/following.vue (100%)
 rename src/{ => server}/web/app/mobile/views/pages/home.vue (100%)
 rename src/{ => server}/web/app/mobile/views/pages/index.vue (100%)
 rename src/{ => server}/web/app/mobile/views/pages/messaging-room.vue (100%)
 rename src/{ => server}/web/app/mobile/views/pages/messaging.vue (100%)
 rename src/{ => server}/web/app/mobile/views/pages/notifications.vue (100%)
 rename src/{ => server}/web/app/mobile/views/pages/othello.vue (100%)
 rename src/{ => server}/web/app/mobile/views/pages/post.vue (100%)
 rename src/{ => server}/web/app/mobile/views/pages/profile-setting.vue (100%)
 rename src/{ => server}/web/app/mobile/views/pages/search.vue (100%)
 rename src/{ => server}/web/app/mobile/views/pages/selectdrive.vue (100%)
 rename src/{ => server}/web/app/mobile/views/pages/settings.vue (100%)
 rename src/{ => server}/web/app/mobile/views/pages/signup.vue (100%)
 rename src/{ => server}/web/app/mobile/views/pages/user.vue (100%)
 rename src/{ => server}/web/app/mobile/views/pages/user/home.followers-you-know.vue (100%)
 rename src/{ => server}/web/app/mobile/views/pages/user/home.friends.vue (100%)
 rename src/{ => server}/web/app/mobile/views/pages/user/home.photos.vue (100%)
 rename src/{ => server}/web/app/mobile/views/pages/user/home.posts.vue (100%)
 rename src/{ => server}/web/app/mobile/views/pages/user/home.vue (100%)
 rename src/{ => server}/web/app/mobile/views/pages/welcome.vue (100%)
 rename src/{ => server}/web/app/mobile/views/widgets/activity.vue (100%)
 rename src/{ => server}/web/app/mobile/views/widgets/index.ts (100%)
 rename src/{ => server}/web/app/mobile/views/widgets/profile.vue (100%)
 rename src/{ => server}/web/app/reset.styl (100%)
 rename src/{ => server}/web/app/safe.js (100%)
 rename src/{ => server}/web/app/stats/style.styl (100%)
 rename src/{ => server}/web/app/stats/tags/index.tag (100%)
 rename src/{ => server}/web/app/stats/tags/index.ts (100%)
 rename src/{ => server}/web/app/status/style.styl (100%)
 rename src/{ => server}/web/app/status/tags/index.tag (100%)
 rename src/{ => server}/web/app/status/tags/index.ts (100%)
 rename src/{ => server}/web/app/sw.js (100%)
 rename src/{ => server}/web/app/tsconfig.json (100%)
 rename src/{ => server}/web/app/v.d.ts (100%)
 rename src/{ => server}/web/assets/404.js (100%)
 rename src/{ => server}/web/assets/code-highlight.css (100%)
 rename src/{ => server}/web/assets/error.jpg (100%)
 rename src/{ => server}/web/assets/favicon.ico (100%)
 rename src/{ => server}/web/assets/label.svg (100%)
 rename src/{ => server}/web/assets/manifest.json (100%)
 rename src/{ => server}/web/assets/message.mp3 (100%)
 rename src/{ => server}/web/assets/othello-put-me.mp3 (100%)
 rename src/{ => server}/web/assets/othello-put-you.mp3 (100%)
 rename src/{ => server}/web/assets/post.mp3 (100%)
 rename src/{ => server}/web/assets/reactions/angry.png (100%)
 rename src/{ => server}/web/assets/reactions/confused.png (100%)
 rename src/{ => server}/web/assets/reactions/congrats.png (100%)
 rename src/{ => server}/web/assets/reactions/hmm.png (100%)
 rename src/{ => server}/web/assets/reactions/laugh.png (100%)
 rename src/{ => server}/web/assets/reactions/like.png (100%)
 rename src/{ => server}/web/assets/reactions/love.png (100%)
 rename src/{ => server}/web/assets/reactions/pudding.png (100%)
 rename src/{ => server}/web/assets/reactions/surprise.png (100%)
 rename src/{ => server}/web/assets/recover.html (100%)
 rename src/{ => server}/web/assets/title.svg (100%)
 rename src/{ => server}/web/assets/unread.svg (100%)
 rename src/{ => server}/web/assets/welcome-bg.svg (100%)
 rename src/{ => server}/web/assets/welcome-fg.svg (100%)
 rename src/{ => server}/web/const.styl (74%)
 rename src/{ => server}/web/docs/about.en.pug (100%)
 rename src/{ => server}/web/docs/about.ja.pug (100%)
 rename src/{ => server}/web/docs/api.ja.pug (100%)
 rename src/{ => server}/web/docs/api/endpoints/posts/create.yaml (100%)
 rename src/{ => server}/web/docs/api/endpoints/posts/timeline.yaml (100%)
 rename src/{ => server}/web/docs/api/endpoints/style.styl (100%)
 rename src/{ => server}/web/docs/api/endpoints/view.pug (100%)
 rename src/{ => server}/web/docs/api/entities/drive-file.yaml (100%)
 rename src/{ => server}/web/docs/api/entities/post.yaml (100%)
 rename src/{ => server}/web/docs/api/entities/style.styl (100%)
 rename src/{ => server}/web/docs/api/entities/user.yaml (100%)
 rename src/{ => server}/web/docs/api/entities/view.pug (100%)
 rename src/{ => server}/web/docs/api/gulpfile.ts (80%)
 rename src/{ => server}/web/docs/api/mixins.pug (100%)
 rename src/{ => server}/web/docs/api/style.styl (100%)
 rename src/{ => server}/web/docs/gulpfile.ts (74%)
 rename src/{ => server}/web/docs/index.en.pug (100%)
 rename src/{ => server}/web/docs/index.ja.pug (100%)
 rename src/{ => server}/web/docs/layout.pug (100%)
 rename src/{ => server}/web/docs/license.en.pug (100%)
 rename src/{ => server}/web/docs/license.ja.pug (100%)
 rename src/{ => server}/web/docs/mute.ja.pug (100%)
 rename src/{ => server}/web/docs/search.ja.pug (100%)
 rename src/{ => server}/web/docs/server.ts (100%)
 rename src/{ => server}/web/docs/style.styl (100%)
 rename src/{ => server}/web/docs/tou.ja.pug (100%)
 rename src/{ => server}/web/docs/ui.styl (100%)
 rename src/{ => server}/web/docs/vars.ts (76%)
 rename src/{ => server}/web/element.scss (91%)
 rename src/{ => server}/web/server.ts (100%)
 rename src/{ => server}/web/service/url-preview.ts (100%)
 rename src/{ => server}/web/style.styl (100%)

diff --git a/gulpfile.ts b/gulpfile.ts
index 9c61e3a1c..11f34c962 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -22,7 +22,7 @@ import * as replace from 'gulp-replace';
 import * as htmlmin from 'gulp-htmlmin';
 const uglifyes = require('uglify-es');
 
-import { fa } from './src/common/build/fa';
+import { fa } from './src/build/fa';
 import version from './src/version';
 import config from './src/conf';
 
@@ -39,7 +39,7 @@ if (isDebug) {
 
 const constants = require('./src/const.json');
 
-require('./src/web/docs/gulpfile.ts');
+require('./src/server/web/docs/gulpfile.ts');
 
 gulp.task('build', [
 	'build:js',
@@ -52,7 +52,7 @@ gulp.task('build', [
 gulp.task('rebuild', ['clean', 'build']);
 
 gulp.task('build:js', () =>
-	gulp.src(['./src/**/*.js', '!./src/web/**/*.js'])
+	gulp.src(['./src/**/*.js', '!./src/server/web/**/*.js'])
 		.pipe(gulp.dest('./built/'))
 );
 
@@ -71,7 +71,7 @@ gulp.task('build:copy', () =>
 	gulp.src([
 		'./build/Release/crypto_key.node',
 		'./src/**/assets/**/*',
-		'!./src/web/app/**/assets/**/*'
+		'!./src/server/web/app/**/assets/**/*'
 	]).pipe(gulp.dest('./built/'))
 );
 
@@ -121,7 +121,7 @@ gulp.task('build:client', [
 ]);
 
 gulp.task('build:client:script', () =>
-	gulp.src(['./src/web/app/boot.js', './src/web/app/safe.js'])
+	gulp.src(['./src/server/web/app/boot.js', './src/server/web/app/safe.js'])
 		.pipe(replace('VERSION', JSON.stringify(version)))
 		.pipe(replace('API', JSON.stringify(config.api_url)))
 		.pipe(replace('ENV', JSON.stringify(env)))
@@ -129,15 +129,15 @@ gulp.task('build:client:script', () =>
 		.pipe(isProduction ? uglify({
 			toplevel: true
 		} as any) : gutil.noop())
-		.pipe(gulp.dest('./built/web/assets/')) as any
+		.pipe(gulp.dest('./built/server/web/assets/')) as any
 );
 
 gulp.task('build:client:styles', () =>
-	gulp.src('./src/web/app/init.css')
+	gulp.src('./src/server/web/app/init.css')
 		.pipe(isProduction
 			? (cssnano as any)()
 			: gutil.noop())
-		.pipe(gulp.dest('./built/web/assets/'))
+		.pipe(gulp.dest('./built/server/web/assets/'))
 );
 
 gulp.task('copy:client', [
@@ -145,14 +145,14 @@ gulp.task('copy:client', [
 ], () =>
 		gulp.src([
 			'./assets/**/*',
-			'./src/web/assets/**/*',
-			'./src/web/app/*/assets/**/*'
+			'./src/server/web/assets/**/*',
+			'./src/server/web/app/*/assets/**/*'
 		])
 			.pipe(isProduction ? (imagemin as any)() : gutil.noop())
 			.pipe(rename(path => {
 				path.dirname = path.dirname.replace('assets', '.');
 			}))
-			.pipe(gulp.dest('./built/web/assets/'))
+			.pipe(gulp.dest('./built/server/web/assets/'))
 );
 
 gulp.task('build:client:pug', [
@@ -160,13 +160,13 @@ gulp.task('build:client:pug', [
 	'build:client:script',
 	'build:client:styles'
 ], () =>
-		gulp.src('./src/web/app/base.pug')
+		gulp.src('./src/server/web/app/base.pug')
 			.pipe(pug({
 				locals: {
 					themeColor: constants.themeColor,
 					facss: fa.dom.css(),
 					//hljscss: fs.readFileSync('./node_modules/highlight.js/styles/default.css', 'utf8')
-					hljscss: fs.readFileSync('./src/web/assets/code-highlight.css', 'utf8')
+					hljscss: fs.readFileSync('./src/server/web/assets/code-highlight.css', 'utf8')
 				}
 			}))
 			.pipe(htmlmin({
@@ -201,5 +201,5 @@ gulp.task('build:client:pug', [
 				// CSSも圧縮する
 				minifyCSS: true
 			}))
-			.pipe(gulp.dest('./built/web/app/'))
+			.pipe(gulp.dest('./built/server/web/app/'))
 );
diff --git a/package.json b/package.json
index d9ed80b47..290fb00d7 100644
--- a/package.json
+++ b/package.json
@@ -135,6 +135,7 @@
 		"is-url": "1.2.3",
 		"js-yaml": "3.11.0",
 		"jsdom": "^11.6.2",
+		"kue": "^0.11.6",
 		"license-checker": "18.0.0",
 		"loader-utils": "1.1.0",
 		"mecab-async": "0.1.2",
@@ -149,6 +150,7 @@
 		"nan": "^2.10.0",
 		"node-sass": "4.7.2",
 		"node-sass-json-importer": "3.1.5",
+		"nopt": "^4.0.1",
 		"nprogress": "0.2.0",
 		"object-assign-deep": "0.3.1",
 		"on-build-webpack": "0.1.0",
diff --git a/src/common/build/fa.ts b/src/build/fa.ts
similarity index 100%
rename from src/common/build/fa.ts
rename to src/build/fa.ts
diff --git a/src/common/build/i18n.ts b/src/build/i18n.ts
similarity index 96%
rename from src/common/build/i18n.ts
rename to src/build/i18n.ts
index 5e3c0381a..b9b740321 100644
--- a/src/common/build/i18n.ts
+++ b/src/build/i18n.ts
@@ -2,7 +2,7 @@
  * Replace i18n texts
  */
 
-import locale from '../../../locales';
+import locale from '../../locales';
 
 export default class Replacer {
 	private lang: string;
diff --git a/src/common/build/license.ts b/src/build/license.ts
similarity index 72%
rename from src/common/build/license.ts
rename to src/build/license.ts
index e5c264df8..d36af665c 100644
--- a/src/common/build/license.ts
+++ b/src/build/license.ts
@@ -1,6 +1,6 @@
 import * as fs from 'fs';
 
-const license = fs.readFileSync(__dirname + '/../../../LICENSE', 'utf-8');
+const license = fs.readFileSync(__dirname + '/../../LICENSE', 'utf-8');
 
 const licenseHtml = license
 	.replace(/\r\n/g, '\n')
diff --git a/src/index.ts b/src/index.ts
index 218455d6f..bd9b094d9 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -24,6 +24,8 @@ import stats from './utils/stats';
 import { Config, path as configPath } from './config';
 import loadConfig from './config';
 
+import parseOpt from './parse-opt';
+
 const clusterLog = debug('misskey:cluster');
 const ev = new Xev();
 
@@ -36,20 +38,22 @@ main();
  * Init process
  */
 function main() {
+	const opt = parseOpt(process.argv, 2);
+
 	if (cluster.isMaster) {
-		masterMain();
+		masterMain(opt);
 
 		ev.mount();
 		stats();
 	} else {
-		workerMain();
+		workerMain(opt);
 	}
 }
 
 /**
  * Init master process
  */
-async function masterMain() {
+async function masterMain(opt) {
 	let config: Config;
 
 	try {
@@ -69,19 +73,35 @@ async function masterMain() {
 	}
 
 	spawnWorkers(() => {
-		Logger.info(chalk.bold.green(
-			`Now listening on port ${chalk.underline(config.port.toString())}`));
+		if (!opt['only-processor']) {
+			Logger.info(chalk.bold.green(
+				`Now listening on port ${chalk.underline(config.port.toString())}`));
 
-		Logger.info(chalk.bold.green(config.url));
+			Logger.info(chalk.bold.green(config.url));
+		}
+
+		if (!opt['only-server']) {
+			Logger.info(chalk.bold.green('Now processing jobs'));
+		}
 	});
 }
 
 /**
  * Init worker process
  */
-function workerMain() {
-	// start server
-	require('./server');
+async function workerMain(opt) {
+	if (!opt['only-processor']) {
+		// start server
+		await require('./server').default();
+	}
+
+	if (!opt['only-server']) {
+		// start processor
+		require('./processor').default();
+	}
+
+	// Send a 'ready' message to parent process
+	process.send('ready');
 }
 
 /**
diff --git a/src/parse-opt.ts b/src/parse-opt.ts
new file mode 100644
index 000000000..61dc60c6c
--- /dev/null
+++ b/src/parse-opt.ts
@@ -0,0 +1,17 @@
+const nopt = require('nopt');
+
+export default (vector, index) => {
+	const parsed = nopt({
+		'only-processor': Boolean,
+		'only-server': Boolean
+	}, {
+		p: ['--only-processor'],
+		s: ['--only-server']
+	}, vector, index);
+
+	if (parsed['only-processor'] && parsed['only-server']) {
+		throw 'only-processor option and only-server option cannot be set at the same time';
+	}
+
+	return parsed;
+};
diff --git a/src/processor/index.ts b/src/processor/index.ts
new file mode 100644
index 000000000..f06cf24e8
--- /dev/null
+++ b/src/processor/index.ts
@@ -0,0 +1,4 @@
+import queue from '../queue';
+import reportGitHubFailure from './report-github-failure';
+
+export default () => queue.process('gitHubFailureReport', reportGitHubFailure);
diff --git a/src/processor/report-github-failure.ts b/src/processor/report-github-failure.ts
new file mode 100644
index 000000000..f42071142
--- /dev/null
+++ b/src/processor/report-github-failure.ts
@@ -0,0 +1,29 @@
+import * as request from 'request';
+import User from '../server/api/models/user';
+const createPost = require('../server/api/endpoints/posts/create');
+
+export default ({ data }, done) => {
+	const asyncBot = User.findOne({ _id: data.userId });
+
+	// Fetch parent status
+	request({
+		url: `${data.parentUrl}/statuses`,
+		headers: {
+			'User-Agent': 'misskey'
+		}
+	}, async (err, res, body) => {
+		if (err) {
+			console.error(err);
+			return;
+		}
+		const parentStatuses = JSON.parse(body);
+		const parentState = parentStatuses[0].state;
+		const stillFailed = parentState == 'failure' || parentState == 'error';
+		const text = stillFailed ?
+			`**⚠️BUILD STILL FAILED⚠️**: ?[${data.message}](${data.htmlUrl})` :
+			`**🚨BUILD FAILED🚨**: →→→?[${data.message}](${data.htmlUrl})←←←`;
+
+		createPost({ text }, await asyncBot);
+		done();
+	});
+};
diff --git a/src/queue.ts b/src/queue.ts
new file mode 100644
index 000000000..6089e0a7f
--- /dev/null
+++ b/src/queue.ts
@@ -0,0 +1,10 @@
+import { createQueue } from 'kue';
+import config from './conf';
+
+export default createQueue({
+	redis: {
+		port: config.redis.port,
+		host: config.redis.host,
+		auth: config.redis.pass
+	}
+});
diff --git a/src/api/api-handler.ts b/src/server/api/api-handler.ts
similarity index 100%
rename from src/api/api-handler.ts
rename to src/server/api/api-handler.ts
diff --git a/src/api/authenticate.ts b/src/server/api/authenticate.ts
similarity index 100%
rename from src/api/authenticate.ts
rename to src/server/api/authenticate.ts
diff --git a/src/api/bot/core.ts b/src/server/api/bot/core.ts
similarity index 100%
rename from src/api/bot/core.ts
rename to src/server/api/bot/core.ts
diff --git a/src/api/bot/interfaces/line.ts b/src/server/api/bot/interfaces/line.ts
similarity index 98%
rename from src/api/bot/interfaces/line.ts
rename to src/server/api/bot/interfaces/line.ts
index 8036b2fde..5b3e9107f 100644
--- a/src/api/bot/interfaces/line.ts
+++ b/src/server/api/bot/interfaces/line.ts
@@ -3,9 +3,9 @@ import * as express from 'express';
 import * as request from 'request';
 import * as crypto from 'crypto';
 import User from '../../models/user';
-import config from '../../../conf';
+import config from '../../../../conf';
 import BotCore from '../core';
-import _redis from '../../../db/redis';
+import _redis from '../../../../db/redis';
 import prominence = require('prominence');
 import getAcct from '../../../common/user/get-acct';
 import parseAcct from '../../../common/user/parse-acct';
diff --git a/src/api/common/drive/add-file.ts b/src/server/api/common/drive/add-file.ts
similarity index 99%
rename from src/api/common/drive/add-file.ts
rename to src/server/api/common/drive/add-file.ts
index c4f2f212a..5f3c69c15 100644
--- a/src/api/common/drive/add-file.ts
+++ b/src/server/api/common/drive/add-file.ts
@@ -15,7 +15,7 @@ import DriveFolder from '../../models/drive-folder';
 import { pack } from '../../models/drive-file';
 import event, { publishDriveStream } from '../../event';
 import getAcct from '../../../common/user/get-acct';
-import config from '../../../conf';
+import config from '../../../../conf';
 
 const gm = _gm.subClass({
 	imageMagick: true
diff --git a/src/api/common/drive/upload_from_url.ts b/src/server/api/common/drive/upload_from_url.ts
similarity index 100%
rename from src/api/common/drive/upload_from_url.ts
rename to src/server/api/common/drive/upload_from_url.ts
diff --git a/src/api/common/generate-native-user-token.ts b/src/server/api/common/generate-native-user-token.ts
similarity index 100%
rename from src/api/common/generate-native-user-token.ts
rename to src/server/api/common/generate-native-user-token.ts
diff --git a/src/api/common/get-friends.ts b/src/server/api/common/get-friends.ts
similarity index 100%
rename from src/api/common/get-friends.ts
rename to src/server/api/common/get-friends.ts
diff --git a/src/api/common/get-host-lower.ts b/src/server/api/common/get-host-lower.ts
similarity index 100%
rename from src/api/common/get-host-lower.ts
rename to src/server/api/common/get-host-lower.ts
diff --git a/src/api/common/is-native-token.ts b/src/server/api/common/is-native-token.ts
similarity index 100%
rename from src/api/common/is-native-token.ts
rename to src/server/api/common/is-native-token.ts
diff --git a/src/api/common/notify.ts b/src/server/api/common/notify.ts
similarity index 100%
rename from src/api/common/notify.ts
rename to src/server/api/common/notify.ts
diff --git a/src/api/common/push-sw.ts b/src/server/api/common/push-sw.ts
similarity index 97%
rename from src/api/common/push-sw.ts
rename to src/server/api/common/push-sw.ts
index 2993c760e..b33715eb1 100644
--- a/src/api/common/push-sw.ts
+++ b/src/server/api/common/push-sw.ts
@@ -1,7 +1,7 @@
 const push = require('web-push');
 import * as mongo from 'mongodb';
 import Subscription from '../models/sw-subscription';
-import config from '../../conf';
+import config from '../../../conf';
 
 if (config.sw) {
 	// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録
diff --git a/src/api/common/read-messaging-message.ts b/src/server/api/common/read-messaging-message.ts
similarity index 100%
rename from src/api/common/read-messaging-message.ts
rename to src/server/api/common/read-messaging-message.ts
diff --git a/src/api/common/read-notification.ts b/src/server/api/common/read-notification.ts
similarity index 100%
rename from src/api/common/read-notification.ts
rename to src/server/api/common/read-notification.ts
diff --git a/src/api/common/signin.ts b/src/server/api/common/signin.ts
similarity index 92%
rename from src/api/common/signin.ts
rename to src/server/api/common/signin.ts
index 04c2cdac8..a11ea56c0 100644
--- a/src/api/common/signin.ts
+++ b/src/server/api/common/signin.ts
@@ -1,4 +1,4 @@
-import config from '../../conf';
+import config from '../../../conf';
 
 export default function(res, user, redirect: boolean) {
 	const expires = 1000 * 60 * 60 * 24 * 365; // One Year
diff --git a/src/api/common/text/core/syntax-highlighter.ts b/src/server/api/common/text/core/syntax-highlighter.ts
similarity index 100%
rename from src/api/common/text/core/syntax-highlighter.ts
rename to src/server/api/common/text/core/syntax-highlighter.ts
diff --git a/src/api/common/text/elements/bold.ts b/src/server/api/common/text/elements/bold.ts
similarity index 100%
rename from src/api/common/text/elements/bold.ts
rename to src/server/api/common/text/elements/bold.ts
diff --git a/src/api/common/text/elements/code.ts b/src/server/api/common/text/elements/code.ts
similarity index 100%
rename from src/api/common/text/elements/code.ts
rename to src/server/api/common/text/elements/code.ts
diff --git a/src/api/common/text/elements/emoji.ts b/src/server/api/common/text/elements/emoji.ts
similarity index 100%
rename from src/api/common/text/elements/emoji.ts
rename to src/server/api/common/text/elements/emoji.ts
diff --git a/src/api/common/text/elements/hashtag.ts b/src/server/api/common/text/elements/hashtag.ts
similarity index 100%
rename from src/api/common/text/elements/hashtag.ts
rename to src/server/api/common/text/elements/hashtag.ts
diff --git a/src/api/common/text/elements/inline-code.ts b/src/server/api/common/text/elements/inline-code.ts
similarity index 100%
rename from src/api/common/text/elements/inline-code.ts
rename to src/server/api/common/text/elements/inline-code.ts
diff --git a/src/api/common/text/elements/link.ts b/src/server/api/common/text/elements/link.ts
similarity index 100%
rename from src/api/common/text/elements/link.ts
rename to src/server/api/common/text/elements/link.ts
diff --git a/src/api/common/text/elements/mention.ts b/src/server/api/common/text/elements/mention.ts
similarity index 100%
rename from src/api/common/text/elements/mention.ts
rename to src/server/api/common/text/elements/mention.ts
diff --git a/src/api/common/text/elements/quote.ts b/src/server/api/common/text/elements/quote.ts
similarity index 100%
rename from src/api/common/text/elements/quote.ts
rename to src/server/api/common/text/elements/quote.ts
diff --git a/src/api/common/text/elements/url.ts b/src/server/api/common/text/elements/url.ts
similarity index 100%
rename from src/api/common/text/elements/url.ts
rename to src/server/api/common/text/elements/url.ts
diff --git a/src/api/common/text/index.ts b/src/server/api/common/text/index.ts
similarity index 100%
rename from src/api/common/text/index.ts
rename to src/server/api/common/text/index.ts
diff --git a/src/api/common/watch-post.ts b/src/server/api/common/watch-post.ts
similarity index 100%
rename from src/api/common/watch-post.ts
rename to src/server/api/common/watch-post.ts
diff --git a/src/api/endpoints.ts b/src/server/api/endpoints.ts
similarity index 100%
rename from src/api/endpoints.ts
rename to src/server/api/endpoints.ts
diff --git a/src/api/endpoints/aggregation/posts.ts b/src/server/api/endpoints/aggregation/posts.ts
similarity index 100%
rename from src/api/endpoints/aggregation/posts.ts
rename to src/server/api/endpoints/aggregation/posts.ts
diff --git a/src/api/endpoints/aggregation/posts/reaction.ts b/src/server/api/endpoints/aggregation/posts/reaction.ts
similarity index 100%
rename from src/api/endpoints/aggregation/posts/reaction.ts
rename to src/server/api/endpoints/aggregation/posts/reaction.ts
diff --git a/src/api/endpoints/aggregation/posts/reactions.ts b/src/server/api/endpoints/aggregation/posts/reactions.ts
similarity index 100%
rename from src/api/endpoints/aggregation/posts/reactions.ts
rename to src/server/api/endpoints/aggregation/posts/reactions.ts
diff --git a/src/api/endpoints/aggregation/posts/reply.ts b/src/server/api/endpoints/aggregation/posts/reply.ts
similarity index 100%
rename from src/api/endpoints/aggregation/posts/reply.ts
rename to src/server/api/endpoints/aggregation/posts/reply.ts
diff --git a/src/api/endpoints/aggregation/posts/repost.ts b/src/server/api/endpoints/aggregation/posts/repost.ts
similarity index 100%
rename from src/api/endpoints/aggregation/posts/repost.ts
rename to src/server/api/endpoints/aggregation/posts/repost.ts
diff --git a/src/api/endpoints/aggregation/users.ts b/src/server/api/endpoints/aggregation/users.ts
similarity index 100%
rename from src/api/endpoints/aggregation/users.ts
rename to src/server/api/endpoints/aggregation/users.ts
diff --git a/src/api/endpoints/aggregation/users/activity.ts b/src/server/api/endpoints/aggregation/users/activity.ts
similarity index 100%
rename from src/api/endpoints/aggregation/users/activity.ts
rename to src/server/api/endpoints/aggregation/users/activity.ts
diff --git a/src/api/endpoints/aggregation/users/followers.ts b/src/server/api/endpoints/aggregation/users/followers.ts
similarity index 100%
rename from src/api/endpoints/aggregation/users/followers.ts
rename to src/server/api/endpoints/aggregation/users/followers.ts
diff --git a/src/api/endpoints/aggregation/users/following.ts b/src/server/api/endpoints/aggregation/users/following.ts
similarity index 100%
rename from src/api/endpoints/aggregation/users/following.ts
rename to src/server/api/endpoints/aggregation/users/following.ts
diff --git a/src/api/endpoints/aggregation/users/post.ts b/src/server/api/endpoints/aggregation/users/post.ts
similarity index 100%
rename from src/api/endpoints/aggregation/users/post.ts
rename to src/server/api/endpoints/aggregation/users/post.ts
diff --git a/src/api/endpoints/aggregation/users/reaction.ts b/src/server/api/endpoints/aggregation/users/reaction.ts
similarity index 100%
rename from src/api/endpoints/aggregation/users/reaction.ts
rename to src/server/api/endpoints/aggregation/users/reaction.ts
diff --git a/src/api/endpoints/app/create.ts b/src/server/api/endpoints/app/create.ts
similarity index 100%
rename from src/api/endpoints/app/create.ts
rename to src/server/api/endpoints/app/create.ts
diff --git a/src/api/endpoints/app/name_id/available.ts b/src/server/api/endpoints/app/name_id/available.ts
similarity index 100%
rename from src/api/endpoints/app/name_id/available.ts
rename to src/server/api/endpoints/app/name_id/available.ts
diff --git a/src/api/endpoints/app/show.ts b/src/server/api/endpoints/app/show.ts
similarity index 100%
rename from src/api/endpoints/app/show.ts
rename to src/server/api/endpoints/app/show.ts
diff --git a/src/api/endpoints/auth/accept.ts b/src/server/api/endpoints/auth/accept.ts
similarity index 100%
rename from src/api/endpoints/auth/accept.ts
rename to src/server/api/endpoints/auth/accept.ts
diff --git a/src/api/endpoints/auth/session/generate.ts b/src/server/api/endpoints/auth/session/generate.ts
similarity index 97%
rename from src/api/endpoints/auth/session/generate.ts
rename to src/server/api/endpoints/auth/session/generate.ts
index 510382247..dc6a045b6 100644
--- a/src/api/endpoints/auth/session/generate.ts
+++ b/src/server/api/endpoints/auth/session/generate.ts
@@ -5,7 +5,7 @@ import * as uuid from 'uuid';
 import $ from 'cafy';
 import App from '../../../models/app';
 import AuthSess from '../../../models/auth-session';
-import config from '../../../../conf';
+import config from '../../../../../conf';
 
 /**
  * @swagger
diff --git a/src/api/endpoints/auth/session/show.ts b/src/server/api/endpoints/auth/session/show.ts
similarity index 100%
rename from src/api/endpoints/auth/session/show.ts
rename to src/server/api/endpoints/auth/session/show.ts
diff --git a/src/api/endpoints/auth/session/userkey.ts b/src/server/api/endpoints/auth/session/userkey.ts
similarity index 100%
rename from src/api/endpoints/auth/session/userkey.ts
rename to src/server/api/endpoints/auth/session/userkey.ts
diff --git a/src/api/endpoints/channels.ts b/src/server/api/endpoints/channels.ts
similarity index 100%
rename from src/api/endpoints/channels.ts
rename to src/server/api/endpoints/channels.ts
diff --git a/src/api/endpoints/channels/create.ts b/src/server/api/endpoints/channels/create.ts
similarity index 100%
rename from src/api/endpoints/channels/create.ts
rename to src/server/api/endpoints/channels/create.ts
diff --git a/src/api/endpoints/channels/posts.ts b/src/server/api/endpoints/channels/posts.ts
similarity index 100%
rename from src/api/endpoints/channels/posts.ts
rename to src/server/api/endpoints/channels/posts.ts
diff --git a/src/api/endpoints/channels/show.ts b/src/server/api/endpoints/channels/show.ts
similarity index 100%
rename from src/api/endpoints/channels/show.ts
rename to src/server/api/endpoints/channels/show.ts
diff --git a/src/api/endpoints/channels/unwatch.ts b/src/server/api/endpoints/channels/unwatch.ts
similarity index 100%
rename from src/api/endpoints/channels/unwatch.ts
rename to src/server/api/endpoints/channels/unwatch.ts
diff --git a/src/api/endpoints/channels/watch.ts b/src/server/api/endpoints/channels/watch.ts
similarity index 100%
rename from src/api/endpoints/channels/watch.ts
rename to src/server/api/endpoints/channels/watch.ts
diff --git a/src/api/endpoints/drive.ts b/src/server/api/endpoints/drive.ts
similarity index 100%
rename from src/api/endpoints/drive.ts
rename to src/server/api/endpoints/drive.ts
diff --git a/src/api/endpoints/drive/files.ts b/src/server/api/endpoints/drive/files.ts
similarity index 100%
rename from src/api/endpoints/drive/files.ts
rename to src/server/api/endpoints/drive/files.ts
diff --git a/src/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts
similarity index 100%
rename from src/api/endpoints/drive/files/create.ts
rename to src/server/api/endpoints/drive/files/create.ts
diff --git a/src/api/endpoints/drive/files/find.ts b/src/server/api/endpoints/drive/files/find.ts
similarity index 100%
rename from src/api/endpoints/drive/files/find.ts
rename to src/server/api/endpoints/drive/files/find.ts
diff --git a/src/api/endpoints/drive/files/show.ts b/src/server/api/endpoints/drive/files/show.ts
similarity index 100%
rename from src/api/endpoints/drive/files/show.ts
rename to src/server/api/endpoints/drive/files/show.ts
diff --git a/src/api/endpoints/drive/files/update.ts b/src/server/api/endpoints/drive/files/update.ts
similarity index 100%
rename from src/api/endpoints/drive/files/update.ts
rename to src/server/api/endpoints/drive/files/update.ts
diff --git a/src/api/endpoints/drive/files/upload_from_url.ts b/src/server/api/endpoints/drive/files/upload_from_url.ts
similarity index 100%
rename from src/api/endpoints/drive/files/upload_from_url.ts
rename to src/server/api/endpoints/drive/files/upload_from_url.ts
diff --git a/src/api/endpoints/drive/folders.ts b/src/server/api/endpoints/drive/folders.ts
similarity index 100%
rename from src/api/endpoints/drive/folders.ts
rename to src/server/api/endpoints/drive/folders.ts
diff --git a/src/api/endpoints/drive/folders/create.ts b/src/server/api/endpoints/drive/folders/create.ts
similarity index 100%
rename from src/api/endpoints/drive/folders/create.ts
rename to src/server/api/endpoints/drive/folders/create.ts
diff --git a/src/api/endpoints/drive/folders/find.ts b/src/server/api/endpoints/drive/folders/find.ts
similarity index 100%
rename from src/api/endpoints/drive/folders/find.ts
rename to src/server/api/endpoints/drive/folders/find.ts
diff --git a/src/api/endpoints/drive/folders/show.ts b/src/server/api/endpoints/drive/folders/show.ts
similarity index 100%
rename from src/api/endpoints/drive/folders/show.ts
rename to src/server/api/endpoints/drive/folders/show.ts
diff --git a/src/api/endpoints/drive/folders/update.ts b/src/server/api/endpoints/drive/folders/update.ts
similarity index 100%
rename from src/api/endpoints/drive/folders/update.ts
rename to src/server/api/endpoints/drive/folders/update.ts
diff --git a/src/api/endpoints/drive/stream.ts b/src/server/api/endpoints/drive/stream.ts
similarity index 100%
rename from src/api/endpoints/drive/stream.ts
rename to src/server/api/endpoints/drive/stream.ts
diff --git a/src/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts
similarity index 100%
rename from src/api/endpoints/following/create.ts
rename to src/server/api/endpoints/following/create.ts
diff --git a/src/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts
similarity index 100%
rename from src/api/endpoints/following/delete.ts
rename to src/server/api/endpoints/following/delete.ts
diff --git a/src/api/endpoints/i.ts b/src/server/api/endpoints/i.ts
similarity index 100%
rename from src/api/endpoints/i.ts
rename to src/server/api/endpoints/i.ts
diff --git a/src/api/endpoints/i/2fa/done.ts b/src/server/api/endpoints/i/2fa/done.ts
similarity index 100%
rename from src/api/endpoints/i/2fa/done.ts
rename to src/server/api/endpoints/i/2fa/done.ts
diff --git a/src/api/endpoints/i/2fa/register.ts b/src/server/api/endpoints/i/2fa/register.ts
similarity index 96%
rename from src/api/endpoints/i/2fa/register.ts
rename to src/server/api/endpoints/i/2fa/register.ts
index 24abfcdfc..e2cc1487b 100644
--- a/src/api/endpoints/i/2fa/register.ts
+++ b/src/server/api/endpoints/i/2fa/register.ts
@@ -6,7 +6,7 @@ import * as bcrypt from 'bcryptjs';
 import * as speakeasy from 'speakeasy';
 import * as QRCode from 'qrcode';
 import User from '../../../models/user';
-import config from '../../../../conf';
+import config from '../../../../../conf';
 
 module.exports = async (params, user) => new Promise(async (res, rej) => {
 	// Get 'password' parameter
diff --git a/src/api/endpoints/i/2fa/unregister.ts b/src/server/api/endpoints/i/2fa/unregister.ts
similarity index 100%
rename from src/api/endpoints/i/2fa/unregister.ts
rename to src/server/api/endpoints/i/2fa/unregister.ts
diff --git a/src/api/endpoints/i/appdata/get.ts b/src/server/api/endpoints/i/appdata/get.ts
similarity index 100%
rename from src/api/endpoints/i/appdata/get.ts
rename to src/server/api/endpoints/i/appdata/get.ts
diff --git a/src/api/endpoints/i/appdata/set.ts b/src/server/api/endpoints/i/appdata/set.ts
similarity index 100%
rename from src/api/endpoints/i/appdata/set.ts
rename to src/server/api/endpoints/i/appdata/set.ts
diff --git a/src/api/endpoints/i/authorized_apps.ts b/src/server/api/endpoints/i/authorized_apps.ts
similarity index 100%
rename from src/api/endpoints/i/authorized_apps.ts
rename to src/server/api/endpoints/i/authorized_apps.ts
diff --git a/src/api/endpoints/i/change_password.ts b/src/server/api/endpoints/i/change_password.ts
similarity index 100%
rename from src/api/endpoints/i/change_password.ts
rename to src/server/api/endpoints/i/change_password.ts
diff --git a/src/api/endpoints/i/favorites.ts b/src/server/api/endpoints/i/favorites.ts
similarity index 100%
rename from src/api/endpoints/i/favorites.ts
rename to src/server/api/endpoints/i/favorites.ts
diff --git a/src/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts
similarity index 100%
rename from src/api/endpoints/i/notifications.ts
rename to src/server/api/endpoints/i/notifications.ts
diff --git a/src/api/endpoints/i/pin.ts b/src/server/api/endpoints/i/pin.ts
similarity index 100%
rename from src/api/endpoints/i/pin.ts
rename to src/server/api/endpoints/i/pin.ts
diff --git a/src/api/endpoints/i/regenerate_token.ts b/src/server/api/endpoints/i/regenerate_token.ts
similarity index 100%
rename from src/api/endpoints/i/regenerate_token.ts
rename to src/server/api/endpoints/i/regenerate_token.ts
diff --git a/src/api/endpoints/i/signin_history.ts b/src/server/api/endpoints/i/signin_history.ts
similarity index 100%
rename from src/api/endpoints/i/signin_history.ts
rename to src/server/api/endpoints/i/signin_history.ts
diff --git a/src/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
similarity index 98%
rename from src/api/endpoints/i/update.ts
rename to src/server/api/endpoints/i/update.ts
index db8a3f25b..3d52de2cc 100644
--- a/src/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../models/user';
 import event from '../../event';
-import config from '../../../conf';
+import config from '../../../../conf';
 
 /**
  * Update myself
diff --git a/src/api/endpoints/i/update_client_setting.ts b/src/server/api/endpoints/i/update_client_setting.ts
similarity index 100%
rename from src/api/endpoints/i/update_client_setting.ts
rename to src/server/api/endpoints/i/update_client_setting.ts
diff --git a/src/api/endpoints/i/update_home.ts b/src/server/api/endpoints/i/update_home.ts
similarity index 100%
rename from src/api/endpoints/i/update_home.ts
rename to src/server/api/endpoints/i/update_home.ts
diff --git a/src/api/endpoints/i/update_mobile_home.ts b/src/server/api/endpoints/i/update_mobile_home.ts
similarity index 100%
rename from src/api/endpoints/i/update_mobile_home.ts
rename to src/server/api/endpoints/i/update_mobile_home.ts
diff --git a/src/api/endpoints/messaging/history.ts b/src/server/api/endpoints/messaging/history.ts
similarity index 100%
rename from src/api/endpoints/messaging/history.ts
rename to src/server/api/endpoints/messaging/history.ts
diff --git a/src/api/endpoints/messaging/messages.ts b/src/server/api/endpoints/messaging/messages.ts
similarity index 100%
rename from src/api/endpoints/messaging/messages.ts
rename to src/server/api/endpoints/messaging/messages.ts
diff --git a/src/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts
similarity index 99%
rename from src/api/endpoints/messaging/messages/create.ts
rename to src/server/api/endpoints/messaging/messages/create.ts
index 1b8a5f59e..5184b2bd3 100644
--- a/src/api/endpoints/messaging/messages/create.ts
+++ b/src/server/api/endpoints/messaging/messages/create.ts
@@ -11,7 +11,7 @@ import DriveFile from '../../../models/drive-file';
 import { pack } from '../../../models/messaging-message';
 import publishUserStream from '../../../event';
 import { publishMessagingStream, publishMessagingIndexStream, pushSw } from '../../../event';
-import config from '../../../../conf';
+import config from '../../../../../conf';
 
 /**
  * Create a message
diff --git a/src/api/endpoints/messaging/unread.ts b/src/server/api/endpoints/messaging/unread.ts
similarity index 100%
rename from src/api/endpoints/messaging/unread.ts
rename to src/server/api/endpoints/messaging/unread.ts
diff --git a/src/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts
similarity index 94%
rename from src/api/endpoints/meta.ts
rename to src/server/api/endpoints/meta.ts
index 1370ead3c..10625ec66 100644
--- a/src/api/endpoints/meta.ts
+++ b/src/server/api/endpoints/meta.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import * as os from 'os';
-import version from '../../version';
-import config from '../../conf';
+import version from '../../../version';
+import config from '../../../conf';
 import Meta from '../models/meta';
 
 /**
diff --git a/src/api/endpoints/mute/create.ts b/src/server/api/endpoints/mute/create.ts
similarity index 100%
rename from src/api/endpoints/mute/create.ts
rename to src/server/api/endpoints/mute/create.ts
diff --git a/src/api/endpoints/mute/delete.ts b/src/server/api/endpoints/mute/delete.ts
similarity index 100%
rename from src/api/endpoints/mute/delete.ts
rename to src/server/api/endpoints/mute/delete.ts
diff --git a/src/api/endpoints/mute/list.ts b/src/server/api/endpoints/mute/list.ts
similarity index 100%
rename from src/api/endpoints/mute/list.ts
rename to src/server/api/endpoints/mute/list.ts
diff --git a/src/api/endpoints/my/apps.ts b/src/server/api/endpoints/my/apps.ts
similarity index 100%
rename from src/api/endpoints/my/apps.ts
rename to src/server/api/endpoints/my/apps.ts
diff --git a/src/api/endpoints/notifications/get_unread_count.ts b/src/server/api/endpoints/notifications/get_unread_count.ts
similarity index 100%
rename from src/api/endpoints/notifications/get_unread_count.ts
rename to src/server/api/endpoints/notifications/get_unread_count.ts
diff --git a/src/api/endpoints/notifications/mark_as_read_all.ts b/src/server/api/endpoints/notifications/mark_as_read_all.ts
similarity index 100%
rename from src/api/endpoints/notifications/mark_as_read_all.ts
rename to src/server/api/endpoints/notifications/mark_as_read_all.ts
diff --git a/src/api/endpoints/othello/games.ts b/src/server/api/endpoints/othello/games.ts
similarity index 100%
rename from src/api/endpoints/othello/games.ts
rename to src/server/api/endpoints/othello/games.ts
diff --git a/src/api/endpoints/othello/games/show.ts b/src/server/api/endpoints/othello/games/show.ts
similarity index 100%
rename from src/api/endpoints/othello/games/show.ts
rename to src/server/api/endpoints/othello/games/show.ts
diff --git a/src/api/endpoints/othello/invitations.ts b/src/server/api/endpoints/othello/invitations.ts
similarity index 100%
rename from src/api/endpoints/othello/invitations.ts
rename to src/server/api/endpoints/othello/invitations.ts
diff --git a/src/api/endpoints/othello/match.ts b/src/server/api/endpoints/othello/match.ts
similarity index 100%
rename from src/api/endpoints/othello/match.ts
rename to src/server/api/endpoints/othello/match.ts
diff --git a/src/api/endpoints/othello/match/cancel.ts b/src/server/api/endpoints/othello/match/cancel.ts
similarity index 100%
rename from src/api/endpoints/othello/match/cancel.ts
rename to src/server/api/endpoints/othello/match/cancel.ts
diff --git a/src/api/endpoints/posts.ts b/src/server/api/endpoints/posts.ts
similarity index 100%
rename from src/api/endpoints/posts.ts
rename to src/server/api/endpoints/posts.ts
diff --git a/src/api/endpoints/posts/categorize.ts b/src/server/api/endpoints/posts/categorize.ts
similarity index 100%
rename from src/api/endpoints/posts/categorize.ts
rename to src/server/api/endpoints/posts/categorize.ts
diff --git a/src/api/endpoints/posts/context.ts b/src/server/api/endpoints/posts/context.ts
similarity index 100%
rename from src/api/endpoints/posts/context.ts
rename to src/server/api/endpoints/posts/context.ts
diff --git a/src/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts
similarity index 99%
rename from src/api/endpoints/posts/create.ts
rename to src/server/api/endpoints/posts/create.ts
index 286e18bb7..bc9af843b 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/server/api/endpoints/posts/create.ts
@@ -18,7 +18,7 @@ import watch from '../../common/watch-post';
 import event, { pushSw, publishChannelStream } from '../../event';
 import getAcct from '../../../common/user/get-acct';
 import parseAcct from '../../../common/user/parse-acct';
-import config from '../../../conf';
+import config from '../../../../conf';
 
 /**
  * Create a post
diff --git a/src/api/endpoints/posts/favorites/create.ts b/src/server/api/endpoints/posts/favorites/create.ts
similarity index 100%
rename from src/api/endpoints/posts/favorites/create.ts
rename to src/server/api/endpoints/posts/favorites/create.ts
diff --git a/src/api/endpoints/posts/favorites/delete.ts b/src/server/api/endpoints/posts/favorites/delete.ts
similarity index 100%
rename from src/api/endpoints/posts/favorites/delete.ts
rename to src/server/api/endpoints/posts/favorites/delete.ts
diff --git a/src/api/endpoints/posts/mentions.ts b/src/server/api/endpoints/posts/mentions.ts
similarity index 100%
rename from src/api/endpoints/posts/mentions.ts
rename to src/server/api/endpoints/posts/mentions.ts
diff --git a/src/api/endpoints/posts/polls/recommendation.ts b/src/server/api/endpoints/posts/polls/recommendation.ts
similarity index 100%
rename from src/api/endpoints/posts/polls/recommendation.ts
rename to src/server/api/endpoints/posts/polls/recommendation.ts
diff --git a/src/api/endpoints/posts/polls/vote.ts b/src/server/api/endpoints/posts/polls/vote.ts
similarity index 100%
rename from src/api/endpoints/posts/polls/vote.ts
rename to src/server/api/endpoints/posts/polls/vote.ts
diff --git a/src/api/endpoints/posts/reactions.ts b/src/server/api/endpoints/posts/reactions.ts
similarity index 100%
rename from src/api/endpoints/posts/reactions.ts
rename to src/server/api/endpoints/posts/reactions.ts
diff --git a/src/api/endpoints/posts/reactions/create.ts b/src/server/api/endpoints/posts/reactions/create.ts
similarity index 100%
rename from src/api/endpoints/posts/reactions/create.ts
rename to src/server/api/endpoints/posts/reactions/create.ts
diff --git a/src/api/endpoints/posts/reactions/delete.ts b/src/server/api/endpoints/posts/reactions/delete.ts
similarity index 100%
rename from src/api/endpoints/posts/reactions/delete.ts
rename to src/server/api/endpoints/posts/reactions/delete.ts
diff --git a/src/api/endpoints/posts/replies.ts b/src/server/api/endpoints/posts/replies.ts
similarity index 100%
rename from src/api/endpoints/posts/replies.ts
rename to src/server/api/endpoints/posts/replies.ts
diff --git a/src/api/endpoints/posts/reposts.ts b/src/server/api/endpoints/posts/reposts.ts
similarity index 100%
rename from src/api/endpoints/posts/reposts.ts
rename to src/server/api/endpoints/posts/reposts.ts
diff --git a/src/api/endpoints/posts/search.ts b/src/server/api/endpoints/posts/search.ts
similarity index 100%
rename from src/api/endpoints/posts/search.ts
rename to src/server/api/endpoints/posts/search.ts
diff --git a/src/api/endpoints/posts/show.ts b/src/server/api/endpoints/posts/show.ts
similarity index 100%
rename from src/api/endpoints/posts/show.ts
rename to src/server/api/endpoints/posts/show.ts
diff --git a/src/api/endpoints/posts/timeline.ts b/src/server/api/endpoints/posts/timeline.ts
similarity index 100%
rename from src/api/endpoints/posts/timeline.ts
rename to src/server/api/endpoints/posts/timeline.ts
diff --git a/src/api/endpoints/posts/trend.ts b/src/server/api/endpoints/posts/trend.ts
similarity index 100%
rename from src/api/endpoints/posts/trend.ts
rename to src/server/api/endpoints/posts/trend.ts
diff --git a/src/api/endpoints/stats.ts b/src/server/api/endpoints/stats.ts
similarity index 100%
rename from src/api/endpoints/stats.ts
rename to src/server/api/endpoints/stats.ts
diff --git a/src/api/endpoints/sw/register.ts b/src/server/api/endpoints/sw/register.ts
similarity index 100%
rename from src/api/endpoints/sw/register.ts
rename to src/server/api/endpoints/sw/register.ts
diff --git a/src/api/endpoints/username/available.ts b/src/server/api/endpoints/username/available.ts
similarity index 100%
rename from src/api/endpoints/username/available.ts
rename to src/server/api/endpoints/username/available.ts
diff --git a/src/api/endpoints/users.ts b/src/server/api/endpoints/users.ts
similarity index 100%
rename from src/api/endpoints/users.ts
rename to src/server/api/endpoints/users.ts
diff --git a/src/api/endpoints/users/followers.ts b/src/server/api/endpoints/users/followers.ts
similarity index 100%
rename from src/api/endpoints/users/followers.ts
rename to src/server/api/endpoints/users/followers.ts
diff --git a/src/api/endpoints/users/following.ts b/src/server/api/endpoints/users/following.ts
similarity index 100%
rename from src/api/endpoints/users/following.ts
rename to src/server/api/endpoints/users/following.ts
diff --git a/src/api/endpoints/users/get_frequently_replied_users.ts b/src/server/api/endpoints/users/get_frequently_replied_users.ts
similarity index 100%
rename from src/api/endpoints/users/get_frequently_replied_users.ts
rename to src/server/api/endpoints/users/get_frequently_replied_users.ts
diff --git a/src/api/endpoints/users/posts.ts b/src/server/api/endpoints/users/posts.ts
similarity index 100%
rename from src/api/endpoints/users/posts.ts
rename to src/server/api/endpoints/users/posts.ts
diff --git a/src/api/endpoints/users/recommendation.ts b/src/server/api/endpoints/users/recommendation.ts
similarity index 100%
rename from src/api/endpoints/users/recommendation.ts
rename to src/server/api/endpoints/users/recommendation.ts
diff --git a/src/api/endpoints/users/search.ts b/src/server/api/endpoints/users/search.ts
similarity index 98%
rename from src/api/endpoints/users/search.ts
rename to src/server/api/endpoints/users/search.ts
index 39e2ff989..3c8157644 100644
--- a/src/api/endpoints/users/search.ts
+++ b/src/server/api/endpoints/users/search.ts
@@ -4,7 +4,7 @@
 import * as mongo from 'mongodb';
 import $ from 'cafy';
 import User, { pack } from '../../models/user';
-import config from '../../../conf';
+import config from '../../../../conf';
 const escapeRegexp = require('escape-regexp');
 
 /**
diff --git a/src/api/endpoints/users/search_by_username.ts b/src/server/api/endpoints/users/search_by_username.ts
similarity index 100%
rename from src/api/endpoints/users/search_by_username.ts
rename to src/server/api/endpoints/users/search_by_username.ts
diff --git a/src/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts
similarity index 100%
rename from src/api/endpoints/users/show.ts
rename to src/server/api/endpoints/users/show.ts
diff --git a/src/api/event.ts b/src/server/api/event.ts
similarity index 98%
rename from src/api/event.ts
rename to src/server/api/event.ts
index 4c9cc18e4..98bf16113 100644
--- a/src/api/event.ts
+++ b/src/server/api/event.ts
@@ -1,7 +1,7 @@
 import * as mongo from 'mongodb';
 import * as redis from 'redis';
 import swPush from './common/push-sw';
-import config from '../conf';
+import config from '../../conf';
 
 type ID = string | mongo.ObjectID;
 
diff --git a/src/api/limitter.ts b/src/server/api/limitter.ts
similarity index 97%
rename from src/api/limitter.ts
rename to src/server/api/limitter.ts
index 9d2c42d33..33337fbb1 100644
--- a/src/api/limitter.ts
+++ b/src/server/api/limitter.ts
@@ -1,6 +1,6 @@
 import * as Limiter from 'ratelimiter';
 import * as debug from 'debug';
-import limiterDB from '../db/redis';
+import limiterDB from '../../db/redis';
 import { Endpoint } from './endpoints';
 import { IAuthContext } from './authenticate';
 import getAcct from '../common/user/get-acct';
diff --git a/src/api/models/access-token.ts b/src/server/api/models/access-token.ts
similarity index 86%
rename from src/api/models/access-token.ts
rename to src/server/api/models/access-token.ts
index 9985be501..2bf91f309 100644
--- a/src/api/models/access-token.ts
+++ b/src/server/api/models/access-token.ts
@@ -1,4 +1,4 @@
-import db from '../../db/mongodb';
+import db from '../../../db/mongodb';
 
 const collection = db.get('access_tokens');
 
diff --git a/src/api/models/app.ts b/src/server/api/models/app.ts
similarity index 96%
rename from src/api/models/app.ts
rename to src/server/api/models/app.ts
index 34e9867db..17db82eca 100644
--- a/src/api/models/app.ts
+++ b/src/server/api/models/app.ts
@@ -1,8 +1,8 @@
 import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
 import AccessToken from './access-token';
-import db from '../../db/mongodb';
-import config from '../../conf';
+import db from '../../../db/mongodb';
+import config from '../../../conf';
 
 const App = db.get<IApp>('apps');
 App.createIndex('name_id');
diff --git a/src/api/models/appdata.ts b/src/server/api/models/appdata.ts
similarity index 63%
rename from src/api/models/appdata.ts
rename to src/server/api/models/appdata.ts
index 3e68354fa..dda3c9893 100644
--- a/src/api/models/appdata.ts
+++ b/src/server/api/models/appdata.ts
@@ -1,3 +1,3 @@
-import db from '../../db/mongodb';
+import db from '../../../db/mongodb';
 
 export default db.get('appdata') as any; // fuck type definition
diff --git a/src/api/models/auth-session.ts b/src/server/api/models/auth-session.ts
similarity index 95%
rename from src/api/models/auth-session.ts
rename to src/server/api/models/auth-session.ts
index 997ec61c2..a79d901df 100644
--- a/src/api/models/auth-session.ts
+++ b/src/server/api/models/auth-session.ts
@@ -1,6 +1,6 @@
 import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
-import db from '../../db/mongodb';
+import db from '../../../db/mongodb';
 import { pack as packApp } from './app';
 
 const AuthSession = db.get('auth_sessions');
diff --git a/src/api/models/channel-watching.ts b/src/server/api/models/channel-watching.ts
similarity index 66%
rename from src/api/models/channel-watching.ts
rename to src/server/api/models/channel-watching.ts
index 6184ae408..4c6fae28d 100644
--- a/src/api/models/channel-watching.ts
+++ b/src/server/api/models/channel-watching.ts
@@ -1,3 +1,3 @@
-import db from '../../db/mongodb';
+import db from '../../../db/mongodb';
 
 export default db.get('channel_watching') as any; // fuck type definition
diff --git a/src/api/models/channel.ts b/src/server/api/models/channel.ts
similarity index 97%
rename from src/api/models/channel.ts
rename to src/server/api/models/channel.ts
index 815d53593..97999bd9e 100644
--- a/src/api/models/channel.ts
+++ b/src/server/api/models/channel.ts
@@ -2,7 +2,7 @@ import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
 import { IUser } from './user';
 import Watching from './channel-watching';
-import db from '../../db/mongodb';
+import db from '../../../db/mongodb';
 
 const Channel = db.get<IChannel>('channels');
 export default Channel;
diff --git a/src/api/models/drive-file.ts b/src/server/api/models/drive-file.ts
similarity index 96%
rename from src/api/models/drive-file.ts
rename to src/server/api/models/drive-file.ts
index 2a46d8dc4..851a79a0e 100644
--- a/src/api/models/drive-file.ts
+++ b/src/server/api/models/drive-file.ts
@@ -1,8 +1,8 @@
 import * as mongodb from 'mongodb';
 import deepcopy = require('deepcopy');
 import { pack as packFolder } from './drive-folder';
-import config from '../../conf';
-import monkDb, { nativeDbConn } from '../../db/mongodb';
+import config from '../../../conf';
+import monkDb, { nativeDbConn } from '../../../db/mongodb';
 
 const DriveFile = monkDb.get<IDriveFile>('drive_files.files');
 
diff --git a/src/api/models/drive-folder.ts b/src/server/api/models/drive-folder.ts
similarity index 97%
rename from src/api/models/drive-folder.ts
rename to src/server/api/models/drive-folder.ts
index 54b45049b..505556376 100644
--- a/src/api/models/drive-folder.ts
+++ b/src/server/api/models/drive-folder.ts
@@ -1,6 +1,6 @@
 import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
-import db from '../../db/mongodb';
+import db from '../../../db/mongodb';
 import DriveFile from './drive-file';
 
 const DriveFolder = db.get<IDriveFolder>('drive_folders');
diff --git a/src/api/models/drive-tag.ts b/src/server/api/models/drive-tag.ts
similarity index 64%
rename from src/api/models/drive-tag.ts
rename to src/server/api/models/drive-tag.ts
index 991c935e8..d1c68365a 100644
--- a/src/api/models/drive-tag.ts
+++ b/src/server/api/models/drive-tag.ts
@@ -1,3 +1,3 @@
-import db from '../../db/mongodb';
+import db from '../../../db/mongodb';
 
 export default db.get('drive_tags') as any; // fuck type definition
diff --git a/src/api/models/favorite.ts b/src/server/api/models/favorite.ts
similarity index 64%
rename from src/api/models/favorite.ts
rename to src/server/api/models/favorite.ts
index e01d9e343..314261764 100644
--- a/src/api/models/favorite.ts
+++ b/src/server/api/models/favorite.ts
@@ -1,3 +1,3 @@
-import db from '../../db/mongodb';
+import db from '../../../db/mongodb';
 
 export default db.get('favorites') as any; // fuck type definition
diff --git a/src/api/models/following.ts b/src/server/api/models/following.ts
similarity index 64%
rename from src/api/models/following.ts
rename to src/server/api/models/following.ts
index cb3db9b53..92d7b6d31 100644
--- a/src/api/models/following.ts
+++ b/src/server/api/models/following.ts
@@ -1,3 +1,3 @@
-import db from '../../db/mongodb';
+import db from '../../../db/mongodb';
 
 export default db.get('following') as any; // fuck type definition
diff --git a/src/api/models/messaging-history.ts b/src/server/api/models/messaging-history.ts
similarity index 67%
rename from src/api/models/messaging-history.ts
rename to src/server/api/models/messaging-history.ts
index c06987e45..ea9f317ee 100644
--- a/src/api/models/messaging-history.ts
+++ b/src/server/api/models/messaging-history.ts
@@ -1,3 +1,3 @@
-import db from '../../db/mongodb';
+import db from '../../../db/mongodb';
 
 export default db.get('messaging_histories') as any; // fuck type definition
diff --git a/src/api/models/messaging-message.ts b/src/server/api/models/messaging-message.ts
similarity index 97%
rename from src/api/models/messaging-message.ts
rename to src/server/api/models/messaging-message.ts
index fcb356c5c..be484d635 100644
--- a/src/api/models/messaging-message.ts
+++ b/src/server/api/models/messaging-message.ts
@@ -2,7 +2,7 @@ import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
 import { pack as packUser } from './user';
 import { pack as packFile } from './drive-file';
-import db from '../../db/mongodb';
+import db from '../../../db/mongodb';
 import parse from '../common/text';
 
 const MessagingMessage = db.get<IMessagingMessage>('messaging_messages');
diff --git a/src/api/models/meta.ts b/src/server/api/models/meta.ts
similarity index 74%
rename from src/api/models/meta.ts
rename to src/server/api/models/meta.ts
index c7dba8fcb..ee1ada18f 100644
--- a/src/api/models/meta.ts
+++ b/src/server/api/models/meta.ts
@@ -1,4 +1,4 @@
-import db from '../../db/mongodb';
+import db from '../../../db/mongodb';
 
 export default db.get('meta') as any; // fuck type definition
 
diff --git a/src/api/models/mute.ts b/src/server/api/models/mute.ts
similarity index 62%
rename from src/api/models/mute.ts
rename to src/server/api/models/mute.ts
index 16018b82f..02f652c30 100644
--- a/src/api/models/mute.ts
+++ b/src/server/api/models/mute.ts
@@ -1,3 +1,3 @@
-import db from '../../db/mongodb';
+import db from '../../../db/mongodb';
 
 export default db.get('mute') as any; // fuck type definition
diff --git a/src/api/models/notification.ts b/src/server/api/models/notification.ts
similarity index 98%
rename from src/api/models/notification.ts
rename to src/server/api/models/notification.ts
index fa7049d31..bcb25534d 100644
--- a/src/api/models/notification.ts
+++ b/src/server/api/models/notification.ts
@@ -1,6 +1,6 @@
 import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
-import db from '../../db/mongodb';
+import db from '../../../db/mongodb';
 import { IUser, pack as packUser } from './user';
 import { pack as packPost } from './post';
 
diff --git a/src/api/models/othello-game.ts b/src/server/api/models/othello-game.ts
similarity index 98%
rename from src/api/models/othello-game.ts
rename to src/server/api/models/othello-game.ts
index 01c6ca6c0..97508e46d 100644
--- a/src/api/models/othello-game.ts
+++ b/src/server/api/models/othello-game.ts
@@ -1,6 +1,6 @@
 import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
-import db from '../../db/mongodb';
+import db from '../../../db/mongodb';
 import { IUser, pack as packUser } from './user';
 
 const Game = db.get<IGame>('othello_games');
diff --git a/src/api/models/othello-matching.ts b/src/server/api/models/othello-matching.ts
similarity index 96%
rename from src/api/models/othello-matching.ts
rename to src/server/api/models/othello-matching.ts
index 5cc39cae1..3c29e6a00 100644
--- a/src/api/models/othello-matching.ts
+++ b/src/server/api/models/othello-matching.ts
@@ -1,6 +1,6 @@
 import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
-import db from '../../db/mongodb';
+import db from '../../../db/mongodb';
 import { IUser, pack as packUser } from './user';
 
 const Matching = db.get<IMatching>('othello_matchings');
diff --git a/src/api/models/poll-vote.ts b/src/server/api/models/poll-vote.ts
similarity index 64%
rename from src/api/models/poll-vote.ts
rename to src/server/api/models/poll-vote.ts
index af77a2643..c6638ccf1 100644
--- a/src/api/models/poll-vote.ts
+++ b/src/server/api/models/poll-vote.ts
@@ -1,3 +1,3 @@
-import db from '../../db/mongodb';
+import db from '../../../db/mongodb';
 
 export default db.get('poll_votes') as any; // fuck type definition
diff --git a/src/api/models/post-reaction.ts b/src/server/api/models/post-reaction.ts
similarity index 96%
rename from src/api/models/post-reaction.ts
rename to src/server/api/models/post-reaction.ts
index 639a70e00..5cd122d76 100644
--- a/src/api/models/post-reaction.ts
+++ b/src/server/api/models/post-reaction.ts
@@ -1,6 +1,6 @@
 import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
-import db from '../../db/mongodb';
+import db from '../../../db/mongodb';
 import Reaction from './post-reaction';
 import { pack as packUser } from './user';
 
diff --git a/src/api/models/post-watching.ts b/src/server/api/models/post-watching.ts
similarity index 65%
rename from src/api/models/post-watching.ts
rename to src/server/api/models/post-watching.ts
index 41d37e270..9a4163c8d 100644
--- a/src/api/models/post-watching.ts
+++ b/src/server/api/models/post-watching.ts
@@ -1,3 +1,3 @@
-import db from '../../db/mongodb';
+import db from '../../../db/mongodb';
 
 export default db.get('post_watching') as any; // fuck type definition
diff --git a/src/api/models/post.ts b/src/server/api/models/post.ts
similarity index 99%
rename from src/api/models/post.ts
rename to src/server/api/models/post.ts
index c37c8371c..3f648e08c 100644
--- a/src/api/models/post.ts
+++ b/src/server/api/models/post.ts
@@ -1,7 +1,7 @@
 import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
 import rap from '@prezzemolo/rap';
-import db from '../../db/mongodb';
+import db from '../../../db/mongodb';
 import { IUser, pack as packUser } from './user';
 import { pack as packApp } from './app';
 import { pack as packChannel } from './channel';
diff --git a/src/api/models/signin.ts b/src/server/api/models/signin.ts
similarity index 93%
rename from src/api/models/signin.ts
rename to src/server/api/models/signin.ts
index 262c8707e..5cffb3c31 100644
--- a/src/api/models/signin.ts
+++ b/src/server/api/models/signin.ts
@@ -1,6 +1,6 @@
 import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
-import db from '../../db/mongodb';
+import db from '../../../db/mongodb';
 
 const Signin = db.get<ISignin>('signin');
 export default Signin;
diff --git a/src/api/models/sw-subscription.ts b/src/server/api/models/sw-subscription.ts
similarity index 66%
rename from src/api/models/sw-subscription.ts
rename to src/server/api/models/sw-subscription.ts
index ecca04cb9..4506a982f 100644
--- a/src/api/models/sw-subscription.ts
+++ b/src/server/api/models/sw-subscription.ts
@@ -1,3 +1,3 @@
-import db from '../../db/mongodb';
+import db from '../../../db/mongodb';
 
 export default db.get('sw_subscriptions') as any; // fuck type definition
diff --git a/src/api/models/user.ts b/src/server/api/models/user.ts
similarity index 99%
rename from src/api/models/user.ts
rename to src/server/api/models/user.ts
index e73c95faf..8e7d50baa 100644
--- a/src/api/models/user.ts
+++ b/src/server/api/models/user.ts
@@ -1,12 +1,12 @@
 import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
 import rap from '@prezzemolo/rap';
-import db from '../../db/mongodb';
+import db from '../../../db/mongodb';
 import { IPost, pack as packPost } from './post';
 import Following from './following';
 import Mute from './mute';
 import getFriends from '../common/get-friends';
-import config from '../../conf';
+import config from '../../../conf';
 
 const User = db.get<IUser>('users');
 
diff --git a/src/api/private/signin.ts b/src/server/api/private/signin.ts
similarity index 98%
rename from src/api/private/signin.ts
rename to src/server/api/private/signin.ts
index 00dcb8afc..bbc990899 100644
--- a/src/api/private/signin.ts
+++ b/src/server/api/private/signin.ts
@@ -5,7 +5,7 @@ import { default as User, ILocalAccount, IUser } from '../models/user';
 import Signin, { pack } from '../models/signin';
 import event from '../event';
 import signin from '../common/signin';
-import config from '../../conf';
+import config from '../../../conf';
 
 export default async (req: express.Request, res: express.Response) => {
 	res.header('Access-Control-Allow-Origin', config.url);
diff --git a/src/api/private/signup.ts b/src/server/api/private/signup.ts
similarity index 96%
rename from src/api/private/signup.ts
rename to src/server/api/private/signup.ts
index 96e049570..9f5539331 100644
--- a/src/api/private/signup.ts
+++ b/src/server/api/private/signup.ts
@@ -1,11 +1,11 @@
 import * as uuid from 'uuid';
 import * as express from 'express';
 import * as bcrypt from 'bcryptjs';
-import { generate as generateKeypair } from '../../crypto_key';
+import { generate as generateKeypair } from '../../../crypto_key';
 import recaptcha = require('recaptcha-promise');
 import User, { IUser, validateUsername, validatePassword, pack } from '../models/user';
 import generateUserToken from '../common/generate-native-user-token';
-import config from '../../conf';
+import config from '../../../conf';
 
 recaptcha.init({
 	secret_key: config.recaptcha.secret_key
diff --git a/src/api/reply.ts b/src/server/api/reply.ts
similarity index 100%
rename from src/api/reply.ts
rename to src/server/api/reply.ts
diff --git a/src/api/server.ts b/src/server/api/server.ts
similarity index 100%
rename from src/api/server.ts
rename to src/server/api/server.ts
diff --git a/src/api/service/github.ts b/src/server/api/service/github.ts
similarity index 77%
rename from src/api/service/github.ts
rename to src/server/api/service/github.ts
index 1c78267c0..a33d35975 100644
--- a/src/api/service/github.ts
+++ b/src/server/api/service/github.ts
@@ -1,9 +1,9 @@
 import * as EventEmitter from 'events';
 import * as express from 'express';
-import * as request from 'request';
 const crypto = require('crypto');
 import User from '../models/user';
-import config from '../../conf';
+import config from '../../../conf';
+import queue from '../../../queue';
 
 module.exports = async (app: express.Application) => {
 	if (config.github_bot == null) return;
@@ -25,12 +25,12 @@ module.exports = async (app: express.Application) => {
 		// req.headers['x-hub-signature'] および
 		// req.headers['x-github-event'] は常に string ですが、型定義の都合上
 		// string | string[] になっているので string を明示しています
-		if ((new Buffer(req.headers['x-hub-signature'] as string)).equals(new Buffer(`sha1=${crypto.createHmac('sha1', config.github_bot.hook_secret).update(JSON.stringify(req.body)).digest('hex')}`))) {
+//		if ((new Buffer(req.headers['x-hub-signature'] as string)).equals(new Buffer(`sha1=${crypto.createHmac('sha1', config.github_bot.hook_secret).update(JSON.stringify(req.body)).digest('hex')}`))) {
 			handler.emit(req.headers['x-github-event'] as string, req.body);
 			res.sendStatus(200);
-		} else {
-			res.sendStatus(400);
-		}
+//		} else {
+//			res.sendStatus(400);
+//		}
 	});
 
 	handler.on('status', event => {
@@ -41,26 +41,12 @@ module.exports = async (app: express.Application) => {
 				const commit = event.commit;
 				const parent = commit.parents[0];
 
-				// Fetch parent status
-				request({
-					url: `${parent.url}/statuses`,
-					headers: {
-						'User-Agent': 'misskey'
-					}
-				}, (err, res, body) => {
-					if (err) {
-						console.error(err);
-						return;
-					}
-					const parentStatuses = JSON.parse(body);
-					const parentState = parentStatuses[0].state;
-					const stillFailed = parentState == 'failure' || parentState == 'error';
-					if (stillFailed) {
-						post(`**⚠️BUILD STILL FAILED⚠️**: ?[${commit.commit.message}](${commit.html_url})`);
-					} else {
-						post(`**🚨BUILD FAILED🚨**: →→→?[${commit.commit.message}](${commit.html_url})←←←`);
-					}
-				});
+				queue.create('gitHubFailureReport', {
+					userId: bot._id,
+					parentUrl: parent.url,
+					htmlUrl: commit.html_url,
+					message: commit.commit.message,
+				}).save();
 				break;
 		}
 	});
diff --git a/src/api/service/twitter.ts b/src/server/api/service/twitter.ts
similarity index 98%
rename from src/api/service/twitter.ts
rename to src/server/api/service/twitter.ts
index c1f2e48a6..861f63ed6 100644
--- a/src/api/service/twitter.ts
+++ b/src/server/api/service/twitter.ts
@@ -4,10 +4,10 @@ import * as uuid from 'uuid';
 // import * as Twitter from 'twitter';
 // const Twitter = require('twitter');
 import autwh from 'autwh';
-import redis from '../../db/redis';
+import redis from '../../../db/redis';
 import User, { pack } from '../models/user';
 import event from '../event';
-import config from '../../conf';
+import config from '../../../conf';
 import signin from '../common/signin';
 
 module.exports = (app: express.Application) => {
diff --git a/src/api/stream/channel.ts b/src/server/api/stream/channel.ts
similarity index 100%
rename from src/api/stream/channel.ts
rename to src/server/api/stream/channel.ts
diff --git a/src/api/stream/drive.ts b/src/server/api/stream/drive.ts
similarity index 100%
rename from src/api/stream/drive.ts
rename to src/server/api/stream/drive.ts
diff --git a/src/api/stream/home.ts b/src/server/api/stream/home.ts
similarity index 100%
rename from src/api/stream/home.ts
rename to src/server/api/stream/home.ts
diff --git a/src/api/stream/messaging-index.ts b/src/server/api/stream/messaging-index.ts
similarity index 100%
rename from src/api/stream/messaging-index.ts
rename to src/server/api/stream/messaging-index.ts
diff --git a/src/api/stream/messaging.ts b/src/server/api/stream/messaging.ts
similarity index 100%
rename from src/api/stream/messaging.ts
rename to src/server/api/stream/messaging.ts
diff --git a/src/api/stream/othello-game.ts b/src/server/api/stream/othello-game.ts
similarity index 100%
rename from src/api/stream/othello-game.ts
rename to src/server/api/stream/othello-game.ts
diff --git a/src/api/stream/othello.ts b/src/server/api/stream/othello.ts
similarity index 100%
rename from src/api/stream/othello.ts
rename to src/server/api/stream/othello.ts
diff --git a/src/api/stream/requests.ts b/src/server/api/stream/requests.ts
similarity index 100%
rename from src/api/stream/requests.ts
rename to src/server/api/stream/requests.ts
diff --git a/src/api/stream/server.ts b/src/server/api/stream/server.ts
similarity index 100%
rename from src/api/stream/server.ts
rename to src/server/api/stream/server.ts
diff --git a/src/api/streaming.ts b/src/server/api/streaming.ts
similarity index 98%
rename from src/api/streaming.ts
rename to src/server/api/streaming.ts
index a6759e414..95f444e00 100644
--- a/src/api/streaming.ts
+++ b/src/server/api/streaming.ts
@@ -1,7 +1,7 @@
 import * as http from 'http';
 import * as websocket from 'websocket';
 import * as redis from 'redis';
-import config from '../conf';
+import config from '../../conf';
 import { default as User, IUser } from './models/user';
 import AccessToken from './models/access-token';
 import isNativeToken from './common/is-native-token';
diff --git a/src/common/get-notification-summary.ts b/src/server/common/get-notification-summary.ts
similarity index 100%
rename from src/common/get-notification-summary.ts
rename to src/server/common/get-notification-summary.ts
diff --git a/src/common/get-post-summary.ts b/src/server/common/get-post-summary.ts
similarity index 100%
rename from src/common/get-post-summary.ts
rename to src/server/common/get-post-summary.ts
diff --git a/src/common/get-reaction-emoji.ts b/src/server/common/get-reaction-emoji.ts
similarity index 100%
rename from src/common/get-reaction-emoji.ts
rename to src/server/common/get-reaction-emoji.ts
diff --git a/src/common/othello/ai/back.ts b/src/server/common/othello/ai/back.ts
similarity index 99%
rename from src/common/othello/ai/back.ts
rename to src/server/common/othello/ai/back.ts
index 27dbc3952..c20c6fed2 100644
--- a/src/common/othello/ai/back.ts
+++ b/src/server/common/othello/ai/back.ts
@@ -8,7 +8,7 @@
 
 import * as request from 'request-promise-native';
 import Othello, { Color } from '../core';
-import conf from '../../../conf';
+import conf from '../../../../conf';
 
 let game;
 let form;
diff --git a/src/common/othello/ai/front.ts b/src/server/common/othello/ai/front.ts
similarity index 99%
rename from src/common/othello/ai/front.ts
rename to src/server/common/othello/ai/front.ts
index d892afbed..af0b748fc 100644
--- a/src/common/othello/ai/front.ts
+++ b/src/server/common/othello/ai/front.ts
@@ -10,7 +10,7 @@ import * as childProcess from 'child_process';
 const WebSocket = require('ws');
 import * as ReconnectingWebSocket from 'reconnecting-websocket';
 import * as request from 'request-promise-native';
-import conf from '../../../conf';
+import conf from '../../../../conf';
 
 // 設定 ////////////////////////////////////////////////////////
 
diff --git a/src/common/othello/ai/index.ts b/src/server/common/othello/ai/index.ts
similarity index 100%
rename from src/common/othello/ai/index.ts
rename to src/server/common/othello/ai/index.ts
diff --git a/src/common/othello/core.ts b/src/server/common/othello/core.ts
similarity index 100%
rename from src/common/othello/core.ts
rename to src/server/common/othello/core.ts
diff --git a/src/common/othello/maps.ts b/src/server/common/othello/maps.ts
similarity index 100%
rename from src/common/othello/maps.ts
rename to src/server/common/othello/maps.ts
diff --git a/src/common/user/get-acct.ts b/src/server/common/user/get-acct.ts
similarity index 100%
rename from src/common/user/get-acct.ts
rename to src/server/common/user/get-acct.ts
diff --git a/src/common/user/get-summary.ts b/src/server/common/user/get-summary.ts
similarity index 100%
rename from src/common/user/get-summary.ts
rename to src/server/common/user/get-summary.ts
diff --git a/src/common/user/parse-acct.ts b/src/server/common/user/parse-acct.ts
similarity index 100%
rename from src/common/user/parse-acct.ts
rename to src/server/common/user/parse-acct.ts
diff --git a/src/file/assets/avatar.jpg b/src/server/file/assets/avatar.jpg
similarity index 100%
rename from src/file/assets/avatar.jpg
rename to src/server/file/assets/avatar.jpg
diff --git a/src/file/assets/bad-egg.png b/src/server/file/assets/bad-egg.png
similarity index 100%
rename from src/file/assets/bad-egg.png
rename to src/server/file/assets/bad-egg.png
diff --git a/src/file/assets/dummy.png b/src/server/file/assets/dummy.png
similarity index 100%
rename from src/file/assets/dummy.png
rename to src/server/file/assets/dummy.png
diff --git a/src/file/assets/not-an-image.png b/src/server/file/assets/not-an-image.png
similarity index 100%
rename from src/file/assets/not-an-image.png
rename to src/server/file/assets/not-an-image.png
diff --git a/src/file/assets/thumbnail-not-available.png b/src/server/file/assets/thumbnail-not-available.png
similarity index 100%
rename from src/file/assets/thumbnail-not-available.png
rename to src/server/file/assets/thumbnail-not-available.png
diff --git a/src/file/server.ts b/src/server/file/server.ts
similarity index 100%
rename from src/file/server.ts
rename to src/server/file/server.ts
diff --git a/src/server.ts b/src/server/index.ts
similarity index 77%
rename from src/server.ts
rename to src/server/index.ts
index 0e030002a..3908b8a52 100644
--- a/src/server.ts
+++ b/src/server/index.ts
@@ -5,13 +5,12 @@
 import * as fs from 'fs';
 import * as http from 'http';
 import * as https from 'https';
-import * as cluster from 'cluster';
 import * as express from 'express';
 import * as morgan from 'morgan';
 import Accesses from 'accesses';
 
 import log from './log-request';
-import config from './conf';
+import config from '../conf';
 
 /**
  * Init app
@@ -56,10 +55,7 @@ app.use('/api', require('./api/server'));
 app.use('/files', require('./file/server'));
 app.use(require('./web/server'));
 
-/**
- * Create server
- */
-const server = (() => {
+function createServer() {
 	if (config.https) {
 		const certs = {};
 		Object.keys(config.https).forEach(k => {
@@ -69,24 +65,18 @@ const server = (() => {
 	} else {
 		return http.createServer(app);
 	}
-})();
+}
 
-/**
- * Steaming
- */
-require('./api/streaming')(server);
+export default () => new Promise(resolve => {
+	const server = createServer();
 
-/**
- * Server listen
- */
-server.listen(config.port, () => {
-	if (cluster.isWorker) {
-		// Send a 'ready' message to parent process
-		process.send('ready');
-	}
+	/**
+	 * Steaming
+	 */
+	require('./api/streaming')(server);
+
+	/**
+	 * Server listen
+	 */
+	server.listen(config.port, resolve);
 });
-
-/**
- * Export app for testing
- */
-module.exports = app;
diff --git a/src/log-request.ts b/src/server/log-request.ts
similarity index 100%
rename from src/log-request.ts
rename to src/server/log-request.ts
diff --git a/src/web/app/animation.styl b/src/server/web/app/animation.styl
similarity index 100%
rename from src/web/app/animation.styl
rename to src/server/web/app/animation.styl
diff --git a/src/web/app/app.styl b/src/server/web/app/app.styl
similarity index 100%
rename from src/web/app/app.styl
rename to src/server/web/app/app.styl
diff --git a/src/web/app/app.vue b/src/server/web/app/app.vue
similarity index 100%
rename from src/web/app/app.vue
rename to src/server/web/app/app.vue
diff --git a/src/web/app/auth/assets/logo.svg b/src/server/web/app/auth/assets/logo.svg
similarity index 100%
rename from src/web/app/auth/assets/logo.svg
rename to src/server/web/app/auth/assets/logo.svg
diff --git a/src/web/app/auth/script.ts b/src/server/web/app/auth/script.ts
similarity index 100%
rename from src/web/app/auth/script.ts
rename to src/server/web/app/auth/script.ts
diff --git a/src/web/app/auth/style.styl b/src/server/web/app/auth/style.styl
similarity index 100%
rename from src/web/app/auth/style.styl
rename to src/server/web/app/auth/style.styl
diff --git a/src/web/app/auth/views/form.vue b/src/server/web/app/auth/views/form.vue
similarity index 100%
rename from src/web/app/auth/views/form.vue
rename to src/server/web/app/auth/views/form.vue
diff --git a/src/web/app/auth/views/index.vue b/src/server/web/app/auth/views/index.vue
similarity index 100%
rename from src/web/app/auth/views/index.vue
rename to src/server/web/app/auth/views/index.vue
diff --git a/src/web/app/base.pug b/src/server/web/app/base.pug
similarity index 76%
rename from src/web/app/base.pug
rename to src/server/web/app/base.pug
index d7c7f0aed..60eb1539e 100644
--- a/src/web/app/base.pug
+++ b/src/server/web/app/base.pug
@@ -14,12 +14,12 @@ html
 		title Misskey
 
 		style
-			include ./../../../built/web/assets/init.css
+			include ./../../../../built/server/web/assets/init.css
 		script
-			include ./../../../built/web/assets/boot.js
+			include ./../../../../built/server/web/assets/boot.js
 
 		script
-			include ./../../../built/web/assets/safe.js
+			include ./../../../../built/server/web/assets/safe.js
 
 		//- FontAwesome style
 		style #{facss}
diff --git a/src/web/app/boot.js b/src/server/web/app/boot.js
similarity index 100%
rename from src/web/app/boot.js
rename to src/server/web/app/boot.js
diff --git a/src/web/app/ch/script.ts b/src/server/web/app/ch/script.ts
similarity index 100%
rename from src/web/app/ch/script.ts
rename to src/server/web/app/ch/script.ts
diff --git a/src/web/app/ch/style.styl b/src/server/web/app/ch/style.styl
similarity index 100%
rename from src/web/app/ch/style.styl
rename to src/server/web/app/ch/style.styl
diff --git a/src/web/app/ch/tags/channel.tag b/src/server/web/app/ch/tags/channel.tag
similarity index 100%
rename from src/web/app/ch/tags/channel.tag
rename to src/server/web/app/ch/tags/channel.tag
diff --git a/src/web/app/ch/tags/header.tag b/src/server/web/app/ch/tags/header.tag
similarity index 100%
rename from src/web/app/ch/tags/header.tag
rename to src/server/web/app/ch/tags/header.tag
diff --git a/src/web/app/ch/tags/index.tag b/src/server/web/app/ch/tags/index.tag
similarity index 100%
rename from src/web/app/ch/tags/index.tag
rename to src/server/web/app/ch/tags/index.tag
diff --git a/src/web/app/ch/tags/index.ts b/src/server/web/app/ch/tags/index.ts
similarity index 100%
rename from src/web/app/ch/tags/index.ts
rename to src/server/web/app/ch/tags/index.ts
diff --git a/src/web/app/common/define-widget.ts b/src/server/web/app/common/define-widget.ts
similarity index 100%
rename from src/web/app/common/define-widget.ts
rename to src/server/web/app/common/define-widget.ts
diff --git a/src/web/app/common/mios.ts b/src/server/web/app/common/mios.ts
similarity index 100%
rename from src/web/app/common/mios.ts
rename to src/server/web/app/common/mios.ts
diff --git a/src/web/app/common/scripts/check-for-update.ts b/src/server/web/app/common/scripts/check-for-update.ts
similarity index 100%
rename from src/web/app/common/scripts/check-for-update.ts
rename to src/server/web/app/common/scripts/check-for-update.ts
diff --git a/src/web/app/common/scripts/compose-notification.ts b/src/server/web/app/common/scripts/compose-notification.ts
similarity index 100%
rename from src/web/app/common/scripts/compose-notification.ts
rename to src/server/web/app/common/scripts/compose-notification.ts
diff --git a/src/web/app/common/scripts/contains.ts b/src/server/web/app/common/scripts/contains.ts
similarity index 100%
rename from src/web/app/common/scripts/contains.ts
rename to src/server/web/app/common/scripts/contains.ts
diff --git a/src/web/app/common/scripts/copy-to-clipboard.ts b/src/server/web/app/common/scripts/copy-to-clipboard.ts
similarity index 100%
rename from src/web/app/common/scripts/copy-to-clipboard.ts
rename to src/server/web/app/common/scripts/copy-to-clipboard.ts
diff --git a/src/web/app/common/scripts/date-stringify.ts b/src/server/web/app/common/scripts/date-stringify.ts
similarity index 100%
rename from src/web/app/common/scripts/date-stringify.ts
rename to src/server/web/app/common/scripts/date-stringify.ts
diff --git a/src/web/app/common/scripts/fuck-ad-block.ts b/src/server/web/app/common/scripts/fuck-ad-block.ts
similarity index 100%
rename from src/web/app/common/scripts/fuck-ad-block.ts
rename to src/server/web/app/common/scripts/fuck-ad-block.ts
diff --git a/src/web/app/common/scripts/gcd.ts b/src/server/web/app/common/scripts/gcd.ts
similarity index 100%
rename from src/web/app/common/scripts/gcd.ts
rename to src/server/web/app/common/scripts/gcd.ts
diff --git a/src/web/app/common/scripts/get-kao.ts b/src/server/web/app/common/scripts/get-kao.ts
similarity index 100%
rename from src/web/app/common/scripts/get-kao.ts
rename to src/server/web/app/common/scripts/get-kao.ts
diff --git a/src/web/app/common/scripts/get-median.ts b/src/server/web/app/common/scripts/get-median.ts
similarity index 100%
rename from src/web/app/common/scripts/get-median.ts
rename to src/server/web/app/common/scripts/get-median.ts
diff --git a/src/web/app/common/scripts/loading.ts b/src/server/web/app/common/scripts/loading.ts
similarity index 100%
rename from src/web/app/common/scripts/loading.ts
rename to src/server/web/app/common/scripts/loading.ts
diff --git a/src/web/app/common/scripts/parse-search-query.ts b/src/server/web/app/common/scripts/parse-search-query.ts
similarity index 100%
rename from src/web/app/common/scripts/parse-search-query.ts
rename to src/server/web/app/common/scripts/parse-search-query.ts
diff --git a/src/web/app/common/scripts/streaming/channel.ts b/src/server/web/app/common/scripts/streaming/channel.ts
similarity index 100%
rename from src/web/app/common/scripts/streaming/channel.ts
rename to src/server/web/app/common/scripts/streaming/channel.ts
diff --git a/src/web/app/common/scripts/streaming/drive.ts b/src/server/web/app/common/scripts/streaming/drive.ts
similarity index 100%
rename from src/web/app/common/scripts/streaming/drive.ts
rename to src/server/web/app/common/scripts/streaming/drive.ts
diff --git a/src/web/app/common/scripts/streaming/home.ts b/src/server/web/app/common/scripts/streaming/home.ts
similarity index 100%
rename from src/web/app/common/scripts/streaming/home.ts
rename to src/server/web/app/common/scripts/streaming/home.ts
diff --git a/src/web/app/common/scripts/streaming/messaging-index.ts b/src/server/web/app/common/scripts/streaming/messaging-index.ts
similarity index 100%
rename from src/web/app/common/scripts/streaming/messaging-index.ts
rename to src/server/web/app/common/scripts/streaming/messaging-index.ts
diff --git a/src/web/app/common/scripts/streaming/messaging.ts b/src/server/web/app/common/scripts/streaming/messaging.ts
similarity index 100%
rename from src/web/app/common/scripts/streaming/messaging.ts
rename to src/server/web/app/common/scripts/streaming/messaging.ts
diff --git a/src/web/app/common/scripts/streaming/othello-game.ts b/src/server/web/app/common/scripts/streaming/othello-game.ts
similarity index 100%
rename from src/web/app/common/scripts/streaming/othello-game.ts
rename to src/server/web/app/common/scripts/streaming/othello-game.ts
diff --git a/src/web/app/common/scripts/streaming/othello.ts b/src/server/web/app/common/scripts/streaming/othello.ts
similarity index 100%
rename from src/web/app/common/scripts/streaming/othello.ts
rename to src/server/web/app/common/scripts/streaming/othello.ts
diff --git a/src/web/app/common/scripts/streaming/requests.ts b/src/server/web/app/common/scripts/streaming/requests.ts
similarity index 100%
rename from src/web/app/common/scripts/streaming/requests.ts
rename to src/server/web/app/common/scripts/streaming/requests.ts
diff --git a/src/web/app/common/scripts/streaming/server.ts b/src/server/web/app/common/scripts/streaming/server.ts
similarity index 100%
rename from src/web/app/common/scripts/streaming/server.ts
rename to src/server/web/app/common/scripts/streaming/server.ts
diff --git a/src/web/app/common/scripts/streaming/stream-manager.ts b/src/server/web/app/common/scripts/streaming/stream-manager.ts
similarity index 100%
rename from src/web/app/common/scripts/streaming/stream-manager.ts
rename to src/server/web/app/common/scripts/streaming/stream-manager.ts
diff --git a/src/web/app/common/scripts/streaming/stream.ts b/src/server/web/app/common/scripts/streaming/stream.ts
similarity index 100%
rename from src/web/app/common/scripts/streaming/stream.ts
rename to src/server/web/app/common/scripts/streaming/stream.ts
diff --git a/src/web/app/common/views/components/autocomplete.vue b/src/server/web/app/common/views/components/autocomplete.vue
similarity index 100%
rename from src/web/app/common/views/components/autocomplete.vue
rename to src/server/web/app/common/views/components/autocomplete.vue
diff --git a/src/web/app/common/views/components/connect-failed.troubleshooter.vue b/src/server/web/app/common/views/components/connect-failed.troubleshooter.vue
similarity index 100%
rename from src/web/app/common/views/components/connect-failed.troubleshooter.vue
rename to src/server/web/app/common/views/components/connect-failed.troubleshooter.vue
diff --git a/src/web/app/common/views/components/connect-failed.vue b/src/server/web/app/common/views/components/connect-failed.vue
similarity index 100%
rename from src/web/app/common/views/components/connect-failed.vue
rename to src/server/web/app/common/views/components/connect-failed.vue
diff --git a/src/web/app/common/views/components/ellipsis.vue b/src/server/web/app/common/views/components/ellipsis.vue
similarity index 100%
rename from src/web/app/common/views/components/ellipsis.vue
rename to src/server/web/app/common/views/components/ellipsis.vue
diff --git a/src/web/app/common/views/components/file-type-icon.vue b/src/server/web/app/common/views/components/file-type-icon.vue
similarity index 100%
rename from src/web/app/common/views/components/file-type-icon.vue
rename to src/server/web/app/common/views/components/file-type-icon.vue
diff --git a/src/web/app/common/views/components/forkit.vue b/src/server/web/app/common/views/components/forkit.vue
similarity index 100%
rename from src/web/app/common/views/components/forkit.vue
rename to src/server/web/app/common/views/components/forkit.vue
diff --git a/src/web/app/common/views/components/index.ts b/src/server/web/app/common/views/components/index.ts
similarity index 100%
rename from src/web/app/common/views/components/index.ts
rename to src/server/web/app/common/views/components/index.ts
diff --git a/src/web/app/common/views/components/media-list.vue b/src/server/web/app/common/views/components/media-list.vue
similarity index 100%
rename from src/web/app/common/views/components/media-list.vue
rename to src/server/web/app/common/views/components/media-list.vue
diff --git a/src/web/app/common/views/components/messaging-room.form.vue b/src/server/web/app/common/views/components/messaging-room.form.vue
similarity index 100%
rename from src/web/app/common/views/components/messaging-room.form.vue
rename to src/server/web/app/common/views/components/messaging-room.form.vue
diff --git a/src/web/app/common/views/components/messaging-room.message.vue b/src/server/web/app/common/views/components/messaging-room.message.vue
similarity index 100%
rename from src/web/app/common/views/components/messaging-room.message.vue
rename to src/server/web/app/common/views/components/messaging-room.message.vue
diff --git a/src/web/app/common/views/components/messaging-room.vue b/src/server/web/app/common/views/components/messaging-room.vue
similarity index 100%
rename from src/web/app/common/views/components/messaging-room.vue
rename to src/server/web/app/common/views/components/messaging-room.vue
diff --git a/src/web/app/common/views/components/messaging.vue b/src/server/web/app/common/views/components/messaging.vue
similarity index 100%
rename from src/web/app/common/views/components/messaging.vue
rename to src/server/web/app/common/views/components/messaging.vue
diff --git a/src/web/app/common/views/components/nav.vue b/src/server/web/app/common/views/components/nav.vue
similarity index 100%
rename from src/web/app/common/views/components/nav.vue
rename to src/server/web/app/common/views/components/nav.vue
diff --git a/src/web/app/common/views/components/othello.game.vue b/src/server/web/app/common/views/components/othello.game.vue
similarity index 100%
rename from src/web/app/common/views/components/othello.game.vue
rename to src/server/web/app/common/views/components/othello.game.vue
diff --git a/src/web/app/common/views/components/othello.gameroom.vue b/src/server/web/app/common/views/components/othello.gameroom.vue
similarity index 100%
rename from src/web/app/common/views/components/othello.gameroom.vue
rename to src/server/web/app/common/views/components/othello.gameroom.vue
diff --git a/src/web/app/common/views/components/othello.room.vue b/src/server/web/app/common/views/components/othello.room.vue
similarity index 100%
rename from src/web/app/common/views/components/othello.room.vue
rename to src/server/web/app/common/views/components/othello.room.vue
diff --git a/src/web/app/common/views/components/othello.vue b/src/server/web/app/common/views/components/othello.vue
similarity index 100%
rename from src/web/app/common/views/components/othello.vue
rename to src/server/web/app/common/views/components/othello.vue
diff --git a/src/web/app/common/views/components/poll-editor.vue b/src/server/web/app/common/views/components/poll-editor.vue
similarity index 100%
rename from src/web/app/common/views/components/poll-editor.vue
rename to src/server/web/app/common/views/components/poll-editor.vue
diff --git a/src/web/app/common/views/components/poll.vue b/src/server/web/app/common/views/components/poll.vue
similarity index 100%
rename from src/web/app/common/views/components/poll.vue
rename to src/server/web/app/common/views/components/poll.vue
diff --git a/src/web/app/common/views/components/post-html.ts b/src/server/web/app/common/views/components/post-html.ts
similarity index 100%
rename from src/web/app/common/views/components/post-html.ts
rename to src/server/web/app/common/views/components/post-html.ts
diff --git a/src/web/app/common/views/components/post-menu.vue b/src/server/web/app/common/views/components/post-menu.vue
similarity index 100%
rename from src/web/app/common/views/components/post-menu.vue
rename to src/server/web/app/common/views/components/post-menu.vue
diff --git a/src/web/app/common/views/components/reaction-icon.vue b/src/server/web/app/common/views/components/reaction-icon.vue
similarity index 100%
rename from src/web/app/common/views/components/reaction-icon.vue
rename to src/server/web/app/common/views/components/reaction-icon.vue
diff --git a/src/web/app/common/views/components/reaction-picker.vue b/src/server/web/app/common/views/components/reaction-picker.vue
similarity index 100%
rename from src/web/app/common/views/components/reaction-picker.vue
rename to src/server/web/app/common/views/components/reaction-picker.vue
diff --git a/src/web/app/common/views/components/reactions-viewer.vue b/src/server/web/app/common/views/components/reactions-viewer.vue
similarity index 100%
rename from src/web/app/common/views/components/reactions-viewer.vue
rename to src/server/web/app/common/views/components/reactions-viewer.vue
diff --git a/src/web/app/common/views/components/signin.vue b/src/server/web/app/common/views/components/signin.vue
similarity index 100%
rename from src/web/app/common/views/components/signin.vue
rename to src/server/web/app/common/views/components/signin.vue
diff --git a/src/web/app/common/views/components/signup.vue b/src/server/web/app/common/views/components/signup.vue
similarity index 100%
rename from src/web/app/common/views/components/signup.vue
rename to src/server/web/app/common/views/components/signup.vue
diff --git a/src/web/app/common/views/components/special-message.vue b/src/server/web/app/common/views/components/special-message.vue
similarity index 100%
rename from src/web/app/common/views/components/special-message.vue
rename to src/server/web/app/common/views/components/special-message.vue
diff --git a/src/web/app/common/views/components/stream-indicator.vue b/src/server/web/app/common/views/components/stream-indicator.vue
similarity index 100%
rename from src/web/app/common/views/components/stream-indicator.vue
rename to src/server/web/app/common/views/components/stream-indicator.vue
diff --git a/src/web/app/common/views/components/switch.vue b/src/server/web/app/common/views/components/switch.vue
similarity index 100%
rename from src/web/app/common/views/components/switch.vue
rename to src/server/web/app/common/views/components/switch.vue
diff --git a/src/web/app/common/views/components/time.vue b/src/server/web/app/common/views/components/time.vue
similarity index 100%
rename from src/web/app/common/views/components/time.vue
rename to src/server/web/app/common/views/components/time.vue
diff --git a/src/web/app/common/views/components/timer.vue b/src/server/web/app/common/views/components/timer.vue
similarity index 100%
rename from src/web/app/common/views/components/timer.vue
rename to src/server/web/app/common/views/components/timer.vue
diff --git a/src/web/app/common/views/components/twitter-setting.vue b/src/server/web/app/common/views/components/twitter-setting.vue
similarity index 100%
rename from src/web/app/common/views/components/twitter-setting.vue
rename to src/server/web/app/common/views/components/twitter-setting.vue
diff --git a/src/web/app/common/views/components/uploader.vue b/src/server/web/app/common/views/components/uploader.vue
similarity index 100%
rename from src/web/app/common/views/components/uploader.vue
rename to src/server/web/app/common/views/components/uploader.vue
diff --git a/src/web/app/common/views/components/url-preview.vue b/src/server/web/app/common/views/components/url-preview.vue
similarity index 100%
rename from src/web/app/common/views/components/url-preview.vue
rename to src/server/web/app/common/views/components/url-preview.vue
diff --git a/src/web/app/common/views/components/url.vue b/src/server/web/app/common/views/components/url.vue
similarity index 100%
rename from src/web/app/common/views/components/url.vue
rename to src/server/web/app/common/views/components/url.vue
diff --git a/src/web/app/common/views/components/welcome-timeline.vue b/src/server/web/app/common/views/components/welcome-timeline.vue
similarity index 100%
rename from src/web/app/common/views/components/welcome-timeline.vue
rename to src/server/web/app/common/views/components/welcome-timeline.vue
diff --git a/src/web/app/common/views/directives/autocomplete.ts b/src/server/web/app/common/views/directives/autocomplete.ts
similarity index 100%
rename from src/web/app/common/views/directives/autocomplete.ts
rename to src/server/web/app/common/views/directives/autocomplete.ts
diff --git a/src/web/app/common/views/directives/index.ts b/src/server/web/app/common/views/directives/index.ts
similarity index 100%
rename from src/web/app/common/views/directives/index.ts
rename to src/server/web/app/common/views/directives/index.ts
diff --git a/src/web/app/common/views/filters/bytes.ts b/src/server/web/app/common/views/filters/bytes.ts
similarity index 100%
rename from src/web/app/common/views/filters/bytes.ts
rename to src/server/web/app/common/views/filters/bytes.ts
diff --git a/src/web/app/common/views/filters/index.ts b/src/server/web/app/common/views/filters/index.ts
similarity index 100%
rename from src/web/app/common/views/filters/index.ts
rename to src/server/web/app/common/views/filters/index.ts
diff --git a/src/web/app/common/views/filters/number.ts b/src/server/web/app/common/views/filters/number.ts
similarity index 100%
rename from src/web/app/common/views/filters/number.ts
rename to src/server/web/app/common/views/filters/number.ts
diff --git a/src/web/app/common/views/widgets/access-log.vue b/src/server/web/app/common/views/widgets/access-log.vue
similarity index 100%
rename from src/web/app/common/views/widgets/access-log.vue
rename to src/server/web/app/common/views/widgets/access-log.vue
diff --git a/src/web/app/common/views/widgets/broadcast.vue b/src/server/web/app/common/views/widgets/broadcast.vue
similarity index 100%
rename from src/web/app/common/views/widgets/broadcast.vue
rename to src/server/web/app/common/views/widgets/broadcast.vue
diff --git a/src/web/app/common/views/widgets/calendar.vue b/src/server/web/app/common/views/widgets/calendar.vue
similarity index 100%
rename from src/web/app/common/views/widgets/calendar.vue
rename to src/server/web/app/common/views/widgets/calendar.vue
diff --git a/src/web/app/common/views/widgets/donation.vue b/src/server/web/app/common/views/widgets/donation.vue
similarity index 100%
rename from src/web/app/common/views/widgets/donation.vue
rename to src/server/web/app/common/views/widgets/donation.vue
diff --git a/src/web/app/common/views/widgets/index.ts b/src/server/web/app/common/views/widgets/index.ts
similarity index 100%
rename from src/web/app/common/views/widgets/index.ts
rename to src/server/web/app/common/views/widgets/index.ts
diff --git a/src/web/app/common/views/widgets/nav.vue b/src/server/web/app/common/views/widgets/nav.vue
similarity index 100%
rename from src/web/app/common/views/widgets/nav.vue
rename to src/server/web/app/common/views/widgets/nav.vue
diff --git a/src/web/app/common/views/widgets/photo-stream.vue b/src/server/web/app/common/views/widgets/photo-stream.vue
similarity index 100%
rename from src/web/app/common/views/widgets/photo-stream.vue
rename to src/server/web/app/common/views/widgets/photo-stream.vue
diff --git a/src/web/app/common/views/widgets/rss.vue b/src/server/web/app/common/views/widgets/rss.vue
similarity index 100%
rename from src/web/app/common/views/widgets/rss.vue
rename to src/server/web/app/common/views/widgets/rss.vue
diff --git a/src/web/app/common/views/widgets/server.cpu-memory.vue b/src/server/web/app/common/views/widgets/server.cpu-memory.vue
similarity index 100%
rename from src/web/app/common/views/widgets/server.cpu-memory.vue
rename to src/server/web/app/common/views/widgets/server.cpu-memory.vue
diff --git a/src/web/app/common/views/widgets/server.cpu.vue b/src/server/web/app/common/views/widgets/server.cpu.vue
similarity index 100%
rename from src/web/app/common/views/widgets/server.cpu.vue
rename to src/server/web/app/common/views/widgets/server.cpu.vue
diff --git a/src/web/app/common/views/widgets/server.disk.vue b/src/server/web/app/common/views/widgets/server.disk.vue
similarity index 100%
rename from src/web/app/common/views/widgets/server.disk.vue
rename to src/server/web/app/common/views/widgets/server.disk.vue
diff --git a/src/web/app/common/views/widgets/server.info.vue b/src/server/web/app/common/views/widgets/server.info.vue
similarity index 100%
rename from src/web/app/common/views/widgets/server.info.vue
rename to src/server/web/app/common/views/widgets/server.info.vue
diff --git a/src/web/app/common/views/widgets/server.memory.vue b/src/server/web/app/common/views/widgets/server.memory.vue
similarity index 100%
rename from src/web/app/common/views/widgets/server.memory.vue
rename to src/server/web/app/common/views/widgets/server.memory.vue
diff --git a/src/web/app/common/views/widgets/server.pie.vue b/src/server/web/app/common/views/widgets/server.pie.vue
similarity index 100%
rename from src/web/app/common/views/widgets/server.pie.vue
rename to src/server/web/app/common/views/widgets/server.pie.vue
diff --git a/src/web/app/common/views/widgets/server.uptimes.vue b/src/server/web/app/common/views/widgets/server.uptimes.vue
similarity index 100%
rename from src/web/app/common/views/widgets/server.uptimes.vue
rename to src/server/web/app/common/views/widgets/server.uptimes.vue
diff --git a/src/web/app/common/views/widgets/server.vue b/src/server/web/app/common/views/widgets/server.vue
similarity index 100%
rename from src/web/app/common/views/widgets/server.vue
rename to src/server/web/app/common/views/widgets/server.vue
diff --git a/src/web/app/common/views/widgets/slideshow.vue b/src/server/web/app/common/views/widgets/slideshow.vue
similarity index 100%
rename from src/web/app/common/views/widgets/slideshow.vue
rename to src/server/web/app/common/views/widgets/slideshow.vue
diff --git a/src/web/app/common/views/widgets/tips.vue b/src/server/web/app/common/views/widgets/tips.vue
similarity index 100%
rename from src/web/app/common/views/widgets/tips.vue
rename to src/server/web/app/common/views/widgets/tips.vue
diff --git a/src/web/app/common/views/widgets/version.vue b/src/server/web/app/common/views/widgets/version.vue
similarity index 100%
rename from src/web/app/common/views/widgets/version.vue
rename to src/server/web/app/common/views/widgets/version.vue
diff --git a/src/web/app/config.ts b/src/server/web/app/config.ts
similarity index 100%
rename from src/web/app/config.ts
rename to src/server/web/app/config.ts
diff --git a/src/web/app/desktop/api/choose-drive-file.ts b/src/server/web/app/desktop/api/choose-drive-file.ts
similarity index 100%
rename from src/web/app/desktop/api/choose-drive-file.ts
rename to src/server/web/app/desktop/api/choose-drive-file.ts
diff --git a/src/web/app/desktop/api/choose-drive-folder.ts b/src/server/web/app/desktop/api/choose-drive-folder.ts
similarity index 100%
rename from src/web/app/desktop/api/choose-drive-folder.ts
rename to src/server/web/app/desktop/api/choose-drive-folder.ts
diff --git a/src/web/app/desktop/api/contextmenu.ts b/src/server/web/app/desktop/api/contextmenu.ts
similarity index 100%
rename from src/web/app/desktop/api/contextmenu.ts
rename to src/server/web/app/desktop/api/contextmenu.ts
diff --git a/src/web/app/desktop/api/dialog.ts b/src/server/web/app/desktop/api/dialog.ts
similarity index 100%
rename from src/web/app/desktop/api/dialog.ts
rename to src/server/web/app/desktop/api/dialog.ts
diff --git a/src/web/app/desktop/api/input.ts b/src/server/web/app/desktop/api/input.ts
similarity index 100%
rename from src/web/app/desktop/api/input.ts
rename to src/server/web/app/desktop/api/input.ts
diff --git a/src/web/app/desktop/api/notify.ts b/src/server/web/app/desktop/api/notify.ts
similarity index 100%
rename from src/web/app/desktop/api/notify.ts
rename to src/server/web/app/desktop/api/notify.ts
diff --git a/src/web/app/desktop/api/post.ts b/src/server/web/app/desktop/api/post.ts
similarity index 100%
rename from src/web/app/desktop/api/post.ts
rename to src/server/web/app/desktop/api/post.ts
diff --git a/src/web/app/desktop/api/update-avatar.ts b/src/server/web/app/desktop/api/update-avatar.ts
similarity index 100%
rename from src/web/app/desktop/api/update-avatar.ts
rename to src/server/web/app/desktop/api/update-avatar.ts
diff --git a/src/web/app/desktop/api/update-banner.ts b/src/server/web/app/desktop/api/update-banner.ts
similarity index 100%
rename from src/web/app/desktop/api/update-banner.ts
rename to src/server/web/app/desktop/api/update-banner.ts
diff --git a/src/web/app/desktop/assets/grid.svg b/src/server/web/app/desktop/assets/grid.svg
similarity index 100%
rename from src/web/app/desktop/assets/grid.svg
rename to src/server/web/app/desktop/assets/grid.svg
diff --git a/src/web/app/desktop/assets/header-logo-white.svg b/src/server/web/app/desktop/assets/header-logo-white.svg
similarity index 100%
rename from src/web/app/desktop/assets/header-logo-white.svg
rename to src/server/web/app/desktop/assets/header-logo-white.svg
diff --git a/src/web/app/desktop/assets/header-logo.svg b/src/server/web/app/desktop/assets/header-logo.svg
similarity index 100%
rename from src/web/app/desktop/assets/header-logo.svg
rename to src/server/web/app/desktop/assets/header-logo.svg
diff --git a/src/web/app/desktop/assets/index.jpg b/src/server/web/app/desktop/assets/index.jpg
similarity index 100%
rename from src/web/app/desktop/assets/index.jpg
rename to src/server/web/app/desktop/assets/index.jpg
diff --git a/src/web/app/desktop/assets/remove.png b/src/server/web/app/desktop/assets/remove.png
similarity index 100%
rename from src/web/app/desktop/assets/remove.png
rename to src/server/web/app/desktop/assets/remove.png
diff --git a/src/web/app/desktop/script.ts b/src/server/web/app/desktop/script.ts
similarity index 100%
rename from src/web/app/desktop/script.ts
rename to src/server/web/app/desktop/script.ts
diff --git a/src/web/app/desktop/style.styl b/src/server/web/app/desktop/style.styl
similarity index 100%
rename from src/web/app/desktop/style.styl
rename to src/server/web/app/desktop/style.styl
diff --git a/src/web/app/desktop/ui.styl b/src/server/web/app/desktop/ui.styl
similarity index 100%
rename from src/web/app/desktop/ui.styl
rename to src/server/web/app/desktop/ui.styl
diff --git a/src/web/app/desktop/views/components/activity.calendar.vue b/src/server/web/app/desktop/views/components/activity.calendar.vue
similarity index 100%
rename from src/web/app/desktop/views/components/activity.calendar.vue
rename to src/server/web/app/desktop/views/components/activity.calendar.vue
diff --git a/src/web/app/desktop/views/components/activity.chart.vue b/src/server/web/app/desktop/views/components/activity.chart.vue
similarity index 100%
rename from src/web/app/desktop/views/components/activity.chart.vue
rename to src/server/web/app/desktop/views/components/activity.chart.vue
diff --git a/src/web/app/desktop/views/components/activity.vue b/src/server/web/app/desktop/views/components/activity.vue
similarity index 100%
rename from src/web/app/desktop/views/components/activity.vue
rename to src/server/web/app/desktop/views/components/activity.vue
diff --git a/src/web/app/desktop/views/components/analog-clock.vue b/src/server/web/app/desktop/views/components/analog-clock.vue
similarity index 100%
rename from src/web/app/desktop/views/components/analog-clock.vue
rename to src/server/web/app/desktop/views/components/analog-clock.vue
diff --git a/src/web/app/desktop/views/components/calendar.vue b/src/server/web/app/desktop/views/components/calendar.vue
similarity index 100%
rename from src/web/app/desktop/views/components/calendar.vue
rename to src/server/web/app/desktop/views/components/calendar.vue
diff --git a/src/web/app/desktop/views/components/choose-file-from-drive-window.vue b/src/server/web/app/desktop/views/components/choose-file-from-drive-window.vue
similarity index 100%
rename from src/web/app/desktop/views/components/choose-file-from-drive-window.vue
rename to src/server/web/app/desktop/views/components/choose-file-from-drive-window.vue
diff --git a/src/web/app/desktop/views/components/choose-folder-from-drive-window.vue b/src/server/web/app/desktop/views/components/choose-folder-from-drive-window.vue
similarity index 100%
rename from src/web/app/desktop/views/components/choose-folder-from-drive-window.vue
rename to src/server/web/app/desktop/views/components/choose-folder-from-drive-window.vue
diff --git a/src/web/app/desktop/views/components/context-menu.menu.vue b/src/server/web/app/desktop/views/components/context-menu.menu.vue
similarity index 100%
rename from src/web/app/desktop/views/components/context-menu.menu.vue
rename to src/server/web/app/desktop/views/components/context-menu.menu.vue
diff --git a/src/web/app/desktop/views/components/context-menu.vue b/src/server/web/app/desktop/views/components/context-menu.vue
similarity index 100%
rename from src/web/app/desktop/views/components/context-menu.vue
rename to src/server/web/app/desktop/views/components/context-menu.vue
diff --git a/src/web/app/desktop/views/components/crop-window.vue b/src/server/web/app/desktop/views/components/crop-window.vue
similarity index 100%
rename from src/web/app/desktop/views/components/crop-window.vue
rename to src/server/web/app/desktop/views/components/crop-window.vue
diff --git a/src/web/app/desktop/views/components/dialog.vue b/src/server/web/app/desktop/views/components/dialog.vue
similarity index 100%
rename from src/web/app/desktop/views/components/dialog.vue
rename to src/server/web/app/desktop/views/components/dialog.vue
diff --git a/src/web/app/desktop/views/components/drive-window.vue b/src/server/web/app/desktop/views/components/drive-window.vue
similarity index 100%
rename from src/web/app/desktop/views/components/drive-window.vue
rename to src/server/web/app/desktop/views/components/drive-window.vue
diff --git a/src/web/app/desktop/views/components/drive.file.vue b/src/server/web/app/desktop/views/components/drive.file.vue
similarity index 100%
rename from src/web/app/desktop/views/components/drive.file.vue
rename to src/server/web/app/desktop/views/components/drive.file.vue
diff --git a/src/web/app/desktop/views/components/drive.folder.vue b/src/server/web/app/desktop/views/components/drive.folder.vue
similarity index 100%
rename from src/web/app/desktop/views/components/drive.folder.vue
rename to src/server/web/app/desktop/views/components/drive.folder.vue
diff --git a/src/web/app/desktop/views/components/drive.nav-folder.vue b/src/server/web/app/desktop/views/components/drive.nav-folder.vue
similarity index 100%
rename from src/web/app/desktop/views/components/drive.nav-folder.vue
rename to src/server/web/app/desktop/views/components/drive.nav-folder.vue
diff --git a/src/web/app/desktop/views/components/drive.vue b/src/server/web/app/desktop/views/components/drive.vue
similarity index 100%
rename from src/web/app/desktop/views/components/drive.vue
rename to src/server/web/app/desktop/views/components/drive.vue
diff --git a/src/web/app/desktop/views/components/ellipsis-icon.vue b/src/server/web/app/desktop/views/components/ellipsis-icon.vue
similarity index 100%
rename from src/web/app/desktop/views/components/ellipsis-icon.vue
rename to src/server/web/app/desktop/views/components/ellipsis-icon.vue
diff --git a/src/web/app/desktop/views/components/follow-button.vue b/src/server/web/app/desktop/views/components/follow-button.vue
similarity index 100%
rename from src/web/app/desktop/views/components/follow-button.vue
rename to src/server/web/app/desktop/views/components/follow-button.vue
diff --git a/src/web/app/desktop/views/components/followers-window.vue b/src/server/web/app/desktop/views/components/followers-window.vue
similarity index 100%
rename from src/web/app/desktop/views/components/followers-window.vue
rename to src/server/web/app/desktop/views/components/followers-window.vue
diff --git a/src/web/app/desktop/views/components/followers.vue b/src/server/web/app/desktop/views/components/followers.vue
similarity index 100%
rename from src/web/app/desktop/views/components/followers.vue
rename to src/server/web/app/desktop/views/components/followers.vue
diff --git a/src/web/app/desktop/views/components/following-window.vue b/src/server/web/app/desktop/views/components/following-window.vue
similarity index 100%
rename from src/web/app/desktop/views/components/following-window.vue
rename to src/server/web/app/desktop/views/components/following-window.vue
diff --git a/src/web/app/desktop/views/components/following.vue b/src/server/web/app/desktop/views/components/following.vue
similarity index 100%
rename from src/web/app/desktop/views/components/following.vue
rename to src/server/web/app/desktop/views/components/following.vue
diff --git a/src/web/app/desktop/views/components/friends-maker.vue b/src/server/web/app/desktop/views/components/friends-maker.vue
similarity index 100%
rename from src/web/app/desktop/views/components/friends-maker.vue
rename to src/server/web/app/desktop/views/components/friends-maker.vue
diff --git a/src/web/app/desktop/views/components/game-window.vue b/src/server/web/app/desktop/views/components/game-window.vue
similarity index 100%
rename from src/web/app/desktop/views/components/game-window.vue
rename to src/server/web/app/desktop/views/components/game-window.vue
diff --git a/src/web/app/desktop/views/components/home.vue b/src/server/web/app/desktop/views/components/home.vue
similarity index 100%
rename from src/web/app/desktop/views/components/home.vue
rename to src/server/web/app/desktop/views/components/home.vue
diff --git a/src/web/app/desktop/views/components/index.ts b/src/server/web/app/desktop/views/components/index.ts
similarity index 100%
rename from src/web/app/desktop/views/components/index.ts
rename to src/server/web/app/desktop/views/components/index.ts
diff --git a/src/web/app/desktop/views/components/input-dialog.vue b/src/server/web/app/desktop/views/components/input-dialog.vue
similarity index 100%
rename from src/web/app/desktop/views/components/input-dialog.vue
rename to src/server/web/app/desktop/views/components/input-dialog.vue
diff --git a/src/web/app/desktop/views/components/media-image-dialog.vue b/src/server/web/app/desktop/views/components/media-image-dialog.vue
similarity index 100%
rename from src/web/app/desktop/views/components/media-image-dialog.vue
rename to src/server/web/app/desktop/views/components/media-image-dialog.vue
diff --git a/src/web/app/desktop/views/components/media-image.vue b/src/server/web/app/desktop/views/components/media-image.vue
similarity index 100%
rename from src/web/app/desktop/views/components/media-image.vue
rename to src/server/web/app/desktop/views/components/media-image.vue
diff --git a/src/web/app/desktop/views/components/media-video-dialog.vue b/src/server/web/app/desktop/views/components/media-video-dialog.vue
similarity index 100%
rename from src/web/app/desktop/views/components/media-video-dialog.vue
rename to src/server/web/app/desktop/views/components/media-video-dialog.vue
diff --git a/src/web/app/desktop/views/components/media-video.vue b/src/server/web/app/desktop/views/components/media-video.vue
similarity index 100%
rename from src/web/app/desktop/views/components/media-video.vue
rename to src/server/web/app/desktop/views/components/media-video.vue
diff --git a/src/web/app/desktop/views/components/mentions.vue b/src/server/web/app/desktop/views/components/mentions.vue
similarity index 100%
rename from src/web/app/desktop/views/components/mentions.vue
rename to src/server/web/app/desktop/views/components/mentions.vue
diff --git a/src/web/app/desktop/views/components/messaging-room-window.vue b/src/server/web/app/desktop/views/components/messaging-room-window.vue
similarity index 100%
rename from src/web/app/desktop/views/components/messaging-room-window.vue
rename to src/server/web/app/desktop/views/components/messaging-room-window.vue
diff --git a/src/web/app/desktop/views/components/messaging-window.vue b/src/server/web/app/desktop/views/components/messaging-window.vue
similarity index 100%
rename from src/web/app/desktop/views/components/messaging-window.vue
rename to src/server/web/app/desktop/views/components/messaging-window.vue
diff --git a/src/web/app/desktop/views/components/notifications.vue b/src/server/web/app/desktop/views/components/notifications.vue
similarity index 100%
rename from src/web/app/desktop/views/components/notifications.vue
rename to src/server/web/app/desktop/views/components/notifications.vue
diff --git a/src/web/app/desktop/views/components/post-detail.sub.vue b/src/server/web/app/desktop/views/components/post-detail.sub.vue
similarity index 100%
rename from src/web/app/desktop/views/components/post-detail.sub.vue
rename to src/server/web/app/desktop/views/components/post-detail.sub.vue
diff --git a/src/web/app/desktop/views/components/post-detail.vue b/src/server/web/app/desktop/views/components/post-detail.vue
similarity index 100%
rename from src/web/app/desktop/views/components/post-detail.vue
rename to src/server/web/app/desktop/views/components/post-detail.vue
diff --git a/src/web/app/desktop/views/components/post-form-window.vue b/src/server/web/app/desktop/views/components/post-form-window.vue
similarity index 100%
rename from src/web/app/desktop/views/components/post-form-window.vue
rename to src/server/web/app/desktop/views/components/post-form-window.vue
diff --git a/src/web/app/desktop/views/components/post-form.vue b/src/server/web/app/desktop/views/components/post-form.vue
similarity index 100%
rename from src/web/app/desktop/views/components/post-form.vue
rename to src/server/web/app/desktop/views/components/post-form.vue
diff --git a/src/web/app/desktop/views/components/post-preview.vue b/src/server/web/app/desktop/views/components/post-preview.vue
similarity index 100%
rename from src/web/app/desktop/views/components/post-preview.vue
rename to src/server/web/app/desktop/views/components/post-preview.vue
diff --git a/src/web/app/desktop/views/components/posts.post.sub.vue b/src/server/web/app/desktop/views/components/posts.post.sub.vue
similarity index 100%
rename from src/web/app/desktop/views/components/posts.post.sub.vue
rename to src/server/web/app/desktop/views/components/posts.post.sub.vue
diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/server/web/app/desktop/views/components/posts.post.vue
similarity index 100%
rename from src/web/app/desktop/views/components/posts.post.vue
rename to src/server/web/app/desktop/views/components/posts.post.vue
diff --git a/src/web/app/desktop/views/components/posts.vue b/src/server/web/app/desktop/views/components/posts.vue
similarity index 100%
rename from src/web/app/desktop/views/components/posts.vue
rename to src/server/web/app/desktop/views/components/posts.vue
diff --git a/src/web/app/desktop/views/components/progress-dialog.vue b/src/server/web/app/desktop/views/components/progress-dialog.vue
similarity index 100%
rename from src/web/app/desktop/views/components/progress-dialog.vue
rename to src/server/web/app/desktop/views/components/progress-dialog.vue
diff --git a/src/web/app/desktop/views/components/repost-form-window.vue b/src/server/web/app/desktop/views/components/repost-form-window.vue
similarity index 100%
rename from src/web/app/desktop/views/components/repost-form-window.vue
rename to src/server/web/app/desktop/views/components/repost-form-window.vue
diff --git a/src/web/app/desktop/views/components/repost-form.vue b/src/server/web/app/desktop/views/components/repost-form.vue
similarity index 100%
rename from src/web/app/desktop/views/components/repost-form.vue
rename to src/server/web/app/desktop/views/components/repost-form.vue
diff --git a/src/web/app/desktop/views/components/settings-window.vue b/src/server/web/app/desktop/views/components/settings-window.vue
similarity index 100%
rename from src/web/app/desktop/views/components/settings-window.vue
rename to src/server/web/app/desktop/views/components/settings-window.vue
diff --git a/src/web/app/desktop/views/components/settings.2fa.vue b/src/server/web/app/desktop/views/components/settings.2fa.vue
similarity index 100%
rename from src/web/app/desktop/views/components/settings.2fa.vue
rename to src/server/web/app/desktop/views/components/settings.2fa.vue
diff --git a/src/web/app/desktop/views/components/settings.api.vue b/src/server/web/app/desktop/views/components/settings.api.vue
similarity index 100%
rename from src/web/app/desktop/views/components/settings.api.vue
rename to src/server/web/app/desktop/views/components/settings.api.vue
diff --git a/src/web/app/desktop/views/components/settings.apps.vue b/src/server/web/app/desktop/views/components/settings.apps.vue
similarity index 100%
rename from src/web/app/desktop/views/components/settings.apps.vue
rename to src/server/web/app/desktop/views/components/settings.apps.vue
diff --git a/src/web/app/desktop/views/components/settings.drive.vue b/src/server/web/app/desktop/views/components/settings.drive.vue
similarity index 100%
rename from src/web/app/desktop/views/components/settings.drive.vue
rename to src/server/web/app/desktop/views/components/settings.drive.vue
diff --git a/src/web/app/desktop/views/components/settings.mute.vue b/src/server/web/app/desktop/views/components/settings.mute.vue
similarity index 100%
rename from src/web/app/desktop/views/components/settings.mute.vue
rename to src/server/web/app/desktop/views/components/settings.mute.vue
diff --git a/src/web/app/desktop/views/components/settings.password.vue b/src/server/web/app/desktop/views/components/settings.password.vue
similarity index 100%
rename from src/web/app/desktop/views/components/settings.password.vue
rename to src/server/web/app/desktop/views/components/settings.password.vue
diff --git a/src/web/app/desktop/views/components/settings.profile.vue b/src/server/web/app/desktop/views/components/settings.profile.vue
similarity index 100%
rename from src/web/app/desktop/views/components/settings.profile.vue
rename to src/server/web/app/desktop/views/components/settings.profile.vue
diff --git a/src/web/app/desktop/views/components/settings.signins.vue b/src/server/web/app/desktop/views/components/settings.signins.vue
similarity index 100%
rename from src/web/app/desktop/views/components/settings.signins.vue
rename to src/server/web/app/desktop/views/components/settings.signins.vue
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/server/web/app/desktop/views/components/settings.vue
similarity index 100%
rename from src/web/app/desktop/views/components/settings.vue
rename to src/server/web/app/desktop/views/components/settings.vue
diff --git a/src/web/app/desktop/views/components/sub-post-content.vue b/src/server/web/app/desktop/views/components/sub-post-content.vue
similarity index 100%
rename from src/web/app/desktop/views/components/sub-post-content.vue
rename to src/server/web/app/desktop/views/components/sub-post-content.vue
diff --git a/src/web/app/desktop/views/components/taskmanager.vue b/src/server/web/app/desktop/views/components/taskmanager.vue
similarity index 100%
rename from src/web/app/desktop/views/components/taskmanager.vue
rename to src/server/web/app/desktop/views/components/taskmanager.vue
diff --git a/src/web/app/desktop/views/components/timeline.vue b/src/server/web/app/desktop/views/components/timeline.vue
similarity index 100%
rename from src/web/app/desktop/views/components/timeline.vue
rename to src/server/web/app/desktop/views/components/timeline.vue
diff --git a/src/web/app/desktop/views/components/ui-notification.vue b/src/server/web/app/desktop/views/components/ui-notification.vue
similarity index 100%
rename from src/web/app/desktop/views/components/ui-notification.vue
rename to src/server/web/app/desktop/views/components/ui-notification.vue
diff --git a/src/web/app/desktop/views/components/ui.header.account.vue b/src/server/web/app/desktop/views/components/ui.header.account.vue
similarity index 100%
rename from src/web/app/desktop/views/components/ui.header.account.vue
rename to src/server/web/app/desktop/views/components/ui.header.account.vue
diff --git a/src/web/app/desktop/views/components/ui.header.clock.vue b/src/server/web/app/desktop/views/components/ui.header.clock.vue
similarity index 100%
rename from src/web/app/desktop/views/components/ui.header.clock.vue
rename to src/server/web/app/desktop/views/components/ui.header.clock.vue
diff --git a/src/web/app/desktop/views/components/ui.header.nav.vue b/src/server/web/app/desktop/views/components/ui.header.nav.vue
similarity index 100%
rename from src/web/app/desktop/views/components/ui.header.nav.vue
rename to src/server/web/app/desktop/views/components/ui.header.nav.vue
diff --git a/src/web/app/desktop/views/components/ui.header.notifications.vue b/src/server/web/app/desktop/views/components/ui.header.notifications.vue
similarity index 100%
rename from src/web/app/desktop/views/components/ui.header.notifications.vue
rename to src/server/web/app/desktop/views/components/ui.header.notifications.vue
diff --git a/src/web/app/desktop/views/components/ui.header.post.vue b/src/server/web/app/desktop/views/components/ui.header.post.vue
similarity index 100%
rename from src/web/app/desktop/views/components/ui.header.post.vue
rename to src/server/web/app/desktop/views/components/ui.header.post.vue
diff --git a/src/web/app/desktop/views/components/ui.header.search.vue b/src/server/web/app/desktop/views/components/ui.header.search.vue
similarity index 100%
rename from src/web/app/desktop/views/components/ui.header.search.vue
rename to src/server/web/app/desktop/views/components/ui.header.search.vue
diff --git a/src/web/app/desktop/views/components/ui.header.vue b/src/server/web/app/desktop/views/components/ui.header.vue
similarity index 100%
rename from src/web/app/desktop/views/components/ui.header.vue
rename to src/server/web/app/desktop/views/components/ui.header.vue
diff --git a/src/web/app/desktop/views/components/ui.vue b/src/server/web/app/desktop/views/components/ui.vue
similarity index 100%
rename from src/web/app/desktop/views/components/ui.vue
rename to src/server/web/app/desktop/views/components/ui.vue
diff --git a/src/web/app/desktop/views/components/user-preview.vue b/src/server/web/app/desktop/views/components/user-preview.vue
similarity index 100%
rename from src/web/app/desktop/views/components/user-preview.vue
rename to src/server/web/app/desktop/views/components/user-preview.vue
diff --git a/src/web/app/desktop/views/components/users-list.item.vue b/src/server/web/app/desktop/views/components/users-list.item.vue
similarity index 100%
rename from src/web/app/desktop/views/components/users-list.item.vue
rename to src/server/web/app/desktop/views/components/users-list.item.vue
diff --git a/src/web/app/desktop/views/components/users-list.vue b/src/server/web/app/desktop/views/components/users-list.vue
similarity index 100%
rename from src/web/app/desktop/views/components/users-list.vue
rename to src/server/web/app/desktop/views/components/users-list.vue
diff --git a/src/web/app/desktop/views/components/widget-container.vue b/src/server/web/app/desktop/views/components/widget-container.vue
similarity index 100%
rename from src/web/app/desktop/views/components/widget-container.vue
rename to src/server/web/app/desktop/views/components/widget-container.vue
diff --git a/src/web/app/desktop/views/components/window.vue b/src/server/web/app/desktop/views/components/window.vue
similarity index 100%
rename from src/web/app/desktop/views/components/window.vue
rename to src/server/web/app/desktop/views/components/window.vue
diff --git a/src/web/app/desktop/views/directives/index.ts b/src/server/web/app/desktop/views/directives/index.ts
similarity index 100%
rename from src/web/app/desktop/views/directives/index.ts
rename to src/server/web/app/desktop/views/directives/index.ts
diff --git a/src/web/app/desktop/views/directives/user-preview.ts b/src/server/web/app/desktop/views/directives/user-preview.ts
similarity index 100%
rename from src/web/app/desktop/views/directives/user-preview.ts
rename to src/server/web/app/desktop/views/directives/user-preview.ts
diff --git a/src/web/app/desktop/views/pages/drive.vue b/src/server/web/app/desktop/views/pages/drive.vue
similarity index 100%
rename from src/web/app/desktop/views/pages/drive.vue
rename to src/server/web/app/desktop/views/pages/drive.vue
diff --git a/src/web/app/desktop/views/pages/home-customize.vue b/src/server/web/app/desktop/views/pages/home-customize.vue
similarity index 100%
rename from src/web/app/desktop/views/pages/home-customize.vue
rename to src/server/web/app/desktop/views/pages/home-customize.vue
diff --git a/src/web/app/desktop/views/pages/home.vue b/src/server/web/app/desktop/views/pages/home.vue
similarity index 100%
rename from src/web/app/desktop/views/pages/home.vue
rename to src/server/web/app/desktop/views/pages/home.vue
diff --git a/src/web/app/desktop/views/pages/index.vue b/src/server/web/app/desktop/views/pages/index.vue
similarity index 100%
rename from src/web/app/desktop/views/pages/index.vue
rename to src/server/web/app/desktop/views/pages/index.vue
diff --git a/src/web/app/desktop/views/pages/messaging-room.vue b/src/server/web/app/desktop/views/pages/messaging-room.vue
similarity index 100%
rename from src/web/app/desktop/views/pages/messaging-room.vue
rename to src/server/web/app/desktop/views/pages/messaging-room.vue
diff --git a/src/web/app/desktop/views/pages/othello.vue b/src/server/web/app/desktop/views/pages/othello.vue
similarity index 100%
rename from src/web/app/desktop/views/pages/othello.vue
rename to src/server/web/app/desktop/views/pages/othello.vue
diff --git a/src/web/app/desktop/views/pages/post.vue b/src/server/web/app/desktop/views/pages/post.vue
similarity index 100%
rename from src/web/app/desktop/views/pages/post.vue
rename to src/server/web/app/desktop/views/pages/post.vue
diff --git a/src/web/app/desktop/views/pages/search.vue b/src/server/web/app/desktop/views/pages/search.vue
similarity index 100%
rename from src/web/app/desktop/views/pages/search.vue
rename to src/server/web/app/desktop/views/pages/search.vue
diff --git a/src/web/app/desktop/views/pages/selectdrive.vue b/src/server/web/app/desktop/views/pages/selectdrive.vue
similarity index 100%
rename from src/web/app/desktop/views/pages/selectdrive.vue
rename to src/server/web/app/desktop/views/pages/selectdrive.vue
diff --git a/src/web/app/desktop/views/pages/user/user.followers-you-know.vue b/src/server/web/app/desktop/views/pages/user/user.followers-you-know.vue
similarity index 100%
rename from src/web/app/desktop/views/pages/user/user.followers-you-know.vue
rename to src/server/web/app/desktop/views/pages/user/user.followers-you-know.vue
diff --git a/src/web/app/desktop/views/pages/user/user.friends.vue b/src/server/web/app/desktop/views/pages/user/user.friends.vue
similarity index 100%
rename from src/web/app/desktop/views/pages/user/user.friends.vue
rename to src/server/web/app/desktop/views/pages/user/user.friends.vue
diff --git a/src/web/app/desktop/views/pages/user/user.header.vue b/src/server/web/app/desktop/views/pages/user/user.header.vue
similarity index 100%
rename from src/web/app/desktop/views/pages/user/user.header.vue
rename to src/server/web/app/desktop/views/pages/user/user.header.vue
diff --git a/src/web/app/desktop/views/pages/user/user.home.vue b/src/server/web/app/desktop/views/pages/user/user.home.vue
similarity index 100%
rename from src/web/app/desktop/views/pages/user/user.home.vue
rename to src/server/web/app/desktop/views/pages/user/user.home.vue
diff --git a/src/web/app/desktop/views/pages/user/user.photos.vue b/src/server/web/app/desktop/views/pages/user/user.photos.vue
similarity index 100%
rename from src/web/app/desktop/views/pages/user/user.photos.vue
rename to src/server/web/app/desktop/views/pages/user/user.photos.vue
diff --git a/src/web/app/desktop/views/pages/user/user.profile.vue b/src/server/web/app/desktop/views/pages/user/user.profile.vue
similarity index 100%
rename from src/web/app/desktop/views/pages/user/user.profile.vue
rename to src/server/web/app/desktop/views/pages/user/user.profile.vue
diff --git a/src/web/app/desktop/views/pages/user/user.timeline.vue b/src/server/web/app/desktop/views/pages/user/user.timeline.vue
similarity index 100%
rename from src/web/app/desktop/views/pages/user/user.timeline.vue
rename to src/server/web/app/desktop/views/pages/user/user.timeline.vue
diff --git a/src/web/app/desktop/views/pages/user/user.vue b/src/server/web/app/desktop/views/pages/user/user.vue
similarity index 100%
rename from src/web/app/desktop/views/pages/user/user.vue
rename to src/server/web/app/desktop/views/pages/user/user.vue
diff --git a/src/web/app/desktop/views/pages/welcome.vue b/src/server/web/app/desktop/views/pages/welcome.vue
similarity index 100%
rename from src/web/app/desktop/views/pages/welcome.vue
rename to src/server/web/app/desktop/views/pages/welcome.vue
diff --git a/src/web/app/desktop/views/widgets/activity.vue b/src/server/web/app/desktop/views/widgets/activity.vue
similarity index 100%
rename from src/web/app/desktop/views/widgets/activity.vue
rename to src/server/web/app/desktop/views/widgets/activity.vue
diff --git a/src/web/app/desktop/views/widgets/channel.channel.form.vue b/src/server/web/app/desktop/views/widgets/channel.channel.form.vue
similarity index 100%
rename from src/web/app/desktop/views/widgets/channel.channel.form.vue
rename to src/server/web/app/desktop/views/widgets/channel.channel.form.vue
diff --git a/src/web/app/desktop/views/widgets/channel.channel.post.vue b/src/server/web/app/desktop/views/widgets/channel.channel.post.vue
similarity index 100%
rename from src/web/app/desktop/views/widgets/channel.channel.post.vue
rename to src/server/web/app/desktop/views/widgets/channel.channel.post.vue
diff --git a/src/web/app/desktop/views/widgets/channel.channel.vue b/src/server/web/app/desktop/views/widgets/channel.channel.vue
similarity index 100%
rename from src/web/app/desktop/views/widgets/channel.channel.vue
rename to src/server/web/app/desktop/views/widgets/channel.channel.vue
diff --git a/src/web/app/desktop/views/widgets/channel.vue b/src/server/web/app/desktop/views/widgets/channel.vue
similarity index 100%
rename from src/web/app/desktop/views/widgets/channel.vue
rename to src/server/web/app/desktop/views/widgets/channel.vue
diff --git a/src/web/app/desktop/views/widgets/index.ts b/src/server/web/app/desktop/views/widgets/index.ts
similarity index 100%
rename from src/web/app/desktop/views/widgets/index.ts
rename to src/server/web/app/desktop/views/widgets/index.ts
diff --git a/src/web/app/desktop/views/widgets/messaging.vue b/src/server/web/app/desktop/views/widgets/messaging.vue
similarity index 100%
rename from src/web/app/desktop/views/widgets/messaging.vue
rename to src/server/web/app/desktop/views/widgets/messaging.vue
diff --git a/src/web/app/desktop/views/widgets/notifications.vue b/src/server/web/app/desktop/views/widgets/notifications.vue
similarity index 100%
rename from src/web/app/desktop/views/widgets/notifications.vue
rename to src/server/web/app/desktop/views/widgets/notifications.vue
diff --git a/src/web/app/desktop/views/widgets/polls.vue b/src/server/web/app/desktop/views/widgets/polls.vue
similarity index 100%
rename from src/web/app/desktop/views/widgets/polls.vue
rename to src/server/web/app/desktop/views/widgets/polls.vue
diff --git a/src/web/app/desktop/views/widgets/post-form.vue b/src/server/web/app/desktop/views/widgets/post-form.vue
similarity index 100%
rename from src/web/app/desktop/views/widgets/post-form.vue
rename to src/server/web/app/desktop/views/widgets/post-form.vue
diff --git a/src/web/app/desktop/views/widgets/profile.vue b/src/server/web/app/desktop/views/widgets/profile.vue
similarity index 100%
rename from src/web/app/desktop/views/widgets/profile.vue
rename to src/server/web/app/desktop/views/widgets/profile.vue
diff --git a/src/web/app/desktop/views/widgets/timemachine.vue b/src/server/web/app/desktop/views/widgets/timemachine.vue
similarity index 100%
rename from src/web/app/desktop/views/widgets/timemachine.vue
rename to src/server/web/app/desktop/views/widgets/timemachine.vue
diff --git a/src/web/app/desktop/views/widgets/trends.vue b/src/server/web/app/desktop/views/widgets/trends.vue
similarity index 100%
rename from src/web/app/desktop/views/widgets/trends.vue
rename to src/server/web/app/desktop/views/widgets/trends.vue
diff --git a/src/web/app/desktop/views/widgets/users.vue b/src/server/web/app/desktop/views/widgets/users.vue
similarity index 100%
rename from src/web/app/desktop/views/widgets/users.vue
rename to src/server/web/app/desktop/views/widgets/users.vue
diff --git a/src/web/app/dev/script.ts b/src/server/web/app/dev/script.ts
similarity index 100%
rename from src/web/app/dev/script.ts
rename to src/server/web/app/dev/script.ts
diff --git a/src/web/app/dev/style.styl b/src/server/web/app/dev/style.styl
similarity index 100%
rename from src/web/app/dev/style.styl
rename to src/server/web/app/dev/style.styl
diff --git a/src/web/app/dev/views/app.vue b/src/server/web/app/dev/views/app.vue
similarity index 100%
rename from src/web/app/dev/views/app.vue
rename to src/server/web/app/dev/views/app.vue
diff --git a/src/web/app/dev/views/apps.vue b/src/server/web/app/dev/views/apps.vue
similarity index 100%
rename from src/web/app/dev/views/apps.vue
rename to src/server/web/app/dev/views/apps.vue
diff --git a/src/web/app/dev/views/index.vue b/src/server/web/app/dev/views/index.vue
similarity index 100%
rename from src/web/app/dev/views/index.vue
rename to src/server/web/app/dev/views/index.vue
diff --git a/src/web/app/dev/views/new-app.vue b/src/server/web/app/dev/views/new-app.vue
similarity index 100%
rename from src/web/app/dev/views/new-app.vue
rename to src/server/web/app/dev/views/new-app.vue
diff --git a/src/web/app/dev/views/ui.vue b/src/server/web/app/dev/views/ui.vue
similarity index 100%
rename from src/web/app/dev/views/ui.vue
rename to src/server/web/app/dev/views/ui.vue
diff --git a/src/web/app/init.css b/src/server/web/app/init.css
similarity index 100%
rename from src/web/app/init.css
rename to src/server/web/app/init.css
diff --git a/src/web/app/init.ts b/src/server/web/app/init.ts
similarity index 100%
rename from src/web/app/init.ts
rename to src/server/web/app/init.ts
diff --git a/src/web/app/mobile/api/choose-drive-file.ts b/src/server/web/app/mobile/api/choose-drive-file.ts
similarity index 100%
rename from src/web/app/mobile/api/choose-drive-file.ts
rename to src/server/web/app/mobile/api/choose-drive-file.ts
diff --git a/src/web/app/mobile/api/choose-drive-folder.ts b/src/server/web/app/mobile/api/choose-drive-folder.ts
similarity index 100%
rename from src/web/app/mobile/api/choose-drive-folder.ts
rename to src/server/web/app/mobile/api/choose-drive-folder.ts
diff --git a/src/web/app/mobile/api/dialog.ts b/src/server/web/app/mobile/api/dialog.ts
similarity index 100%
rename from src/web/app/mobile/api/dialog.ts
rename to src/server/web/app/mobile/api/dialog.ts
diff --git a/src/web/app/mobile/api/input.ts b/src/server/web/app/mobile/api/input.ts
similarity index 100%
rename from src/web/app/mobile/api/input.ts
rename to src/server/web/app/mobile/api/input.ts
diff --git a/src/web/app/mobile/api/notify.ts b/src/server/web/app/mobile/api/notify.ts
similarity index 100%
rename from src/web/app/mobile/api/notify.ts
rename to src/server/web/app/mobile/api/notify.ts
diff --git a/src/web/app/mobile/api/post.ts b/src/server/web/app/mobile/api/post.ts
similarity index 100%
rename from src/web/app/mobile/api/post.ts
rename to src/server/web/app/mobile/api/post.ts
diff --git a/src/web/app/mobile/script.ts b/src/server/web/app/mobile/script.ts
similarity index 100%
rename from src/web/app/mobile/script.ts
rename to src/server/web/app/mobile/script.ts
diff --git a/src/web/app/mobile/style.styl b/src/server/web/app/mobile/style.styl
similarity index 100%
rename from src/web/app/mobile/style.styl
rename to src/server/web/app/mobile/style.styl
diff --git a/src/web/app/mobile/views/components/activity.vue b/src/server/web/app/mobile/views/components/activity.vue
similarity index 100%
rename from src/web/app/mobile/views/components/activity.vue
rename to src/server/web/app/mobile/views/components/activity.vue
diff --git a/src/web/app/mobile/views/components/drive-file-chooser.vue b/src/server/web/app/mobile/views/components/drive-file-chooser.vue
similarity index 100%
rename from src/web/app/mobile/views/components/drive-file-chooser.vue
rename to src/server/web/app/mobile/views/components/drive-file-chooser.vue
diff --git a/src/web/app/mobile/views/components/drive-folder-chooser.vue b/src/server/web/app/mobile/views/components/drive-folder-chooser.vue
similarity index 100%
rename from src/web/app/mobile/views/components/drive-folder-chooser.vue
rename to src/server/web/app/mobile/views/components/drive-folder-chooser.vue
diff --git a/src/web/app/mobile/views/components/drive.file-detail.vue b/src/server/web/app/mobile/views/components/drive.file-detail.vue
similarity index 100%
rename from src/web/app/mobile/views/components/drive.file-detail.vue
rename to src/server/web/app/mobile/views/components/drive.file-detail.vue
diff --git a/src/web/app/mobile/views/components/drive.file.vue b/src/server/web/app/mobile/views/components/drive.file.vue
similarity index 100%
rename from src/web/app/mobile/views/components/drive.file.vue
rename to src/server/web/app/mobile/views/components/drive.file.vue
diff --git a/src/web/app/mobile/views/components/drive.folder.vue b/src/server/web/app/mobile/views/components/drive.folder.vue
similarity index 100%
rename from src/web/app/mobile/views/components/drive.folder.vue
rename to src/server/web/app/mobile/views/components/drive.folder.vue
diff --git a/src/web/app/mobile/views/components/drive.vue b/src/server/web/app/mobile/views/components/drive.vue
similarity index 100%
rename from src/web/app/mobile/views/components/drive.vue
rename to src/server/web/app/mobile/views/components/drive.vue
diff --git a/src/web/app/mobile/views/components/follow-button.vue b/src/server/web/app/mobile/views/components/follow-button.vue
similarity index 100%
rename from src/web/app/mobile/views/components/follow-button.vue
rename to src/server/web/app/mobile/views/components/follow-button.vue
diff --git a/src/web/app/mobile/views/components/friends-maker.vue b/src/server/web/app/mobile/views/components/friends-maker.vue
similarity index 100%
rename from src/web/app/mobile/views/components/friends-maker.vue
rename to src/server/web/app/mobile/views/components/friends-maker.vue
diff --git a/src/web/app/mobile/views/components/index.ts b/src/server/web/app/mobile/views/components/index.ts
similarity index 100%
rename from src/web/app/mobile/views/components/index.ts
rename to src/server/web/app/mobile/views/components/index.ts
diff --git a/src/web/app/mobile/views/components/media-image.vue b/src/server/web/app/mobile/views/components/media-image.vue
similarity index 100%
rename from src/web/app/mobile/views/components/media-image.vue
rename to src/server/web/app/mobile/views/components/media-image.vue
diff --git a/src/web/app/mobile/views/components/media-video.vue b/src/server/web/app/mobile/views/components/media-video.vue
similarity index 100%
rename from src/web/app/mobile/views/components/media-video.vue
rename to src/server/web/app/mobile/views/components/media-video.vue
diff --git a/src/web/app/mobile/views/components/notification-preview.vue b/src/server/web/app/mobile/views/components/notification-preview.vue
similarity index 100%
rename from src/web/app/mobile/views/components/notification-preview.vue
rename to src/server/web/app/mobile/views/components/notification-preview.vue
diff --git a/src/web/app/mobile/views/components/notification.vue b/src/server/web/app/mobile/views/components/notification.vue
similarity index 100%
rename from src/web/app/mobile/views/components/notification.vue
rename to src/server/web/app/mobile/views/components/notification.vue
diff --git a/src/web/app/mobile/views/components/notifications.vue b/src/server/web/app/mobile/views/components/notifications.vue
similarity index 100%
rename from src/web/app/mobile/views/components/notifications.vue
rename to src/server/web/app/mobile/views/components/notifications.vue
diff --git a/src/web/app/mobile/views/components/notify.vue b/src/server/web/app/mobile/views/components/notify.vue
similarity index 100%
rename from src/web/app/mobile/views/components/notify.vue
rename to src/server/web/app/mobile/views/components/notify.vue
diff --git a/src/web/app/mobile/views/components/post-card.vue b/src/server/web/app/mobile/views/components/post-card.vue
similarity index 100%
rename from src/web/app/mobile/views/components/post-card.vue
rename to src/server/web/app/mobile/views/components/post-card.vue
diff --git a/src/web/app/mobile/views/components/post-detail.sub.vue b/src/server/web/app/mobile/views/components/post-detail.sub.vue
similarity index 100%
rename from src/web/app/mobile/views/components/post-detail.sub.vue
rename to src/server/web/app/mobile/views/components/post-detail.sub.vue
diff --git a/src/web/app/mobile/views/components/post-detail.vue b/src/server/web/app/mobile/views/components/post-detail.vue
similarity index 100%
rename from src/web/app/mobile/views/components/post-detail.vue
rename to src/server/web/app/mobile/views/components/post-detail.vue
diff --git a/src/web/app/mobile/views/components/post-form.vue b/src/server/web/app/mobile/views/components/post-form.vue
similarity index 100%
rename from src/web/app/mobile/views/components/post-form.vue
rename to src/server/web/app/mobile/views/components/post-form.vue
diff --git a/src/web/app/mobile/views/components/post-preview.vue b/src/server/web/app/mobile/views/components/post-preview.vue
similarity index 100%
rename from src/web/app/mobile/views/components/post-preview.vue
rename to src/server/web/app/mobile/views/components/post-preview.vue
diff --git a/src/web/app/mobile/views/components/post.sub.vue b/src/server/web/app/mobile/views/components/post.sub.vue
similarity index 100%
rename from src/web/app/mobile/views/components/post.sub.vue
rename to src/server/web/app/mobile/views/components/post.sub.vue
diff --git a/src/web/app/mobile/views/components/post.vue b/src/server/web/app/mobile/views/components/post.vue
similarity index 100%
rename from src/web/app/mobile/views/components/post.vue
rename to src/server/web/app/mobile/views/components/post.vue
diff --git a/src/web/app/mobile/views/components/posts.vue b/src/server/web/app/mobile/views/components/posts.vue
similarity index 100%
rename from src/web/app/mobile/views/components/posts.vue
rename to src/server/web/app/mobile/views/components/posts.vue
diff --git a/src/web/app/mobile/views/components/sub-post-content.vue b/src/server/web/app/mobile/views/components/sub-post-content.vue
similarity index 100%
rename from src/web/app/mobile/views/components/sub-post-content.vue
rename to src/server/web/app/mobile/views/components/sub-post-content.vue
diff --git a/src/web/app/mobile/views/components/timeline.vue b/src/server/web/app/mobile/views/components/timeline.vue
similarity index 100%
rename from src/web/app/mobile/views/components/timeline.vue
rename to src/server/web/app/mobile/views/components/timeline.vue
diff --git a/src/web/app/mobile/views/components/ui.header.vue b/src/server/web/app/mobile/views/components/ui.header.vue
similarity index 100%
rename from src/web/app/mobile/views/components/ui.header.vue
rename to src/server/web/app/mobile/views/components/ui.header.vue
diff --git a/src/web/app/mobile/views/components/ui.nav.vue b/src/server/web/app/mobile/views/components/ui.nav.vue
similarity index 100%
rename from src/web/app/mobile/views/components/ui.nav.vue
rename to src/server/web/app/mobile/views/components/ui.nav.vue
diff --git a/src/web/app/mobile/views/components/ui.vue b/src/server/web/app/mobile/views/components/ui.vue
similarity index 100%
rename from src/web/app/mobile/views/components/ui.vue
rename to src/server/web/app/mobile/views/components/ui.vue
diff --git a/src/web/app/mobile/views/components/user-card.vue b/src/server/web/app/mobile/views/components/user-card.vue
similarity index 100%
rename from src/web/app/mobile/views/components/user-card.vue
rename to src/server/web/app/mobile/views/components/user-card.vue
diff --git a/src/web/app/mobile/views/components/user-preview.vue b/src/server/web/app/mobile/views/components/user-preview.vue
similarity index 100%
rename from src/web/app/mobile/views/components/user-preview.vue
rename to src/server/web/app/mobile/views/components/user-preview.vue
diff --git a/src/web/app/mobile/views/components/user-timeline.vue b/src/server/web/app/mobile/views/components/user-timeline.vue
similarity index 100%
rename from src/web/app/mobile/views/components/user-timeline.vue
rename to src/server/web/app/mobile/views/components/user-timeline.vue
diff --git a/src/web/app/mobile/views/components/users-list.vue b/src/server/web/app/mobile/views/components/users-list.vue
similarity index 100%
rename from src/web/app/mobile/views/components/users-list.vue
rename to src/server/web/app/mobile/views/components/users-list.vue
diff --git a/src/web/app/mobile/views/components/widget-container.vue b/src/server/web/app/mobile/views/components/widget-container.vue
similarity index 100%
rename from src/web/app/mobile/views/components/widget-container.vue
rename to src/server/web/app/mobile/views/components/widget-container.vue
diff --git a/src/web/app/mobile/views/directives/index.ts b/src/server/web/app/mobile/views/directives/index.ts
similarity index 100%
rename from src/web/app/mobile/views/directives/index.ts
rename to src/server/web/app/mobile/views/directives/index.ts
diff --git a/src/web/app/mobile/views/directives/user-preview.ts b/src/server/web/app/mobile/views/directives/user-preview.ts
similarity index 100%
rename from src/web/app/mobile/views/directives/user-preview.ts
rename to src/server/web/app/mobile/views/directives/user-preview.ts
diff --git a/src/web/app/mobile/views/pages/drive.vue b/src/server/web/app/mobile/views/pages/drive.vue
similarity index 100%
rename from src/web/app/mobile/views/pages/drive.vue
rename to src/server/web/app/mobile/views/pages/drive.vue
diff --git a/src/web/app/mobile/views/pages/followers.vue b/src/server/web/app/mobile/views/pages/followers.vue
similarity index 100%
rename from src/web/app/mobile/views/pages/followers.vue
rename to src/server/web/app/mobile/views/pages/followers.vue
diff --git a/src/web/app/mobile/views/pages/following.vue b/src/server/web/app/mobile/views/pages/following.vue
similarity index 100%
rename from src/web/app/mobile/views/pages/following.vue
rename to src/server/web/app/mobile/views/pages/following.vue
diff --git a/src/web/app/mobile/views/pages/home.vue b/src/server/web/app/mobile/views/pages/home.vue
similarity index 100%
rename from src/web/app/mobile/views/pages/home.vue
rename to src/server/web/app/mobile/views/pages/home.vue
diff --git a/src/web/app/mobile/views/pages/index.vue b/src/server/web/app/mobile/views/pages/index.vue
similarity index 100%
rename from src/web/app/mobile/views/pages/index.vue
rename to src/server/web/app/mobile/views/pages/index.vue
diff --git a/src/web/app/mobile/views/pages/messaging-room.vue b/src/server/web/app/mobile/views/pages/messaging-room.vue
similarity index 100%
rename from src/web/app/mobile/views/pages/messaging-room.vue
rename to src/server/web/app/mobile/views/pages/messaging-room.vue
diff --git a/src/web/app/mobile/views/pages/messaging.vue b/src/server/web/app/mobile/views/pages/messaging.vue
similarity index 100%
rename from src/web/app/mobile/views/pages/messaging.vue
rename to src/server/web/app/mobile/views/pages/messaging.vue
diff --git a/src/web/app/mobile/views/pages/notifications.vue b/src/server/web/app/mobile/views/pages/notifications.vue
similarity index 100%
rename from src/web/app/mobile/views/pages/notifications.vue
rename to src/server/web/app/mobile/views/pages/notifications.vue
diff --git a/src/web/app/mobile/views/pages/othello.vue b/src/server/web/app/mobile/views/pages/othello.vue
similarity index 100%
rename from src/web/app/mobile/views/pages/othello.vue
rename to src/server/web/app/mobile/views/pages/othello.vue
diff --git a/src/web/app/mobile/views/pages/post.vue b/src/server/web/app/mobile/views/pages/post.vue
similarity index 100%
rename from src/web/app/mobile/views/pages/post.vue
rename to src/server/web/app/mobile/views/pages/post.vue
diff --git a/src/web/app/mobile/views/pages/profile-setting.vue b/src/server/web/app/mobile/views/pages/profile-setting.vue
similarity index 100%
rename from src/web/app/mobile/views/pages/profile-setting.vue
rename to src/server/web/app/mobile/views/pages/profile-setting.vue
diff --git a/src/web/app/mobile/views/pages/search.vue b/src/server/web/app/mobile/views/pages/search.vue
similarity index 100%
rename from src/web/app/mobile/views/pages/search.vue
rename to src/server/web/app/mobile/views/pages/search.vue
diff --git a/src/web/app/mobile/views/pages/selectdrive.vue b/src/server/web/app/mobile/views/pages/selectdrive.vue
similarity index 100%
rename from src/web/app/mobile/views/pages/selectdrive.vue
rename to src/server/web/app/mobile/views/pages/selectdrive.vue
diff --git a/src/web/app/mobile/views/pages/settings.vue b/src/server/web/app/mobile/views/pages/settings.vue
similarity index 100%
rename from src/web/app/mobile/views/pages/settings.vue
rename to src/server/web/app/mobile/views/pages/settings.vue
diff --git a/src/web/app/mobile/views/pages/signup.vue b/src/server/web/app/mobile/views/pages/signup.vue
similarity index 100%
rename from src/web/app/mobile/views/pages/signup.vue
rename to src/server/web/app/mobile/views/pages/signup.vue
diff --git a/src/web/app/mobile/views/pages/user.vue b/src/server/web/app/mobile/views/pages/user.vue
similarity index 100%
rename from src/web/app/mobile/views/pages/user.vue
rename to src/server/web/app/mobile/views/pages/user.vue
diff --git a/src/web/app/mobile/views/pages/user/home.followers-you-know.vue b/src/server/web/app/mobile/views/pages/user/home.followers-you-know.vue
similarity index 100%
rename from src/web/app/mobile/views/pages/user/home.followers-you-know.vue
rename to src/server/web/app/mobile/views/pages/user/home.followers-you-know.vue
diff --git a/src/web/app/mobile/views/pages/user/home.friends.vue b/src/server/web/app/mobile/views/pages/user/home.friends.vue
similarity index 100%
rename from src/web/app/mobile/views/pages/user/home.friends.vue
rename to src/server/web/app/mobile/views/pages/user/home.friends.vue
diff --git a/src/web/app/mobile/views/pages/user/home.photos.vue b/src/server/web/app/mobile/views/pages/user/home.photos.vue
similarity index 100%
rename from src/web/app/mobile/views/pages/user/home.photos.vue
rename to src/server/web/app/mobile/views/pages/user/home.photos.vue
diff --git a/src/web/app/mobile/views/pages/user/home.posts.vue b/src/server/web/app/mobile/views/pages/user/home.posts.vue
similarity index 100%
rename from src/web/app/mobile/views/pages/user/home.posts.vue
rename to src/server/web/app/mobile/views/pages/user/home.posts.vue
diff --git a/src/web/app/mobile/views/pages/user/home.vue b/src/server/web/app/mobile/views/pages/user/home.vue
similarity index 100%
rename from src/web/app/mobile/views/pages/user/home.vue
rename to src/server/web/app/mobile/views/pages/user/home.vue
diff --git a/src/web/app/mobile/views/pages/welcome.vue b/src/server/web/app/mobile/views/pages/welcome.vue
similarity index 100%
rename from src/web/app/mobile/views/pages/welcome.vue
rename to src/server/web/app/mobile/views/pages/welcome.vue
diff --git a/src/web/app/mobile/views/widgets/activity.vue b/src/server/web/app/mobile/views/widgets/activity.vue
similarity index 100%
rename from src/web/app/mobile/views/widgets/activity.vue
rename to src/server/web/app/mobile/views/widgets/activity.vue
diff --git a/src/web/app/mobile/views/widgets/index.ts b/src/server/web/app/mobile/views/widgets/index.ts
similarity index 100%
rename from src/web/app/mobile/views/widgets/index.ts
rename to src/server/web/app/mobile/views/widgets/index.ts
diff --git a/src/web/app/mobile/views/widgets/profile.vue b/src/server/web/app/mobile/views/widgets/profile.vue
similarity index 100%
rename from src/web/app/mobile/views/widgets/profile.vue
rename to src/server/web/app/mobile/views/widgets/profile.vue
diff --git a/src/web/app/reset.styl b/src/server/web/app/reset.styl
similarity index 100%
rename from src/web/app/reset.styl
rename to src/server/web/app/reset.styl
diff --git a/src/web/app/safe.js b/src/server/web/app/safe.js
similarity index 100%
rename from src/web/app/safe.js
rename to src/server/web/app/safe.js
diff --git a/src/web/app/stats/style.styl b/src/server/web/app/stats/style.styl
similarity index 100%
rename from src/web/app/stats/style.styl
rename to src/server/web/app/stats/style.styl
diff --git a/src/web/app/stats/tags/index.tag b/src/server/web/app/stats/tags/index.tag
similarity index 100%
rename from src/web/app/stats/tags/index.tag
rename to src/server/web/app/stats/tags/index.tag
diff --git a/src/web/app/stats/tags/index.ts b/src/server/web/app/stats/tags/index.ts
similarity index 100%
rename from src/web/app/stats/tags/index.ts
rename to src/server/web/app/stats/tags/index.ts
diff --git a/src/web/app/status/style.styl b/src/server/web/app/status/style.styl
similarity index 100%
rename from src/web/app/status/style.styl
rename to src/server/web/app/status/style.styl
diff --git a/src/web/app/status/tags/index.tag b/src/server/web/app/status/tags/index.tag
similarity index 100%
rename from src/web/app/status/tags/index.tag
rename to src/server/web/app/status/tags/index.tag
diff --git a/src/web/app/status/tags/index.ts b/src/server/web/app/status/tags/index.ts
similarity index 100%
rename from src/web/app/status/tags/index.ts
rename to src/server/web/app/status/tags/index.ts
diff --git a/src/web/app/sw.js b/src/server/web/app/sw.js
similarity index 100%
rename from src/web/app/sw.js
rename to src/server/web/app/sw.js
diff --git a/src/web/app/tsconfig.json b/src/server/web/app/tsconfig.json
similarity index 100%
rename from src/web/app/tsconfig.json
rename to src/server/web/app/tsconfig.json
diff --git a/src/web/app/v.d.ts b/src/server/web/app/v.d.ts
similarity index 100%
rename from src/web/app/v.d.ts
rename to src/server/web/app/v.d.ts
diff --git a/src/web/assets/404.js b/src/server/web/assets/404.js
similarity index 100%
rename from src/web/assets/404.js
rename to src/server/web/assets/404.js
diff --git a/src/web/assets/code-highlight.css b/src/server/web/assets/code-highlight.css
similarity index 100%
rename from src/web/assets/code-highlight.css
rename to src/server/web/assets/code-highlight.css
diff --git a/src/web/assets/error.jpg b/src/server/web/assets/error.jpg
similarity index 100%
rename from src/web/assets/error.jpg
rename to src/server/web/assets/error.jpg
diff --git a/src/web/assets/favicon.ico b/src/server/web/assets/favicon.ico
similarity index 100%
rename from src/web/assets/favicon.ico
rename to src/server/web/assets/favicon.ico
diff --git a/src/web/assets/label.svg b/src/server/web/assets/label.svg
similarity index 100%
rename from src/web/assets/label.svg
rename to src/server/web/assets/label.svg
diff --git a/src/web/assets/manifest.json b/src/server/web/assets/manifest.json
similarity index 100%
rename from src/web/assets/manifest.json
rename to src/server/web/assets/manifest.json
diff --git a/src/web/assets/message.mp3 b/src/server/web/assets/message.mp3
similarity index 100%
rename from src/web/assets/message.mp3
rename to src/server/web/assets/message.mp3
diff --git a/src/web/assets/othello-put-me.mp3 b/src/server/web/assets/othello-put-me.mp3
similarity index 100%
rename from src/web/assets/othello-put-me.mp3
rename to src/server/web/assets/othello-put-me.mp3
diff --git a/src/web/assets/othello-put-you.mp3 b/src/server/web/assets/othello-put-you.mp3
similarity index 100%
rename from src/web/assets/othello-put-you.mp3
rename to src/server/web/assets/othello-put-you.mp3
diff --git a/src/web/assets/post.mp3 b/src/server/web/assets/post.mp3
similarity index 100%
rename from src/web/assets/post.mp3
rename to src/server/web/assets/post.mp3
diff --git a/src/web/assets/reactions/angry.png b/src/server/web/assets/reactions/angry.png
similarity index 100%
rename from src/web/assets/reactions/angry.png
rename to src/server/web/assets/reactions/angry.png
diff --git a/src/web/assets/reactions/confused.png b/src/server/web/assets/reactions/confused.png
similarity index 100%
rename from src/web/assets/reactions/confused.png
rename to src/server/web/assets/reactions/confused.png
diff --git a/src/web/assets/reactions/congrats.png b/src/server/web/assets/reactions/congrats.png
similarity index 100%
rename from src/web/assets/reactions/congrats.png
rename to src/server/web/assets/reactions/congrats.png
diff --git a/src/web/assets/reactions/hmm.png b/src/server/web/assets/reactions/hmm.png
similarity index 100%
rename from src/web/assets/reactions/hmm.png
rename to src/server/web/assets/reactions/hmm.png
diff --git a/src/web/assets/reactions/laugh.png b/src/server/web/assets/reactions/laugh.png
similarity index 100%
rename from src/web/assets/reactions/laugh.png
rename to src/server/web/assets/reactions/laugh.png
diff --git a/src/web/assets/reactions/like.png b/src/server/web/assets/reactions/like.png
similarity index 100%
rename from src/web/assets/reactions/like.png
rename to src/server/web/assets/reactions/like.png
diff --git a/src/web/assets/reactions/love.png b/src/server/web/assets/reactions/love.png
similarity index 100%
rename from src/web/assets/reactions/love.png
rename to src/server/web/assets/reactions/love.png
diff --git a/src/web/assets/reactions/pudding.png b/src/server/web/assets/reactions/pudding.png
similarity index 100%
rename from src/web/assets/reactions/pudding.png
rename to src/server/web/assets/reactions/pudding.png
diff --git a/src/web/assets/reactions/surprise.png b/src/server/web/assets/reactions/surprise.png
similarity index 100%
rename from src/web/assets/reactions/surprise.png
rename to src/server/web/assets/reactions/surprise.png
diff --git a/src/web/assets/recover.html b/src/server/web/assets/recover.html
similarity index 100%
rename from src/web/assets/recover.html
rename to src/server/web/assets/recover.html
diff --git a/src/web/assets/title.svg b/src/server/web/assets/title.svg
similarity index 100%
rename from src/web/assets/title.svg
rename to src/server/web/assets/title.svg
diff --git a/src/web/assets/unread.svg b/src/server/web/assets/unread.svg
similarity index 100%
rename from src/web/assets/unread.svg
rename to src/server/web/assets/unread.svg
diff --git a/src/web/assets/welcome-bg.svg b/src/server/web/assets/welcome-bg.svg
similarity index 100%
rename from src/web/assets/welcome-bg.svg
rename to src/server/web/assets/welcome-bg.svg
diff --git a/src/web/assets/welcome-fg.svg b/src/server/web/assets/welcome-fg.svg
similarity index 100%
rename from src/web/assets/welcome-fg.svg
rename to src/server/web/assets/welcome-fg.svg
diff --git a/src/web/const.styl b/src/server/web/const.styl
similarity index 74%
rename from src/web/const.styl
rename to src/server/web/const.styl
index b6560701d..f16e07782 100644
--- a/src/web/const.styl
+++ b/src/server/web/const.styl
@@ -1,4 +1,4 @@
-json('../const.json')
+json('../../const.json')
 
 $theme-color = themeColor
 $theme-color-foreground = themeColorForeground
diff --git a/src/web/docs/about.en.pug b/src/server/web/docs/about.en.pug
similarity index 100%
rename from src/web/docs/about.en.pug
rename to src/server/web/docs/about.en.pug
diff --git a/src/web/docs/about.ja.pug b/src/server/web/docs/about.ja.pug
similarity index 100%
rename from src/web/docs/about.ja.pug
rename to src/server/web/docs/about.ja.pug
diff --git a/src/web/docs/api.ja.pug b/src/server/web/docs/api.ja.pug
similarity index 100%
rename from src/web/docs/api.ja.pug
rename to src/server/web/docs/api.ja.pug
diff --git a/src/web/docs/api/endpoints/posts/create.yaml b/src/server/web/docs/api/endpoints/posts/create.yaml
similarity index 100%
rename from src/web/docs/api/endpoints/posts/create.yaml
rename to src/server/web/docs/api/endpoints/posts/create.yaml
diff --git a/src/web/docs/api/endpoints/posts/timeline.yaml b/src/server/web/docs/api/endpoints/posts/timeline.yaml
similarity index 100%
rename from src/web/docs/api/endpoints/posts/timeline.yaml
rename to src/server/web/docs/api/endpoints/posts/timeline.yaml
diff --git a/src/web/docs/api/endpoints/style.styl b/src/server/web/docs/api/endpoints/style.styl
similarity index 100%
rename from src/web/docs/api/endpoints/style.styl
rename to src/server/web/docs/api/endpoints/style.styl
diff --git a/src/web/docs/api/endpoints/view.pug b/src/server/web/docs/api/endpoints/view.pug
similarity index 100%
rename from src/web/docs/api/endpoints/view.pug
rename to src/server/web/docs/api/endpoints/view.pug
diff --git a/src/web/docs/api/entities/drive-file.yaml b/src/server/web/docs/api/entities/drive-file.yaml
similarity index 100%
rename from src/web/docs/api/entities/drive-file.yaml
rename to src/server/web/docs/api/entities/drive-file.yaml
diff --git a/src/web/docs/api/entities/post.yaml b/src/server/web/docs/api/entities/post.yaml
similarity index 100%
rename from src/web/docs/api/entities/post.yaml
rename to src/server/web/docs/api/entities/post.yaml
diff --git a/src/web/docs/api/entities/style.styl b/src/server/web/docs/api/entities/style.styl
similarity index 100%
rename from src/web/docs/api/entities/style.styl
rename to src/server/web/docs/api/entities/style.styl
diff --git a/src/web/docs/api/entities/user.yaml b/src/server/web/docs/api/entities/user.yaml
similarity index 100%
rename from src/web/docs/api/entities/user.yaml
rename to src/server/web/docs/api/entities/user.yaml
diff --git a/src/web/docs/api/entities/view.pug b/src/server/web/docs/api/entities/view.pug
similarity index 100%
rename from src/web/docs/api/entities/view.pug
rename to src/server/web/docs/api/entities/view.pug
diff --git a/src/web/docs/api/gulpfile.ts b/src/server/web/docs/api/gulpfile.ts
similarity index 80%
rename from src/web/docs/api/gulpfile.ts
rename to src/server/web/docs/api/gulpfile.ts
index cd1bf1530..37935413d 100644
--- a/src/web/docs/api/gulpfile.ts
+++ b/src/server/web/docs/api/gulpfile.ts
@@ -10,10 +10,10 @@ import * as pug from 'pug';
 import * as yaml from 'js-yaml';
 import * as mkdirp from 'mkdirp';
 
-import locales from '../../../../locales';
-import I18nReplacer from '../../../common/build/i18n';
-import fa from '../../../common/build/fa';
-import config from './../../../conf';
+import locales from '../../../../../locales';
+import I18nReplacer from '../../../../build/i18n';
+import fa from '../../../../build/fa';
+import config from './../../../../conf';
 
 import generateVars from '../vars';
 
@@ -94,7 +94,7 @@ gulp.task('doc:api', [
 
 gulp.task('doc:api:endpoints', async () => {
 	const commonVars = await generateVars();
-	glob('./src/web/docs/api/endpoints/**/*.yaml', (globErr, files) => {
+	glob('./src/server/web/docs/api/endpoints/**/*.yaml', (globErr, files) => {
 		if (globErr) {
 			console.error(globErr);
 			return;
@@ -115,10 +115,10 @@ gulp.task('doc:api:endpoints', async () => {
 				resDefs: ep.res ? extractDefs(ep.res) : null,
 			};
 			langs.forEach(lang => {
-				pug.renderFile('./src/web/docs/api/endpoints/view.pug', Object.assign({}, vars, {
+				pug.renderFile('./src/server/web/docs/api/endpoints/view.pug', Object.assign({}, vars, {
 					lang,
 					title: ep.endpoint,
-					src: `https://github.com/syuilo/misskey/tree/master/src/web/docs/api/endpoints/${ep.endpoint}.yaml`,
+					src: `https://github.com/syuilo/misskey/tree/master/src/server/web/docs/api/endpoints/${ep.endpoint}.yaml`,
 					kebab,
 					common: commonVars
 				}), (renderErr, html) => {
@@ -129,7 +129,7 @@ gulp.task('doc:api:endpoints', async () => {
 					const i18n = new I18nReplacer(lang);
 					html = html.replace(i18n.pattern, i18n.replacement);
 					html = fa(html);
-					const htmlPath = `./built/web/docs/${lang}/api/endpoints/${ep.endpoint}.html`;
+					const htmlPath = `./built/server/web/docs/${lang}/api/endpoints/${ep.endpoint}.html`;
 					mkdirp(path.dirname(htmlPath), (mkdirErr) => {
 						if (mkdirErr) {
 							console.error(mkdirErr);
@@ -145,7 +145,7 @@ gulp.task('doc:api:endpoints', async () => {
 
 gulp.task('doc:api:entities', async () => {
 	const commonVars = await generateVars();
-	glob('./src/web/docs/api/entities/**/*.yaml', (globErr, files) => {
+	glob('./src/server/web/docs/api/entities/**/*.yaml', (globErr, files) => {
 		if (globErr) {
 			console.error(globErr);
 			return;
@@ -159,10 +159,10 @@ gulp.task('doc:api:entities', async () => {
 				propDefs: extractDefs(entity.props),
 			};
 			langs.forEach(lang => {
-				pug.renderFile('./src/web/docs/api/entities/view.pug', Object.assign({}, vars, {
+				pug.renderFile('./src/server/web/docs/api/entities/view.pug', Object.assign({}, vars, {
 					lang,
 					title: entity.name,
-					src: `https://github.com/syuilo/misskey/tree/master/src/web/docs/api/entities/${kebab(entity.name)}.yaml`,
+					src: `https://github.com/syuilo/misskey/tree/master/src/server/web/docs/api/entities/${kebab(entity.name)}.yaml`,
 					kebab,
 					common: commonVars
 				}), (renderErr, html) => {
@@ -173,7 +173,7 @@ gulp.task('doc:api:entities', async () => {
 					const i18n = new I18nReplacer(lang);
 					html = html.replace(i18n.pattern, i18n.replacement);
 					html = fa(html);
-					const htmlPath = `./built/web/docs/${lang}/api/entities/${kebab(entity.name)}.html`;
+					const htmlPath = `./built/server/web/docs/${lang}/api/entities/${kebab(entity.name)}.html`;
 					mkdirp(path.dirname(htmlPath), (mkdirErr) => {
 						if (mkdirErr) {
 							console.error(mkdirErr);
diff --git a/src/web/docs/api/mixins.pug b/src/server/web/docs/api/mixins.pug
similarity index 100%
rename from src/web/docs/api/mixins.pug
rename to src/server/web/docs/api/mixins.pug
diff --git a/src/web/docs/api/style.styl b/src/server/web/docs/api/style.styl
similarity index 100%
rename from src/web/docs/api/style.styl
rename to src/server/web/docs/api/style.styl
diff --git a/src/web/docs/gulpfile.ts b/src/server/web/docs/gulpfile.ts
similarity index 74%
rename from src/web/docs/gulpfile.ts
rename to src/server/web/docs/gulpfile.ts
index d5ddda108..7b36cf667 100644
--- a/src/web/docs/gulpfile.ts
+++ b/src/server/web/docs/gulpfile.ts
@@ -11,8 +11,8 @@ import * as mkdirp from 'mkdirp';
 import stylus = require('gulp-stylus');
 import cssnano = require('gulp-cssnano');
 
-import I18nReplacer from '../../common/build/i18n';
-import fa from '../../common/build/fa';
+import I18nReplacer from '../../../build/i18n';
+import fa from '../../../build/fa';
 import generateVars from './vars';
 
 require('./api/gulpfile.ts');
@@ -26,7 +26,7 @@ gulp.task('doc', [
 gulp.task('doc:docs', async () => {
 	const commonVars = await generateVars();
 
-	glob('./src/web/docs/**/*.*.pug', (globErr, files) => {
+	glob('./src/server/web/docs/**/*.*.pug', (globErr, files) => {
 		if (globErr) {
 			console.error(globErr);
 			return;
@@ -37,7 +37,7 @@ gulp.task('doc:docs', async () => {
 				common: commonVars,
 				lang: lang,
 				title: fs.readFileSync(file, 'utf-8').match(/^h1 (.+?)\r?\n/)[1],
-				src: `https://github.com/syuilo/misskey/tree/master/src/web/docs/${name}.${lang}.pug`,
+				src: `https://github.com/syuilo/misskey/tree/master/src/server/web/docs/${name}.${lang}.pug`,
 			};
 			pug.renderFile(file, vars, (renderErr, content) => {
 				if (renderErr) {
@@ -45,7 +45,7 @@ gulp.task('doc:docs', async () => {
 					return;
 				}
 
-				pug.renderFile('./src/web/docs/layout.pug', Object.assign({}, vars, {
+				pug.renderFile('./src/server/web/docs/layout.pug', Object.assign({}, vars, {
 					content
 				}), (renderErr2, html) => {
 					if (renderErr2) {
@@ -55,7 +55,7 @@ gulp.task('doc:docs', async () => {
 					const i18n = new I18nReplacer(lang);
 					html = html.replace(i18n.pattern, i18n.replacement);
 					html = fa(html);
-					const htmlPath = `./built/web/docs/${lang}/${name}.html`;
+					const htmlPath = `./built/server/web/docs/${lang}/${name}.html`;
 					mkdirp(path.dirname(htmlPath), (mkdirErr) => {
 						if (mkdirErr) {
 							console.error(mkdirErr);
@@ -70,8 +70,8 @@ gulp.task('doc:docs', async () => {
 });
 
 gulp.task('doc:styles', () =>
-	gulp.src('./src/web/docs/**/*.styl')
+	gulp.src('./src/server/web/docs/**/*.styl')
 		.pipe(stylus())
 		.pipe((cssnano as any)())
-		.pipe(gulp.dest('./built/web/docs/assets/'))
+		.pipe(gulp.dest('./built/server/web/docs/assets/'))
 );
diff --git a/src/web/docs/index.en.pug b/src/server/web/docs/index.en.pug
similarity index 100%
rename from src/web/docs/index.en.pug
rename to src/server/web/docs/index.en.pug
diff --git a/src/web/docs/index.ja.pug b/src/server/web/docs/index.ja.pug
similarity index 100%
rename from src/web/docs/index.ja.pug
rename to src/server/web/docs/index.ja.pug
diff --git a/src/web/docs/layout.pug b/src/server/web/docs/layout.pug
similarity index 100%
rename from src/web/docs/layout.pug
rename to src/server/web/docs/layout.pug
diff --git a/src/web/docs/license.en.pug b/src/server/web/docs/license.en.pug
similarity index 100%
rename from src/web/docs/license.en.pug
rename to src/server/web/docs/license.en.pug
diff --git a/src/web/docs/license.ja.pug b/src/server/web/docs/license.ja.pug
similarity index 100%
rename from src/web/docs/license.ja.pug
rename to src/server/web/docs/license.ja.pug
diff --git a/src/web/docs/mute.ja.pug b/src/server/web/docs/mute.ja.pug
similarity index 100%
rename from src/web/docs/mute.ja.pug
rename to src/server/web/docs/mute.ja.pug
diff --git a/src/web/docs/search.ja.pug b/src/server/web/docs/search.ja.pug
similarity index 100%
rename from src/web/docs/search.ja.pug
rename to src/server/web/docs/search.ja.pug
diff --git a/src/web/docs/server.ts b/src/server/web/docs/server.ts
similarity index 100%
rename from src/web/docs/server.ts
rename to src/server/web/docs/server.ts
diff --git a/src/web/docs/style.styl b/src/server/web/docs/style.styl
similarity index 100%
rename from src/web/docs/style.styl
rename to src/server/web/docs/style.styl
diff --git a/src/web/docs/tou.ja.pug b/src/server/web/docs/tou.ja.pug
similarity index 100%
rename from src/web/docs/tou.ja.pug
rename to src/server/web/docs/tou.ja.pug
diff --git a/src/web/docs/ui.styl b/src/server/web/docs/ui.styl
similarity index 100%
rename from src/web/docs/ui.styl
rename to src/server/web/docs/ui.styl
diff --git a/src/web/docs/vars.ts b/src/server/web/docs/vars.ts
similarity index 76%
rename from src/web/docs/vars.ts
rename to src/server/web/docs/vars.ts
index 6f713f21d..5096a39c9 100644
--- a/src/web/docs/vars.ts
+++ b/src/server/web/docs/vars.ts
@@ -5,27 +5,27 @@ import * as yaml from 'js-yaml';
 import * as licenseChecker from 'license-checker';
 import * as tmp from 'tmp';
 
-import { fa } from '../../common/build/fa';
-import config from '../../conf';
-import { licenseHtml } from '../../common/build/license';
-const constants = require('../../const.json');
+import { fa } from '../../../build/fa';
+import config from '../../../conf';
+import { licenseHtml } from '../../../build/license';
+const constants = require('../../../const.json');
 
 export default async function(): Promise<{ [key: string]: any }> {
 	const vars = {} as { [key: string]: any };
 
-	const endpoints = glob.sync('./src/web/docs/api/endpoints/**/*.yaml');
+	const endpoints = glob.sync('./src/server/web/docs/api/endpoints/**/*.yaml');
 	vars['endpoints'] = endpoints.map(ep => {
 		const _ep = yaml.safeLoad(fs.readFileSync(ep, 'utf-8'));
 		return _ep.endpoint;
 	});
 
-	const entities = glob.sync('./src/web/docs/api/entities/**/*.yaml');
+	const entities = glob.sync('./src/server/web/docs/api/entities/**/*.yaml');
 	vars['entities'] = entities.map(x => {
 		const _x = yaml.safeLoad(fs.readFileSync(x, 'utf-8'));
 		return _x.name;
 	});
 
-	const docs = glob.sync('./src/web/docs/**/*.*.pug');
+	const docs = glob.sync('./src/server/web/docs/**/*.*.pug');
 	vars['docs'] = {};
 	docs.forEach(x => {
 		const [, name, lang] = x.match(/docs\/(.+?)\.(.+?)\.pug$/);
@@ -53,7 +53,7 @@ export default async function(): Promise<{ [key: string]: any }> {
 		licenseText: ''
 	}), 'utf-8');
 	const dependencies = await util.promisify(licenseChecker.init).bind(licenseChecker)({
-		start: __dirname + '/../../../',
+		start: __dirname + '/../../../../',
 		customPath: tmpObj.name
 	});
 	tmpObj.removeCallback();
diff --git a/src/web/element.scss b/src/server/web/element.scss
similarity index 91%
rename from src/web/element.scss
rename to src/server/web/element.scss
index 917198e02..7e6d0e709 100644
--- a/src/web/element.scss
+++ b/src/server/web/element.scss
@@ -1,7 +1,7 @@
 /* Element variable definitons */
 /* SEE: http://element.eleme.io/#/en-US/component/custom-theme */
 
-@import '../const.json';
+@import '../../const.json';
 
 /* theme color */
 $--color-primary: $themeColor;
diff --git a/src/web/server.ts b/src/server/web/server.ts
similarity index 100%
rename from src/web/server.ts
rename to src/server/web/server.ts
diff --git a/src/web/service/url-preview.ts b/src/server/web/service/url-preview.ts
similarity index 100%
rename from src/web/service/url-preview.ts
rename to src/server/web/service/url-preview.ts
diff --git a/src/web/style.styl b/src/server/web/style.styl
similarity index 100%
rename from src/web/style.styl
rename to src/server/web/style.styl
diff --git a/src/tools/analysis/core.ts b/src/tools/analysis/core.ts
index 20e5fa6c5..839fffd3c 100644
--- a/src/tools/analysis/core.ts
+++ b/src/tools/analysis/core.ts
@@ -1,7 +1,7 @@
 const bayes = require('./naive-bayes.js');
 
 const MeCab = require('./mecab');
-import Post from '../../api/models/post';
+import Post from '../../server/api/models/post';
 
 /**
  * 投稿を学習したり与えられた投稿のカテゴリを予測します
diff --git a/src/tools/analysis/extract-user-domains.ts b/src/tools/analysis/extract-user-domains.ts
index bc120f5c1..ba472b89a 100644
--- a/src/tools/analysis/extract-user-domains.ts
+++ b/src/tools/analysis/extract-user-domains.ts
@@ -1,8 +1,8 @@
 import * as URL from 'url';
 
-import Post from '../../api/models/post';
-import User from '../../api/models/user';
-import parse from '../../api/common/text';
+import Post from '../../server/api/models/post';
+import User from '../../server/api/models/user';
+import parse from '../../server/api/common/text';
 
 process.on('unhandledRejection', console.dir);
 
diff --git a/src/tools/analysis/extract-user-keywords.ts b/src/tools/analysis/extract-user-keywords.ts
index b99ca9321..4fa9b384e 100644
--- a/src/tools/analysis/extract-user-keywords.ts
+++ b/src/tools/analysis/extract-user-keywords.ts
@@ -1,9 +1,9 @@
 const moji = require('moji');
 
 const MeCab = require('./mecab');
-import Post from '../../api/models/post';
-import User from '../../api/models/user';
-import parse from '../../api/common/text';
+import Post from '../../server/api/models/post';
+import User from '../../server/api/models/user';
+import parse from '../../server/api/common/text';
 
 process.on('unhandledRejection', console.dir);
 
diff --git a/src/tools/analysis/predict-all-post-category.ts b/src/tools/analysis/predict-all-post-category.ts
index 058c4f99e..8564fd1b1 100644
--- a/src/tools/analysis/predict-all-post-category.ts
+++ b/src/tools/analysis/predict-all-post-category.ts
@@ -1,4 +1,4 @@
-import Post from '../../api/models/post';
+import Post from '../../server/api/models/post';
 import Core from './core';
 
 const c = new Core();
diff --git a/src/tools/analysis/predict-user-interst.ts b/src/tools/analysis/predict-user-interst.ts
index 99bdfa420..6599fb220 100644
--- a/src/tools/analysis/predict-user-interst.ts
+++ b/src/tools/analysis/predict-user-interst.ts
@@ -1,5 +1,5 @@
-import Post from '../../api/models/post';
-import User from '../../api/models/user';
+import Post from '../../server/api/models/post';
+import User from '../../server/api/models/user';
 
 export async function predictOne(id) {
 	console.log(`predict interest of ${id} ...`);
diff --git a/test/api.js b/test/api.js
index b8b2aecc9..c2c08dd95 100644
--- a/test/api.js
+++ b/test/api.js
@@ -17,7 +17,7 @@ const should = _chai.should();
 
 _chai.use(chaiHttp);
 
-const server = require('../built/api/server');
+const server = require('../built/server/api/server');
 const db = require('../built/db/mongodb').default;
 
 const async = fn => (done) => {
diff --git a/test/text.js b/test/text.js
index 3b27aa23d..4f739cc1b 100644
--- a/test/text.js
+++ b/test/text.js
@@ -4,8 +4,8 @@
 
 const assert = require('assert');
 
-const analyze = require('../built/api/common/text').default;
-const syntaxhighlighter = require('../built/api/common/text/core/syntax-highlighter').default;
+const analyze = require('../built/server/api/common/text').default;
+const syntaxhighlighter = require('../built/server/api/common/text/core/syntax-highlighter').default;
 
 describe('Text', () => {
 	it('can be analyzed', () => {
diff --git a/tsconfig.json b/tsconfig.json
index 47aa521bf..574c11bac 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -21,6 +21,6 @@
     "./src/**/*.ts"
   ],
   "exclude": [
-    "./src/web/app/**/*.ts"
+    "./src/server/web/app/**/*.ts"
   ]
 }
diff --git a/webpack.config.ts b/webpack.config.ts
index 9a952c8ef..6f16fcbfa 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -11,11 +11,11 @@ const WebpackOnBuildPlugin = require('on-build-webpack');
 //const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
 const ProgressBarPlugin = require('progress-bar-webpack-plugin');
 
-import I18nReplacer from './src/common/build/i18n';
-import { pattern as faPattern, replacement as faReplacement } from './src/common/build/fa';
+import I18nReplacer from './src/build/i18n';
+import { pattern as faPattern, replacement as faReplacement } from './src/build/fa';
 const constants = require('./src/const.json');
 import config from './src/conf';
-import { licenseHtml } from './src/common/build/license';
+import { licenseHtml } from './src/build/license';
 
 import locales from './locales';
 const meta = require('./package.json');
@@ -33,7 +33,7 @@ global['collapseSpacesReplacement'] = html => {
 };
 
 global['base64replacement'] = (_, key) => {
-	return fs.readFileSync(__dirname + '/src/web/' + key, 'base64');
+	return fs.readFileSync(__dirname + '/src/server/web/' + key, 'base64');
 };
 //#endregion
 
@@ -51,18 +51,18 @@ module.exports = entries.map(x => {
 
 	// 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',
-		sw: './src/web/app/sw.js'
+		desktop: './src/server/web/app/desktop/script.ts',
+		mobile: './src/server/web/app/mobile/script.ts',
+		//ch: './src/server/web/app/ch/script.ts',
+		//stats: './src/server/web/app/stats/script.ts',
+		//status: './src/server/web/app/status/script.ts',
+		dev: './src/server/web/app/dev/script.ts',
+		auth: './src/server/web/app/auth/script.ts',
+		sw: './src/server/web/app/sw.js'
 	};
 
 	const output = {
-		path: __dirname + '/built/web/assets',
+		path: __dirname + '/built/server/web/assets',
 		filename: `[name].${version}.${lang}.${isProduction ? 'min' : 'raw'}.js`
 	};
 
@@ -206,7 +206,7 @@ module.exports = entries.map(x => {
 					loader: 'ts-loader',
 					options: {
 						happyPackMode: true,
-						configFile: __dirname + '/../src/web/app/tsconfig.json',
+						configFile: __dirname + '/../src/server/web/app/tsconfig.json',
 						appendTsSuffixTo: [/\.vue$/]
 					}
 				}, {
@@ -231,7 +231,7 @@ module.exports = entries.map(x => {
 				'.js', '.ts', '.json'
 			],
 			alias: {
-				'const.styl': __dirname + '/src/web/const.styl'
+				'const.styl': __dirname + '/src/server/web/const.styl'
 			}
 		},
 		resolveLoader: {

From 069c36dd7daccc7ba3bf0c4b72f5c08db640261a Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 29 Mar 2018 11:08:53 +0900
Subject: [PATCH 0917/1250] oops

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 6984f52f1..65d56a0f1 100644
--- a/README.md
+++ b/README.md
@@ -32,7 +32,7 @@ please see [Setup and installation guide](./docs/setup.en.md).
 
 Donation
 ----------------------------------------------------------------
-If you want to donate to Misskey, please see [this][./docs/donate.ja.md].
+If you want to donate to Misskey, please see [this](./docs/donate.ja.md).
 
 [List of all donors](./DONORS.md)
 

From 661fba515137d6e9b798a10bc0732fd48e0c3f0d Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 29 Mar 2018 11:13:40 +0900
Subject: [PATCH 0918/1250] Update README.md

---
 README.md | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/README.md b/README.md
index 65d56a0f1..b743914f5 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ Misskey
 **[Misskey](https://misskey.xyz)** is a completely open source,
 ultimately sophisticated new type of mini-blog based SNS.
 
-Key features
+:sparkles: Features
 ----------------------------------------------------------------
 * Automatically updated timeline
 * Private messages
@@ -25,18 +25,18 @@ Key features
 
 and more! You can touch with your own eyes at https://misskey.xyz/.
 
-Setup and Installation
+:package: Setup and Installation
 ----------------------------------------------------------------
 If you want to run your own instance of Misskey,
 please see [Setup and installation guide](./docs/setup.en.md).
 
-Donation
+:yen: Donation
 ----------------------------------------------------------------
 If you want to donate to Misskey, please see [this](./docs/donate.ja.md).
 
 [List of all donors](./DONORS.md)
 
-Notable contributors
+:mortar_board: Notable contributors
 ----------------------------------------------------------------
 | ![syuilo][syuilo-icon] | ![Morisawa Aya][ayamorisawa-icon] | ![otofune][otofune-icon] | ![akihikodaki][akihikodaki-icon] |
 |:-:|:-:|:-:|:-:|
@@ -44,7 +44,7 @@ Notable contributors
 
 [List of all contributors](https://github.com/syuilo/misskey/graphs/contributors)
 
-Copyright
+:four_leaf_clover: Copyright
 ----------------------------------------------------------------
 > Copyright (c) 2014-2018 syuilo
 

From c015375c14862781d2d543a5bcad2f0049092738 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 29 Mar 2018 11:19:53 +0900
Subject: [PATCH 0919/1250] Update Dockerfile

---
 docker/Dockerfile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index ef04fc9e2..7cee650de 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -14,7 +14,7 @@ RUN pacman -S --noconfirm pacman
 RUN pacman-db-upgrade
 RUN pacman -S --noconfirm archlinux-keyring
 RUN pacman -Syyu --noconfirm
-RUN pacman -S --noconfirm git nodejs npm mongodb redis graphicsmagick
+RUN pacman -S --noconfirm git nodejs npm mongodb redis imagemagick
 
 COPY misskey.sh /root/misskey.sh
 RUN chmod u+x /root/misskey.sh

From c84add61cd9822fce60bee07a6424d2302f0e455 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 29 Mar 2018 11:34:20 +0900
Subject: [PATCH 0920/1250] :v:

---
 docs/config.md => .config/example.yml | 3 ---
 .gitignore                            | 3 ++-
 docs/setup.en.md                      | 6 +++---
 docs/setup.ja.md                      | 6 +++---
 4 files changed, 8 insertions(+), 10 deletions(-)
 rename docs/config.md => .config/example.yml (98%)

diff --git a/docs/config.md b/.config/example.yml
similarity index 98%
rename from docs/config.md
rename to .config/example.yml
index c4a54c0be..0e167ccb7 100644
--- a/docs/config.md
+++ b/.config/example.yml
@@ -1,4 +1,3 @@
-``` yaml
 # サーバーのメンテナ情報
 maintainer:
   # メンテナの名前
@@ -56,5 +55,3 @@ twitter:
 
   # インテグレーション用アプリのコンシューマーシークレット
   consumer_secret:
-
-```
diff --git a/.gitignore b/.gitignore
index d0ae0b808..be8689e2e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
-/.config
+/.config/*
+!/.config/example.yml
 /.vscode
 /node_modules
 /build
diff --git a/docs/setup.en.md b/docs/setup.en.md
index 08cd16857..a436d751c 100644
--- a/docs/setup.en.md
+++ b/docs/setup.en.md
@@ -37,9 +37,9 @@ Please install and setup these softwares:
 
 *3.* Prepare configuration
 ----------------------------------------------------------------
-First, you need to create a `.config` directory in the directory that
-Misskey installed. And then you need to create a `default.yml` file in
-the directory. The template of configuration is available [here](./config.md).
+1. Copy `example.yml` of `.config` directory
+2. Rename it to `default.yml`
+3. Edit it
 
 *4.* Install and build Misskey
 ----------------------------------------------------------------
diff --git a/docs/setup.ja.md b/docs/setup.ja.md
index 9fa56acb2..6605461d9 100644
--- a/docs/setup.ja.md
+++ b/docs/setup.ja.md
@@ -37,9 +37,9 @@ web-push generate-vapid-keys
 
 *3.* 設定ファイルを用意する
 ----------------------------------------------------------------
-Misskeyをインストールしたディレクトリに、`.config`というディレクトリを作成し、
-その中に`default.yml`という名前で設定ファイルを作ってください。
-設定ファイルの下書きは[ここ](./config.md)にありますので、コピペしてご利用ください。
+1. `.config`ディレクトリ内の`example.yml`をコピー
+2. `default.yml`にリネーム
+3. 編集する
 
 *4.* Misskeyのインストール(とビルド)
 ----------------------------------------------------------------

From ab668557dd8ee74107b5384bd82e24a4fa765935 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 29 Mar 2018 11:44:24 +0900
Subject: [PATCH 0921/1250] Update README.md

---
 README.md | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/README.md b/README.md
index b743914f5..ef12958c2 100644
--- a/README.md
+++ b/README.md
@@ -38,9 +38,9 @@ If you want to donate to Misskey, please see [this](./docs/donate.ja.md).
 
 :mortar_board: Notable contributors
 ----------------------------------------------------------------
-| ![syuilo][syuilo-icon] | ![Morisawa Aya][ayamorisawa-icon] | ![otofune][otofune-icon] | ![akihikodaki][akihikodaki-icon] |
-|:-:|:-:|:-:|:-:|
-| [syuilo][syuilo-link]<br>Owner | [Aya Morisawa][ayamorisawa-link]<br>Collaborator | [otofune][otofune-link]<br>Collaborator | [akihikodaki][akihikodaki-link] |
+| ![syuilo][syuilo-icon] | ![Morisawa Aya][ayamorisawa-icon] | ![otofune][otofune-icon] | ![akihikodaki][akihikodaki-icon] | ![rinsuki][rinsuki-icon] |
+|:-:|:-:|:-:|:-:|:-:|
+| [syuilo][syuilo-link]<br>Owner | [Aya Morisawa][ayamorisawa-link]<br>Collaborator | [otofune][otofune-link]<br>Collaborator | [akihikodaki][akihikodaki-link] | [rinsuki][rinsuki-link] |
 
 [List of all contributors](https://github.com/syuilo/misskey/graphs/contributors)
 
@@ -69,3 +69,5 @@ Misskey is an open-source software licensed under [GNU AGPLv3](LICENSE).
 [otofune-icon]:     https://avatars0.githubusercontent.com/u/15062473?v=3&s=70
 [akihikodaki-link]: https://github.com/akihikodaki
 [akihikodaki-icon]: https://avatars2.githubusercontent.com/u/17036990?s=70&v=4
+[rinsuki-link]:     https://github.com/rinsuki
+[rinsuki-icon]:     https://avatars0.githubusercontent.com/u/6533808?s=70&v=4

From a6fd3022435007a96fdac115a526664d613e2e0b Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 29 Mar 2018 04:12:35 +0000
Subject: [PATCH 0922/1250] fix(package): update element-ui to version 2.3.2

Closes #1330
---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 1e1736a59..008a1cef7 100644
--- a/package.json
+++ b/package.json
@@ -103,7 +103,7 @@
 		"deepcopy": "0.6.3",
 		"diskusage": "0.2.4",
 		"elasticsearch": "14.2.1",
-		"element-ui": "2.3.0",
+		"element-ui": "2.3.2",
 		"emojilib": "2.2.12",
 		"escape-regexp": "0.0.1",
 		"eslint": "4.19.1",

From 6be1ade1a369e2450a1a0d9c6beb302f5434de9c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 29 Mar 2018 13:35:42 +0900
Subject: [PATCH 0923/1250] Remove unused const

---
 gulpfile.ts | 1 -
 1 file changed, 1 deletion(-)

diff --git a/gulpfile.ts b/gulpfile.ts
index 9c61e3a1c..c72dedda2 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -125,7 +125,6 @@ gulp.task('build:client:script', () =>
 		.pipe(replace('VERSION', JSON.stringify(version)))
 		.pipe(replace('API', JSON.stringify(config.api_url)))
 		.pipe(replace('ENV', JSON.stringify(env)))
-		.pipe(replace('HOST', JSON.stringify(config.host)))
 		.pipe(isProduction ? uglify({
 			toplevel: true
 		} as any) : gutil.noop())

From b157aff6f7f68bfcac54913caa92f0c1eaa2dade Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 29 Mar 2018 14:39:52 +0900
Subject: [PATCH 0924/1250] oops

---
 src/config.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/config.ts b/src/config.ts
index 0d8df39f4..6d3e7740b 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -80,7 +80,7 @@ type Source = {
 	};
 	line_bot?: {
 		channel_secret: string;
-		channel_accessToken: string;
+		channel_access_token: string;
 	};
 	analysis?: {
 		mecab_command?: string;

From 47a5ec94f072153208e3f66ec80c7bd5e7123c22 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 29 Mar 2018 14:48:47 +0900
Subject: [PATCH 0925/1250] Resolve conflicts

---
 docs/config.md => .config/example.yml         |   3 -
 .gitignore                                    |   3 +-
 CHANGELOG.md                                  | 532 --------------
 LICENSE                                       | 674 +++++++++++++++++-
 LICENSE_AGPL-3.0                              | 661 -----------------
 README.md                                     |  55 +-
 docker/Dockerfile                             |   2 +-
 docs/setup.en.md                              |   6 +-
 docs/setup.ja.md                              |   6 +-
 gulpfile.ts                                   |   1 -
 locales/ja.yml                                |   2 +-
 src/index.ts                                  |   1 -
 src/server/api/authenticate.ts                |   4 +-
 src/server/api/bot/core.ts                    |   8 +-
 src/server/api/bot/interfaces/line.ts         |  12 +-
 src/server/api/common/drive/add-file.ts       |  16 +-
 src/server/api/common/get-friends.ts          |   8 +-
 src/server/api/common/notify.ts               |  18 +-
 src/server/api/common/push-sw.ts              |   4 +-
 .../api/common/read-messaging-message.ts      |  12 +-
 src/server/api/common/read-notification.ts    |   8 +-
 src/server/api/common/watch-post.ts           |  14 +-
 src/server/api/endpoints.ts                   |   2 +-
 src/server/api/endpoints/aggregation/posts.ts |  16 +-
 .../endpoints/aggregation/posts/reaction.ts   |  16 +-
 .../endpoints/aggregation/posts/reactions.ts  |  16 +-
 .../api/endpoints/aggregation/posts/reply.ts  |  14 +-
 .../api/endpoints/aggregation/posts/repost.ts |  16 +-
 src/server/api/endpoints/aggregation/users.ts |   8 +-
 .../endpoints/aggregation/users/activity.ts   |  24 +-
 .../endpoints/aggregation/users/followers.ts  |  20 +-
 .../endpoints/aggregation/users/following.ts  |  20 +-
 .../api/endpoints/aggregation/users/post.ts   |  24 +-
 .../endpoints/aggregation/users/reaction.ts   |  16 +-
 src/server/api/endpoints/app/create.ts        |  26 +-
 .../api/endpoints/app/name_id/available.ts    |  18 +-
 src/server/api/endpoints/app/show.ts          |  24 +-
 src/server/api/endpoints/auth/accept.ts       |  14 +-
 .../api/endpoints/auth/session/generate.ts    |  12 +-
 src/server/api/endpoints/auth/session/show.ts |   6 +-
 .../api/endpoints/auth/session/userkey.ts     |  20 +-
 src/server/api/endpoints/channels.ts          |  16 +-
 src/server/api/endpoints/channels/create.ts   |  12 +-
 src/server/api/endpoints/channels/posts.ts    |  24 +-
 src/server/api/endpoints/channels/show.ts     |   6 +-
 src/server/api/endpoints/channels/unwatch.ts  |  16 +-
 src/server/api/endpoints/channels/watch.ts    |  20 +-
 src/server/api/endpoints/drive.ts             |   4 +-
 src/server/api/endpoints/drive/files.ts       |  26 +-
 .../api/endpoints/drive/files/create.ts       |   6 +-
 src/server/api/endpoints/drive/files/find.ts  |  10 +-
 src/server/api/endpoints/drive/files/show.ts  |   8 +-
 .../api/endpoints/drive/files/update.ts       |  22 +-
 .../endpoints/drive/files/upload_from_url.ts  |   6 +-
 src/server/api/endpoints/drive/folders.ts     |  26 +-
 .../api/endpoints/drive/folders/create.ts     |  14 +-
 .../api/endpoints/drive/folders/find.ts       |  10 +-
 .../api/endpoints/drive/folders/show.ts       |   8 +-
 .../api/endpoints/drive/folders/update.ts     |  32 +-
 src/server/api/endpoints/drive/stream.ts      |  18 +-
 src/server/api/endpoints/following/create.ts  |  22 +-
 src/server/api/endpoints/following/delete.ts  |  18 +-
 src/server/api/endpoints/i.ts                 |   2 +-
 src/server/api/endpoints/i/2fa/done.ts        |   8 +-
 src/server/api/endpoints/i/2fa/register.ts    |   2 +-
 src/server/api/endpoints/i/2fa/unregister.ts  |   4 +-
 src/server/api/endpoints/i/appdata/get.ts     |   4 +-
 src/server/api/endpoints/i/appdata/set.ts     |   8 +-
 src/server/api/endpoints/i/authorized_apps.ts |   4 +-
 src/server/api/endpoints/i/change_password.ts |  12 +-
 src/server/api/endpoints/i/favorites.ts       |   4 +-
 src/server/api/endpoints/i/notifications.ts   |  34 +-
 src/server/api/endpoints/i/pin.ts             |  10 +-
 src/server/api/endpoints/i/signin_history.ts  |  18 +-
 src/server/api/endpoints/i/update.ts          |  38 +-
 .../api/endpoints/i/update_client_setting.ts  |   4 +-
 src/server/api/endpoints/i/update_home.ts     |   6 +-
 .../api/endpoints/i/update_mobile_home.ts     |   6 +-
 src/server/api/endpoints/messaging/history.ts |  14 +-
 .../api/endpoints/messaging/messages.ts       |  36 +-
 .../endpoints/messaging/messages/create.ts    |  74 +-
 src/server/api/endpoints/messaging/unread.ts  |  12 +-
 src/server/api/endpoints/meta.ts              |   1 -
 src/server/api/endpoints/mute/create.ts       |  18 +-
 src/server/api/endpoints/mute/delete.ts       |  14 +-
 src/server/api/endpoints/mute/list.ts         |   8 +-
 src/server/api/endpoints/my/apps.ts           |   2 +-
 .../notifications/get_unread_count.ts         |  12 +-
 .../notifications/mark_as_read_all.ts         |   6 +-
 src/server/api/endpoints/othello/games.ts     |  28 +-
 .../api/endpoints/othello/games/show.ts       |  16 +-
 .../api/endpoints/othello/invitations.ts      |   2 +-
 src/server/api/endpoints/othello/match.ts     |  44 +-
 .../api/endpoints/othello/match/cancel.ts     |   2 +-
 src/server/api/endpoints/posts.ts             |  24 +-
 src/server/api/endpoints/posts/categorize.ts  |   8 +-
 src/server/api/endpoints/posts/context.ts     |  14 +-
 src/server/api/endpoints/posts/create.ts      | 166 ++---
 .../api/endpoints/posts/favorites/create.ts   |  16 +-
 .../api/endpoints/posts/favorites/delete.ts   |  12 +-
 src/server/api/endpoints/posts/mentions.ts    |  18 +-
 .../endpoints/posts/polls/recommendation.ts   |   8 +-
 src/server/api/endpoints/posts/polls/vote.ts  |  34 +-
 src/server/api/endpoints/posts/reactions.ts   |  10 +-
 .../api/endpoints/posts/reactions/create.ts   |  46 +-
 .../api/endpoints/posts/reactions/delete.ts   |  16 +-
 src/server/api/endpoints/posts/replies.ts     |   8 +-
 src/server/api/endpoints/posts/reposts.ts     |  24 +-
 src/server/api/endpoints/posts/search.ts      |  98 +--
 src/server/api/endpoints/posts/show.ts        |   6 +-
 src/server/api/endpoints/posts/timeline.ts    |  58 +-
 src/server/api/endpoints/posts/trend.ts       |  12 +-
 src/server/api/endpoints/stats.ts             |   8 +-
 src/server/api/endpoints/sw/register.ts       |   6 +-
 .../api/endpoints/username/available.ts       |   2 +-
 src/server/api/endpoints/users.ts             |   4 +-
 src/server/api/endpoints/users/followers.ts   |  14 +-
 src/server/api/endpoints/users/following.ts   |  14 +-
 .../users/get_frequently_replied_users.ts     |  20 +-
 src/server/api/endpoints/users/posts.ts       |  62 +-
 .../api/endpoints/users/recommendation.ts     |   4 +-
 src/server/api/endpoints/users/search.ts      |   2 +-
 .../api/endpoints/users/search_by_username.ts |   2 +-
 src/server/api/endpoints/users/show.ts        |  56 +-
 src/server/api/models/access-token.ts         |  18 +-
 src/server/api/models/app.ts                  |  24 +-
 src/server/api/models/appdata.ts              |   3 -
 src/server/api/models/auth-session.ts         |   9 +-
 src/server/api/models/channel-watching.ts     |  12 +-
 src/server/api/models/channel.ts              |  15 +-
 src/server/api/models/drive-file.ts           |  14 +-
 src/server/api/models/drive-folder.ts         |  18 +-
 src/server/api/models/drive-tag.ts            |   3 -
 src/server/api/models/favorite.ts             |  11 +-
 src/server/api/models/following.ts            |  12 +-
 src/server/api/models/messaging-history.ts    |  12 +-
 src/server/api/models/messaging-message.ts    |  19 +-
 src/server/api/models/meta.ts                 |   5 +-
 src/server/api/models/mute.ts                 |  12 +-
 src/server/api/models/notification.ts         |  22 +-
 src/server/api/models/othello-game.ts         |  42 +-
 src/server/api/models/othello-matching.ts     |  12 +-
 src/server/api/models/poll-vote.ts            |  16 +-
 src/server/api/models/post-reaction.ts        |  10 +-
 src/server/api/models/post-watching.ts        |  11 +-
 src/server/api/models/post.ts                 |  62 +-
 src/server/api/models/signin.ts               |   5 +
 src/server/api/models/sw-subscription.ts      |  12 +-
 src/server/api/models/user.ts                 | 133 ++--
 src/server/api/private/signin.ts              |  10 +-
 src/server/api/private/signup.ts              |  26 +-
 src/server/api/service/github.ts              |   2 +-
 src/server/api/service/twitter.ts             |  10 +-
 src/server/api/stream/home.ts                 |  16 +-
 src/server/api/stream/othello-game.ts         | 128 ++--
 src/server/api/stream/othello.ts              |   6 +-
 src/server/api/streaming.ts                   |   2 +-
 src/server/common/get-post-summary.ts         |   4 +-
 src/server/common/othello/ai/back.ts          |  30 +-
 src/server/common/othello/ai/front.ts         |  16 +-
 src/server/common/user/get-summary.ts         |   2 +-
 src/server/web/app/auth/views/form.vue        |   2 +-
 src/server/web/app/auth/views/index.vue       |  12 +-
 src/server/web/app/ch/tags/channel.tag        |  28 +-
 src/server/web/app/common/define-widget.ts    |   4 +-
 src/server/web/app/common/mios.ts             |   4 +-
 .../common/scripts/compose-notification.ts    |  12 +-
 .../app/common/scripts/parse-search-query.ts  |   4 +-
 .../web/app/common/scripts/streaming/home.ts  |   2 +-
 .../common/views/components/autocomplete.vue  |   2 +-
 .../views/components/messaging-room.form.vue  |   6 +-
 .../components/messaging-room.message.vue     |  12 +-
 .../views/components/messaging-room.vue       |  16 +-
 .../app/common/views/components/messaging.vue |  16 +-
 .../common/views/components/othello.game.vue  |  58 +-
 .../views/components/othello.gameroom.vue     |   2 +-
 .../common/views/components/othello.room.vue  |  26 +-
 .../app/common/views/components/othello.vue   |  22 +-
 .../web/app/common/views/components/poll.vue  |  10 +-
 .../app/common/views/components/post-menu.vue |   4 +-
 .../views/components/reaction-picker.vue      |   2 +-
 .../views/components/reactions-viewer.vue     |   2 +-
 .../app/common/views/components/signin.vue    |   6 +-
 .../app/common/views/components/signup.vue    |   2 +-
 .../views/components/twitter-setting.vue      |   4 +-
 .../app/common/views/components/uploader.vue  |   2 +-
 .../views/components/welcome-timeline.vue     |   4 +-
 .../common/views/directives/autocomplete.ts   |   2 +-
 .../app/common/views/widgets/slideshow.vue    |   2 +-
 .../web/app/common/views/widgets/version.vue  |   7 +-
 src/server/web/app/config.ts                  |   4 +-
 .../web/app/desktop/api/update-avatar.ts      |   8 +-
 .../web/app/desktop/api/update-banner.ts      |   8 +-
 .../app/desktop/views/components/activity.vue |   2 +-
 .../desktop/views/components/drive.file.vue   |  14 +-
 .../desktop/views/components/drive.folder.vue |  10 +-
 .../views/components/drive.nav-folder.vue     |   8 +-
 .../app/desktop/views/components/drive.vue    |  28 +-
 .../views/components/follow-button.vue        |  22 +-
 .../views/components/followers-window.vue     |   2 +-
 .../desktop/views/components/followers.vue    |   6 +-
 .../views/components/following-window.vue     |   2 +-
 .../desktop/views/components/following.vue    |   6 +-
 .../views/components/friends-maker.vue        |   2 +-
 .../web/app/desktop/views/components/home.vue |  20 +-
 .../desktop/views/components/media-image.vue  |   2 +-
 .../app/desktop/views/components/mentions.vue |   2 +-
 .../views/components/notifications.vue        |  38 +-
 .../views/components/post-detail.sub.vue      |   8 +-
 .../desktop/views/components/post-detail.vue  |  32 +-
 .../desktop/views/components/post-form.vue    |   8 +-
 .../desktop/views/components/post-preview.vue |   8 +-
 .../views/components/posts.post.sub.vue       |   8 +-
 .../desktop/views/components/posts.post.vue   |  36 +-
 .../app/desktop/views/components/posts.vue    |   4 +-
 .../desktop/views/components/repost-form.vue  |   2 +-
 .../desktop/views/components/settings.2fa.vue |   8 +-
 .../views/components/settings.password.vue    |   4 +-
 .../views/components/settings.profile.vue     |   6 +-
 .../views/components/settings.signins.vue     |   2 +-
 .../app/desktop/views/components/settings.vue |  14 +-
 .../views/components/sub-post-content.vue     |   4 +-
 .../app/desktop/views/components/timeline.vue |   8 +-
 .../views/components/ui.header.account.vue    |   2 +-
 .../desktop/views/components/ui.header.vue    |   4 +-
 .../desktop/views/components/user-preview.vue |  12 +-
 .../views/components/users-list.item.vue      |   4 +-
 .../views/components/widget-container.vue     |   4 +-
 .../app/desktop/views/components/window.vue   |   4 +-
 .../web/app/desktop/views/pages/home.vue      |   2 +-
 .../web/app/desktop/views/pages/othello.vue   |   2 +-
 .../web/app/desktop/views/pages/post.vue      |   2 +-
 .../pages/user/user.followers-you-know.vue    |   4 +-
 .../desktop/views/pages/user/user.friends.vue |   4 +-
 .../desktop/views/pages/user/user.header.vue  |  10 +-
 .../desktop/views/pages/user/user.home.vue    |   6 +-
 .../desktop/views/pages/user/user.photos.vue  |   4 +-
 .../desktop/views/pages/user/user.profile.vue |  22 +-
 .../views/pages/user/user.timeline.vue        |   8 +-
 .../web/app/desktop/views/pages/welcome.vue   |   2 +-
 .../views/widgets/channel.channel.form.vue    |   4 +-
 .../desktop/views/widgets/channel.channel.vue |   2 +-
 .../web/app/desktop/views/widgets/channel.vue |   2 +-
 .../web/app/desktop/views/widgets/profile.vue |   4 +-
 .../web/app/desktop/views/widgets/users.vue   |   2 +-
 src/server/web/app/dev/views/app.vue          |   2 +-
 src/server/web/app/dev/views/new-app.vue      |  12 +-
 src/server/web/app/init.ts                    |   4 +-
 src/server/web/app/mobile/api/post.ts         |   2 +-
 .../app/mobile/views/components/activity.vue  |   2 +-
 .../views/components/drive.file-detail.vue    |  14 +-
 .../mobile/views/components/drive.file.vue    |   4 +-
 .../web/app/mobile/views/components/drive.vue |  38 +-
 .../mobile/views/components/follow-button.vue |  22 +-
 .../mobile/views/components/media-image.vue   |   2 +-
 .../views/components/notification-preview.vue |  14 +-
 .../mobile/views/components/notification.vue  |  16 +-
 .../mobile/views/components/notifications.vue |   6 +-
 .../app/mobile/views/components/post-card.vue |   2 +-
 .../views/components/post-detail.sub.vue      |   4 +-
 .../mobile/views/components/post-detail.vue   |  28 +-
 .../app/mobile/views/components/post-form.vue |   8 +-
 .../mobile/views/components/post-preview.vue  |   4 +-
 .../app/mobile/views/components/post.sub.vue  |   4 +-
 .../web/app/mobile/views/components/post.vue  |  30 +-
 .../web/app/mobile/views/components/posts.vue |   4 +-
 .../views/components/sub-post-content.vue     |   4 +-
 .../app/mobile/views/components/timeline.vue  |   6 +-
 .../app/mobile/views/components/ui.header.vue |   4 +-
 .../app/mobile/views/components/ui.nav.vue    |   2 +-
 .../app/mobile/views/components/user-card.vue |   4 +-
 .../mobile/views/components/user-preview.vue  |   2 +-
 .../mobile/views/components/user-timeline.vue |  10 +-
 .../web/app/mobile/views/pages/followers.vue  |   8 +-
 .../web/app/mobile/views/pages/following.vue  |   8 +-
 .../web/app/mobile/views/pages/home.vue       |  22 +-
 .../app/mobile/views/pages/notifications.vue  |   2 +-
 .../web/app/mobile/views/pages/othello.vue    |   2 +-
 .../web/app/mobile/views/pages/post.vue       |   2 +-
 .../mobile/views/pages/profile-setting.vue    |   8 +-
 .../web/app/mobile/views/pages/settings.vue   |   7 +-
 .../web/app/mobile/views/pages/user.vue       |  12 +-
 .../pages/user/home.followers-you-know.vue    |   4 +-
 .../mobile/views/pages/user/home.friends.vue  |   2 +-
 .../mobile/views/pages/user/home.photos.vue   |   4 +-
 .../mobile/views/pages/user/home.posts.vue    |   2 +-
 .../web/app/mobile/views/pages/user/home.vue  |   4 +-
 .../web/app/mobile/views/pages/welcome.vue    |   8 +-
 .../web/app/mobile/views/widgets/profile.vue  |   4 +-
 src/server/web/app/stats/tags/index.tag       |   4 +-
 src/server/web/docs/api.ja.pug                |   4 +-
 .../web/docs/api/endpoints/posts/create.yaml  |   8 +-
 .../docs/api/endpoints/posts/timeline.yaml    |   8 +-
 .../web/docs/api/entities/drive-file.yaml     |   6 +-
 src/server/web/docs/api/entities/post.yaml    |  18 +-
 src/server/web/docs/api/entities/user.yaml    |  36 +-
 src/tools/analysis/extract-user-domains.ts    |   2 +-
 src/tools/analysis/extract-user-keywords.ts   |   2 +-
 src/tools/analysis/predict-user-interst.ts    |   2 +-
 swagger.js                                    |  10 +-
 .../1.js}                                     |   6 +-
 tools/migration/nighthike/2.js                |  39 +
 tools/migration/nighthike/3.js                |  73 ++
 tools/migration/nighthike/4.js                | 232 ++++++
 .../shell.1522038492.user-account.js          |  41 --
 tools/migration/shell.1522116709.user-host.js |   1 -
 .../shell.1522116710.user-host_lower.js       |   1 -
 webpack.config.ts                             |   3 +-
 308 files changed, 3045 insertions(+), 3200 deletions(-)
 rename docs/config.md => .config/example.yml (98%)
 delete mode 100644 CHANGELOG.md
 delete mode 100644 LICENSE_AGPL-3.0
 delete mode 100644 src/server/api/models/appdata.ts
 delete mode 100644 src/server/api/models/drive-tag.ts
 rename tools/migration/{node.1522066477.user-account-keypair.js => nighthike/1.js} (79%)
 create mode 100644 tools/migration/nighthike/2.js
 create mode 100644 tools/migration/nighthike/3.js
 create mode 100644 tools/migration/nighthike/4.js
 delete mode 100644 tools/migration/shell.1522038492.user-account.js
 delete mode 100644 tools/migration/shell.1522116709.user-host.js
 delete mode 100644 tools/migration/shell.1522116710.user-host_lower.js

diff --git a/docs/config.md b/.config/example.yml
similarity index 98%
rename from docs/config.md
rename to .config/example.yml
index c4a54c0be..0e167ccb7 100644
--- a/docs/config.md
+++ b/.config/example.yml
@@ -1,4 +1,3 @@
-``` yaml
 # サーバーのメンテナ情報
 maintainer:
   # メンテナの名前
@@ -56,5 +55,3 @@ twitter:
 
   # インテグレーション用アプリのコンシューマーシークレット
   consumer_secret:
-
-```
diff --git a/.gitignore b/.gitignore
index d0ae0b808..be8689e2e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
-/.config
+/.config/*
+!/.config/example.yml
 /.vscode
 /node_modules
 /build
diff --git a/CHANGELOG.md b/CHANGELOG.md
deleted file mode 100644
index 6e69a7319..000000000
--- a/CHANGELOG.md
+++ /dev/null
@@ -1,532 +0,0 @@
-ChangeLog (Release Notes)
-=========================
-主に notable な changes を書いていきます
-
-3493 (2018/01/01)
------------------
-* なんか
-
-3460 (2017/12/23)
------------------
-* 検索で複数のユーザーを指定できるように
-* 検索でユーザーを除外できるように
-* など
-
-3451 (2017/12/22)
------------------
-* ミュート機能
-
-3430 (2017/12/21)
------------------
-* oops
-
-3428 (2017/12/21)
------------------
-* バグ修正
-
-3426 (2017/12/21)
------------------
-* 検索にpoll追加
-
-3424 (2017/12/21)
------------------
-* 検索にrepost追加
-* など
-
-3422 (2017/12/21)
------------------
-* 検索にfollow追加 #1023
-
-3420 (2017/12/21)
------------------
-* 検索機能を大幅に強化
-
-3415 (2017/12/19)
------------------
-* デザインの調整
-
-3404 (2017/12/17)
------------------
-* なんか
-
-3400 (2017/12/17)
------------------
-* なんか
-
-3392 (2017/12/17)
------------------
-* ドキュメントなど
-
-3390 (2017/12/16)
------------------
-* ドキュメントなど
-
-3347 (2017/12/11)
------------------
-* バグ修正
-
-3342 (2017/12/11)
------------------
-* なんか
-
-3339 (2017/12/11)
------------------
-* なんか
-
-3334 (2017/12/10)
------------------
-* いい感じにした
-
-3322 (2017/12/10)
------------------
-* :art:
-
-3320 (2017/12/10)
------------------
-* なんか
-
-3310 (2017/12/09)
------------------
-* i18nなど
-
-3308 (2017/12/09)
------------------
-* :art:
-
-3294 (2017/12/09)
------------------
-* バグ修正
-
-3292 (2017/12/09)
------------------
-* ユーザビリティの向上
-
-3281 (2017/12/08)
------------------
-* 二段階認証の実装 (#967)
-
-3278 (2017/12/08)
------------------
-* :v:
-
-3272 (2017/12/08)
------------------
-* Fix bug
-
-3268 (2017/12/08)
------------------
-* :v:
-
-3263 (2017/12/08)
------------------
-* FontAwesome5に移行
-
-3230 (2017/11/28)
------------------
-* :v:
-
-3219 (2017/11/28)
------------------
-* なんか
-
-3212 (2017/11/27)
------------------
-* なんか
-
-3201 (2017/11/23)
------------------
-* Twitterログインを実装 (#939)
-
-3196 (2017/11/23)
------------------
-* バグ修正
-
-3194 (2017/11/23)
------------------
-* バグ修正
-
-3191 (2017/11/23)
------------------
-* :v:
-
-3188 (2017/11/22)
------------------
-* バグ修正
-
-3180 (2017/11/21)
------------------
-* バグ修正
-
-3177 (2017/11/21)
------------------
-* ServiceWorker support
-  * Misskeyを開いていないときでも通知を受け取れるように(Chromeのみ)
-
-3165 (2017/11/20)
------------------
-* デスクトップ版でも通知バッジを表示 (#918)
-* デザインの調整
-* バグ修正
-
-3155 (2017/11/20)
------------------
-* デスクトップ版でユーザーの投稿グラフを見れるように
-
-3142 (2017/11/18)
------------------
-* バグ修正
-
-3140 (2017/11/18)
------------------
-* ウィジェットをスクロールに追従させるように
-
-3136 (2017/11/17)
------------------
-* バグ修正
-* 通信の最適化
-
-3131 (2017/11/17)
------------------
-* バグ修正
-* 通信の最適化
-
-3124 (2017/11/16)
------------------
-* バグ修正
-
-3121 (2017/11/16)
------------------
-* ブロードキャストウィジェットの強化
-* デザインのグリッチの修正
-* 通信の最適化
-
-3113 (2017/11/15)
------------------
-* アクティビティのレンダリングの問題の修正など
-
-3110 (2017/11/15)
------------------
-* デザインの調整など
-
-3107 (2017/11/14)
------------------
-* デザインの調整
-
-3104 (2017/11/14)
------------------
-* デスクトップ版ユーザーページのデザインの改良
-* バグ修正
-
-3099 (2017/11/14)
------------------
-* デスクトップ版ユーザーページの強化
-* バグ修正
-
-3093 (2017/11/14)
------------------
-* やった
-
-3089 (2017/11/14)
------------------
-* なんか
-
-3069 (2017/11/14)
------------------
-* ドライブウィンドウもポップアウトできるように
-* デザインの調整
-
-3066 (2017/11/14)
------------------
-* メッセージウィジェット追加
-* アクセスログウィジェット追加
-
-3057 (2017/11/13)
------------------
-* グリッチ修正
-
-3055 (2017/11/13)
------------------
-* メッセージのウィンドウのポップアウト (#911)
-
-3050 (2017/11/13)
------------------
-* 通信の最適化
-  * これで例えばサーバー情報ウィジェットを5000兆個設置しても利用するコネクションは一つだけになりウィジェットを1つ設置したときと(ネットワーク的な)負荷は変わらなくなる
-* デザインの調整
-* ユーザビリティの向上
-
-3040 (2017/11/12)
------------------
-* バグ修正
-
-3038 (2017/11/12)
------------------
-* 投稿フォームウィジェットの追加
-* タイムライン上部にもウィジェットを配置できるように
-
-3035 (2017/11/12)
------------------
-* ウィジェットの強化
-
-3033 (2017/11/12)
------------------
-* デザインの調整
-
-3031 (2017/11/12)
------------------
-* ウィジェットの強化
-
-3028 (2017/11/12)
------------------
-* ウィジェットの表示をコンパクトにできるように
-
-3026 (2017/11/12)
------------------
-* バグ修正
-
-3024 (2017/11/12)
------------------
-* いい感じにするなど
-
-3020 (2017/11/12)
------------------
-* 通信の最適化
-
-3017 (2017/11/11)
------------------
-* 誤字修正など
-
-3012 (2017/11/11)
------------------
-* デザインの調整
-
-3010 (2017/11/11)
------------------
-* デザインの調整
-
-3008 (2017/11/11)
------------------
-* カレンダー(タイムマシン)ウィジェットの追加
-
-3006 (2017/11/11)
------------------
-* デザインの調整
-* など
-
-2996 (2017/11/10)
------------------
-* デザインの調整
-* など
-
-2991 (2017/11/09)
------------------
-* デザインの調整
-
-2988 (2017/11/09)
------------------
-* チャンネルウィジェットを追加
-
-2984 (2017/11/09)
------------------
-* スライドショーウィジェットを追加
-
-2974 (2017/11/08)
------------------
-* ホームのカスタマイズを実装するなど
-
-2971 (2017/11/08)
------------------
-* バグ修正
-* デザインの調整
-* i18n
-
-2944 (2017/11/07)
------------------
-* パフォーマンスの向上
-  * GirdFSになるなどした
-* 依存関係の更新
-
-2807 (2017/11/02)
------------------
-* いい感じに
-
-2805 (2017/11/02)
------------------
-* いい感じに
-
-2801 (2017/11/01)
------------------
-* チャンネルのWatch実装
-
-2799 (2017/11/01)
------------------
-* いい感じに
-
-2795 (2017/11/01)
------------------
-* いい感じに
-
-2793 (2017/11/01)
------------------
-* なんか
-
-2783 (2017/11/01)
------------------
-* なんか
-
-2777 (2017/11/01)
------------------
-* 細かいブラッシュアップ
-
-2775 (2017/11/01)
------------------
-* Fix: バグ修正
-
-2769 (2017/11/01)
------------------
-* New: チャンネルシステム
-
-2752 (2017/10/30)
------------------
-* New: 未読の通知がある場合アイコンを表示するように
-
-2747 (2017/10/25)
------------------
-* Fix: 非ログイン状態ですべてのページが致命的な問題を発生させる (#89)
-
-2742 (2017/10/25)
------------------
-* New: トラブルシューティングを実装するなど
-
-2735 (2017/10/22)
------------------
-* New: モバイル版からでもクライアントバージョンを確認できるように
-
-2732 (2017/10/22)
------------------
-* 依存関係の更新など
-
-2584 (2017/09/08)
------------------
-* New: ユーザーページによく使うドメインを表示 (#771)
-* New: よくリプライするユーザーをユーザーページに表示 (#770)
-
-2566 (2017/09/07)
------------------
-* New: 投稿することの多いキーワードをユーザーページに表示する (#768)
-* l10n
-* デザインの修正
-
-2544 (2017/09/06)
------------------
-* 投稿のカテゴリに関する実験的な実装
-* l10n
-* ユーザビリティの向上
-
-2520 (2017/08/30)
------------------
-* デザインの調整
-
-2518 (2017/08/30)
------------------
-* Fix: モバイル版のタイムラインからリアクションやメニューを開けない
-* デザインの調整
-
-2515 (2017/08/30)
------------------
-* New: 投稿のピン留め (#746)
-* New: モバイル版のユーザーページに知り合いのフォロワーを表示するように
-* New: ホームストリームにメッセージを流すことでlast_used_atを更新できるようにする (#745)
-* その他細かな修正
-
-2508 (2017/08/30)
------------------
-* New: モバイル版のユーザーページのアクティビティチャートを変更
-* New: モバイル版のユーザーページに最終ログイン日時を表示するように
-* デザインの調整
-
-2503 (2017/08/30)
------------------
-* デザインの調整
-
-2502 (2017/08/30)
------------------
-* デザインの修正・調整
-
-2501 (2017/08/30)
------------------
-* New: モバイルのユーザーページを刷新
-
-2498 (2017/08/29)
------------------
-* Fix: repostのborder-radiusが効いていない (#743)
-* テーマカラーを赤に戻してみた
-* ユーザビリティの向上
-* デザインの調整
-
-2493-2 (2017/08/29)
--------------------
-* デザインの修正
-
-2493 (2017/08/29)
------------------
-* デザインの変更など
-
-2491 (2017/08/29)
------------------
-* デザインの修正と調整
-
-2489 (2017/08/29)
------------------
-* ユーザビリティの向上
-* デザインの調整
-
-2487 (2017/08/29)
------------------
-* New: パスワードを変更する際に新しいパスワードを二度入力させる (#739)
-* New: ドナーを表示する (#738)
-* Fix: 投稿のリンクが機能していない問題を修正
-* Fix: アカウント作成フォームのユーザーページURLプレビューが正しく機能していなかった問題を修正
-* l10n
-* デザインの調整
-
-2470 (2017/08/29)
------------------
-* New: トークンを再生成できるように (#497)
-* New: パスワードを変更する機能 (#364)
-
-2461 (2017/08/28)
------------------
-* Fix: モバイル版からアバターとバナーの設定を行えなかった問題を修正
-* デザインの修正
-
-2458 (2017/08/28)
------------------
-* New: モバイル版からプロフィールを設定できるように
-* New: モバイル版からサインアウトを行えるように
-* New: 投稿ページに次の投稿/前の投稿リンクを作成 (#734)
-* New: タイムラインの投稿をダブルクリックすることで詳細な情報が見れるように
-* Fix: モバイル版でおすすめユーザーをフォローしてもタイムラインが更新されない (#736)
-* Fix: モバイル版で設定にアクセスできない
-* デザインの調整
-* 依存関係の更新
-
-2380
-----
-アプリケーションが作れない問題を修正
-
-2367
-----
-Statsのユーザー数グラフに「アカウントが作成された**回数**」(その日時点での「アカウント数」**ではなく**)グラフも併記するようにした
-
-2364
-----
-デザインの微調整
-
-2361
-----
-Statsを実装するなど
-
-2357
-----
-Statusを実装するなど
diff --git a/LICENSE b/LICENSE
index 0b6e30e45..dba13ed2d 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,21 +1,661 @@
-The MIT License (MIT)
+                    GNU AFFERO GENERAL PUBLIC LICENSE
+                       Version 3, 19 November 2007
 
-Copyright (c) 2014-2018 syuilo
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
 
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+                            Preamble
 
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
+  The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
 
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+  A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate.  Many developers of free software are heartened and
+encouraged by the resulting cooperation.  However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+  The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community.  It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server.  Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+  An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals.  This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU Affero General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Remote Network Interaction; Use with the GNU General Public License.
+
+  Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software.  This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time.  Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source.  For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code.  There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<http://www.gnu.org/licenses/>.
diff --git a/LICENSE_AGPL-3.0 b/LICENSE_AGPL-3.0
deleted file mode 100644
index dba13ed2d..000000000
--- a/LICENSE_AGPL-3.0
+++ /dev/null
@@ -1,661 +0,0 @@
-                    GNU AFFERO GENERAL PUBLIC LICENSE
-                       Version 3, 19 November 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
-                            Preamble
-
-  The GNU Affero General Public License is a free, copyleft license for
-software and other kinds of works, specifically designed to ensure
-cooperation with the community in the case of network server software.
-
-  The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works.  By contrast,
-our General Public Licenses are intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users.
-
-  When we speak of free software, we are referring to freedom, not
-price.  Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
-  Developers that use our General Public Licenses protect your rights
-with two steps: (1) assert copyright on the software, and (2) offer
-you this License which gives you legal permission to copy, distribute
-and/or modify the software.
-
-  A secondary benefit of defending all users' freedom is that
-improvements made in alternate versions of the program, if they
-receive widespread use, become available for other developers to
-incorporate.  Many developers of free software are heartened and
-encouraged by the resulting cooperation.  However, in the case of
-software used on network servers, this result may fail to come about.
-The GNU General Public License permits making a modified version and
-letting the public access it on a server without ever releasing its
-source code to the public.
-
-  The GNU Affero General Public License is designed specifically to
-ensure that, in such cases, the modified source code becomes available
-to the community.  It requires the operator of a network server to
-provide the source code of the modified version running there to the
-users of that server.  Therefore, public use of a modified version, on
-a publicly accessible server, gives the public access to the source
-code of the modified version.
-
-  An older license, called the Affero General Public License and
-published by Affero, was designed to accomplish similar goals.  This is
-a different license, not a version of the Affero GPL, but Affero has
-released a new version of the Affero GPL which permits relicensing under
-this license.
-
-  The precise terms and conditions for copying, distribution and
-modification follow.
-
-                       TERMS AND CONDITIONS
-
-  0. Definitions.
-
-  "This License" refers to version 3 of the GNU Affero General Public License.
-
-  "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
-  "The Program" refers to any copyrightable work licensed under this
-License.  Each licensee is addressed as "you".  "Licensees" and
-"recipients" may be individuals or organizations.
-
-  To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy.  The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
-  A "covered work" means either the unmodified Program or a work based
-on the Program.
-
-  To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy.  Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
-  To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies.  Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
-  An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License.  If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
-  1. Source Code.
-
-  The "source code" for a work means the preferred form of the work
-for making modifications to it.  "Object code" means any non-source
-form of a work.
-
-  A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
-  The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form.  A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
-  The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities.  However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work.  For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
-  The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
-  The Corresponding Source for a work in source code form is that
-same work.
-
-  2. Basic Permissions.
-
-  All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met.  This License explicitly affirms your unlimited
-permission to run the unmodified Program.  The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work.  This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
-  You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force.  You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright.  Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
-  Conveying under any other circumstances is permitted solely under
-the conditions stated below.  Sublicensing is not allowed; section 10
-makes it unnecessary.
-
-  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
-  No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
-  When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
-  4. Conveying Verbatim Copies.
-
-  You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
-  You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
-  5. Conveying Modified Source Versions.
-
-  You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
-    a) The work must carry prominent notices stating that you modified
-    it, and giving a relevant date.
-
-    b) The work must carry prominent notices stating that it is
-    released under this License and any conditions added under section
-    7.  This requirement modifies the requirement in section 4 to
-    "keep intact all notices".
-
-    c) You must license the entire work, as a whole, under this
-    License to anyone who comes into possession of a copy.  This
-    License will therefore apply, along with any applicable section 7
-    additional terms, to the whole of the work, and all its parts,
-    regardless of how they are packaged.  This License gives no
-    permission to license the work in any other way, but it does not
-    invalidate such permission if you have separately received it.
-
-    d) If the work has interactive user interfaces, each must display
-    Appropriate Legal Notices; however, if the Program has interactive
-    interfaces that do not display Appropriate Legal Notices, your
-    work need not make them do so.
-
-  A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit.  Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
-  6. Conveying Non-Source Forms.
-
-  You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
-    a) Convey the object code in, or embodied in, a physical product
-    (including a physical distribution medium), accompanied by the
-    Corresponding Source fixed on a durable physical medium
-    customarily used for software interchange.
-
-    b) Convey the object code in, or embodied in, a physical product
-    (including a physical distribution medium), accompanied by a
-    written offer, valid for at least three years and valid for as
-    long as you offer spare parts or customer support for that product
-    model, to give anyone who possesses the object code either (1) a
-    copy of the Corresponding Source for all the software in the
-    product that is covered by this License, on a durable physical
-    medium customarily used for software interchange, for a price no
-    more than your reasonable cost of physically performing this
-    conveying of source, or (2) access to copy the
-    Corresponding Source from a network server at no charge.
-
-    c) Convey individual copies of the object code with a copy of the
-    written offer to provide the Corresponding Source.  This
-    alternative is allowed only occasionally and noncommercially, and
-    only if you received the object code with such an offer, in accord
-    with subsection 6b.
-
-    d) Convey the object code by offering access from a designated
-    place (gratis or for a charge), and offer equivalent access to the
-    Corresponding Source in the same way through the same place at no
-    further charge.  You need not require recipients to copy the
-    Corresponding Source along with the object code.  If the place to
-    copy the object code is a network server, the Corresponding Source
-    may be on a different server (operated by you or a third party)
-    that supports equivalent copying facilities, provided you maintain
-    clear directions next to the object code saying where to find the
-    Corresponding Source.  Regardless of what server hosts the
-    Corresponding Source, you remain obligated to ensure that it is
-    available for as long as needed to satisfy these requirements.
-
-    e) Convey the object code using peer-to-peer transmission, provided
-    you inform other peers where the object code and Corresponding
-    Source of the work are being offered to the general public at no
-    charge under subsection 6d.
-
-  A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
-  A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling.  In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage.  For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product.  A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
-  "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source.  The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
-  If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information.  But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
-  The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed.  Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
-  Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
-  7. Additional Terms.
-
-  "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law.  If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
-  When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it.  (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.)  You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
-  Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
-    a) Disclaiming warranty or limiting liability differently from the
-    terms of sections 15 and 16 of this License; or
-
-    b) Requiring preservation of specified reasonable legal notices or
-    author attributions in that material or in the Appropriate Legal
-    Notices displayed by works containing it; or
-
-    c) Prohibiting misrepresentation of the origin of that material, or
-    requiring that modified versions of such material be marked in
-    reasonable ways as different from the original version; or
-
-    d) Limiting the use for publicity purposes of names of licensors or
-    authors of the material; or
-
-    e) Declining to grant rights under trademark law for use of some
-    trade names, trademarks, or service marks; or
-
-    f) Requiring indemnification of licensors and authors of that
-    material by anyone who conveys the material (or modified versions of
-    it) with contractual assumptions of liability to the recipient, for
-    any liability that these contractual assumptions directly impose on
-    those licensors and authors.
-
-  All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10.  If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term.  If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
-  If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
-  Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
-  8. Termination.
-
-  You may not propagate or modify a covered work except as expressly
-provided under this License.  Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
-  However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
-  Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
-  Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License.  If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
-  9. Acceptance Not Required for Having Copies.
-
-  You are not required to accept this License in order to receive or
-run a copy of the Program.  Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance.  However,
-nothing other than this License grants you permission to propagate or
-modify any covered work.  These actions infringe copyright if you do
-not accept this License.  Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
-  10. Automatic Licensing of Downstream Recipients.
-
-  Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License.  You are not responsible
-for enforcing compliance by third parties with this License.
-
-  An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations.  If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
-  You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License.  For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
-  11. Patents.
-
-  A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based.  The
-work thus licensed is called the contributor's "contributor version".
-
-  A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version.  For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
-  Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
-  In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement).  To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
-  If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients.  "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
-  If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
-  A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License.  You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
-  Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
-  12. No Surrender of Others' Freedom.
-
-  If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License.  If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all.  For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
-  13. Remote Network Interaction; Use with the GNU General Public License.
-
-  Notwithstanding any other provision of this License, if you modify the
-Program, your modified version must prominently offer all users
-interacting with it remotely through a computer network (if your version
-supports such interaction) an opportunity to receive the Corresponding
-Source of your version by providing access to the Corresponding Source
-from a network server at no charge, through some standard or customary
-means of facilitating copying of software.  This Corresponding Source
-shall include the Corresponding Source for any work covered by version 3
-of the GNU General Public License that is incorporated pursuant to the
-following paragraph.
-
-  Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU General Public License into a single
-combined work, and to convey the resulting work.  The terms of this
-License will continue to apply to the part which is the covered work,
-but the work with which it is combined will remain governed by version
-3 of the GNU General Public License.
-
-  14. Revised Versions of this License.
-
-  The Free Software Foundation may publish revised and/or new versions of
-the GNU Affero General Public License from time to time.  Such new versions
-will be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
-  Each version is given a distinguishing version number.  If the
-Program specifies that a certain numbered version of the GNU Affero General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation.  If the Program does not specify a version number of the
-GNU Affero General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
-  If the Program specifies that a proxy can decide which future
-versions of the GNU Affero General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
-  Later license versions may give you additional or different
-permissions.  However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
-  15. Disclaimer of Warranty.
-
-  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
-  16. Limitation of Liability.
-
-  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
-  17. Interpretation of Sections 15 and 16.
-
-  If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
-                     END OF TERMS AND CONDITIONS
-
-            How to Apply These Terms to Your New Programs
-
-  If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
-  To do so, attach the following notices to the program.  It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-    <one line to give the program's name and a brief idea of what it does.>
-    Copyright (C) <year>  <name of author>
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as published by
-    the Free Software Foundation, either version 3 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-Also add information on how to contact you by electronic and paper mail.
-
-  If your software can interact with users remotely through a computer
-network, you should also make sure that it provides a way for users to
-get its source.  For example, if your program is a web application, its
-interface could display a "Source" link that leads users to an archive
-of the code.  There are many ways you could offer source, and different
-solutions will be better for different programs; see section 13 for the
-specific requirements.
-
-  You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU AGPL, see
-<http://www.gnu.org/licenses/>.
diff --git a/README.md b/README.md
index c50566cc9..ef12958c2 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,6 @@
-![Misskey](./assets/title.png)
+<img src="https://github.com/syuilo/misskey/blob/b3f42e62af698a67c2250533c437569559f1fdf9/src/himasaku/resources/himasaku.png?raw=true" align="right" width="320px"/>
+
+Misskey
 ================================================================
 
 [![][travis-badge]][travis-link]
@@ -7,57 +9,46 @@
 [![][sakurako-badge]][himasaku]
 [![][agpl-3.0-badge]][AGPL-3.0]
 
-[Misskey](https://misskey.xyz) is a completely open source,
+> Lead Maintainer: [syuilo][syuilo-link]
+
+**[Misskey](https://misskey.xyz)** is a completely open source,
 ultimately sophisticated new type of mini-blog based SNS.
 
-![ss](./assets/ss.jpg)
-
-Key features
+:sparkles: Features
 ----------------------------------------------------------------
 * Automatically updated timeline
 * Private messages
 * Two-Factor Authentication support
 * ServiceWorker support
 * Web API for third-party applications
-* No ads
+* ActivityPub compatible
 
 and more! You can touch with your own eyes at https://misskey.xyz/.
 
-Setup and Installation
+:package: Setup and Installation
 ----------------------------------------------------------------
 If you want to run your own instance of Misskey,
 please see [Setup and installation guide](./docs/setup.en.md).
 
-Contribution
+:yen: Donation
 ----------------------------------------------------------------
-Please see [Contribution guide](./CONTRIBUTING.md).
+If you want to donate to Misskey, please see [this](./docs/donate.ja.md).
 
-Release Notes
+[List of all donors](./DONORS.md)
+
+:mortar_board: Notable contributors
 ----------------------------------------------------------------
-Please see [ChangeLog](./CHANGELOG.md).
-
-Sponsors & Backers
-----------------------------------------------------------------
-Misskey has no 100+ GitHub stars currently. However, a donation is always welcome!
-If you want to donate to Misskey, please get in touch with [@syuilo][syuilo-link].
-
-**Note:** When you donate to Misskey, your name will be listed in [donors](./DONORS.md).
-
-Collaborators
-----------------------------------------------------------------
-| ![syuilo][syuilo-icon] | ![Morisawa Aya][ayamorisawa-icon] | ![otofune][otofune-icon]        |
-|------------------------|-----------------------------------|---------------------------------|
-| [syuilo][syuilo-link]  | [Aya Morisawa][ayamorisawa-link]  | [otofune][otofune-link] |
+| ![syuilo][syuilo-icon] | ![Morisawa Aya][ayamorisawa-icon] | ![otofune][otofune-icon] | ![akihikodaki][akihikodaki-icon] | ![rinsuki][rinsuki-icon] |
+|:-:|:-:|:-:|:-:|:-:|
+| [syuilo][syuilo-link]<br>Owner | [Aya Morisawa][ayamorisawa-link]<br>Collaborator | [otofune][otofune-link]<br>Collaborator | [akihikodaki][akihikodaki-link] | [rinsuki][rinsuki-link] |
 
 [List of all contributors](https://github.com/syuilo/misskey/graphs/contributors)
 
-Copyright
+:four_leaf_clover: Copyright
 ----------------------------------------------------------------
-Misskey is an open-source software licensed under [The MIT License](LICENSE).
+> Copyright (c) 2014-2018 syuilo
 
-The portions of Misskey contributed by Akihiko Odaki <nekomanma@pixiv.co.jp> is
-licensed under GNU Affero General Public License (only version 3.0 of the
-license is applied.) See Git log to identify them.
+Misskey is an open-source software licensed under [GNU AGPLv3](LICENSE).
 
 [agpl-3.0]:           https://www.gnu.org/licenses/agpl-3.0.en.html
 [agpl-3.0-badge]:     https://img.shields.io/badge/license-AGPL--3.0-444444.svg?style=flat-square
@@ -69,10 +60,14 @@ license is applied.) See Git log to identify them.
 [himawari-badge]:     https://img.shields.io/badge/%E5%8F%A4%E8%B0%B7-%E5%90%91%E6%97%A5%E8%91%B5-1684c5.svg?style=flat-square
 [sakurako-badge]:     https://img.shields.io/badge/%E5%A4%A7%E5%AE%A4-%E6%AB%BB%E5%AD%90-efb02a.svg?style=flat-square
 
-<!-- Collaborators Info -->
+<!-- Contributors Info -->
 [syuilo-link]:      https://syuilo.com
 [syuilo-icon]:      https://avatars2.githubusercontent.com/u/4439005?v=3&s=70
 [ayamorisawa-link]: https://github.com/ayamorisawa
 [ayamorisawa-icon]: https://avatars0.githubusercontent.com/u/10798641?v=3&s=70
 [otofune-link]:     https://github.com/otofune
 [otofune-icon]:     https://avatars0.githubusercontent.com/u/15062473?v=3&s=70
+[akihikodaki-link]: https://github.com/akihikodaki
+[akihikodaki-icon]: https://avatars2.githubusercontent.com/u/17036990?s=70&v=4
+[rinsuki-link]:     https://github.com/rinsuki
+[rinsuki-icon]:     https://avatars0.githubusercontent.com/u/6533808?s=70&v=4
diff --git a/docker/Dockerfile b/docker/Dockerfile
index ef04fc9e2..7cee650de 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -14,7 +14,7 @@ RUN pacman -S --noconfirm pacman
 RUN pacman-db-upgrade
 RUN pacman -S --noconfirm archlinux-keyring
 RUN pacman -Syyu --noconfirm
-RUN pacman -S --noconfirm git nodejs npm mongodb redis graphicsmagick
+RUN pacman -S --noconfirm git nodejs npm mongodb redis imagemagick
 
 COPY misskey.sh /root/misskey.sh
 RUN chmod u+x /root/misskey.sh
diff --git a/docs/setup.en.md b/docs/setup.en.md
index 08cd16857..a436d751c 100644
--- a/docs/setup.en.md
+++ b/docs/setup.en.md
@@ -37,9 +37,9 @@ Please install and setup these softwares:
 
 *3.* Prepare configuration
 ----------------------------------------------------------------
-First, you need to create a `.config` directory in the directory that
-Misskey installed. And then you need to create a `default.yml` file in
-the directory. The template of configuration is available [here](./config.md).
+1. Copy `example.yml` of `.config` directory
+2. Rename it to `default.yml`
+3. Edit it
 
 *4.* Install and build Misskey
 ----------------------------------------------------------------
diff --git a/docs/setup.ja.md b/docs/setup.ja.md
index 9fa56acb2..6605461d9 100644
--- a/docs/setup.ja.md
+++ b/docs/setup.ja.md
@@ -37,9 +37,9 @@ web-push generate-vapid-keys
 
 *3.* 設定ファイルを用意する
 ----------------------------------------------------------------
-Misskeyをインストールしたディレクトリに、`.config`というディレクトリを作成し、
-その中に`default.yml`という名前で設定ファイルを作ってください。
-設定ファイルの下書きは[ここ](./config.md)にありますので、コピペしてご利用ください。
+1. `.config`ディレクトリ内の`example.yml`をコピー
+2. `default.yml`にリネーム
+3. 編集する
 
 *4.* Misskeyのインストール(とビルド)
 ----------------------------------------------------------------
diff --git a/gulpfile.ts b/gulpfile.ts
index 11f34c962..46727126c 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -125,7 +125,6 @@ gulp.task('build:client:script', () =>
 		.pipe(replace('VERSION', JSON.stringify(version)))
 		.pipe(replace('API', JSON.stringify(config.api_url)))
 		.pipe(replace('ENV', JSON.stringify(env)))
-		.pipe(replace('HOST', JSON.stringify(config.host)))
 		.pipe(isProduction ? uglify({
 			toplevel: true
 		} as any) : gutil.noop())
diff --git a/locales/ja.yml b/locales/ja.yml
index f826b1b6c..fd140ecc3 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -147,7 +147,7 @@ common:
       available: "利用できます"
       unavailable: "既に利用されています"
       error: "通信エラー"
-      invalid-format: "a~z、A~Z、0~9、-(ハイフン)が使えます"
+      invalid-format: "a~z、A~Z、0~9、_が使えます"
       too-short: "3文字以上でお願いします!"
       too-long: "20文字以内でお願いします"
       password: "パスワード"
diff --git a/src/index.ts b/src/index.ts
index bd9b094d9..f86c768fd 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -109,7 +109,6 @@ async function workerMain(opt) {
  */
 async function init(): Promise<Config> {
 	Logger.info('Welcome to Misskey!');
-	Logger.info(chalk.bold('Misskey <aoi>'));
 	Logger.info('Initializing...');
 
 	EnvironmentInfo.show();
diff --git a/src/server/api/authenticate.ts b/src/server/api/authenticate.ts
index 537c3d1e1..7b3983a83 100644
--- a/src/server/api/authenticate.ts
+++ b/src/server/api/authenticate.ts
@@ -55,10 +55,10 @@ export default (req: express.Request) => new Promise<IAuthContext>(async (resolv
 		}
 
 		const app = await App
-			.findOne({ _id: accessToken.app_id });
+			.findOne({ _id: accessToken.appId });
 
 		const user = await User
-			.findOne({ _id: accessToken.user_id });
+			.findOne({ _id: accessToken.userId });
 
 		return resolve({
 			app: app,
diff --git a/src/server/api/bot/core.ts b/src/server/api/bot/core.ts
index 77a68aaee..ec7c935f9 100644
--- a/src/server/api/bot/core.ts
+++ b/src/server/api/bot/core.ts
@@ -67,7 +67,7 @@ export default class BotCore extends EventEmitter {
 			return await this.context.q(query);
 		}
 
-		if (/^@[a-zA-Z0-9-]+$/.test(query)) {
+		if (/^@[a-zA-Z0-9_]+$/.test(query)) {
 			return await this.showUserCommand(query);
 		}
 
@@ -208,7 +208,7 @@ class SigninContext extends Context {
 		if (this.temporaryUser == null) {
 			// Fetch user
 			const user: IUser = await User.findOne({
-				username_lower: query.toLowerCase(),
+				usernameLower: query.toLowerCase(),
 				host: null
 			}, {
 				fields: {
@@ -297,7 +297,7 @@ class TlContext extends Context {
 	private async getTl() {
 		const tl = await require('../endpoints/posts/timeline')({
 			limit: 5,
-			until_id: this.next ? this.next : undefined
+			untilId: this.next ? this.next : undefined
 		}, this.bot.user);
 
 		if (tl.length > 0) {
@@ -349,7 +349,7 @@ class NotificationsContext extends Context {
 	private async getNotifications() {
 		const notifications = await require('../endpoints/i/notifications')({
 			limit: 5,
-			until_id: this.next ? this.next : undefined
+			untilId: this.next ? this.next : undefined
 		}, this.bot.user);
 
 		if (notifications.length > 0) {
diff --git a/src/server/api/bot/interfaces/line.ts b/src/server/api/bot/interfaces/line.ts
index 5b3e9107f..1340ac992 100644
--- a/src/server/api/bot/interfaces/line.ts
+++ b/src/server/api/bot/interfaces/line.ts
@@ -115,7 +115,7 @@ class LineBot extends BotCore {
 			actions.push({
 				type: 'uri',
 				label: 'Twitterアカウントを見る',
-				uri: `https://twitter.com/${user.account.twitter.screen_name}`
+				uri: `https://twitter.com/${user.account.twitter.screenName}`
 			});
 		}
 
@@ -130,7 +130,7 @@ class LineBot extends BotCore {
 			altText: await super.showUserCommand(q),
 			template: {
 				type: 'buttons',
-				thumbnailImageUrl: `${user.avatar_url}?thumbnail&size=1024`,
+				thumbnailImageUrl: `${user.avatarUrl}?thumbnail&size=1024`,
 				title: `${user.name} (@${acct})`,
 				text: user.description || '(no description)',
 				actions: actions
@@ -142,7 +142,7 @@ class LineBot extends BotCore {
 
 	public async showUserTimelinePostback(userId: string) {
 		const tl = await require('../../endpoints/users/posts')({
-			user_id: userId,
+			userId: userId,
 			limit: 5
 		}, this.user);
 
@@ -174,7 +174,7 @@ module.exports = async (app: express.Application) => {
 			const user = await User.findOne({
 				host: null,
 				'account.line': {
-					user_id: sourceId
+					userId: sourceId
 				}
 			});
 
@@ -184,7 +184,7 @@ module.exports = async (app: express.Application) => {
 				User.update(user._id, {
 					$set: {
 						'account.line': {
-							user_id: sourceId
+							userId: sourceId
 						}
 					}
 				});
@@ -194,7 +194,7 @@ module.exports = async (app: express.Application) => {
 				User.update(user._id, {
 					$set: {
 						'account.line': {
-							user_id: null
+							userId: null
 						}
 					}
 				});
diff --git a/src/server/api/common/drive/add-file.ts b/src/server/api/common/drive/add-file.ts
index 5f3c69c15..21ddd1aae 100644
--- a/src/server/api/common/drive/add-file.ts
+++ b/src/server/api/common/drive/add-file.ts
@@ -100,7 +100,7 @@ const addFile = async (
 		// Check if there is a file with the same hash
 		const much = await DriveFile.findOne({
 			md5: hash,
-			'metadata.user_id': user._id
+			'metadata.userId': user._id
 		});
 
 		if (much !== null) {
@@ -172,7 +172,7 @@ const addFile = async (
 			}
 			const driveFolder = await DriveFolder.findOne({
 				_id: folderId,
-				user_id: user._id
+				userId: user._id
 			});
 			if (!driveFolder) {
 				throw 'folder-not-found';
@@ -184,7 +184,7 @@ const addFile = async (
 			// Calculate drive usage
 			const usage = await DriveFile
 				.aggregate([{
-					$match: { 'metadata.user_id': user._id }
+					$match: { 'metadata.userId': user._id }
 				}, {
 					$project: {
 						length: true
@@ -205,7 +205,7 @@ const addFile = async (
 			log(`drive usage is ${usage}`);
 
 			// If usage limit exceeded
-			if (usage + size > user.drive_capacity) {
+			if (usage + size > user.driveCapacity) {
 				throw 'no-free-space';
 			}
 		})()
@@ -221,12 +221,12 @@ const addFile = async (
 	}
 
 	if (averageColor) {
-		properties['average_color'] = averageColor;
+		properties['avgColor'] = averageColor;
 	}
 
 	return addToGridFS(detectedName, readable, mime, {
-		user_id: user._id,
-		folder_id: folder !== null ? folder._id : null,
+		userId: user._id,
+		folderId: folder !== null ? folder._id : null,
 		comment: comment,
 		properties: properties
 	});
@@ -297,7 +297,7 @@ export default (user: any, file: string | stream.Readable, ...args) => new Promi
 					id: file._id.toString(),
 					body: {
 						name: file.name,
-						user_id: user._id.toString()
+						userId: user._id.toString()
 					}
 				});
 			}
diff --git a/src/server/api/common/get-friends.ts b/src/server/api/common/get-friends.ts
index db6313816..7f548b3bb 100644
--- a/src/server/api/common/get-friends.ts
+++ b/src/server/api/common/get-friends.ts
@@ -6,17 +6,17 @@ export default async (me: mongodb.ObjectID, includeMe: boolean = true) => {
 	// SELECT followee
 	const myfollowing = await Following
 		.find({
-			follower_id: me,
+			followerId: me,
 			// 削除されたドキュメントは除く
-			deleted_at: { $exists: false }
+			deletedAt: { $exists: false }
 		}, {
 			fields: {
-				followee_id: true
+				followeeId: true
 			}
 		});
 
 	// ID list of other users who the I follows
-	const myfollowingIds = myfollowing.map(follow => follow.followee_id);
+	const myfollowingIds = myfollowing.map(follow => follow.followeeId);
 
 	if (includeMe) {
 		myfollowingIds.push(me);
diff --git a/src/server/api/common/notify.ts b/src/server/api/common/notify.ts
index ae5669b84..c4df17f88 100644
--- a/src/server/api/common/notify.ts
+++ b/src/server/api/common/notify.ts
@@ -16,11 +16,11 @@ export default (
 
 	// Create notification
 	const notification = await Notification.insert(Object.assign({
-		created_at: new Date(),
-		notifiee_id: notifiee,
-		notifier_id: notifier,
+		createdAt: new Date(),
+		notifieeId: notifiee,
+		notifierId: notifier,
 		type: type,
-		is_read: false
+		isRead: false
 	}, content));
 
 	resolve(notification);
@@ -31,14 +31,14 @@ export default (
 
 	// 3秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
 	setTimeout(async () => {
-		const fresh = await Notification.findOne({ _id: notification._id }, { is_read: true });
-		if (!fresh.is_read) {
+		const fresh = await Notification.findOne({ _id: notification._id }, { isRead: true });
+		if (!fresh.isRead) {
 			//#region ただしミュートしているユーザーからの通知なら無視
 			const mute = await Mute.find({
-				muter_id: notifiee,
-				deleted_at: { $exists: false }
+				muterId: notifiee,
+				deletedAt: { $exists: false }
 			});
-			const mutedUserIds = mute.map(m => m.mutee_id.toString());
+			const mutedUserIds = mute.map(m => m.muteeId.toString());
 			if (mutedUserIds.indexOf(notifier.toString()) != -1) {
 				return;
 			}
diff --git a/src/server/api/common/push-sw.ts b/src/server/api/common/push-sw.ts
index b33715eb1..e5fbec10e 100644
--- a/src/server/api/common/push-sw.ts
+++ b/src/server/api/common/push-sw.ts
@@ -20,7 +20,7 @@ export default async function(userId: mongo.ObjectID | string, type, body?) {
 
 	// Fetch
 	const subscriptions = await Subscription.find({
-		user_id: userId
+		userId: userId
 	});
 
 	subscriptions.forEach(subscription => {
@@ -41,7 +41,7 @@ export default async function(userId: mongo.ObjectID | string, type, body?) {
 
 			if (err.statusCode == 410) {
 				Subscription.remove({
-					user_id: userId,
+					userId: userId,
 					endpoint: subscription.endpoint,
 					auth: subscription.auth,
 					publickey: subscription.publickey
diff --git a/src/server/api/common/read-messaging-message.ts b/src/server/api/common/read-messaging-message.ts
index 8e5e5b2b6..9047edec8 100644
--- a/src/server/api/common/read-messaging-message.ts
+++ b/src/server/api/common/read-messaging-message.ts
@@ -37,12 +37,12 @@ export default (
 	// Update documents
 	await Message.update({
 		_id: { $in: ids },
-		user_id: otherpartyId,
-		recipient_id: userId,
-		is_read: false
+		userId: otherpartyId,
+		recipientId: userId,
+		isRead: false
 	}, {
 		$set: {
-			is_read: true
+			isRead: true
 		}
 	}, {
 		multi: true
@@ -55,8 +55,8 @@ export default (
 	// Calc count of my unread messages
 	const count = await Message
 		.count({
-			recipient_id: userId,
-			is_read: false
+			recipientId: userId,
+			isRead: false
 		});
 
 	if (count == 0) {
diff --git a/src/server/api/common/read-notification.ts b/src/server/api/common/read-notification.ts
index 3009cc5d0..5bbf13632 100644
--- a/src/server/api/common/read-notification.ts
+++ b/src/server/api/common/read-notification.ts
@@ -29,10 +29,10 @@ export default (
 	// Update documents
 	await Notification.update({
 		_id: { $in: ids },
-		is_read: false
+		isRead: false
 	}, {
 		$set: {
-			is_read: true
+			isRead: true
 		}
 	}, {
 		multi: true
@@ -41,8 +41,8 @@ export default (
 	// Calc count of my unread notifications
 	const count = await Notification
 		.count({
-			notifiee_id: userId,
-			is_read: false
+			notifieeId: userId,
+			isRead: false
 		});
 
 	if (count == 0) {
diff --git a/src/server/api/common/watch-post.ts b/src/server/api/common/watch-post.ts
index 1a50f0eda..61ea44443 100644
--- a/src/server/api/common/watch-post.ts
+++ b/src/server/api/common/watch-post.ts
@@ -3,15 +3,15 @@ import Watching from '../models/post-watching';
 
 export default async (me: mongodb.ObjectID, post: object) => {
 	// 自分の投稿はwatchできない
-	if (me.equals((post as any).user_id)) {
+	if (me.equals((post as any).userId)) {
 		return;
 	}
 
 	// if watching now
 	const exist = await Watching.findOne({
-		post_id: (post as any)._id,
-		user_id: me,
-		deleted_at: { $exists: false }
+		postId: (post as any)._id,
+		userId: me,
+		deletedAt: { $exists: false }
 	});
 
 	if (exist !== null) {
@@ -19,8 +19,8 @@ export default async (me: mongodb.ObjectID, post: object) => {
 	}
 
 	await Watching.insert({
-		created_at: new Date(),
-		post_id: (post as any)._id,
-		user_id: me
+		createdAt: new Date(),
+		postId: (post as any)._id,
+		userId: me
 	});
 };
diff --git a/src/server/api/endpoints.ts b/src/server/api/endpoints.ts
index c7100bd03..979d8ac29 100644
--- a/src/server/api/endpoints.ts
+++ b/src/server/api/endpoints.ts
@@ -289,7 +289,7 @@ const endpoints: Endpoint[] = [
 		kind: 'notification-write'
 	},
 	{
-		name: 'notifications/mark_as_read_all',
+		name: 'notifications/markAsRead_all',
 		withCredential: true,
 		kind: 'notification-write'
 	},
diff --git a/src/server/api/endpoints/aggregation/posts.ts b/src/server/api/endpoints/aggregation/posts.ts
index 9d8bccbdb..67d261964 100644
--- a/src/server/api/endpoints/aggregation/posts.ts
+++ b/src/server/api/endpoints/aggregation/posts.ts
@@ -18,23 +18,23 @@ module.exports = params => new Promise(async (res, rej) => {
 	const datas = await Post
 		.aggregate([
 			{ $project: {
-				repost_id: '$repost_id',
-				reply_id: '$reply_id',
-				created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
+				repostId: '$repostId',
+				replyId: '$replyId',
+				createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
 			}},
 			{ $project: {
 				date: {
-					year: { $year: '$created_at' },
-					month: { $month: '$created_at' },
-					day: { $dayOfMonth: '$created_at' }
+					year: { $year: '$createdAt' },
+					month: { $month: '$createdAt' },
+					day: { $dayOfMonth: '$createdAt' }
 				},
 				type: {
 					$cond: {
-						if: { $ne: ['$repost_id', null] },
+						if: { $ne: ['$repostId', null] },
 						then: 'repost',
 						else: {
 							$cond: {
-								if: { $ne: ['$reply_id', null] },
+								if: { $ne: ['$replyId', null] },
 								then: 'reply',
 								else: 'post'
 							}
diff --git a/src/server/api/endpoints/aggregation/posts/reaction.ts b/src/server/api/endpoints/aggregation/posts/reaction.ts
index eb99b9d08..9f9a4f37e 100644
--- a/src/server/api/endpoints/aggregation/posts/reaction.ts
+++ b/src/server/api/endpoints/aggregation/posts/reaction.ts
@@ -12,9 +12,9 @@ import Reaction from '../../../models/post-reaction';
  * @return {Promise<any>}
  */
 module.exports = (params) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Lookup post
 	const post = await Post.findOne({
@@ -27,15 +27,15 @@ module.exports = (params) => new Promise(async (res, rej) => {
 
 	const datas = await Reaction
 		.aggregate([
-			{ $match: { post_id: post._id } },
+			{ $match: { postId: post._id } },
 			{ $project: {
-				created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
+				createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
 			}},
 			{ $project: {
 				date: {
-					year: { $year: '$created_at' },
-					month: { $month: '$created_at' },
-					day: { $dayOfMonth: '$created_at' }
+					year: { $year: '$createdAt' },
+					month: { $month: '$createdAt' },
+					day: { $dayOfMonth: '$createdAt' }
 				}
 			}},
 			{ $group: {
diff --git a/src/server/api/endpoints/aggregation/posts/reactions.ts b/src/server/api/endpoints/aggregation/posts/reactions.ts
index 790b523be..2dc989281 100644
--- a/src/server/api/endpoints/aggregation/posts/reactions.ts
+++ b/src/server/api/endpoints/aggregation/posts/reactions.ts
@@ -12,9 +12,9 @@ import Reaction from '../../../models/post-reaction';
  * @return {Promise<any>}
  */
 module.exports = (params) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Lookup post
 	const post = await Post.findOne({
@@ -29,10 +29,10 @@ module.exports = (params) => new Promise(async (res, rej) => {
 
 	const reactions = await Reaction
 		.find({
-			post_id: post._id,
+			postId: post._id,
 			$or: [
-				{ deleted_at: { $exists: false } },
-				{ deleted_at: { $gt: startTime } }
+				{ deletedAt: { $exists: false } },
+				{ deletedAt: { $gt: startTime } }
 			]
 		}, {
 			sort: {
@@ -40,7 +40,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
 			},
 			fields: {
 				_id: false,
-				post_id: false
+				postId: false
 			}
 		});
 
@@ -55,7 +55,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
 		// day = day.getTime();
 
 		const count = reactions.filter(r =>
-			r.created_at < day && (r.deleted_at == null || r.deleted_at > day)
+			r.createdAt < day && (r.deletedAt == null || r.deletedAt > day)
 		).length;
 
 		graph.push({
diff --git a/src/server/api/endpoints/aggregation/posts/reply.ts b/src/server/api/endpoints/aggregation/posts/reply.ts
index b114c34e1..3b050582a 100644
--- a/src/server/api/endpoints/aggregation/posts/reply.ts
+++ b/src/server/api/endpoints/aggregation/posts/reply.ts
@@ -11,9 +11,9 @@ import Post from '../../../models/post';
  * @return {Promise<any>}
  */
 module.exports = (params) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Lookup post
 	const post = await Post.findOne({
@@ -28,13 +28,13 @@ module.exports = (params) => new Promise(async (res, rej) => {
 		.aggregate([
 			{ $match: { reply: post._id } },
 			{ $project: {
-				created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
+				createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
 			}},
 			{ $project: {
 				date: {
-					year: { $year: '$created_at' },
-					month: { $month: '$created_at' },
-					day: { $dayOfMonth: '$created_at' }
+					year: { $year: '$createdAt' },
+					month: { $month: '$createdAt' },
+					day: { $dayOfMonth: '$createdAt' }
 				}
 			}},
 			{ $group: {
diff --git a/src/server/api/endpoints/aggregation/posts/repost.ts b/src/server/api/endpoints/aggregation/posts/repost.ts
index 217159caa..d9f3e36a0 100644
--- a/src/server/api/endpoints/aggregation/posts/repost.ts
+++ b/src/server/api/endpoints/aggregation/posts/repost.ts
@@ -11,9 +11,9 @@ import Post from '../../../models/post';
  * @return {Promise<any>}
  */
 module.exports = (params) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Lookup post
 	const post = await Post.findOne({
@@ -26,15 +26,15 @@ module.exports = (params) => new Promise(async (res, rej) => {
 
 	const datas = await Post
 		.aggregate([
-			{ $match: { repost_id: post._id } },
+			{ $match: { repostId: post._id } },
 			{ $project: {
-				created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
+				createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
 			}},
 			{ $project: {
 				date: {
-					year: { $year: '$created_at' },
-					month: { $month: '$created_at' },
-					day: { $dayOfMonth: '$created_at' }
+					year: { $year: '$createdAt' },
+					month: { $month: '$createdAt' },
+					day: { $dayOfMonth: '$createdAt' }
 				}
 			}},
 			{ $group: {
diff --git a/src/server/api/endpoints/aggregation/users.ts b/src/server/api/endpoints/aggregation/users.ts
index e38ce92ff..a4e91a228 100644
--- a/src/server/api/endpoints/aggregation/users.ts
+++ b/src/server/api/endpoints/aggregation/users.ts
@@ -22,8 +22,8 @@ module.exports = params => new Promise(async (res, rej) => {
 			},
 			fields: {
 				_id: false,
-				created_at: true,
-				deleted_at: true
+				createdAt: true,
+				deletedAt: true
 			}
 		});
 
@@ -44,11 +44,11 @@ module.exports = params => new Promise(async (res, rej) => {
 		// day = day.getTime();
 
 		const total = users.filter(u =>
-			u.created_at < dayEnd && (u.deleted_at == null || u.deleted_at > dayEnd)
+			u.createdAt < dayEnd && (u.deletedAt == null || u.deletedAt > dayEnd)
 		).length;
 
 		const created = users.filter(u =>
-			u.created_at < dayEnd && u.created_at > dayStart
+			u.createdAt < dayEnd && u.createdAt > dayStart
 		).length;
 
 		graph.push({
diff --git a/src/server/api/endpoints/aggregation/users/activity.ts b/src/server/api/endpoints/aggregation/users/activity.ts
index 102a71d7c..d47761657 100644
--- a/src/server/api/endpoints/aggregation/users/activity.ts
+++ b/src/server/api/endpoints/aggregation/users/activity.ts
@@ -18,9 +18,9 @@ module.exports = (params) => new Promise(async (res, rej) => {
 	const [limit = 365, limitErr] = $(params.limit).optional.number().range(1, 365).$;
 	if (limitErr) return rej('invalid limit param');
 
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [userId, userIdErr] = $(params.userId).id().$;
+	if (userIdErr) return rej('invalid userId param');
 
 	// Lookup user
 	const user = await User.findOne({
@@ -37,25 +37,25 @@ module.exports = (params) => new Promise(async (res, rej) => {
 
 	const datas = await Post
 		.aggregate([
-			{ $match: { user_id: user._id } },
+			{ $match: { userId: user._id } },
 			{ $project: {
-				repost_id: '$repost_id',
-				reply_id: '$reply_id',
-				created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
+				repostId: '$repostId',
+				replyId: '$replyId',
+				createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
 			}},
 			{ $project: {
 				date: {
-					year: { $year: '$created_at' },
-					month: { $month: '$created_at' },
-					day: { $dayOfMonth: '$created_at' }
+					year: { $year: '$createdAt' },
+					month: { $month: '$createdAt' },
+					day: { $dayOfMonth: '$createdAt' }
 				},
 				type: {
 					$cond: {
-						if: { $ne: ['$repost_id', null] },
+						if: { $ne: ['$repostId', null] },
 						then: 'repost',
 						else: {
 							$cond: {
-								if: { $ne: ['$reply_id', null] },
+								if: { $ne: ['$replyId', null] },
 								then: 'reply',
 								else: 'post'
 							}
diff --git a/src/server/api/endpoints/aggregation/users/followers.ts b/src/server/api/endpoints/aggregation/users/followers.ts
index 3022b2b00..73a30281b 100644
--- a/src/server/api/endpoints/aggregation/users/followers.ts
+++ b/src/server/api/endpoints/aggregation/users/followers.ts
@@ -12,9 +12,9 @@ import Following from '../../../models/following';
  * @return {Promise<any>}
  */
 module.exports = (params) => new Promise(async (res, rej) => {
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [userId, userIdErr] = $(params.userId).id().$;
+	if (userIdErr) return rej('invalid userId param');
 
 	// Lookup user
 	const user = await User.findOne({
@@ -33,17 +33,17 @@ module.exports = (params) => new Promise(async (res, rej) => {
 
 	const following = await Following
 		.find({
-			followee_id: user._id,
+			followeeId: user._id,
 			$or: [
-				{ deleted_at: { $exists: false } },
-				{ deleted_at: { $gt: startTime } }
+				{ deletedAt: { $exists: false } },
+				{ deletedAt: { $gt: startTime } }
 			]
 		}, {
 			_id: false,
-			follower_id: false,
-			followee_id: false
+			followerId: false,
+			followeeId: false
 		}, {
-			sort: { created_at: -1 }
+			sort: { createdAt: -1 }
 		});
 
 	const graph = [];
@@ -57,7 +57,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
 		// day = day.getTime();
 
 		const count = following.filter(f =>
-			f.created_at < day && (f.deleted_at == null || f.deleted_at > day)
+			f.createdAt < day && (f.deletedAt == null || f.deletedAt > day)
 		).length;
 
 		graph.push({
diff --git a/src/server/api/endpoints/aggregation/users/following.ts b/src/server/api/endpoints/aggregation/users/following.ts
index 92da7e692..16fc568d5 100644
--- a/src/server/api/endpoints/aggregation/users/following.ts
+++ b/src/server/api/endpoints/aggregation/users/following.ts
@@ -12,9 +12,9 @@ import Following from '../../../models/following';
  * @return {Promise<any>}
  */
 module.exports = (params) => new Promise(async (res, rej) => {
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [userId, userIdErr] = $(params.userId).id().$;
+	if (userIdErr) return rej('invalid userId param');
 
 	// Lookup user
 	const user = await User.findOne({
@@ -33,17 +33,17 @@ module.exports = (params) => new Promise(async (res, rej) => {
 
 	const following = await Following
 		.find({
-			follower_id: user._id,
+			followerId: user._id,
 			$or: [
-				{ deleted_at: { $exists: false } },
-				{ deleted_at: { $gt: startTime } }
+				{ deletedAt: { $exists: false } },
+				{ deletedAt: { $gt: startTime } }
 			]
 		}, {
 			_id: false,
-			follower_id: false,
-			followee_id: false
+			followerId: false,
+			followeeId: false
 		}, {
-			sort: { created_at: -1 }
+			sort: { createdAt: -1 }
 		});
 
 	const graph = [];
@@ -56,7 +56,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
 		day = new Date(day.setHours(23));
 
 		const count = following.filter(f =>
-			f.created_at < day && (f.deleted_at == null || f.deleted_at > day)
+			f.createdAt < day && (f.deletedAt == null || f.deletedAt > day)
 		).length;
 
 		graph.push({
diff --git a/src/server/api/endpoints/aggregation/users/post.ts b/src/server/api/endpoints/aggregation/users/post.ts
index c6a75eee3..c98874859 100644
--- a/src/server/api/endpoints/aggregation/users/post.ts
+++ b/src/server/api/endpoints/aggregation/users/post.ts
@@ -12,9 +12,9 @@ import Post from '../../../models/post';
  * @return {Promise<any>}
  */
 module.exports = (params) => new Promise(async (res, rej) => {
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [userId, userIdErr] = $(params.userId).id().$;
+	if (userIdErr) return rej('invalid userId param');
 
 	// Lookup user
 	const user = await User.findOne({
@@ -31,25 +31,25 @@ module.exports = (params) => new Promise(async (res, rej) => {
 
 	const datas = await Post
 		.aggregate([
-			{ $match: { user_id: user._id } },
+			{ $match: { userId: user._id } },
 			{ $project: {
-				repost_id: '$repost_id',
-				reply_id: '$reply_id',
-				created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
+				repostId: '$repostId',
+				replyId: '$replyId',
+				createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
 			}},
 			{ $project: {
 				date: {
-					year: { $year: '$created_at' },
-					month: { $month: '$created_at' },
-					day: { $dayOfMonth: '$created_at' }
+					year: { $year: '$createdAt' },
+					month: { $month: '$createdAt' },
+					day: { $dayOfMonth: '$createdAt' }
 				},
 				type: {
 					$cond: {
-						if: { $ne: ['$repost_id', null] },
+						if: { $ne: ['$repostId', null] },
 						then: 'repost',
 						else: {
 							$cond: {
-								if: { $ne: ['$reply_id', null] },
+								if: { $ne: ['$replyId', null] },
 								then: 'reply',
 								else: 'post'
 							}
diff --git a/src/server/api/endpoints/aggregation/users/reaction.ts b/src/server/api/endpoints/aggregation/users/reaction.ts
index 0a082ed1b..60b33e9d1 100644
--- a/src/server/api/endpoints/aggregation/users/reaction.ts
+++ b/src/server/api/endpoints/aggregation/users/reaction.ts
@@ -12,9 +12,9 @@ import Reaction from '../../../models/post-reaction';
  * @return {Promise<any>}
  */
 module.exports = (params) => new Promise(async (res, rej) => {
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [userId, userIdErr] = $(params.userId).id().$;
+	if (userIdErr) return rej('invalid userId param');
 
 	// Lookup user
 	const user = await User.findOne({
@@ -31,15 +31,15 @@ module.exports = (params) => new Promise(async (res, rej) => {
 
 	const datas = await Reaction
 		.aggregate([
-			{ $match: { user_id: user._id } },
+			{ $match: { userId: user._id } },
 			{ $project: {
-				created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
+				createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
 			}},
 			{ $project: {
 				date: {
-					year: { $year: '$created_at' },
-					month: { $month: '$created_at' },
-					day: { $dayOfMonth: '$created_at' }
+					year: { $year: '$createdAt' },
+					month: { $month: '$createdAt' },
+					day: { $dayOfMonth: '$createdAt' }
 				}
 			}},
 			{ $group: {
diff --git a/src/server/api/endpoints/app/create.ts b/src/server/api/endpoints/app/create.ts
index 0f688792a..713078463 100644
--- a/src/server/api/endpoints/app/create.ts
+++ b/src/server/api/endpoints/app/create.ts
@@ -13,7 +13,7 @@ import App, { isValidNameId, pack } from '../../models/app';
  *     parameters:
  *       - $ref: "#/parameters/AccessToken"
  *       -
- *         name: name_id
+ *         name: nameId
  *         description: Application unique name
  *         in: formData
  *         required: true
@@ -40,7 +40,7 @@ import App, { isValidNameId, pack } from '../../models/app';
  *           type: string
  *           collectionFormat: csv
  *       -
- *         name: callback_url
+ *         name: callbackUrl
  *         description: URL called back after authentication
  *         in: formData
  *         required: false
@@ -66,9 +66,9 @@ import App, { isValidNameId, pack } from '../../models/app';
  * @return {Promise<any>}
  */
 module.exports = async (params, user) => new Promise(async (res, rej) => {
-	// Get 'name_id' parameter
-	const [nameId, nameIdErr] = $(params.name_id).string().pipe(isValidNameId).$;
-	if (nameIdErr) return rej('invalid name_id param');
+	// Get 'nameId' parameter
+	const [nameId, nameIdErr] = $(params.nameId).string().pipe(isValidNameId).$;
+	if (nameIdErr) return rej('invalid nameId param');
 
 	// Get 'name' parameter
 	const [name, nameErr] = $(params.name).string().$;
@@ -82,24 +82,24 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	const [permission, permissionErr] = $(params.permission).array('string').unique().$;
 	if (permissionErr) return rej('invalid permission param');
 
-	// Get 'callback_url' parameter
+	// Get 'callbackUrl' parameter
 	// TODO: Check it is valid url
-	const [callbackUrl = null, callbackUrlErr] = $(params.callback_url).optional.nullable.string().$;
-	if (callbackUrlErr) return rej('invalid callback_url param');
+	const [callbackUrl = null, callbackUrlErr] = $(params.callbackUrl).optional.nullable.string().$;
+	if (callbackUrlErr) return rej('invalid callbackUrl param');
 
 	// Generate secret
 	const secret = rndstr('a-zA-Z0-9', 32);
 
 	// Create account
 	const app = await App.insert({
-		created_at: new Date(),
-		user_id: user._id,
+		createdAt: new Date(),
+		userId: user._id,
 		name: name,
-		name_id: nameId,
-		name_id_lower: nameId.toLowerCase(),
+		nameId: nameId,
+		nameIdLower: nameId.toLowerCase(),
 		description: description,
 		permission: permission,
-		callback_url: callbackUrl,
+		callbackUrl: callbackUrl,
 		secret: secret
 	});
 
diff --git a/src/server/api/endpoints/app/name_id/available.ts b/src/server/api/endpoints/app/name_id/available.ts
index 3d2c71032..6d02b26d2 100644
--- a/src/server/api/endpoints/app/name_id/available.ts
+++ b/src/server/api/endpoints/app/name_id/available.ts
@@ -7,12 +7,12 @@ import { isValidNameId } from '../../../models/app';
 
 /**
  * @swagger
- * /app/name_id/available:
+ * /app/nameId/available:
  *   post:
- *     summary: Check available name_id on creation an application
+ *     summary: Check available nameId on creation an application
  *     parameters:
  *       -
- *         name: name_id
+ *         name: nameId
  *         description: Application unique name
  *         in: formData
  *         required: true
@@ -25,7 +25,7 @@ import { isValidNameId } from '../../../models/app';
  *           type: object
  *           properties:
  *             available:
- *               description: Whether name_id is available
+ *               description: Whether nameId is available
  *               type: boolean
  *
  *       default:
@@ -35,20 +35,20 @@ import { isValidNameId } from '../../../models/app';
  */
 
 /**
- * Check available name_id of app
+ * Check available nameId of app
  *
  * @param {any} params
  * @return {Promise<any>}
  */
 module.exports = async (params) => new Promise(async (res, rej) => {
-	// Get 'name_id' parameter
-	const [nameId, nameIdErr] = $(params.name_id).string().pipe(isValidNameId).$;
-	if (nameIdErr) return rej('invalid name_id param');
+	// Get 'nameId' parameter
+	const [nameId, nameIdErr] = $(params.nameId).string().pipe(isValidNameId).$;
+	if (nameIdErr) return rej('invalid nameId param');
 
 	// Get exist
 	const exist = await App
 		.count({
-			name_id_lower: nameId.toLowerCase()
+			nameIdLower: nameId.toLowerCase()
 		}, {
 			limit: 1
 		});
diff --git a/src/server/api/endpoints/app/show.ts b/src/server/api/endpoints/app/show.ts
index 8bc3dda42..34bb958ee 100644
--- a/src/server/api/endpoints/app/show.ts
+++ b/src/server/api/endpoints/app/show.ts
@@ -9,15 +9,15 @@ import App, { pack } from '../../models/app';
  * /app/show:
  *   post:
  *     summary: Show an application's information
- *     description: Require app_id or name_id
+ *     description: Require appId or nameId
  *     parameters:
  *       -
- *         name: app_id
+ *         name: appId
  *         description: Application ID
  *         in: formData
  *         type: string
  *       -
- *         name: name_id
+ *         name: nameId
  *         description: Application unique name
  *         in: formData
  *         type: string
@@ -44,22 +44,22 @@ import App, { pack } from '../../models/app';
  * @return {Promise<any>}
  */
 module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) => {
-	// Get 'app_id' parameter
-	const [appId, appIdErr] = $(params.app_id).optional.id().$;
-	if (appIdErr) return rej('invalid app_id param');
+	// Get 'appId' parameter
+	const [appId, appIdErr] = $(params.appId).optional.id().$;
+	if (appIdErr) return rej('invalid appId param');
 
-	// Get 'name_id' parameter
-	const [nameId, nameIdErr] = $(params.name_id).optional.string().$;
-	if (nameIdErr) return rej('invalid name_id param');
+	// Get 'nameId' parameter
+	const [nameId, nameIdErr] = $(params.nameId).optional.string().$;
+	if (nameIdErr) return rej('invalid nameId param');
 
 	if (appId === undefined && nameId === undefined) {
-		return rej('app_id or name_id is required');
+		return rej('appId or nameId is required');
 	}
 
 	// Lookup app
 	const app = appId !== undefined
 		? await App.findOne({ _id: appId })
-		: await App.findOne({ name_id_lower: nameId.toLowerCase() });
+		: await App.findOne({ nameIdLower: nameId.toLowerCase() });
 
 	if (app === null) {
 		return rej('app not found');
@@ -67,6 +67,6 @@ module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) =>
 
 	// Send response
 	res(await pack(app, user, {
-		includeSecret: isSecure && app.user_id.equals(user._id)
+		includeSecret: isSecure && app.userId.equals(user._id)
 	}));
 });
diff --git a/src/server/api/endpoints/auth/accept.ts b/src/server/api/endpoints/auth/accept.ts
index 4ee20a6d2..5a1925144 100644
--- a/src/server/api/endpoints/auth/accept.ts
+++ b/src/server/api/endpoints/auth/accept.ts
@@ -56,14 +56,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Fetch exist access token
 	const exist = await AccessToken.findOne({
-		app_id: session.app_id,
-		user_id: user._id,
+		appId: session.appId,
+		userId: user._id,
 	});
 
 	if (exist === null) {
 		// Lookup app
 		const app = await App.findOne({
-			_id: session.app_id
+			_id: session.appId
 		});
 
 		// Generate Hash
@@ -73,9 +73,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 		// Insert access token doc
 		await AccessToken.insert({
-			created_at: new Date(),
-			app_id: session.app_id,
-			user_id: user._id,
+			createdAt: new Date(),
+			appId: session.appId,
+			userId: user._id,
 			token: accessToken,
 			hash: hash
 		});
@@ -84,7 +84,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Update session
 	await AuthSess.update(session._id, {
 		$set: {
-			user_id: user._id
+			userId: user._id
 		}
 	});
 
diff --git a/src/server/api/endpoints/auth/session/generate.ts b/src/server/api/endpoints/auth/session/generate.ts
index dc6a045b6..180ad83cc 100644
--- a/src/server/api/endpoints/auth/session/generate.ts
+++ b/src/server/api/endpoints/auth/session/generate.ts
@@ -14,7 +14,7 @@ import config from '../../../../../conf';
  *     summary: Generate a session
  *     parameters:
  *       -
- *         name: app_secret
+ *         name: appSecret
  *         description: App Secret
  *         in: formData
  *         required: true
@@ -45,9 +45,9 @@ import config from '../../../../../conf';
  * @return {Promise<any>}
  */
 module.exports = (params) => new Promise(async (res, rej) => {
-	// Get 'app_secret' parameter
-	const [appSecret, appSecretErr] = $(params.app_secret).string().$;
-	if (appSecretErr) return rej('invalid app_secret param');
+	// Get 'appSecret' parameter
+	const [appSecret, appSecretErr] = $(params.appSecret).string().$;
+	if (appSecretErr) return rej('invalid appSecret param');
 
 	// Lookup app
 	const app = await App.findOne({
@@ -63,8 +63,8 @@ module.exports = (params) => new Promise(async (res, rej) => {
 
 	// Create session token document
 	const doc = await AuthSess.insert({
-		created_at: new Date(),
-		app_id: app._id,
+		createdAt: new Date(),
+		appId: app._id,
 		token: token
 	});
 
diff --git a/src/server/api/endpoints/auth/session/show.ts b/src/server/api/endpoints/auth/session/show.ts
index 73ac3185f..869b714e7 100644
--- a/src/server/api/endpoints/auth/session/show.ts
+++ b/src/server/api/endpoints/auth/session/show.ts
@@ -23,17 +23,17 @@ import AuthSess, { pack } from '../../../models/auth-session';
  *         schema:
  *           type: object
  *           properties:
- *             created_at:
+ *             createdAt:
  *               type: string
  *               format: date-time
  *               description: Date and time of the session creation
- *             app_id:
+ *             appId:
  *               type: string
  *               description: Application ID
  *             token:
  *               type: string
  *               description: Session Token
- *             user_id:
+ *             userId:
  *               type: string
  *               description: ID of user who create the session
  *             app:
diff --git a/src/server/api/endpoints/auth/session/userkey.ts b/src/server/api/endpoints/auth/session/userkey.ts
index fc989bf8c..5d9983af6 100644
--- a/src/server/api/endpoints/auth/session/userkey.ts
+++ b/src/server/api/endpoints/auth/session/userkey.ts
@@ -14,7 +14,7 @@ import { pack } from '../../../models/user';
  *     summary: Get an access token(userkey)
  *     parameters:
  *       -
- *         name: app_secret
+ *         name: appSecret
  *         description: App Secret
  *         in: formData
  *         required: true
@@ -50,9 +50,9 @@ import { pack } from '../../../models/user';
  * @return {Promise<any>}
  */
 module.exports = (params) => new Promise(async (res, rej) => {
-	// Get 'app_secret' parameter
-	const [appSecret, appSecretErr] = $(params.app_secret).string().$;
-	if (appSecretErr) return rej('invalid app_secret param');
+	// Get 'appSecret' parameter
+	const [appSecret, appSecretErr] = $(params.appSecret).string().$;
+	if (appSecretErr) return rej('invalid appSecret param');
 
 	// Lookup app
 	const app = await App.findOne({
@@ -71,21 +71,21 @@ module.exports = (params) => new Promise(async (res, rej) => {
 	const session = await AuthSess
 		.findOne({
 			token: token,
-			app_id: app._id
+			appId: app._id
 		});
 
 	if (session === null) {
 		return rej('session not found');
 	}
 
-	if (session.user_id == null) {
+	if (session.userId == null) {
 		return rej('this session is not allowed yet');
 	}
 
 	// Lookup access token
 	const accessToken = await AccessToken.findOne({
-		app_id: app._id,
-		user_id: session.user_id
+		appId: app._id,
+		userId: session.userId
 	});
 
 	// Delete session
@@ -101,8 +101,8 @@ module.exports = (params) => new Promise(async (res, rej) => {
 
 	// Response
 	res({
-		access_token: accessToken.token,
-		user: await pack(session.user_id, null, {
+		accessToken: accessToken.token,
+		user: await pack(session.userId, null, {
 			detail: true
 		})
 	});
diff --git a/src/server/api/endpoints/channels.ts b/src/server/api/endpoints/channels.ts
index b9a7d1b78..a4acc0660 100644
--- a/src/server/api/endpoints/channels.ts
+++ b/src/server/api/endpoints/channels.ts
@@ -16,17 +16,17 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) return rej('invalid limit param');
 
-	// Get 'since_id' parameter
-	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) return rej('invalid since_id param');
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) return rej('invalid sinceId param');
 
-	// Get 'until_id' parameter
-	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
-	if (untilIdErr) return rej('invalid until_id param');
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) return rej('invalid untilId param');
 
-	// Check if both of since_id and until_id is specified
+	// Check if both of sinceId and untilId is specified
 	if (sinceId && untilId) {
-		return rej('cannot set since_id and until_id');
+		return rej('cannot set sinceId and untilId');
 	}
 
 	// Construct query
diff --git a/src/server/api/endpoints/channels/create.ts b/src/server/api/endpoints/channels/create.ts
index 695b4515b..1dc453c4a 100644
--- a/src/server/api/endpoints/channels/create.ts
+++ b/src/server/api/endpoints/channels/create.ts
@@ -20,11 +20,11 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 	// Create a channel
 	const channel = await Channel.insert({
-		created_at: new Date(),
-		user_id: user._id,
+		createdAt: new Date(),
+		userId: user._id,
 		title: title,
 		index: 0,
-		watching_count: 1
+		watchingCount: 1
 	});
 
 	// Response
@@ -32,8 +32,8 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 	// Create Watching
 	await Watching.insert({
-		created_at: new Date(),
-		user_id: user._id,
-		channel_id: channel._id
+		createdAt: new Date(),
+		userId: user._id,
+		channelId: channel._id
 	});
 });
diff --git a/src/server/api/endpoints/channels/posts.ts b/src/server/api/endpoints/channels/posts.ts
index d722589c2..348dbb108 100644
--- a/src/server/api/endpoints/channels/posts.ts
+++ b/src/server/api/endpoints/channels/posts.ts
@@ -17,22 +17,22 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [limit = 1000, limitErr] = $(params.limit).optional.number().range(1, 1000).$;
 	if (limitErr) return rej('invalid limit param');
 
-	// Get 'since_id' parameter
-	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) return rej('invalid since_id param');
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) return rej('invalid sinceId param');
 
-	// Get 'until_id' parameter
-	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
-	if (untilIdErr) return rej('invalid until_id param');
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) return rej('invalid untilId param');
 
-	// Check if both of since_id and until_id is specified
+	// Check if both of sinceId and untilId is specified
 	if (sinceId && untilId) {
-		return rej('cannot set since_id and until_id');
+		return rej('cannot set sinceId and untilId');
 	}
 
-	// Get 'channel_id' parameter
-	const [channelId, channelIdErr] = $(params.channel_id).id().$;
-	if (channelIdErr) return rej('invalid channel_id param');
+	// Get 'channelId' parameter
+	const [channelId, channelIdErr] = $(params.channelId).id().$;
+	if (channelIdErr) return rej('invalid channelId param');
 
 	// Fetch channel
 	const channel: IChannel = await Channel.findOne({
@@ -49,7 +49,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	};
 
 	const query = {
-		channel_id: channel._id
+		channelId: channel._id
 	} as any;
 
 	if (sinceId) {
diff --git a/src/server/api/endpoints/channels/show.ts b/src/server/api/endpoints/channels/show.ts
index 332da6467..5874ed18a 100644
--- a/src/server/api/endpoints/channels/show.ts
+++ b/src/server/api/endpoints/channels/show.ts
@@ -12,9 +12,9 @@ import Channel, { IChannel, pack } from '../../models/channel';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'channel_id' parameter
-	const [channelId, channelIdErr] = $(params.channel_id).id().$;
-	if (channelIdErr) return rej('invalid channel_id param');
+	// Get 'channelId' parameter
+	const [channelId, channelIdErr] = $(params.channelId).id().$;
+	if (channelIdErr) return rej('invalid channelId param');
 
 	// Fetch channel
 	const channel: IChannel = await Channel.findOne({
diff --git a/src/server/api/endpoints/channels/unwatch.ts b/src/server/api/endpoints/channels/unwatch.ts
index 19d3be118..709313bc6 100644
--- a/src/server/api/endpoints/channels/unwatch.ts
+++ b/src/server/api/endpoints/channels/unwatch.ts
@@ -13,9 +13,9 @@ import Watching from '../../models/channel-watching';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'channel_id' parameter
-	const [channelId, channelIdErr] = $(params.channel_id).id().$;
-	if (channelIdErr) return rej('invalid channel_id param');
+	// Get 'channelId' parameter
+	const [channelId, channelIdErr] = $(params.channelId).id().$;
+	if (channelIdErr) return rej('invalid channelId param');
 
 	//#region Fetch channel
 	const channel = await Channel.findOne({
@@ -29,9 +29,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	//#region Check whether not watching
 	const exist = await Watching.findOne({
-		user_id: user._id,
-		channel_id: channel._id,
-		deleted_at: { $exists: false }
+		userId: user._id,
+		channelId: channel._id,
+		deletedAt: { $exists: false }
 	});
 
 	if (exist === null) {
@@ -44,7 +44,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		_id: exist._id
 	}, {
 		$set: {
-			deleted_at: new Date()
+			deletedAt: new Date()
 		}
 	});
 
@@ -54,7 +54,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Decrement watching count
 	Channel.update(channel._id, {
 		$inc: {
-			watching_count: -1
+			watchingCount: -1
 		}
 	});
 });
diff --git a/src/server/api/endpoints/channels/watch.ts b/src/server/api/endpoints/channels/watch.ts
index 030e0dd41..df9e70d5c 100644
--- a/src/server/api/endpoints/channels/watch.ts
+++ b/src/server/api/endpoints/channels/watch.ts
@@ -13,9 +13,9 @@ import Watching from '../../models/channel-watching';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'channel_id' parameter
-	const [channelId, channelIdErr] = $(params.channel_id).id().$;
-	if (channelIdErr) return rej('invalid channel_id param');
+	// Get 'channelId' parameter
+	const [channelId, channelIdErr] = $(params.channelId).id().$;
+	if (channelIdErr) return rej('invalid channelId param');
 
 	//#region Fetch channel
 	const channel = await Channel.findOne({
@@ -29,9 +29,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	//#region Check whether already watching
 	const exist = await Watching.findOne({
-		user_id: user._id,
-		channel_id: channel._id,
-		deleted_at: { $exists: false }
+		userId: user._id,
+		channelId: channel._id,
+		deletedAt: { $exists: false }
 	});
 
 	if (exist !== null) {
@@ -41,9 +41,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Create Watching
 	await Watching.insert({
-		created_at: new Date(),
-		user_id: user._id,
-		channel_id: channel._id
+		createdAt: new Date(),
+		userId: user._id,
+		channelId: channel._id
 	});
 
 	// Send response
@@ -52,7 +52,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Increment watching count
 	Channel.update(channel._id, {
 		$inc: {
-			watching_count: 1
+			watchingCount: 1
 		}
 	});
 });
diff --git a/src/server/api/endpoints/drive.ts b/src/server/api/endpoints/drive.ts
index d92473633..eb2185391 100644
--- a/src/server/api/endpoints/drive.ts
+++ b/src/server/api/endpoints/drive.ts
@@ -14,7 +14,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Calculate drive usage
 	const usage = ((await DriveFile
 		.aggregate([
-			{ $match: { 'metadata.user_id': user._id } },
+			{ $match: { 'metadata.userId': user._id } },
 			{
 				$project: {
 					length: true
@@ -31,7 +31,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		}).usage;
 
 	res({
-		capacity: user.drive_capacity,
+		capacity: user.driveCapacity,
 		usage: usage
 	});
 });
diff --git a/src/server/api/endpoints/drive/files.ts b/src/server/api/endpoints/drive/files.ts
index 89915331e..f982ef62e 100644
--- a/src/server/api/endpoints/drive/files.ts
+++ b/src/server/api/endpoints/drive/files.ts
@@ -17,22 +17,22 @@ module.exports = async (params, user, app) => {
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) throw 'invalid limit param';
 
-	// Get 'since_id' parameter
-	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) throw 'invalid since_id param';
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) throw 'invalid sinceId param';
 
-	// Get 'until_id' parameter
-	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
-	if (untilIdErr) throw 'invalid until_id param';
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) throw 'invalid untilId param';
 
-	// Check if both of since_id and until_id is specified
+	// Check if both of sinceId and untilId is specified
 	if (sinceId && untilId) {
-		throw 'cannot set since_id and until_id';
+		throw 'cannot set sinceId and untilId';
 	}
 
-	// Get 'folder_id' parameter
-	const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
-	if (folderIdErr) throw 'invalid folder_id param';
+	// Get 'folderId' parameter
+	const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$;
+	if (folderIdErr) throw 'invalid folderId param';
 
 	// Get 'type' parameter
 	const [type, typeErr] = $(params.type).optional.string().match(/^[a-zA-Z\/\-\*]+$/).$;
@@ -43,8 +43,8 @@ module.exports = async (params, user, app) => {
 		_id: -1
 	};
 	const query = {
-		'metadata.user_id': user._id,
-		'metadata.folder_id': folderId
+		'metadata.userId': user._id,
+		'metadata.folderId': folderId
 	} as any;
 	if (sinceId) {
 		sort._id = 1;
diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts
index db801b61f..2cd89a8fa 100644
--- a/src/server/api/endpoints/drive/files/create.ts
+++ b/src/server/api/endpoints/drive/files/create.ts
@@ -33,9 +33,9 @@ module.exports = async (file, params, user): Promise<any> => {
 		name = null;
 	}
 
-	// Get 'folder_id' parameter
-	const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
-	if (folderIdErr) throw 'invalid folder_id param';
+	// Get 'folderId' parameter
+	const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$;
+	if (folderIdErr) throw 'invalid folderId param';
 
 	try {
 		// Create file
diff --git a/src/server/api/endpoints/drive/files/find.ts b/src/server/api/endpoints/drive/files/find.ts
index e026afe93..47ce89130 100644
--- a/src/server/api/endpoints/drive/files/find.ts
+++ b/src/server/api/endpoints/drive/files/find.ts
@@ -16,16 +16,16 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [name, nameErr] = $(params.name).string().$;
 	if (nameErr) return rej('invalid name param');
 
-	// Get 'folder_id' parameter
-	const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
-	if (folderIdErr) return rej('invalid folder_id param');
+	// Get 'folderId' parameter
+	const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$;
+	if (folderIdErr) return rej('invalid folderId param');
 
 	// Issue query
 	const files = await DriveFile
 		.find({
 			filename: name,
-			'metadata.user_id': user._id,
-			'metadata.folder_id': folderId
+			'metadata.userId': user._id,
+			'metadata.folderId': folderId
 		});
 
 	// Serialize
diff --git a/src/server/api/endpoints/drive/files/show.ts b/src/server/api/endpoints/drive/files/show.ts
index 21664f7ba..63920db7f 100644
--- a/src/server/api/endpoints/drive/files/show.ts
+++ b/src/server/api/endpoints/drive/files/show.ts
@@ -12,15 +12,15 @@ import DriveFile, { pack } from '../../../models/drive-file';
  * @return {Promise<any>}
  */
 module.exports = async (params, user) => {
-	// Get 'file_id' parameter
-	const [fileId, fileIdErr] = $(params.file_id).id().$;
-	if (fileIdErr) throw 'invalid file_id param';
+	// Get 'fileId' parameter
+	const [fileId, fileIdErr] = $(params.fileId).id().$;
+	if (fileIdErr) throw 'invalid fileId param';
 
 	// Fetch file
 	const file = await DriveFile
 		.findOne({
 			_id: fileId,
-			'metadata.user_id': user._id
+			'metadata.userId': user._id
 		});
 
 	if (file === null) {
diff --git a/src/server/api/endpoints/drive/files/update.ts b/src/server/api/endpoints/drive/files/update.ts
index 83da46211..bfad45b0a 100644
--- a/src/server/api/endpoints/drive/files/update.ts
+++ b/src/server/api/endpoints/drive/files/update.ts
@@ -14,15 +14,15 @@ import { publishDriveStream } from '../../../event';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'file_id' parameter
-	const [fileId, fileIdErr] = $(params.file_id).id().$;
-	if (fileIdErr) return rej('invalid file_id param');
+	// Get 'fileId' parameter
+	const [fileId, fileIdErr] = $(params.fileId).id().$;
+	if (fileIdErr) return rej('invalid fileId param');
 
 	// Fetch file
 	const file = await DriveFile
 		.findOne({
 			_id: fileId,
-			'metadata.user_id': user._id
+			'metadata.userId': user._id
 		});
 
 	if (file === null) {
@@ -34,33 +34,33 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	if (nameErr) return rej('invalid name param');
 	if (name) file.filename = name;
 
-	// Get 'folder_id' parameter
-	const [folderId, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
-	if (folderIdErr) return rej('invalid folder_id param');
+	// Get 'folderId' parameter
+	const [folderId, folderIdErr] = $(params.folderId).optional.nullable.id().$;
+	if (folderIdErr) return rej('invalid folderId param');
 
 	if (folderId !== undefined) {
 		if (folderId === null) {
-			file.metadata.folder_id = null;
+			file.metadata.folderId = null;
 		} else {
 			// Fetch folder
 			const folder = await DriveFolder
 				.findOne({
 					_id: folderId,
-					user_id: user._id
+					userId: user._id
 				});
 
 			if (folder === null) {
 				return rej('folder-not-found');
 			}
 
-			file.metadata.folder_id = folder._id;
+			file.metadata.folderId = folder._id;
 		}
 	}
 
 	await DriveFile.update(file._id, {
 		$set: {
 			filename: file.filename,
-			'metadata.folder_id': file.metadata.folder_id
+			'metadata.folderId': file.metadata.folderId
 		}
 	});
 
diff --git a/src/server/api/endpoints/drive/files/upload_from_url.ts b/src/server/api/endpoints/drive/files/upload_from_url.ts
index 346633c61..1a4ce0bf0 100644
--- a/src/server/api/endpoints/drive/files/upload_from_url.ts
+++ b/src/server/api/endpoints/drive/files/upload_from_url.ts
@@ -18,9 +18,9 @@ module.exports = async (params, user): Promise<any> => {
 	const [url, urlErr] = $(params.url).string().$;
 	if (urlErr) throw 'invalid url param';
 
-	// Get 'folder_id' parameter
-	const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
-	if (folderIdErr) throw 'invalid folder_id param';
+	// Get 'folderId' parameter
+	const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$;
+	if (folderIdErr) throw 'invalid folderId param';
 
 	return pack(await uploadFromUrl(url, user, folderId));
 };
diff --git a/src/server/api/endpoints/drive/folders.ts b/src/server/api/endpoints/drive/folders.ts
index 428bde350..c314837f7 100644
--- a/src/server/api/endpoints/drive/folders.ts
+++ b/src/server/api/endpoints/drive/folders.ts
@@ -17,30 +17,30 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) return rej('invalid limit param');
 
-	// Get 'since_id' parameter
-	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) return rej('invalid since_id param');
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) return rej('invalid sinceId param');
 
-	// Get 'until_id' parameter
-	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
-	if (untilIdErr) return rej('invalid until_id param');
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) return rej('invalid untilId param');
 
-	// Check if both of since_id and until_id is specified
+	// Check if both of sinceId and untilId is specified
 	if (sinceId && untilId) {
-		return rej('cannot set since_id and until_id');
+		return rej('cannot set sinceId and untilId');
 	}
 
-	// Get 'folder_id' parameter
-	const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
-	if (folderIdErr) return rej('invalid folder_id param');
+	// Get 'folderId' parameter
+	const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$;
+	if (folderIdErr) return rej('invalid folderId param');
 
 	// Construct query
 	const sort = {
 		_id: -1
 	};
 	const query = {
-		user_id: user._id,
-		parent_id: folderId
+		userId: user._id,
+		parentId: folderId
 	} as any;
 	if (sinceId) {
 		sort._id = 1;
diff --git a/src/server/api/endpoints/drive/folders/create.ts b/src/server/api/endpoints/drive/folders/create.ts
index 03f396ddc..564558606 100644
--- a/src/server/api/endpoints/drive/folders/create.ts
+++ b/src/server/api/endpoints/drive/folders/create.ts
@@ -17,9 +17,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [name = '無題のフォルダー', nameErr] = $(params.name).optional.string().pipe(isValidFolderName).$;
 	if (nameErr) return rej('invalid name param');
 
-	// Get 'parent_id' parameter
-	const [parentId = null, parentIdErr] = $(params.parent_id).optional.nullable.id().$;
-	if (parentIdErr) return rej('invalid parent_id param');
+	// Get 'parentId' parameter
+	const [parentId = null, parentIdErr] = $(params.parentId).optional.nullable.id().$;
+	if (parentIdErr) return rej('invalid parentId param');
 
 	// If the parent folder is specified
 	let parent = null;
@@ -28,7 +28,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		parent = await DriveFolder
 			.findOne({
 				_id: parentId,
-				user_id: user._id
+				userId: user._id
 			});
 
 		if (parent === null) {
@@ -38,10 +38,10 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Create folder
 	const folder = await DriveFolder.insert({
-		created_at: new Date(),
+		createdAt: new Date(),
 		name: name,
-		parent_id: parent !== null ? parent._id : null,
-		user_id: user._id
+		parentId: parent !== null ? parent._id : null,
+		userId: user._id
 	});
 
 	// Serialize
diff --git a/src/server/api/endpoints/drive/folders/find.ts b/src/server/api/endpoints/drive/folders/find.ts
index fc84766bc..f46aaedd3 100644
--- a/src/server/api/endpoints/drive/folders/find.ts
+++ b/src/server/api/endpoints/drive/folders/find.ts
@@ -16,16 +16,16 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [name, nameErr] = $(params.name).string().$;
 	if (nameErr) return rej('invalid name param');
 
-	// Get 'parent_id' parameter
-	const [parentId = null, parentIdErr] = $(params.parent_id).optional.nullable.id().$;
-	if (parentIdErr) return rej('invalid parent_id param');
+	// Get 'parentId' parameter
+	const [parentId = null, parentIdErr] = $(params.parentId).optional.nullable.id().$;
+	if (parentIdErr) return rej('invalid parentId param');
 
 	// Issue query
 	const folders = await DriveFolder
 		.find({
 			name: name,
-			user_id: user._id,
-			parent_id: parentId
+			userId: user._id,
+			parentId: parentId
 		});
 
 	// Serialize
diff --git a/src/server/api/endpoints/drive/folders/show.ts b/src/server/api/endpoints/drive/folders/show.ts
index e07d14d20..a6d7e86df 100644
--- a/src/server/api/endpoints/drive/folders/show.ts
+++ b/src/server/api/endpoints/drive/folders/show.ts
@@ -12,15 +12,15 @@ import DriveFolder, { pack } from '../../../models/drive-folder';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'folder_id' parameter
-	const [folderId, folderIdErr] = $(params.folder_id).id().$;
-	if (folderIdErr) return rej('invalid folder_id param');
+	// Get 'folderId' parameter
+	const [folderId, folderIdErr] = $(params.folderId).id().$;
+	if (folderIdErr) return rej('invalid folderId param');
 
 	// Get folder
 	const folder = await DriveFolder
 		.findOne({
 			_id: folderId,
-			user_id: user._id
+			userId: user._id
 		});
 
 	if (folder === null) {
diff --git a/src/server/api/endpoints/drive/folders/update.ts b/src/server/api/endpoints/drive/folders/update.ts
index d3df8bdae..fcfd24124 100644
--- a/src/server/api/endpoints/drive/folders/update.ts
+++ b/src/server/api/endpoints/drive/folders/update.ts
@@ -13,15 +13,15 @@ import { publishDriveStream } from '../../../event';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'folder_id' parameter
-	const [folderId, folderIdErr] = $(params.folder_id).id().$;
-	if (folderIdErr) return rej('invalid folder_id param');
+	// Get 'folderId' parameter
+	const [folderId, folderIdErr] = $(params.folderId).id().$;
+	if (folderIdErr) return rej('invalid folderId param');
 
 	// Fetch folder
 	const folder = await DriveFolder
 		.findOne({
 			_id: folderId,
-			user_id: user._id
+			userId: user._id
 		});
 
 	if (folder === null) {
@@ -33,18 +33,18 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	if (nameErr) return rej('invalid name param');
 	if (name) folder.name = name;
 
-	// Get 'parent_id' parameter
-	const [parentId, parentIdErr] = $(params.parent_id).optional.nullable.id().$;
-	if (parentIdErr) return rej('invalid parent_id param');
+	// Get 'parentId' parameter
+	const [parentId, parentIdErr] = $(params.parentId).optional.nullable.id().$;
+	if (parentIdErr) return rej('invalid parentId param');
 	if (parentId !== undefined) {
 		if (parentId === null) {
-			folder.parent_id = null;
+			folder.parentId = null;
 		} else {
 			// Get parent folder
 			const parent = await DriveFolder
 				.findOne({
 					_id: parentId,
-					user_id: user._id
+					userId: user._id
 				});
 
 			if (parent === null) {
@@ -58,25 +58,25 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 					_id: folderId
 				}, {
 					_id: true,
-					parent_id: true
+					parentId: true
 				});
 
 				if (folder2._id.equals(folder._id)) {
 					return true;
-				} else if (folder2.parent_id) {
-					return await checkCircle(folder2.parent_id);
+				} else if (folder2.parentId) {
+					return await checkCircle(folder2.parentId);
 				} else {
 					return false;
 				}
 			}
 
-			if (parent.parent_id !== null) {
-				if (await checkCircle(parent.parent_id)) {
+			if (parent.parentId !== null) {
+				if (await checkCircle(parent.parentId)) {
 					return rej('detected-circular-definition');
 				}
 			}
 
-			folder.parent_id = parent._id;
+			folder.parentId = parent._id;
 		}
 	}
 
@@ -84,7 +84,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	DriveFolder.update(folder._id, {
 		$set: {
 			name: folder.name,
-			parent_id: folder.parent_id
+			parentId: folder.parentId
 		}
 	});
 
diff --git a/src/server/api/endpoints/drive/stream.ts b/src/server/api/endpoints/drive/stream.ts
index 8352c7dd4..71db38f3b 100644
--- a/src/server/api/endpoints/drive/stream.ts
+++ b/src/server/api/endpoints/drive/stream.ts
@@ -16,17 +16,17 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) return rej('invalid limit param');
 
-	// Get 'since_id' parameter
-	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) return rej('invalid since_id param');
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) return rej('invalid sinceId param');
 
-	// Get 'until_id' parameter
-	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
-	if (untilIdErr) return rej('invalid until_id param');
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) return rej('invalid untilId param');
 
-	// Check if both of since_id and until_id is specified
+	// Check if both of sinceId and untilId is specified
 	if (sinceId && untilId) {
-		return rej('cannot set since_id and until_id');
+		return rej('cannot set sinceId and untilId');
 	}
 
 	// Get 'type' parameter
@@ -38,7 +38,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		_id: -1
 	};
 	const query = {
-		'metadata.user_id': user._id
+		'metadata.userId': user._id
 	} as any;
 	if (sinceId) {
 		sort._id = 1;
diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts
index 767b837b3..983d8040f 100644
--- a/src/server/api/endpoints/following/create.ts
+++ b/src/server/api/endpoints/following/create.ts
@@ -17,9 +17,9 @@ import event from '../../event';
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	const follower = user;
 
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [userId, userIdErr] = $(params.userId).id().$;
+	if (userIdErr) return rej('invalid userId param');
 
 	// 自分自身
 	if (user._id.equals(userId)) {
@@ -42,9 +42,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Check if already following
 	const exist = await Following.findOne({
-		follower_id: follower._id,
-		followee_id: followee._id,
-		deleted_at: { $exists: false }
+		followerId: follower._id,
+		followeeId: followee._id,
+		deletedAt: { $exists: false }
 	});
 
 	if (exist !== null) {
@@ -53,9 +53,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Create following
 	await Following.insert({
-		created_at: new Date(),
-		follower_id: follower._id,
-		followee_id: followee._id
+		createdAt: new Date(),
+		followerId: follower._id,
+		followeeId: followee._id
 	});
 
 	// Send response
@@ -64,14 +64,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Increment following count
 	User.update(follower._id, {
 		$inc: {
-			following_count: 1
+			followingCount: 1
 		}
 	});
 
 	// Increment followers count
 	User.update({ _id: followee._id }, {
 		$inc: {
-			followers_count: 1
+			followersCount: 1
 		}
 	});
 
diff --git a/src/server/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts
index 64b9a8cec..25eba8b26 100644
--- a/src/server/api/endpoints/following/delete.ts
+++ b/src/server/api/endpoints/following/delete.ts
@@ -16,9 +16,9 @@ import event from '../../event';
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	const follower = user;
 
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [userId, userIdErr] = $(params.userId).id().$;
+	if (userIdErr) return rej('invalid userId param');
 
 	// Check if the followee is yourself
 	if (user._id.equals(userId)) {
@@ -41,9 +41,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Check not following
 	const exist = await Following.findOne({
-		follower_id: follower._id,
-		followee_id: followee._id,
-		deleted_at: { $exists: false }
+		followerId: follower._id,
+		followeeId: followee._id,
+		deletedAt: { $exists: false }
 	});
 
 	if (exist === null) {
@@ -55,7 +55,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		_id: exist._id
 	}, {
 		$set: {
-			deleted_at: new Date()
+			deletedAt: new Date()
 		}
 	});
 
@@ -65,14 +65,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Decrement following count
 	User.update({ _id: follower._id }, {
 		$inc: {
-			following_count: -1
+			followingCount: -1
 		}
 	});
 
 	// Decrement followers count
 	User.update({ _id: followee._id }, {
 		$inc: {
-			followers_count: -1
+			followersCount: -1
 		}
 	});
 
diff --git a/src/server/api/endpoints/i.ts b/src/server/api/endpoints/i.ts
index 32b0382fa..f5e92b4de 100644
--- a/src/server/api/endpoints/i.ts
+++ b/src/server/api/endpoints/i.ts
@@ -22,7 +22,7 @@ module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) =>
 	// Update lastUsedAt
 	User.update({ _id: user._id }, {
 		$set: {
-			'account.last_used_at': new Date()
+			'account.lastUsedAt': new Date()
 		}
 	});
 });
diff --git a/src/server/api/endpoints/i/2fa/done.ts b/src/server/api/endpoints/i/2fa/done.ts
index 0f1db7382..d61ebbe6f 100644
--- a/src/server/api/endpoints/i/2fa/done.ts
+++ b/src/server/api/endpoints/i/2fa/done.ts
@@ -12,12 +12,12 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 	const _token = token.replace(/\s/g, '');
 
-	if (user.two_factor_temp_secret == null) {
+	if (user.twoFactorTempSecret == null) {
 		return rej('二段階認証の設定が開始されていません');
 	}
 
 	const verified = (speakeasy as any).totp.verify({
-		secret: user.two_factor_temp_secret,
+		secret: user.twoFactorTempSecret,
 		encoding: 'base32',
 		token: _token
 	});
@@ -28,8 +28,8 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 	await User.update(user._id, {
 		$set: {
-			'account.two_factor_secret': user.two_factor_temp_secret,
-			'account.two_factor_enabled': true
+			'account.twoFactorSecret': user.twoFactorTempSecret,
+			'account.twoFactorEnabled': true
 		}
 	});
 
diff --git a/src/server/api/endpoints/i/2fa/register.ts b/src/server/api/endpoints/i/2fa/register.ts
index e2cc1487b..0b49ad882 100644
--- a/src/server/api/endpoints/i/2fa/register.ts
+++ b/src/server/api/endpoints/i/2fa/register.ts
@@ -27,7 +27,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 	await User.update(user._id, {
 		$set: {
-			two_factor_temp_secret: secret.base32
+			twoFactorTempSecret: secret.base32
 		}
 	});
 
diff --git a/src/server/api/endpoints/i/2fa/unregister.ts b/src/server/api/endpoints/i/2fa/unregister.ts
index c43f9ccc4..0221ecb96 100644
--- a/src/server/api/endpoints/i/2fa/unregister.ts
+++ b/src/server/api/endpoints/i/2fa/unregister.ts
@@ -19,8 +19,8 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 	await User.update(user._id, {
 		$set: {
-			'account.two_factor_secret': null,
-			'account.two_factor_enabled': false
+			'account.twoFactorSecret': null,
+			'account.twoFactorEnabled': false
 		}
 	});
 
diff --git a/src/server/api/endpoints/i/appdata/get.ts b/src/server/api/endpoints/i/appdata/get.ts
index 571208d46..0b34643f7 100644
--- a/src/server/api/endpoints/i/appdata/get.ts
+++ b/src/server/api/endpoints/i/appdata/get.ts
@@ -25,8 +25,8 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
 		select[`data.${key}`] = true;
 	}
 	const appdata = await Appdata.findOne({
-		app_id: app._id,
-		user_id: user._id
+		appId: app._id,
+		userId: user._id
 	}, {
 		fields: select
 	});
diff --git a/src/server/api/endpoints/i/appdata/set.ts b/src/server/api/endpoints/i/appdata/set.ts
index 2804a14cb..1e3232ce3 100644
--- a/src/server/api/endpoints/i/appdata/set.ts
+++ b/src/server/api/endpoints/i/appdata/set.ts
@@ -43,11 +43,11 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
 	}
 
 	await Appdata.update({
-		app_id: app._id,
-		user_id: user._id
+		appId: app._id,
+		userId: user._id
 	}, Object.assign({
-		app_id: app._id,
-		user_id: user._id
+		appId: app._id,
+		userId: user._id
 	}, {
 			$set: set
 		}), {
diff --git a/src/server/api/endpoints/i/authorized_apps.ts b/src/server/api/endpoints/i/authorized_apps.ts
index 40ce7a68c..5a38d7c18 100644
--- a/src/server/api/endpoints/i/authorized_apps.ts
+++ b/src/server/api/endpoints/i/authorized_apps.ts
@@ -28,7 +28,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Get tokens
 	const tokens = await AccessToken
 		.find({
-			user_id: user._id
+			userId: user._id
 		}, {
 			limit: limit,
 			skip: offset,
@@ -39,5 +39,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(tokens.map(async token =>
-		await pack(token.app_id))));
+		await pack(token.appId))));
 });
diff --git a/src/server/api/endpoints/i/change_password.ts b/src/server/api/endpoints/i/change_password.ts
index 88fb36b1f..e3b0127e7 100644
--- a/src/server/api/endpoints/i/change_password.ts
+++ b/src/server/api/endpoints/i/change_password.ts
@@ -13,13 +13,13 @@ import User from '../../models/user';
  * @return {Promise<any>}
  */
 module.exports = async (params, user) => new Promise(async (res, rej) => {
-	// Get 'current_password' parameter
-	const [currentPassword, currentPasswordErr] = $(params.current_password).string().$;
-	if (currentPasswordErr) return rej('invalid current_password param');
+	// Get 'currentPasword' parameter
+	const [currentPassword, currentPasswordErr] = $(params.currentPasword).string().$;
+	if (currentPasswordErr) return rej('invalid currentPasword param');
 
-	// Get 'new_password' parameter
-	const [newPassword, newPasswordErr] = $(params.new_password).string().$;
-	if (newPasswordErr) return rej('invalid new_password param');
+	// Get 'newPassword' parameter
+	const [newPassword, newPasswordErr] = $(params.newPassword).string().$;
+	if (newPasswordErr) return rej('invalid newPassword param');
 
 	// Compare password
 	const same = await bcrypt.compare(currentPassword, user.account.password);
diff --git a/src/server/api/endpoints/i/favorites.ts b/src/server/api/endpoints/i/favorites.ts
index eb464cf0f..9f8becf21 100644
--- a/src/server/api/endpoints/i/favorites.ts
+++ b/src/server/api/endpoints/i/favorites.ts
@@ -28,7 +28,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Get favorites
 	const favorites = await Favorite
 		.find({
-			user_id: user._id
+			userId: user._id
 		}, {
 			limit: limit,
 			skip: offset,
@@ -39,6 +39,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(favorites.map(async favorite =>
-		await pack(favorite.post)
+		await pack(favorite.postId)
 	)));
 });
diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts
index 688039a0d..7119bf6ea 100644
--- a/src/server/api/endpoints/i/notifications.ts
+++ b/src/server/api/endpoints/i/notifications.ts
@@ -21,9 +21,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		$(params.following).optional.boolean().$;
 	if (followingError) return rej('invalid following param');
 
-	// Get 'mark_as_read' parameter
-	const [markAsRead = true, markAsReadErr] = $(params.mark_as_read).optional.boolean().$;
-	if (markAsReadErr) return rej('invalid mark_as_read param');
+	// Get 'markAsRead' parameter
+	const [markAsRead = true, markAsReadErr] = $(params.markAsRead).optional.boolean().$;
+	if (markAsReadErr) return rej('invalid markAsRead param');
 
 	// Get 'type' parameter
 	const [type, typeErr] = $(params.type).optional.array('string').unique().$;
@@ -33,29 +33,29 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) return rej('invalid limit param');
 
-	// Get 'since_id' parameter
-	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) return rej('invalid since_id param');
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) return rej('invalid sinceId param');
 
-	// Get 'until_id' parameter
-	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
-	if (untilIdErr) return rej('invalid until_id param');
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) return rej('invalid untilId param');
 
-	// Check if both of since_id and until_id is specified
+	// Check if both of sinceId and untilId is specified
 	if (sinceId && untilId) {
-		return rej('cannot set since_id and until_id');
+		return rej('cannot set sinceId and untilId');
 	}
 
 	const mute = await Mute.find({
-		muter_id: user._id,
-		deleted_at: { $exists: false }
+		muterId: user._id,
+		deletedAt: { $exists: false }
 	});
 
 	const query = {
-		notifiee_id: user._id,
+		notifieeId: user._id,
 		$and: [{
-			notifier_id: {
-				$nin: mute.map(m => m.mutee_id)
+			notifierId: {
+				$nin: mute.map(m => m.muteeId)
 			}
 		}]
 	} as any;
@@ -69,7 +69,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		const followingIds = await getFriends(user._id);
 
 		query.$and.push({
-			notifier_id: {
+			notifierId: {
 				$in: followingIds
 			}
 		});
diff --git a/src/server/api/endpoints/i/pin.ts b/src/server/api/endpoints/i/pin.ts
index ff546fc2b..886a3edeb 100644
--- a/src/server/api/endpoints/i/pin.ts
+++ b/src/server/api/endpoints/i/pin.ts
@@ -14,14 +14,14 @@ import { pack } from '../../models/user';
  * @return {Promise<any>}
  */
 module.exports = async (params, user) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Fetch pinee
 	const post = await Post.findOne({
 		_id: postId,
-		user_id: user._id
+		userId: user._id
 	});
 
 	if (post === null) {
@@ -30,7 +30,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 	await User.update(user._id, {
 		$set: {
-			pinned_post_id: post._id
+			pinnedPostId: post._id
 		}
 	});
 
diff --git a/src/server/api/endpoints/i/signin_history.ts b/src/server/api/endpoints/i/signin_history.ts
index 859e81653..a4ba22790 100644
--- a/src/server/api/endpoints/i/signin_history.ts
+++ b/src/server/api/endpoints/i/signin_history.ts
@@ -16,21 +16,21 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) return rej('invalid limit param');
 
-	// Get 'since_id' parameter
-	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) return rej('invalid since_id param');
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) return rej('invalid sinceId param');
 
-	// Get 'until_id' parameter
-	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
-	if (untilIdErr) return rej('invalid until_id param');
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) return rej('invalid untilId param');
 
-	// Check if both of since_id and until_id is specified
+	// Check if both of sinceId and untilId is specified
 	if (sinceId && untilId) {
-		return rej('cannot set since_id and until_id');
+		return rej('cannot set sinceId and untilId');
 	}
 
 	const query = {
-		user_id: user._id
+		userId: user._id
 	} as any;
 
 	const sort = {
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index 3d52de2cc..8147b1bba 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -36,34 +36,34 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re
 	if (birthdayErr) return rej('invalid birthday param');
 	if (birthday !== undefined) user.account.profile.birthday = birthday;
 
-	// Get 'avatar_id' parameter
-	const [avatarId, avatarIdErr] = $(params.avatar_id).optional.id().$;
-	if (avatarIdErr) return rej('invalid avatar_id param');
-	if (avatarId) user.avatar_id = avatarId;
+	// Get 'avatarId' parameter
+	const [avatarId, avatarIdErr] = $(params.avatarId).optional.id().$;
+	if (avatarIdErr) return rej('invalid avatarId param');
+	if (avatarId) user.avatarId = avatarId;
 
-	// Get 'banner_id' parameter
-	const [bannerId, bannerIdErr] = $(params.banner_id).optional.id().$;
-	if (bannerIdErr) return rej('invalid banner_id param');
-	if (bannerId) user.banner_id = bannerId;
+	// Get 'bannerId' parameter
+	const [bannerId, bannerIdErr] = $(params.bannerId).optional.id().$;
+	if (bannerIdErr) return rej('invalid bannerId param');
+	if (bannerId) user.bannerId = bannerId;
 
-	// Get 'is_bot' parameter
-	const [isBot, isBotErr] = $(params.is_bot).optional.boolean().$;
-	if (isBotErr) return rej('invalid is_bot param');
-	if (isBot != null) user.account.is_bot = isBot;
+	// Get 'isBot' parameter
+	const [isBot, isBotErr] = $(params.isBot).optional.boolean().$;
+	if (isBotErr) return rej('invalid isBot param');
+	if (isBot != null) user.account.isBot = isBot;
 
-	// Get 'auto_watch' parameter
-	const [autoWatch, autoWatchErr] = $(params.auto_watch).optional.boolean().$;
-	if (autoWatchErr) return rej('invalid auto_watch param');
-	if (autoWatch != null) user.account.settings.auto_watch = autoWatch;
+	// Get 'autoWatch' parameter
+	const [autoWatch, autoWatchErr] = $(params.autoWatch).optional.boolean().$;
+	if (autoWatchErr) return rej('invalid autoWatch param');
+	if (autoWatch != null) user.account.settings.autoWatch = autoWatch;
 
 	await User.update(user._id, {
 		$set: {
 			name: user.name,
 			description: user.description,
-			avatar_id: user.avatar_id,
-			banner_id: user.banner_id,
+			avatarId: user.avatarId,
+			bannerId: user.bannerId,
 			'account.profile': user.account.profile,
-			'account.is_bot': user.account.is_bot,
+			'account.isBot': user.account.isBot,
 			'account.settings': user.account.settings
 		}
 	});
diff --git a/src/server/api/endpoints/i/update_client_setting.ts b/src/server/api/endpoints/i/update_client_setting.ts
index c772ed5dc..a0bef5e59 100644
--- a/src/server/api/endpoints/i/update_client_setting.ts
+++ b/src/server/api/endpoints/i/update_client_setting.ts
@@ -22,14 +22,14 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	if (valueErr) return rej('invalid value param');
 
 	const x = {};
-	x[`account.client_settings.${name}`] = value;
+	x[`account.clientSettings.${name}`] = value;
 
 	await User.update(user._id, {
 		$set: x
 	});
 
 	// Serialize
-	user.account.client_settings[name] = value;
+	user.account.clientSettings[name] = value;
 	const iObj = await pack(user, user, {
 		detail: true,
 		includeSecrets: true
diff --git a/src/server/api/endpoints/i/update_home.ts b/src/server/api/endpoints/i/update_home.ts
index 9ce44e25e..151c3e205 100644
--- a/src/server/api/endpoints/i/update_home.ts
+++ b/src/server/api/endpoints/i/update_home.ts
@@ -26,7 +26,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	if (home) {
 		await User.update(user._id, {
 			$set: {
-				'account.client_settings.home': home
+				'account.clientSettings.home': home
 			}
 		});
 
@@ -38,7 +38,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	} else {
 		if (id == null && data == null) return rej('you need to set id and data params if home param unset');
 
-		const _home = user.account.client_settings.home;
+		const _home = user.account.clientSettings.home;
 		const widget = _home.find(w => w.id == id);
 
 		if (widget == null) return rej('widget not found');
@@ -47,7 +47,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 		await User.update(user._id, {
 			$set: {
-				'account.client_settings.home': _home
+				'account.clientSettings.home': _home
 			}
 		});
 
diff --git a/src/server/api/endpoints/i/update_mobile_home.ts b/src/server/api/endpoints/i/update_mobile_home.ts
index 1daddf42b..a8436b940 100644
--- a/src/server/api/endpoints/i/update_mobile_home.ts
+++ b/src/server/api/endpoints/i/update_mobile_home.ts
@@ -25,7 +25,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	if (home) {
 		await User.update(user._id, {
 			$set: {
-				'account.client_settings.mobile_home': home
+				'account.clientSettings.mobile_home': home
 			}
 		});
 
@@ -37,7 +37,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	} else {
 		if (id == null && data == null) return rej('you need to set id and data params if home param unset');
 
-		const _home = user.account.client_settings.mobile_home || [];
+		const _home = user.account.clientSettings.mobile_home || [];
 		const widget = _home.find(w => w.id == id);
 
 		if (widget == null) return rej('widget not found');
@@ -46,7 +46,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 		await User.update(user._id, {
 			$set: {
-				'account.client_settings.mobile_home': _home
+				'account.clientSettings.mobile_home': _home
 			}
 		});
 
diff --git a/src/server/api/endpoints/messaging/history.ts b/src/server/api/endpoints/messaging/history.ts
index 1683ca7a8..2bf3ed996 100644
--- a/src/server/api/endpoints/messaging/history.ts
+++ b/src/server/api/endpoints/messaging/history.ts
@@ -19,25 +19,25 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	if (limitErr) return rej('invalid limit param');
 
 	const mute = await Mute.find({
-		muter_id: user._id,
-		deleted_at: { $exists: false }
+		muterId: user._id,
+		deletedAt: { $exists: false }
 	});
 
 	// Get history
 	const history = await History
 		.find({
-			user_id: user._id,
-			partner: {
-				$nin: mute.map(m => m.mutee_id)
+			userId: user._id,
+			partnerId: {
+				$nin: mute.map(m => m.muteeId)
 			}
 		}, {
 			limit: limit,
 			sort: {
-				updated_at: -1
+				updatedAt: -1
 			}
 		});
 
 	// Serialize
 	res(await Promise.all(history.map(async h =>
-		await pack(h.message, user))));
+		await pack(h.messageId, user))));
 });
diff --git a/src/server/api/endpoints/messaging/messages.ts b/src/server/api/endpoints/messaging/messages.ts
index 67ba5e9d6..dd80e41d0 100644
--- a/src/server/api/endpoints/messaging/messages.ts
+++ b/src/server/api/endpoints/messaging/messages.ts
@@ -15,9 +15,9 @@ import read from '../../common/read-messaging-message';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'user_id' parameter
-	const [recipientId, recipientIdErr] = $(params.user_id).id().$;
-	if (recipientIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [recipientId, recipientIdErr] = $(params.userId).id().$;
+	if (recipientIdErr) return rej('invalid userId param');
 
 	// Fetch recipient
 	const recipient = await User.findOne({
@@ -32,34 +32,34 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		return rej('user not found');
 	}
 
-	// Get 'mark_as_read' parameter
-	const [markAsRead = true, markAsReadErr] = $(params.mark_as_read).optional.boolean().$;
-	if (markAsReadErr) return rej('invalid mark_as_read param');
+	// Get 'markAsRead' parameter
+	const [markAsRead = true, markAsReadErr] = $(params.markAsRead).optional.boolean().$;
+	if (markAsReadErr) return rej('invalid markAsRead param');
 
 	// Get 'limit' parameter
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) return rej('invalid limit param');
 
-	// Get 'since_id' parameter
-	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) return rej('invalid since_id param');
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) return rej('invalid sinceId param');
 
-	// Get 'until_id' parameter
-	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
-	if (untilIdErr) return rej('invalid until_id param');
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) return rej('invalid untilId param');
 
-	// Check if both of since_id and until_id is specified
+	// Check if both of sinceId and untilId is specified
 	if (sinceId && untilId) {
-		return rej('cannot set since_id and until_id');
+		return rej('cannot set sinceId and untilId');
 	}
 
 	const query = {
 		$or: [{
-			user_id: user._id,
-			recipient_id: recipient._id
+			userId: user._id,
+			recipientId: recipient._id
 		}, {
-			user_id: recipient._id,
-			recipient_id: user._id
+			userId: recipient._id,
+			recipientId: user._id
 		}]
 	} as any;
 
diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts
index 5184b2bd3..4edd72655 100644
--- a/src/server/api/endpoints/messaging/messages/create.ts
+++ b/src/server/api/endpoints/messaging/messages/create.ts
@@ -21,9 +21,9 @@ import config from '../../../../../conf';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'user_id' parameter
-	const [recipientId, recipientIdErr] = $(params.user_id).id().$;
-	if (recipientIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [recipientId, recipientIdErr] = $(params.userId).id().$;
+	if (recipientIdErr) return rej('invalid userId param');
 
 	// Myself
 	if (recipientId.equals(user._id)) {
@@ -47,15 +47,15 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [text, textErr] = $(params.text).optional.string().pipe(isValidText).$;
 	if (textErr) return rej('invalid text');
 
-	// Get 'file_id' parameter
-	const [fileId, fileIdErr] = $(params.file_id).optional.id().$;
-	if (fileIdErr) return rej('invalid file_id param');
+	// Get 'fileId' parameter
+	const [fileId, fileIdErr] = $(params.fileId).optional.id().$;
+	if (fileIdErr) return rej('invalid fileId param');
 
 	let file = null;
 	if (fileId !== undefined) {
 		file = await DriveFile.findOne({
 			_id: fileId,
-			'metadata.user_id': user._id
+			'metadata.userId': user._id
 		});
 
 		if (file === null) {
@@ -70,12 +70,12 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// メッセージを作成
 	const message = await Message.insert({
-		created_at: new Date(),
-		file_id: file ? file._id : undefined,
-		recipient_id: recipient._id,
+		createdAt: new Date(),
+		fileId: file ? file._id : undefined,
+		recipientId: recipient._id,
 		text: text ? text : undefined,
-		user_id: user._id,
-		is_read: false
+		userId: user._id,
+		isRead: false
 	});
 
 	// Serialize
@@ -85,32 +85,32 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	res(messageObj);
 
 	// 自分のストリーム
-	publishMessagingStream(message.user_id, message.recipient_id, 'message', messageObj);
-	publishMessagingIndexStream(message.user_id, 'message', messageObj);
-	publishUserStream(message.user_id, 'messaging_message', messageObj);
+	publishMessagingStream(message.userId, message.recipientId, 'message', messageObj);
+	publishMessagingIndexStream(message.userId, 'message', messageObj);
+	publishUserStream(message.userId, 'messaging_message', messageObj);
 
 	// 相手のストリーム
-	publishMessagingStream(message.recipient_id, message.user_id, 'message', messageObj);
-	publishMessagingIndexStream(message.recipient_id, 'message', messageObj);
-	publishUserStream(message.recipient_id, 'messaging_message', messageObj);
+	publishMessagingStream(message.recipientId, message.userId, 'message', messageObj);
+	publishMessagingIndexStream(message.recipientId, 'message', messageObj);
+	publishUserStream(message.recipientId, 'messaging_message', messageObj);
 
 	// 3秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する
 	setTimeout(async () => {
-		const freshMessage = await Message.findOne({ _id: message._id }, { is_read: true });
-		if (!freshMessage.is_read) {
+		const freshMessage = await Message.findOne({ _id: message._id }, { isRead: true });
+		if (!freshMessage.isRead) {
 			//#region ただしミュートされているなら発行しない
 			const mute = await Mute.find({
-				muter_id: recipient._id,
-				deleted_at: { $exists: false }
+				muterId: recipient._id,
+				deletedAt: { $exists: false }
 			});
-			const mutedUserIds = mute.map(m => m.mutee_id.toString());
+			const mutedUserIds = mute.map(m => m.muteeId.toString());
 			if (mutedUserIds.indexOf(user._id.toString()) != -1) {
 				return;
 			}
 			//#endregion
 
-			publishUserStream(message.recipient_id, 'unread_messaging_message', messageObj);
-			pushSw(message.recipient_id, 'unread_messaging_message', messageObj);
+			publishUserStream(message.recipientId, 'unread_messaging_message', messageObj);
+			pushSw(message.recipientId, 'unread_messaging_message', messageObj);
 		}
 	}, 3000);
 
@@ -130,26 +130,26 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// 履歴作成(自分)
 	History.update({
-		user_id: user._id,
-		partner: recipient._id
+		userId: user._id,
+		partnerId: recipient._id
 	}, {
-		updated_at: new Date(),
-		user_id: user._id,
-		partner: recipient._id,
-		message: message._id
+		updatedAt: new Date(),
+		userId: user._id,
+		partnerId: recipient._id,
+		messageId: message._id
 	}, {
 		upsert: true
 	});
 
 	// 履歴作成(相手)
 	History.update({
-		user_id: recipient._id,
-		partner: user._id
+		userId: recipient._id,
+		partnerId: user._id
 	}, {
-		updated_at: new Date(),
-		user_id: recipient._id,
-		partner: user._id,
-		message: message._id
+		updatedAt: new Date(),
+		userId: recipient._id,
+		partnerId: user._id,
+		messageId: message._id
 	}, {
 		upsert: true
 	});
diff --git a/src/server/api/endpoints/messaging/unread.ts b/src/server/api/endpoints/messaging/unread.ts
index c4326e1d2..f7f4047b6 100644
--- a/src/server/api/endpoints/messaging/unread.ts
+++ b/src/server/api/endpoints/messaging/unread.ts
@@ -13,18 +13,18 @@ import Mute from '../../models/mute';
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	const mute = await Mute.find({
-		muter_id: user._id,
-		deleted_at: { $exists: false }
+		muterId: user._id,
+		deletedAt: { $exists: false }
 	});
-	const mutedUserIds = mute.map(m => m.mutee_id);
+	const mutedUserIds = mute.map(m => m.muteeId);
 
 	const count = await Message
 		.count({
-			user_id: {
+			userId: {
 				$nin: mutedUserIds
 			},
-			recipient_id: user._id,
-			is_read: false
+			recipientId: user._id,
+			isRead: false
 		});
 
 	res({
diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts
index 10625ec66..cb47ede57 100644
--- a/src/server/api/endpoints/meta.ts
+++ b/src/server/api/endpoints/meta.ts
@@ -53,7 +53,6 @@ module.exports = (params) => new Promise(async (res, rej) => {
 			model: os.cpus()[0].model,
 			cores: os.cpus().length
 		},
-		top_image: meta.top_image,
 		broadcasts: meta.broadcasts
 	});
 });
diff --git a/src/server/api/endpoints/mute/create.ts b/src/server/api/endpoints/mute/create.ts
index f99b40d32..e86023508 100644
--- a/src/server/api/endpoints/mute/create.ts
+++ b/src/server/api/endpoints/mute/create.ts
@@ -15,9 +15,9 @@ import Mute from '../../models/mute';
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	const muter = user;
 
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [userId, userIdErr] = $(params.userId).id().$;
+	if (userIdErr) return rej('invalid userId param');
 
 	// 自分自身
 	if (user._id.equals(userId)) {
@@ -40,9 +40,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Check if already muting
 	const exist = await Mute.findOne({
-		muter_id: muter._id,
-		mutee_id: mutee._id,
-		deleted_at: { $exists: false }
+		muterId: muter._id,
+		muteeId: mutee._id,
+		deletedAt: { $exists: false }
 	});
 
 	if (exist !== null) {
@@ -51,9 +51,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Create mute
 	await Mute.insert({
-		created_at: new Date(),
-		muter_id: muter._id,
-		mutee_id: mutee._id,
+		createdAt: new Date(),
+		muterId: muter._id,
+		muteeId: mutee._id,
 	});
 
 	// Send response
diff --git a/src/server/api/endpoints/mute/delete.ts b/src/server/api/endpoints/mute/delete.ts
index 36e2fd101..7e361b479 100644
--- a/src/server/api/endpoints/mute/delete.ts
+++ b/src/server/api/endpoints/mute/delete.ts
@@ -15,9 +15,9 @@ import Mute from '../../models/mute';
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	const muter = user;
 
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [userId, userIdErr] = $(params.userId).id().$;
+	if (userIdErr) return rej('invalid userId param');
 
 	// Check if the mutee is yourself
 	if (user._id.equals(userId)) {
@@ -40,9 +40,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Check not muting
 	const exist = await Mute.findOne({
-		muter_id: muter._id,
-		mutee_id: mutee._id,
-		deleted_at: { $exists: false }
+		muterId: muter._id,
+		muteeId: mutee._id,
+		deletedAt: { $exists: false }
 	});
 
 	if (exist === null) {
@@ -54,7 +54,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		_id: exist._id
 	}, {
 		$set: {
-			deleted_at: new Date()
+			deletedAt: new Date()
 		}
 	});
 
diff --git a/src/server/api/endpoints/mute/list.ts b/src/server/api/endpoints/mute/list.ts
index 19e3b157e..3401fba64 100644
--- a/src/server/api/endpoints/mute/list.ts
+++ b/src/server/api/endpoints/mute/list.ts
@@ -28,15 +28,15 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Construct query
 	const query = {
-		muter_id: me._id,
-		deleted_at: { $exists: false }
+		muterId: me._id,
+		deletedAt: { $exists: false }
 	} as any;
 
 	if (iknow) {
 		// Get my friends
 		const myFriends = await getFriends(me._id);
 
-		query.mutee_id = {
+		query.muteeId = {
 			$in: myFriends
 		};
 	}
@@ -63,7 +63,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Serialize
 	const users = await Promise.all(mutes.map(async m =>
-		await pack(m.mutee_id, me, { detail: true })));
+		await pack(m.muteeId, me, { detail: true })));
 
 	// Response
 	res({
diff --git a/src/server/api/endpoints/my/apps.ts b/src/server/api/endpoints/my/apps.ts
index b23619050..bc1290cac 100644
--- a/src/server/api/endpoints/my/apps.ts
+++ b/src/server/api/endpoints/my/apps.ts
@@ -21,7 +21,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	if (offsetErr) return rej('invalid offset param');
 
 	const query = {
-		user_id: user._id
+		userId: user._id
 	};
 
 	// Execute query
diff --git a/src/server/api/endpoints/notifications/get_unread_count.ts b/src/server/api/endpoints/notifications/get_unread_count.ts
index 845d6b29c..8f9719fff 100644
--- a/src/server/api/endpoints/notifications/get_unread_count.ts
+++ b/src/server/api/endpoints/notifications/get_unread_count.ts
@@ -13,18 +13,18 @@ import Mute from '../../models/mute';
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	const mute = await Mute.find({
-		muter_id: user._id,
-		deleted_at: { $exists: false }
+		muterId: user._id,
+		deletedAt: { $exists: false }
 	});
-	const mutedUserIds = mute.map(m => m.mutee_id);
+	const mutedUserIds = mute.map(m => m.muteeId);
 
 	const count = await Notification
 		.count({
-			notifiee_id: user._id,
-			notifier_id: {
+			notifieeId: user._id,
+			notifierId: {
 				$nin: mutedUserIds
 			},
-			is_read: false
+			isRead: false
 		});
 
 	res({
diff --git a/src/server/api/endpoints/notifications/mark_as_read_all.ts b/src/server/api/endpoints/notifications/mark_as_read_all.ts
index 3550e344c..693de3d0e 100644
--- a/src/server/api/endpoints/notifications/mark_as_read_all.ts
+++ b/src/server/api/endpoints/notifications/mark_as_read_all.ts
@@ -14,11 +14,11 @@ import event from '../../event';
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Update documents
 	await Notification.update({
-		notifiee_id: user._id,
-		is_read: false
+		notifieeId: user._id,
+		isRead: false
 	}, {
 		$set: {
-			is_read: true
+			isRead: true
 		}
 	}, {
 		multi: true
diff --git a/src/server/api/endpoints/othello/games.ts b/src/server/api/endpoints/othello/games.ts
index 2a6bbb404..37fa38418 100644
--- a/src/server/api/endpoints/othello/games.ts
+++ b/src/server/api/endpoints/othello/games.ts
@@ -1,5 +1,5 @@
 import $ from 'cafy';
-import Game, { pack } from '../../models/othello-game';
+import OthelloGame, { pack } from '../../models/othello-game';
 
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Get 'my' parameter
@@ -10,28 +10,28 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) return rej('invalid limit param');
 
-	// Get 'since_id' parameter
-	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) return rej('invalid since_id param');
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) return rej('invalid sinceId param');
 
-	// Get 'until_id' parameter
-	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
-	if (untilIdErr) return rej('invalid until_id param');
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) return rej('invalid untilId param');
 
-	// Check if both of since_id and until_id is specified
+	// Check if both of sinceId and untilId is specified
 	if (sinceId && untilId) {
-		return rej('cannot set since_id and until_id');
+		return rej('cannot set sinceId and untilId');
 	}
 
 	const q: any = my ? {
-		is_started: true,
+		isStarted: true,
 		$or: [{
-			user1_id: user._id
+			user1Id: user._id
 		}, {
-			user2_id: user._id
+			user2Id: user._id
 		}]
 	} : {
-		is_started: true
+		isStarted: true
 	};
 
 	const sort = {
@@ -50,7 +50,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}
 
 	// Fetch games
-	const games = await Game.find(q, {
+	const games = await OthelloGame.find(q, {
 		sort,
 		limit
 	});
diff --git a/src/server/api/endpoints/othello/games/show.ts b/src/server/api/endpoints/othello/games/show.ts
index 2b0db4dd0..f9084682f 100644
--- a/src/server/api/endpoints/othello/games/show.ts
+++ b/src/server/api/endpoints/othello/games/show.ts
@@ -1,22 +1,22 @@
 import $ from 'cafy';
-import Game, { pack } from '../../../models/othello-game';
+import OthelloGame, { pack } from '../../../models/othello-game';
 import Othello from '../../../../common/othello/core';
 
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'game_id' parameter
-	const [gameId, gameIdErr] = $(params.game_id).id().$;
-	if (gameIdErr) return rej('invalid game_id param');
+	// Get 'gameId' parameter
+	const [gameId, gameIdErr] = $(params.gameId).id().$;
+	if (gameIdErr) return rej('invalid gameId param');
 
-	const game = await Game.findOne({ _id: gameId });
+	const game = await OthelloGame.findOne({ _id: gameId });
 
 	if (game == null) {
 		return rej('game not found');
 	}
 
 	const o = new Othello(game.settings.map, {
-		isLlotheo: game.settings.is_llotheo,
-		canPutEverywhere: game.settings.can_put_everywhere,
-		loopedBoard: game.settings.looped_board
+		isLlotheo: game.settings.isLlotheo,
+		canPutEverywhere: game.settings.canPutEverywhere,
+		loopedBoard: game.settings.loopedBoard
 	});
 
 	game.logs.forEach(log => {
diff --git a/src/server/api/endpoints/othello/invitations.ts b/src/server/api/endpoints/othello/invitations.ts
index 02fb421fb..f6e0071a6 100644
--- a/src/server/api/endpoints/othello/invitations.ts
+++ b/src/server/api/endpoints/othello/invitations.ts
@@ -3,7 +3,7 @@ import Matching, { pack as packMatching } from '../../models/othello-matching';
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Find session
 	const invitations = await Matching.find({
-		child_id: user._id
+		childId: user._id
 	}, {
 		sort: {
 			_id: -1
diff --git a/src/server/api/endpoints/othello/match.ts b/src/server/api/endpoints/othello/match.ts
index b73e105ef..f503c5834 100644
--- a/src/server/api/endpoints/othello/match.ts
+++ b/src/server/api/endpoints/othello/match.ts
@@ -1,24 +1,24 @@
 import $ from 'cafy';
 import Matching, { pack as packMatching } from '../../models/othello-matching';
-import Game, { pack as packGame } from '../../models/othello-game';
+import OthelloGame, { pack as packGame } from '../../models/othello-game';
 import User from '../../models/user';
 import publishUserStream, { publishOthelloStream } from '../../event';
 import { eighteight } from '../../../common/othello/maps';
 
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'user_id' parameter
-	const [childId, childIdErr] = $(params.user_id).id().$;
-	if (childIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [childId, childIdErr] = $(params.userId).id().$;
+	if (childIdErr) return rej('invalid userId param');
 
 	// Myself
 	if (childId.equals(user._id)) {
-		return rej('invalid user_id param');
+		return rej('invalid userId param');
 	}
 
 	// Find session
 	const exist = await Matching.findOne({
-		parent_id: childId,
-		child_id: user._id
+		parentId: childId,
+		childId: user._id
 	});
 
 	if (exist) {
@@ -28,29 +28,29 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		});
 
 		// Create game
-		const game = await Game.insert({
-			created_at: new Date(),
-			user1_id: exist.parent_id,
-			user2_id: user._id,
-			user1_accepted: false,
-			user2_accepted: false,
-			is_started: false,
-			is_ended: false,
+		const game = await OthelloGame.insert({
+			createdAt: new Date(),
+			user1Id: exist.parentId,
+			user2Id: user._id,
+			user1Accepted: false,
+			user2Accepted: false,
+			isStarted: false,
+			isEnded: false,
 			logs: [],
 			settings: {
 				map: eighteight.data,
 				bw: 'random',
-				is_llotheo: false
+				isLlotheo: false
 			}
 		});
 
 		// Reponse
 		res(await packGame(game, user));
 
-		publishOthelloStream(exist.parent_id, 'matched', await packGame(game, exist.parent_id));
+		publishOthelloStream(exist.parentId, 'matched', await packGame(game, exist.parentId));
 
 		const other = await Matching.count({
-			child_id: user._id
+			childId: user._id
 		});
 
 		if (other == 0) {
@@ -72,14 +72,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 		// 以前のセッションはすべて削除しておく
 		await Matching.remove({
-			parent_id: user._id
+			parentId: user._id
 		});
 
 		// セッションを作成
 		const matching = await Matching.insert({
-			created_at: new Date(),
-			parent_id: user._id,
-			child_id: child._id
+			createdAt: new Date(),
+			parentId: user._id,
+			childId: child._id
 		});
 
 		// Reponse
diff --git a/src/server/api/endpoints/othello/match/cancel.ts b/src/server/api/endpoints/othello/match/cancel.ts
index 6f751ef83..ee0f82a61 100644
--- a/src/server/api/endpoints/othello/match/cancel.ts
+++ b/src/server/api/endpoints/othello/match/cancel.ts
@@ -2,7 +2,7 @@ import Matching from '../../../models/othello-matching';
 
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	await Matching.remove({
-		parent_id: user._id
+		parentId: user._id
 	});
 
 	res();
diff --git a/src/server/api/endpoints/posts.ts b/src/server/api/endpoints/posts.ts
index 7df744d2a..bee1de02d 100644
--- a/src/server/api/endpoints/posts.ts
+++ b/src/server/api/endpoints/posts.ts
@@ -35,17 +35,17 @@ module.exports = (params) => new Promise(async (res, rej) => {
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) return rej('invalid limit param');
 
-	// Get 'since_id' parameter
-	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) return rej('invalid since_id param');
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) return rej('invalid sinceId param');
 
-	// Get 'until_id' parameter
-	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
-	if (untilIdErr) return rej('invalid until_id param');
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) return rej('invalid untilId param');
 
-	// Check if both of since_id and until_id is specified
+	// Check if both of sinceId and untilId is specified
 	if (sinceId && untilId) {
-		return rej('cannot set since_id and until_id');
+		return rej('cannot set sinceId and untilId');
 	}
 
 	// Construct query
@@ -65,15 +65,15 @@ module.exports = (params) => new Promise(async (res, rej) => {
 	}
 
 	if (reply != undefined) {
-		query.reply_id = reply ? { $exists: true, $ne: null } : null;
+		query.replyId = reply ? { $exists: true, $ne: null } : null;
 	}
 
 	if (repost != undefined) {
-		query.repost_id = repost ? { $exists: true, $ne: null } : null;
+		query.repostId = repost ? { $exists: true, $ne: null } : null;
 	}
 
 	if (media != undefined) {
-		query.media_ids = media ? { $exists: true, $ne: null } : null;
+		query.mediaIds = media ? { $exists: true, $ne: null } : null;
 	}
 
 	if (poll != undefined) {
@@ -82,7 +82,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
 
 	// TODO
 	//if (bot != undefined) {
-	//	query.is_bot = bot;
+	//	query.isBot = bot;
 	//}
 
 	// Issue query
diff --git a/src/server/api/endpoints/posts/categorize.ts b/src/server/api/endpoints/posts/categorize.ts
index 0c85c2b4e..0436c8e69 100644
--- a/src/server/api/endpoints/posts/categorize.ts
+++ b/src/server/api/endpoints/posts/categorize.ts
@@ -12,13 +12,13 @@ import Post from '../../models/post';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	if (!user.account.is_pro) {
+	if (!user.account.isPro) {
 		return rej('This endpoint is available only from a Pro account');
 	}
 
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Get categorizee
 	const post = await Post.findOne({
diff --git a/src/server/api/endpoints/posts/context.ts b/src/server/api/endpoints/posts/context.ts
index 5ba375897..44a77d102 100644
--- a/src/server/api/endpoints/posts/context.ts
+++ b/src/server/api/endpoints/posts/context.ts
@@ -12,9 +12,9 @@ import Post, { pack } from '../../models/post';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Get 'limit' parameter
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
@@ -48,13 +48,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 			return;
 		}
 
-		if (p.reply_id) {
-			await get(p.reply_id);
+		if (p.replyId) {
+			await get(p.replyId);
 		}
 	}
 
-	if (post.reply_id) {
-		await get(post.reply_id);
+	if (post.replyId) {
+		await get(post.replyId);
 	}
 
 	// Serialize
diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts
index bc9af843b..390370ad4 100644
--- a/src/server/api/endpoints/posts/create.ts
+++ b/src/server/api/endpoints/posts/create.ts
@@ -33,9 +33,9 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 	const [text, textErr] = $(params.text).optional.string().pipe(isValidText).$;
 	if (textErr) return rej('invalid text');
 
-	// Get 'via_mobile' parameter
-	const [viaMobile = false, viaMobileErr] = $(params.via_mobile).optional.boolean().$;
-	if (viaMobileErr) return rej('invalid via_mobile');
+	// Get 'viaMobile' parameter
+	const [viaMobile = false, viaMobileErr] = $(params.viaMobile).optional.boolean().$;
+	if (viaMobileErr) return rej('invalid viaMobile');
 
 	// Get 'tags' parameter
 	const [tags = [], tagsErr] = $(params.tags).optional.array('string').unique().eachQ(t => t.range(1, 32)).$;
@@ -53,9 +53,9 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		.$;
 	if (geoErr) return rej('invalid geo');
 
-	// Get 'media_ids' parameter
-	const [mediaIds, mediaIdsErr] = $(params.media_ids).optional.array('id').unique().range(1, 4).$;
-	if (mediaIdsErr) return rej('invalid media_ids');
+	// Get 'mediaIds' parameter
+	const [mediaIds, mediaIdsErr] = $(params.mediaIds).optional.array('id').unique().range(1, 4).$;
+	if (mediaIdsErr) return rej('invalid mediaIds');
 
 	let files = [];
 	if (mediaIds !== undefined) {
@@ -67,7 +67,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 			// SELECT _id
 			const entity = await DriveFile.findOne({
 				_id: mediaId,
-				'metadata.user_id': user._id
+				'metadata.userId': user._id
 			});
 
 			if (entity === null) {
@@ -80,9 +80,9 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		files = null;
 	}
 
-	// Get 'repost_id' parameter
-	const [repostId, repostIdErr] = $(params.repost_id).optional.id().$;
-	if (repostIdErr) return rej('invalid repost_id');
+	// Get 'repostId' parameter
+	const [repostId, repostIdErr] = $(params.repostId).optional.id().$;
+	if (repostIdErr) return rej('invalid repostId');
 
 	let repost: IPost = null;
 	let isQuote = false;
@@ -94,13 +94,13 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 
 		if (repost == null) {
 			return rej('repostee is not found');
-		} else if (repost.repost_id && !repost.text && !repost.media_ids) {
+		} else if (repost.repostId && !repost.text && !repost.mediaIds) {
 			return rej('cannot repost to repost');
 		}
 
 		// Fetch recently post
 		const latestPost = await Post.findOne({
-			user_id: user._id
+			userId: user._id
 		}, {
 			sort: {
 				_id: -1
@@ -111,8 +111,8 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 
 		// 直近と同じRepost対象かつ引用じゃなかったらエラー
 		if (latestPost &&
-			latestPost.repost_id &&
-			latestPost.repost_id.equals(repost._id) &&
+			latestPost.repostId &&
+			latestPost.repostId.equals(repost._id) &&
 			!isQuote) {
 			return rej('cannot repost same post that already reposted in your latest post');
 		}
@@ -125,9 +125,9 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		}
 	}
 
-	// Get 'reply_id' parameter
-	const [replyId, replyIdErr] = $(params.reply_id).optional.id().$;
-	if (replyIdErr) return rej('invalid reply_id');
+	// Get 'replyId' parameter
+	const [replyId, replyIdErr] = $(params.replyId).optional.id().$;
+	if (replyIdErr) return rej('invalid replyId');
 
 	let reply: IPost = null;
 	if (replyId !== undefined) {
@@ -141,14 +141,14 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		}
 
 		// 返信対象が引用でないRepostだったらエラー
-		if (reply.repost_id && !reply.text && !reply.media_ids) {
+		if (reply.repostId && !reply.text && !reply.mediaIds) {
 			return rej('cannot reply to repost');
 		}
 	}
 
-	// Get 'channel_id' parameter
-	const [channelId, channelIdErr] = $(params.channel_id).optional.id().$;
-	if (channelIdErr) return rej('invalid channel_id');
+	// Get 'channelId' parameter
+	const [channelId, channelIdErr] = $(params.channelId).optional.id().$;
+	if (channelIdErr) return rej('invalid channelId');
 
 	let channel: IChannel = null;
 	if (channelId !== undefined) {
@@ -162,12 +162,12 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		}
 
 		// 返信対象の投稿がこのチャンネルじゃなかったらダメ
-		if (reply && !channelId.equals(reply.channel_id)) {
+		if (reply && !channelId.equals(reply.channelId)) {
 			return rej('チャンネル内部からチャンネル外部の投稿に返信することはできません');
 		}
 
 		// Repost対象の投稿がこのチャンネルじゃなかったらダメ
-		if (repost && !channelId.equals(repost.channel_id)) {
+		if (repost && !channelId.equals(repost.channelId)) {
 			return rej('チャンネル内部からチャンネル外部の投稿をRepostすることはできません');
 		}
 
@@ -177,12 +177,12 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		}
 	} else {
 		// 返信対象の投稿がチャンネルへの投稿だったらダメ
-		if (reply && reply.channel_id != null) {
+		if (reply && reply.channelId != null) {
 			return rej('チャンネル外部からチャンネル内部の投稿に返信することはできません');
 		}
 
 		// Repost対象の投稿がチャンネルへの投稿だったらダメ
-		if (repost && repost.channel_id != null) {
+		if (repost && repost.channelId != null) {
 			return rej('チャンネル外部からチャンネル内部の投稿をRepostすることはできません');
 		}
 	}
@@ -206,22 +206,22 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 
 	// テキストが無いかつ添付ファイルが無いかつRepostも無いかつ投票も無かったらエラー
 	if (text === undefined && files === null && repost === null && poll === undefined) {
-		return rej('text, media_ids, repost_id or poll is required');
+		return rej('text, mediaIds, repostId or poll is required');
 	}
 
 	// 直近の投稿と重複してたらエラー
 	// TODO: 直近の投稿が一日前くらいなら重複とは見なさない
-	if (user.latest_post) {
+	if (user.latestPost) {
 		if (deepEqual({
-			text: user.latest_post.text,
-			reply: user.latest_post.reply_id ? user.latest_post.reply_id.toString() : null,
-			repost: user.latest_post.repost_id ? user.latest_post.repost_id.toString() : null,
-			media_ids: (user.latest_post.media_ids || []).map(id => id.toString())
+			text: user.latestPost.text,
+			reply: user.latestPost.replyId ? user.latestPost.replyId.toString() : null,
+			repost: user.latestPost.repostId ? user.latestPost.repostId.toString() : null,
+			mediaIds: (user.latestPost.mediaIds || []).map(id => id.toString())
 		}, {
 			text: text,
 			reply: reply ? reply._id.toString() : null,
 			repost: repost ? repost._id.toString() : null,
-			media_ids: (files || []).map(file => file._id.toString())
+			mediaIds: (files || []).map(file => file._id.toString())
 		})) {
 			return rej('duplicate');
 		}
@@ -246,23 +246,23 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 
 	// 投稿を作成
 	const post = await Post.insert({
-		created_at: new Date(),
-		channel_id: channel ? channel._id : undefined,
+		createdAt: new Date(),
+		channelId: channel ? channel._id : undefined,
 		index: channel ? channel.index + 1 : undefined,
-		media_ids: files ? files.map(file => file._id) : undefined,
-		reply_id: reply ? reply._id : undefined,
-		repost_id: repost ? repost._id : undefined,
+		mediaIds: files ? files.map(file => file._id) : undefined,
+		replyId: reply ? reply._id : undefined,
+		repostId: repost ? repost._id : undefined,
 		poll: poll,
 		text: text,
 		tags: tags,
-		user_id: user._id,
-		app_id: app ? app._id : null,
-		via_mobile: viaMobile,
+		userId: user._id,
+		appId: app ? app._id : null,
+		viaMobile: viaMobile,
 		geo,
 
 		// 以下非正規化データ
-		_reply: reply ? { user_id: reply.user_id } : undefined,
-		_repost: repost ? { user_id: repost.user_id } : undefined,
+		_reply: reply ? { userId: reply.userId } : undefined,
+		_repost: repost ? { userId: repost.userId } : undefined,
 	});
 
 	// Serialize
@@ -270,14 +270,14 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 
 	// Reponse
 	res({
-		created_post: postObj
+		createdPost: postObj
 	});
 
 	//#region Post processes
 
 	User.update({ _id: user._id }, {
 		$set: {
-			latest_post: post
+			latestPost: post
 		}
 	});
 
@@ -293,10 +293,10 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		// Publish event
 		if (!user._id.equals(mentionee)) {
 			const mentioneeMutes = await Mute.find({
-				muter_id: mentionee,
-				deleted_at: { $exists: false }
+				muterId: mentionee,
+				deletedAt: { $exists: false }
 			});
-			const mentioneesMutedUserIds = mentioneeMutes.map(m => m.mutee_id.toString());
+			const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString());
 			if (mentioneesMutedUserIds.indexOf(user._id.toString()) == -1) {
 				event(mentionee, reason, postObj);
 				pushSw(mentionee, reason, postObj);
@@ -312,17 +312,17 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		// Fetch all followers
 		const followers = await Following
 			.find({
-				followee_id: user._id,
+				followeeId: user._id,
 				// 削除されたドキュメントは除く
-				deleted_at: { $exists: false }
+				deletedAt: { $exists: false }
 			}, {
-				follower_id: true,
+				followerId: true,
 				_id: false
 			});
 
 		// Publish event to followers stream
 		followers.forEach(following =>
-			event(following.follower_id, 'post', postObj));
+			event(following.followerId, 'post', postObj));
 	}
 
 	// チャンネルへの投稿
@@ -339,21 +339,21 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 
 		// Get channel watchers
 		const watches = await ChannelWatching.find({
-			channel_id: channel._id,
+			channelId: channel._id,
 			// 削除されたドキュメントは除く
-			deleted_at: { $exists: false }
+			deletedAt: { $exists: false }
 		});
 
 		// チャンネルの視聴者(のタイムライン)に配信
 		watches.forEach(w => {
-			event(w.user_id, 'post', postObj);
+			event(w.userId, 'post', postObj);
 		});
 	}
 
 	// Increment my posts count
 	User.update({ _id: user._id }, {
 		$inc: {
-			posts_count: 1
+			postsCount: 1
 		}
 	});
 
@@ -362,68 +362,68 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		// Increment replies count
 		Post.update({ _id: reply._id }, {
 			$inc: {
-				replies_count: 1
+				repliesCount: 1
 			}
 		});
 
 		// 自分自身へのリプライでない限りは通知を作成
-		notify(reply.user_id, user._id, 'reply', {
-			post_id: post._id
+		notify(reply.userId, user._id, 'reply', {
+			postId: post._id
 		});
 
 		// Fetch watchers
 		Watching
 			.find({
-				post_id: reply._id,
-				user_id: { $ne: user._id },
+				postId: reply._id,
+				userId: { $ne: user._id },
 				// 削除されたドキュメントは除く
-				deleted_at: { $exists: false }
+				deletedAt: { $exists: false }
 			}, {
 				fields: {
-					user_id: true
+					userId: true
 				}
 			})
 			.then(watchers => {
 				watchers.forEach(watcher => {
-					notify(watcher.user_id, user._id, 'reply', {
-						post_id: post._id
+					notify(watcher.userId, user._id, 'reply', {
+						postId: post._id
 					});
 				});
 			});
 
 		// この投稿をWatchする
-		if ((user.account as ILocalAccount).settings.auto_watch !== false) {
+		if ((user.account as ILocalAccount).settings.autoWatch !== false) {
 			watch(user._id, reply);
 		}
 
 		// Add mention
-		addMention(reply.user_id, 'reply');
+		addMention(reply.userId, 'reply');
 	}
 
 	// If it is repost
 	if (repost) {
 		// Notify
 		const type = text ? 'quote' : 'repost';
-		notify(repost.user_id, user._id, type, {
-			post_id: post._id
+		notify(repost.userId, user._id, type, {
+			postId: post._id
 		});
 
 		// Fetch watchers
 		Watching
 			.find({
-				post_id: repost._id,
-				user_id: { $ne: user._id },
+				postId: repost._id,
+				userId: { $ne: user._id },
 				// 削除されたドキュメントは除く
-				deleted_at: { $exists: false }
+				deletedAt: { $exists: false }
 			}, {
 				fields: {
-					user_id: true
+					userId: true
 				}
 			})
 			.then(watchers => {
 				watchers.forEach(watcher => {
-					notify(watcher.user_id, user._id, type, {
-						post_id: post._id
+					notify(watcher.userId, user._id, type, {
+						postId: post._id
 					});
 				});
 			});
@@ -436,18 +436,18 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		// If it is quote repost
 		if (text) {
 			// Add mention
-			addMention(repost.user_id, 'quote');
+			addMention(repost.userId, 'quote');
 		} else {
 			// Publish event
-			if (!user._id.equals(repost.user_id)) {
-				event(repost.user_id, 'repost', postObj);
+			if (!user._id.equals(repost.userId)) {
+				event(repost.userId, 'repost', postObj);
 			}
 		}
 
 		// 今までで同じ投稿をRepostしているか
 		const existRepost = await Post.findOne({
-			user_id: user._id,
-			repost_id: repost._id,
+			userId: user._id,
+			repostId: repost._id,
 			_id: {
 				$ne: post._id
 			}
@@ -457,7 +457,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 			// Update repostee status
 			Post.update({ _id: repost._id }, {
 				$inc: {
-					repost_count: 1
+					repostCount: 1
 				}
 			});
 		}
@@ -494,15 +494,15 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 			if (mentionee == null) return;
 
 			// 既に言及されたユーザーに対する返信や引用repostの場合も無視
-			if (reply && reply.user_id.equals(mentionee._id)) return;
-			if (repost && repost.user_id.equals(mentionee._id)) return;
+			if (reply && reply.userId.equals(mentionee._id)) return;
+			if (repost && repost.userId.equals(mentionee._id)) return;
 
 			// Add mention
 			addMention(mentionee._id, 'mention');
 
 			// Create notification
 			notify(mentionee._id, user._id, 'mention', {
-				post_id: post._id
+				postId: post._id
 			});
 
 			return;
diff --git a/src/server/api/endpoints/posts/favorites/create.ts b/src/server/api/endpoints/posts/favorites/create.ts
index f9dee271b..6100e10b2 100644
--- a/src/server/api/endpoints/posts/favorites/create.ts
+++ b/src/server/api/endpoints/posts/favorites/create.ts
@@ -13,9 +13,9 @@ import Post from '../../../models/post';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Get favoritee
 	const post = await Post.findOne({
@@ -28,8 +28,8 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// if already favorited
 	const exist = await Favorite.findOne({
-		post_id: post._id,
-		user_id: user._id
+		postId: post._id,
+		userId: user._id
 	});
 
 	if (exist !== null) {
@@ -38,9 +38,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Create favorite
 	await Favorite.insert({
-		created_at: new Date(),
-		post_id: post._id,
-		user_id: user._id
+		createdAt: new Date(),
+		postId: post._id,
+		userId: user._id
 	});
 
 	// Send response
diff --git a/src/server/api/endpoints/posts/favorites/delete.ts b/src/server/api/endpoints/posts/favorites/delete.ts
index c4fe7d323..db52036ec 100644
--- a/src/server/api/endpoints/posts/favorites/delete.ts
+++ b/src/server/api/endpoints/posts/favorites/delete.ts
@@ -13,9 +13,9 @@ import Post from '../../../models/post';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Get favoritee
 	const post = await Post.findOne({
@@ -28,8 +28,8 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// if already favorited
 	const exist = await Favorite.findOne({
-		post_id: post._id,
-		user_id: user._id
+		postId: post._id,
+		userId: user._id
 	});
 
 	if (exist === null) {
@@ -37,7 +37,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}
 
 	// Delete favorite
-	await Favorite.deleteOne({
+	await Favorite.remove({
 		_id: exist._id
 	});
 
diff --git a/src/server/api/endpoints/posts/mentions.ts b/src/server/api/endpoints/posts/mentions.ts
index 7127db0ad..1b342e8de 100644
--- a/src/server/api/endpoints/posts/mentions.ts
+++ b/src/server/api/endpoints/posts/mentions.ts
@@ -23,17 +23,17 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) return rej('invalid limit param');
 
-	// Get 'since_id' parameter
-	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) return rej('invalid since_id param');
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) return rej('invalid sinceId param');
 
-	// Get 'until_id' parameter
-	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
-	if (untilIdErr) return rej('invalid until_id param');
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) return rej('invalid untilId param');
 
-	// Check if both of since_id and until_id is specified
+	// Check if both of sinceId and untilId is specified
 	if (sinceId && untilId) {
-		return rej('cannot set since_id and until_id');
+		return rej('cannot set sinceId and untilId');
 	}
 
 	// Construct query
@@ -48,7 +48,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	if (following) {
 		const followingIds = await getFriends(user._id);
 
-		query.user_id = {
+		query.userId = {
 			$in: followingIds
 		};
 	}
diff --git a/src/server/api/endpoints/posts/polls/recommendation.ts b/src/server/api/endpoints/posts/polls/recommendation.ts
index 4a3fa3f55..19ef0975f 100644
--- a/src/server/api/endpoints/posts/polls/recommendation.ts
+++ b/src/server/api/endpoints/posts/polls/recommendation.ts
@@ -23,22 +23,22 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Get votes
 	const votes = await Vote.find({
-		user_id: user._id
+		userId: user._id
 	}, {
 		fields: {
 			_id: false,
-			post_id: true
+			postId: true
 		}
 	});
 
-	const nin = votes && votes.length != 0 ? votes.map(v => v.post_id) : [];
+	const nin = votes && votes.length != 0 ? votes.map(v => v.postId) : [];
 
 	const posts = await Post
 		.find({
 			_id: {
 				$nin: nin
 			},
-			user_id: {
+			userId: {
 				$ne: user._id
 			},
 			poll: {
diff --git a/src/server/api/endpoints/posts/polls/vote.ts b/src/server/api/endpoints/posts/polls/vote.ts
index 16ce76a6f..734a3a3c4 100644
--- a/src/server/api/endpoints/posts/polls/vote.ts
+++ b/src/server/api/endpoints/posts/polls/vote.ts
@@ -17,9 +17,9 @@ import { publishPostStream } from '../../../event';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Get votee
 	const post = await Post.findOne({
@@ -43,8 +43,8 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// if already voted
 	const exist = await Vote.findOne({
-		post_id: post._id,
-		user_id: user._id
+		postId: post._id,
+		userId: user._id
 	});
 
 	if (exist !== null) {
@@ -53,9 +53,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Create vote
 	await Vote.insert({
-		created_at: new Date(),
-		post_id: post._id,
-		user_id: user._id,
+		createdAt: new Date(),
+		postId: post._id,
+		userId: user._id,
 		choice: choice
 	});
 
@@ -73,34 +73,34 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	publishPostStream(post._id, 'poll_voted');
 
 	// Notify
-	notify(post.user_id, user._id, 'poll_vote', {
-		post_id: post._id,
+	notify(post.userId, user._id, 'poll_vote', {
+		postId: post._id,
 		choice: choice
 	});
 
 	// Fetch watchers
 	Watching
 		.find({
-			post_id: post._id,
-			user_id: { $ne: user._id },
+			postId: post._id,
+			userId: { $ne: user._id },
 			// 削除されたドキュメントは除く
-			deleted_at: { $exists: false }
+			deletedAt: { $exists: false }
 		}, {
 			fields: {
-				user_id: true
+				userId: true
 			}
 		})
 		.then(watchers => {
 			watchers.forEach(watcher => {
-				notify(watcher.user_id, user._id, 'poll_vote', {
-					post_id: post._id,
+				notify(watcher.userId, user._id, 'poll_vote', {
+					postId: post._id,
 					choice: choice
 				});
 			});
 		});
 
 	// この投稿をWatchする
-	if (user.account.settings.auto_watch !== false) {
+	if (user.account.settings.autoWatch !== false) {
 		watch(user._id, post);
 	}
 });
diff --git a/src/server/api/endpoints/posts/reactions.ts b/src/server/api/endpoints/posts/reactions.ts
index feb140ab4..f753ba7c2 100644
--- a/src/server/api/endpoints/posts/reactions.ts
+++ b/src/server/api/endpoints/posts/reactions.ts
@@ -13,9 +13,9 @@ import Reaction, { pack } from '../../models/post-reaction';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Get 'limit' parameter
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
@@ -41,8 +41,8 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Issue query
 	const reactions = await Reaction
 		.find({
-			post_id: post._id,
-			deleted_at: { $exists: false }
+			postId: post._id,
+			deletedAt: { $exists: false }
 		}, {
 			limit: limit,
 			skip: offset,
diff --git a/src/server/api/endpoints/posts/reactions/create.ts b/src/server/api/endpoints/posts/reactions/create.ts
index f77afed40..a1e677980 100644
--- a/src/server/api/endpoints/posts/reactions/create.ts
+++ b/src/server/api/endpoints/posts/reactions/create.ts
@@ -18,9 +18,9 @@ import { publishPostStream, pushSw } from '../../../event';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Get 'reaction' parameter
 	const [reaction, reactionErr] = $(params.reaction).string().or([
@@ -46,15 +46,15 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}
 
 	// Myself
-	if (post.user_id.equals(user._id)) {
+	if (post.userId.equals(user._id)) {
 		return rej('cannot react to my post');
 	}
 
 	// if already reacted
 	const exist = await Reaction.findOne({
-		post_id: post._id,
-		user_id: user._id,
-		deleted_at: { $exists: false }
+		postId: post._id,
+		userId: user._id,
+		deletedAt: { $exists: false }
 	});
 
 	if (exist !== null) {
@@ -63,9 +63,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Create reaction
 	await Reaction.insert({
-		created_at: new Date(),
-		post_id: post._id,
-		user_id: user._id,
+		createdAt: new Date(),
+		postId: post._id,
+		userId: user._id,
 		reaction: reaction
 	});
 
@@ -73,7 +73,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	res();
 
 	const inc = {};
-	inc[`reaction_counts.${reaction}`] = 1;
+	inc[`reactionCounts.${reaction}`] = 1;
 
 	// Increment reactions count
 	await Post.update({ _id: post._id }, {
@@ -83,40 +83,40 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	publishPostStream(post._id, 'reacted');
 
 	// Notify
-	notify(post.user_id, user._id, 'reaction', {
-		post_id: post._id,
+	notify(post.userId, user._id, 'reaction', {
+		postId: post._id,
 		reaction: reaction
 	});
 
-	pushSw(post.user_id, 'reaction', {
-		user: await packUser(user, post.user_id),
-		post: await packPost(post, post.user_id),
+	pushSw(post.userId, 'reaction', {
+		user: await packUser(user, post.userId),
+		post: await packPost(post, post.userId),
 		reaction: reaction
 	});
 
 	// Fetch watchers
 	Watching
 		.find({
-			post_id: post._id,
-			user_id: { $ne: user._id },
+			postId: post._id,
+			userId: { $ne: user._id },
 			// 削除されたドキュメントは除く
-			deleted_at: { $exists: false }
+			deletedAt: { $exists: false }
 		}, {
 			fields: {
-				user_id: true
+				userId: true
 			}
 		})
 		.then(watchers => {
 			watchers.forEach(watcher => {
-				notify(watcher.user_id, user._id, 'reaction', {
-					post_id: post._id,
+				notify(watcher.userId, user._id, 'reaction', {
+					postId: post._id,
 					reaction: reaction
 				});
 			});
 		});
 
 	// この投稿をWatchする
-	if (user.account.settings.auto_watch !== false) {
+	if (user.account.settings.autoWatch !== false) {
 		watch(user._id, post);
 	}
 });
diff --git a/src/server/api/endpoints/posts/reactions/delete.ts b/src/server/api/endpoints/posts/reactions/delete.ts
index 922c57ab1..b09bcbb4b 100644
--- a/src/server/api/endpoints/posts/reactions/delete.ts
+++ b/src/server/api/endpoints/posts/reactions/delete.ts
@@ -14,9 +14,9 @@ import Post from '../../../models/post';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Fetch unreactee
 	const post = await Post.findOne({
@@ -29,9 +29,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// if already unreacted
 	const exist = await Reaction.findOne({
-		post_id: post._id,
-		user_id: user._id,
-		deleted_at: { $exists: false }
+		postId: post._id,
+		userId: user._id,
+		deletedAt: { $exists: false }
 	});
 
 	if (exist === null) {
@@ -43,7 +43,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		_id: exist._id
 	}, {
 			$set: {
-				deleted_at: new Date()
+				deletedAt: new Date()
 			}
 		});
 
@@ -51,7 +51,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	res();
 
 	const dec = {};
-	dec[`reaction_counts.${exist.reaction}`] = -1;
+	dec[`reactionCounts.${exist.reaction}`] = -1;
 
 	// Decrement reactions count
 	Post.update({ _id: post._id }, {
diff --git a/src/server/api/endpoints/posts/replies.ts b/src/server/api/endpoints/posts/replies.ts
index 613c4fa24..db021505f 100644
--- a/src/server/api/endpoints/posts/replies.ts
+++ b/src/server/api/endpoints/posts/replies.ts
@@ -12,9 +12,9 @@ import Post, { pack } from '../../models/post';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Get 'limit' parameter
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
@@ -39,7 +39,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Issue query
 	const replies = await Post
-		.find({ reply_id: post._id }, {
+		.find({ replyId: post._id }, {
 			limit: limit,
 			skip: offset,
 			sort: {
diff --git a/src/server/api/endpoints/posts/reposts.ts b/src/server/api/endpoints/posts/reposts.ts
index 89ab0e3d5..51af41f52 100644
--- a/src/server/api/endpoints/posts/reposts.ts
+++ b/src/server/api/endpoints/posts/reposts.ts
@@ -12,25 +12,25 @@ import Post, { pack } from '../../models/post';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Get 'limit' parameter
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) return rej('invalid limit param');
 
-	// Get 'since_id' parameter
-	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) return rej('invalid since_id param');
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) return rej('invalid sinceId param');
 
-	// Get 'until_id' parameter
-	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
-	if (untilIdErr) return rej('invalid until_id param');
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) return rej('invalid untilId param');
 
-	// Check if both of since_id and until_id is specified
+	// Check if both of sinceId and untilId is specified
 	if (sinceId && untilId) {
-		return rej('cannot set since_id and until_id');
+		return rej('cannot set sinceId and untilId');
 	}
 
 	// Lookup post
@@ -47,7 +47,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		_id: -1
 	};
 	const query = {
-		repost_id: post._id
+		repostId: post._id
 	} as any;
 	if (sinceId) {
 		sort._id = 1;
diff --git a/src/server/api/endpoints/posts/search.ts b/src/server/api/endpoints/posts/search.ts
index a36d1178a..bb5c43892 100644
--- a/src/server/api/endpoints/posts/search.ts
+++ b/src/server/api/endpoints/posts/search.ts
@@ -21,21 +21,21 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [text, textError] = $(params.text).optional.string().$;
 	if (textError) return rej('invalid text param');
 
-	// Get 'include_user_ids' parameter
-	const [includeUserIds = [], includeUserIdsErr] = $(params.include_user_ids).optional.array('id').$;
-	if (includeUserIdsErr) return rej('invalid include_user_ids param');
+	// Get 'includeUserIds' parameter
+	const [includeUserIds = [], includeUserIdsErr] = $(params.includeUserIds).optional.array('id').$;
+	if (includeUserIdsErr) return rej('invalid includeUserIds param');
 
-	// Get 'exclude_user_ids' parameter
-	const [excludeUserIds = [], excludeUserIdsErr] = $(params.exclude_user_ids).optional.array('id').$;
-	if (excludeUserIdsErr) return rej('invalid exclude_user_ids param');
+	// Get 'excludeUserIds' parameter
+	const [excludeUserIds = [], excludeUserIdsErr] = $(params.excludeUserIds).optional.array('id').$;
+	if (excludeUserIdsErr) return rej('invalid excludeUserIds param');
 
-	// Get 'include_user_usernames' parameter
-	const [includeUserUsernames = [], includeUserUsernamesErr] = $(params.include_user_usernames).optional.array('string').$;
-	if (includeUserUsernamesErr) return rej('invalid include_user_usernames param');
+	// Get 'includeUserUsernames' parameter
+	const [includeUserUsernames = [], includeUserUsernamesErr] = $(params.includeUserUsernames).optional.array('string').$;
+	if (includeUserUsernamesErr) return rej('invalid includeUserUsernames param');
 
-	// Get 'exclude_user_usernames' parameter
-	const [excludeUserUsernames = [], excludeUserUsernamesErr] = $(params.exclude_user_usernames).optional.array('string').$;
-	if (excludeUserUsernamesErr) return rej('invalid exclude_user_usernames param');
+	// Get 'excludeUserUsernames' parameter
+	const [excludeUserUsernames = [], excludeUserUsernamesErr] = $(params.excludeUserUsernames).optional.array('string').$;
+	if (excludeUserUsernamesErr) return rej('invalid excludeUserUsernames param');
 
 	// Get 'following' parameter
 	const [following = null, followingErr] = $(params.following).optional.nullable.boolean().$;
@@ -61,13 +61,13 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [poll = null, pollErr] = $(params.poll).optional.nullable.boolean().$;
 	if (pollErr) return rej('invalid poll param');
 
-	// Get 'since_date' parameter
-	const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$;
-	if (sinceDateErr) throw 'invalid since_date param';
+	// Get 'sinceDate' parameter
+	const [sinceDate, sinceDateErr] = $(params.sinceDate).optional.number().$;
+	if (sinceDateErr) throw 'invalid sinceDate param';
 
-	// Get 'until_date' parameter
-	const [untilDate, untilDateErr] = $(params.until_date).optional.number().$;
-	if (untilDateErr) throw 'invalid until_date param';
+	// Get 'untilDate' parameter
+	const [untilDate, untilDateErr] = $(params.untilDate).optional.number().$;
+	if (untilDateErr) throw 'invalid untilDate param';
 
 	// Get 'offset' parameter
 	const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$;
@@ -81,7 +81,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	if (includeUserUsernames != null) {
 		const ids = (await Promise.all(includeUserUsernames.map(async (username) => {
 			const _user = await User.findOne({
-				username_lower: username.toLowerCase()
+				usernameLower: username.toLowerCase()
 			});
 			return _user ? _user._id : null;
 		}))).filter(id => id != null);
@@ -92,7 +92,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	if (excludeUserUsernames != null) {
 		const ids = (await Promise.all(excludeUserUsernames.map(async (username) => {
 			const _user = await User.findOne({
-				username_lower: username.toLowerCase()
+				usernameLower: username.toLowerCase()
 			});
 			return _user ? _user._id : null;
 		}))).filter(id => id != null);
@@ -143,13 +143,13 @@ async function search(
 
 	if (includeUserIds && includeUserIds.length != 0) {
 		push({
-			user_id: {
+			userId: {
 				$in: includeUserIds
 			}
 		});
 	} else if (excludeUserIds && excludeUserIds.length != 0) {
 		push({
-			user_id: {
+			userId: {
 				$nin: excludeUserIds
 			}
 		});
@@ -158,7 +158,7 @@ async function search(
 	if (following != null && me != null) {
 		const ids = await getFriends(me._id, false);
 		push({
-			user_id: following ? {
+			userId: following ? {
 				$in: ids
 			} : {
 				$nin: ids.concat(me._id)
@@ -168,45 +168,45 @@ async function search(
 
 	if (me != null) {
 		const mutes = await Mute.find({
-			muter_id: me._id,
-			deleted_at: { $exists: false }
+			muterId: me._id,
+			deletedAt: { $exists: false }
 		});
-		const mutedUserIds = mutes.map(m => m.mutee_id);
+		const mutedUserIds = mutes.map(m => m.muteeId);
 
 		switch (mute) {
 			case 'mute_all':
 				push({
-					user_id: {
+					userId: {
 						$nin: mutedUserIds
 					},
-					'_reply.user_id': {
+					'_reply.userId': {
 						$nin: mutedUserIds
 					},
-					'_repost.user_id': {
+					'_repost.userId': {
 						$nin: mutedUserIds
 					}
 				});
 				break;
 			case 'mute_related':
 				push({
-					'_reply.user_id': {
+					'_reply.userId': {
 						$nin: mutedUserIds
 					},
-					'_repost.user_id': {
+					'_repost.userId': {
 						$nin: mutedUserIds
 					}
 				});
 				break;
 			case 'mute_direct':
 				push({
-					user_id: {
+					userId: {
 						$nin: mutedUserIds
 					}
 				});
 				break;
 			case 'direct_only':
 				push({
-					user_id: {
+					userId: {
 						$in: mutedUserIds
 					}
 				});
@@ -214,11 +214,11 @@ async function search(
 			case 'related_only':
 				push({
 					$or: [{
-						'_reply.user_id': {
+						'_reply.userId': {
 							$in: mutedUserIds
 						}
 					}, {
-						'_repost.user_id': {
+						'_repost.userId': {
 							$in: mutedUserIds
 						}
 					}]
@@ -227,15 +227,15 @@ async function search(
 			case 'all_only':
 				push({
 					$or: [{
-						user_id: {
+						userId: {
 							$in: mutedUserIds
 						}
 					}, {
-						'_reply.user_id': {
+						'_reply.userId': {
 							$in: mutedUserIds
 						}
 					}, {
-						'_repost.user_id': {
+						'_repost.userId': {
 							$in: mutedUserIds
 						}
 					}]
@@ -247,7 +247,7 @@ async function search(
 	if (reply != null) {
 		if (reply) {
 			push({
-				reply_id: {
+				replyId: {
 					$exists: true,
 					$ne: null
 				}
@@ -255,11 +255,11 @@ async function search(
 		} else {
 			push({
 				$or: [{
-					reply_id: {
+					replyId: {
 						$exists: false
 					}
 				}, {
-					reply_id: null
+					replyId: null
 				}]
 			});
 		}
@@ -268,7 +268,7 @@ async function search(
 	if (repost != null) {
 		if (repost) {
 			push({
-				repost_id: {
+				repostId: {
 					$exists: true,
 					$ne: null
 				}
@@ -276,11 +276,11 @@ async function search(
 		} else {
 			push({
 				$or: [{
-					repost_id: {
+					repostId: {
 						$exists: false
 					}
 				}, {
-					repost_id: null
+					repostId: null
 				}]
 			});
 		}
@@ -289,7 +289,7 @@ async function search(
 	if (media != null) {
 		if (media) {
 			push({
-				media_ids: {
+				mediaIds: {
 					$exists: true,
 					$ne: null
 				}
@@ -297,11 +297,11 @@ async function search(
 		} else {
 			push({
 				$or: [{
-					media_ids: {
+					mediaIds: {
 						$exists: false
 					}
 				}, {
-					media_ids: null
+					mediaIds: null
 				}]
 			});
 		}
@@ -330,7 +330,7 @@ async function search(
 
 	if (sinceDate) {
 		push({
-			created_at: {
+			createdAt: {
 				$gt: new Date(sinceDate)
 			}
 		});
@@ -338,7 +338,7 @@ async function search(
 
 	if (untilDate) {
 		push({
-			created_at: {
+			createdAt: {
 				$lt: new Date(untilDate)
 			}
 		});
diff --git a/src/server/api/endpoints/posts/show.ts b/src/server/api/endpoints/posts/show.ts
index 383949059..bb4bcdb79 100644
--- a/src/server/api/endpoints/posts/show.ts
+++ b/src/server/api/endpoints/posts/show.ts
@@ -12,9 +12,9 @@ import Post, { pack } from '../../models/post';
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'post_id' parameter
-	const [postId, postIdErr] = $(params.post_id).id().$;
-	if (postIdErr) return rej('invalid post_id param');
+	// Get 'postId' parameter
+	const [postId, postIdErr] = $(params.postId).id().$;
+	if (postIdErr) return rej('invalid postId param');
 
 	// Get post
 	const post = await Post.findOne({
diff --git a/src/server/api/endpoints/posts/timeline.ts b/src/server/api/endpoints/posts/timeline.ts
index c41cfdb8b..a3e915f16 100644
--- a/src/server/api/endpoints/posts/timeline.ts
+++ b/src/server/api/endpoints/posts/timeline.ts
@@ -22,25 +22,25 @@ module.exports = async (params, user, app) => {
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) throw 'invalid limit param';
 
-	// Get 'since_id' parameter
-	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) throw 'invalid since_id param';
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) throw 'invalid sinceId param';
 
-	// Get 'until_id' parameter
-	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
-	if (untilIdErr) throw 'invalid until_id param';
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) throw 'invalid untilId param';
 
-	// Get 'since_date' parameter
-	const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$;
-	if (sinceDateErr) throw 'invalid since_date param';
+	// Get 'sinceDate' parameter
+	const [sinceDate, sinceDateErr] = $(params.sinceDate).optional.number().$;
+	if (sinceDateErr) throw 'invalid sinceDate param';
 
-	// Get 'until_date' parameter
-	const [untilDate, untilDateErr] = $(params.until_date).optional.number().$;
-	if (untilDateErr) throw 'invalid until_date param';
+	// Get 'untilDate' parameter
+	const [untilDate, untilDateErr] = $(params.untilDate).optional.number().$;
+	if (untilDateErr) throw 'invalid untilDate param';
 
-	// Check if only one of since_id, until_id, since_date, until_date specified
+	// Check if only one of sinceId, untilId, sinceDate, untilDate specified
 	if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) {
-		throw 'only one of since_id, until_id, since_date, until_date can be specified';
+		throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified';
 	}
 
 	const { followingIds, watchingChannelIds, mutedUserIds } = await rap({
@@ -49,17 +49,17 @@ module.exports = async (params, user, app) => {
 
 		// Watchしているチャンネルを取得
 		watchingChannelIds: ChannelWatching.find({
-			user_id: user._id,
+			userId: user._id,
 			// 削除されたドキュメントは除く
-			deleted_at: { $exists: false }
-		}).then(watches => watches.map(w => w.channel_id)),
+			deletedAt: { $exists: false }
+		}).then(watches => watches.map(w => w.channelId)),
 
 		// ミュートしているユーザーを取得
 		mutedUserIds: Mute.find({
-			muter_id: user._id,
+			muterId: user._id,
 			// 削除されたドキュメントは除く
-			deleted_at: { $exists: false }
-		}).then(ms => ms.map(m => m.mutee_id))
+			deletedAt: { $exists: false }
+		}).then(ms => ms.map(m => m.muteeId))
 	});
 
 	//#region Construct query
@@ -70,31 +70,31 @@ module.exports = async (params, user, app) => {
 	const query = {
 		$or: [{
 			// フォローしている人のタイムラインへの投稿
-			user_id: {
+			userId: {
 				$in: followingIds
 			},
 			// 「タイムラインへの」投稿に限定するためにチャンネルが指定されていないもののみに限る
 			$or: [{
-				channel_id: {
+				channelId: {
 					$exists: false
 				}
 			}, {
-				channel_id: null
+				channelId: null
 			}]
 		}, {
 			// Watchしているチャンネルへの投稿
-			channel_id: {
+			channelId: {
 				$in: watchingChannelIds
 			}
 		}],
 		// mute
-		user_id: {
+		userId: {
 			$nin: mutedUserIds
 		},
-		'_reply.user_id': {
+		'_reply.userId': {
 			$nin: mutedUserIds
 		},
-		'_repost.user_id': {
+		'_repost.userId': {
 			$nin: mutedUserIds
 		},
 	} as any;
@@ -110,11 +110,11 @@ module.exports = async (params, user, app) => {
 		};
 	} else if (sinceDate) {
 		sort._id = 1;
-		query.created_at = {
+		query.createdAt = {
 			$gt: new Date(sinceDate)
 		};
 	} else if (untilDate) {
-		query.created_at = {
+		query.createdAt = {
 			$lt: new Date(untilDate)
 		};
 	}
diff --git a/src/server/api/endpoints/posts/trend.ts b/src/server/api/endpoints/posts/trend.ts
index caded92bf..bc0c47fbc 100644
--- a/src/server/api/endpoints/posts/trend.ts
+++ b/src/server/api/endpoints/posts/trend.ts
@@ -38,24 +38,24 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	if (pollErr) return rej('invalid poll param');
 
 	const query = {
-		created_at: {
+		createdAt: {
 			$gte: new Date(Date.now() - ms('1days'))
 		},
-		repost_count: {
+		repostCount: {
 			$gt: 0
 		}
 	} as any;
 
 	if (reply != undefined) {
-		query.reply_id = reply ? { $exists: true, $ne: null } : null;
+		query.replyId = reply ? { $exists: true, $ne: null } : null;
 	}
 
 	if (repost != undefined) {
-		query.repost_id = repost ? { $exists: true, $ne: null } : null;
+		query.repostId = repost ? { $exists: true, $ne: null } : null;
 	}
 
 	if (media != undefined) {
-		query.media_ids = media ? { $exists: true, $ne: null } : null;
+		query.mediaIds = media ? { $exists: true, $ne: null } : null;
 	}
 
 	if (poll != undefined) {
@@ -68,7 +68,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 			limit: limit,
 			skip: offset,
 			sort: {
-				repost_count: -1,
+				repostCount: -1,
 				_id: -1
 			}
 		});
diff --git a/src/server/api/endpoints/stats.ts b/src/server/api/endpoints/stats.ts
index a6084cd17..719792d40 100644
--- a/src/server/api/endpoints/stats.ts
+++ b/src/server/api/endpoints/stats.ts
@@ -15,10 +15,10 @@ import User from '../models/user';
  *         schema:
  *           type: object
  *           properties:
- *             posts_count:
+ *             postsCount:
  *               description: count of all posts of misskey
  *               type: number
- *             users_count:
+ *             usersCount:
  *               description: count of all users of misskey
  *               type: number
  *
@@ -42,7 +42,7 @@ module.exports = params => new Promise(async (res, rej) => {
 		.count();
 
 	res({
-		posts_count: postsCount,
-		users_count: usersCount
+		postsCount: postsCount,
+		usersCount: usersCount
 	});
 });
diff --git a/src/server/api/endpoints/sw/register.ts b/src/server/api/endpoints/sw/register.ts
index 99406138d..1542e1dbe 100644
--- a/src/server/api/endpoints/sw/register.ts
+++ b/src/server/api/endpoints/sw/register.ts
@@ -28,11 +28,11 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re
 
 	// if already subscribed
 	const exist = await Subscription.findOne({
-		user_id: user._id,
+		userId: user._id,
 		endpoint: endpoint,
 		auth: auth,
 		publickey: publickey,
-		deleted_at: { $exists: false }
+		deletedAt: { $exists: false }
 	});
 
 	if (exist !== null) {
@@ -40,7 +40,7 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re
 	}
 
 	await Subscription.insert({
-		user_id: user._id,
+		userId: user._id,
 		endpoint: endpoint,
 		auth: auth,
 		publickey: publickey
diff --git a/src/server/api/endpoints/username/available.ts b/src/server/api/endpoints/username/available.ts
index aac7fadf5..f23cdbd85 100644
--- a/src/server/api/endpoints/username/available.ts
+++ b/src/server/api/endpoints/username/available.ts
@@ -20,7 +20,7 @@ module.exports = async (params) => new Promise(async (res, rej) => {
 	const exist = await User
 		.count({
 			host: null,
-			username_lower: username.toLowerCase()
+			usernameLower: username.toLowerCase()
 		}, {
 			limit: 1
 		});
diff --git a/src/server/api/endpoints/users.ts b/src/server/api/endpoints/users.ts
index 4acc13c28..393c3479c 100644
--- a/src/server/api/endpoints/users.ts
+++ b/src/server/api/endpoints/users.ts
@@ -29,11 +29,11 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	if (sort) {
 		if (sort == '+follower') {
 			_sort = {
-				followers_count: -1
+				followersCount: -1
 			};
 		} else if (sort == '-follower') {
 			_sort = {
-				followers_count: 1
+				followersCount: 1
 			};
 		}
 	} else {
diff --git a/src/server/api/endpoints/users/followers.ts b/src/server/api/endpoints/users/followers.ts
index b0fb83c68..fc09cfa2c 100644
--- a/src/server/api/endpoints/users/followers.ts
+++ b/src/server/api/endpoints/users/followers.ts
@@ -15,9 +15,9 @@ import getFriends from '../../common/get-friends';
  * @return {Promise<any>}
  */
 module.exports = (params, me) => new Promise(async (res, rej) => {
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [userId, userIdErr] = $(params.userId).id().$;
+	if (userIdErr) return rej('invalid userId param');
 
 	// Get 'iknow' parameter
 	const [iknow = false, iknowErr] = $(params.iknow).optional.boolean().$;
@@ -46,8 +46,8 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Construct query
 	const query = {
-		followee_id: user._id,
-		deleted_at: { $exists: false }
+		followeeId: user._id,
+		deletedAt: { $exists: false }
 	} as any;
 
 	// ログインしていてかつ iknow フラグがあるとき
@@ -55,7 +55,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 		// Get my friends
 		const myFriends = await getFriends(me._id);
 
-		query.follower_id = {
+		query.followerId = {
 			$in: myFriends
 		};
 	}
@@ -82,7 +82,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Serialize
 	const users = await Promise.all(following.map(async f =>
-		await pack(f.follower_id, me, { detail: true })));
+		await pack(f.followerId, me, { detail: true })));
 
 	// Response
 	res({
diff --git a/src/server/api/endpoints/users/following.ts b/src/server/api/endpoints/users/following.ts
index 8e88431e9..3387dab36 100644
--- a/src/server/api/endpoints/users/following.ts
+++ b/src/server/api/endpoints/users/following.ts
@@ -15,9 +15,9 @@ import getFriends from '../../common/get-friends';
  * @return {Promise<any>}
  */
 module.exports = (params, me) => new Promise(async (res, rej) => {
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [userId, userIdErr] = $(params.userId).id().$;
+	if (userIdErr) return rej('invalid userId param');
 
 	// Get 'iknow' parameter
 	const [iknow = false, iknowErr] = $(params.iknow).optional.boolean().$;
@@ -46,8 +46,8 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Construct query
 	const query = {
-		follower_id: user._id,
-		deleted_at: { $exists: false }
+		followerId: user._id,
+		deletedAt: { $exists: false }
 	} as any;
 
 	// ログインしていてかつ iknow フラグがあるとき
@@ -55,7 +55,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 		// Get my friends
 		const myFriends = await getFriends(me._id);
 
-		query.followee_id = {
+		query.followeeId = {
 			$in: myFriends
 		};
 	}
@@ -82,7 +82,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Serialize
 	const users = await Promise.all(following.map(async f =>
-		await pack(f.followee_id, me, { detail: true })));
+		await pack(f.followeeId, me, { detail: true })));
 
 	// Response
 	res({
diff --git a/src/server/api/endpoints/users/get_frequently_replied_users.ts b/src/server/api/endpoints/users/get_frequently_replied_users.ts
index 87f4f77a5..991c5555b 100644
--- a/src/server/api/endpoints/users/get_frequently_replied_users.ts
+++ b/src/server/api/endpoints/users/get_frequently_replied_users.ts
@@ -6,9 +6,9 @@ import Post from '../../models/post';
 import User, { pack } from '../../models/user';
 
 module.exports = (params, me) => new Promise(async (res, rej) => {
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [userId, userIdErr] = $(params.userId).id().$;
+	if (userIdErr) return rej('invalid userId param');
 
 	// Get 'limit' parameter
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
@@ -29,8 +29,8 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Fetch recent posts
 	const recentPosts = await Post.find({
-		user_id: user._id,
-		reply_id: {
+		userId: user._id,
+		replyId: {
 			$exists: true,
 			$ne: null
 		}
@@ -41,7 +41,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 		limit: 1000,
 		fields: {
 			_id: false,
-			reply_id: true
+			replyId: true
 		}
 	});
 
@@ -52,15 +52,15 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	const replyTargetPosts = await Post.find({
 		_id: {
-			$in: recentPosts.map(p => p.reply_id)
+			$in: recentPosts.map(p => p.replyId)
 		},
-		user_id: {
+		userId: {
 			$ne: user._id
 		}
 	}, {
 		fields: {
 			_id: false,
-			user_id: true
+			userId: true
 		}
 	});
 
@@ -68,7 +68,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Extract replies from recent posts
 	replyTargetPosts.forEach(post => {
-		const userId = post.user_id.toString();
+		const userId = post.userId.toString();
 		if (repliedUsers[userId]) {
 			repliedUsers[userId]++;
 		} else {
diff --git a/src/server/api/endpoints/users/posts.ts b/src/server/api/endpoints/users/posts.ts
index 3c84bf0d8..934690749 100644
--- a/src/server/api/endpoints/users/posts.ts
+++ b/src/server/api/endpoints/users/posts.ts
@@ -14,16 +14,16 @@ import User from '../../models/user';
  * @return {Promise<any>}
  */
 module.exports = (params, me) => new Promise(async (res, rej) => {
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).optional.id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [userId, userIdErr] = $(params.userId).optional.id().$;
+	if (userIdErr) return rej('invalid userId param');
 
 	// Get 'username' parameter
 	const [username, usernameErr] = $(params.username).optional.string().$;
 	if (usernameErr) return rej('invalid username param');
 
 	if (userId === undefined && username === undefined) {
-		return rej('user_id or pair of username and host is required');
+		return rej('userId or pair of username and host is required');
 	}
 
 	// Get 'host' parameter
@@ -31,45 +31,45 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	if (hostErr) return rej('invalid host param');
 
 	if (userId === undefined && host === undefined) {
-		return rej('user_id or pair of username and host is required');
+		return rej('userId or pair of username and host is required');
 	}
 
-	// Get 'include_replies' parameter
-	const [includeReplies = true, includeRepliesErr] = $(params.include_replies).optional.boolean().$;
-	if (includeRepliesErr) return rej('invalid include_replies param');
+	// Get 'includeReplies' parameter
+	const [includeReplies = true, includeRepliesErr] = $(params.includeReplies).optional.boolean().$;
+	if (includeRepliesErr) return rej('invalid includeReplies param');
 
-	// Get 'with_media' parameter
-	const [withMedia = false, withMediaErr] = $(params.with_media).optional.boolean().$;
-	if (withMediaErr) return rej('invalid with_media param');
+	// Get 'withMedia' parameter
+	const [withMedia = false, withMediaErr] = $(params.withMedia).optional.boolean().$;
+	if (withMediaErr) return rej('invalid withMedia param');
 
 	// Get 'limit' parameter
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
 	if (limitErr) return rej('invalid limit param');
 
-	// Get 'since_id' parameter
-	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) return rej('invalid since_id param');
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) return rej('invalid sinceId param');
 
-	// Get 'until_id' parameter
-	const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
-	if (untilIdErr) return rej('invalid until_id param');
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) return rej('invalid untilId param');
 
-	// Get 'since_date' parameter
-	const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$;
-	if (sinceDateErr) throw 'invalid since_date param';
+	// Get 'sinceDate' parameter
+	const [sinceDate, sinceDateErr] = $(params.sinceDate).optional.number().$;
+	if (sinceDateErr) throw 'invalid sinceDate param';
 
-	// Get 'until_date' parameter
-	const [untilDate, untilDateErr] = $(params.until_date).optional.number().$;
-	if (untilDateErr) throw 'invalid until_date param';
+	// Get 'untilDate' parameter
+	const [untilDate, untilDateErr] = $(params.untilDate).optional.number().$;
+	if (untilDateErr) throw 'invalid untilDate param';
 
-	// Check if only one of since_id, until_id, since_date, until_date specified
+	// Check if only one of sinceId, untilId, sinceDate, untilDate specified
 	if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) {
-		throw 'only one of since_id, until_id, since_date, until_date can be specified';
+		throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified';
 	}
 
 	const q = userId !== undefined
 		? { _id: userId }
-		: { username_lower: username.toLowerCase(), host_lower: getHostLower(host) } ;
+		: { usernameLower: username.toLowerCase(), hostLower: getHostLower(host) } ;
 
 	// Lookup user
 	const user = await User.findOne(q, {
@@ -88,7 +88,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	};
 
 	const query = {
-		user_id: user._id
+		userId: user._id
 	} as any;
 
 	if (sinceId) {
@@ -102,21 +102,21 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 		};
 	} else if (sinceDate) {
 		sort._id = 1;
-		query.created_at = {
+		query.createdAt = {
 			$gt: new Date(sinceDate)
 		};
 	} else if (untilDate) {
-		query.created_at = {
+		query.createdAt = {
 			$lt: new Date(untilDate)
 		};
 	}
 
 	if (!includeReplies) {
-		query.reply_id = null;
+		query.replyId = null;
 	}
 
 	if (withMedia) {
-		query.media_ids = {
+		query.mediaIds = {
 			$exists: true,
 			$ne: null
 		};
diff --git a/src/server/api/endpoints/users/recommendation.ts b/src/server/api/endpoints/users/recommendation.ts
index 45d90f422..c5297cdc5 100644
--- a/src/server/api/endpoints/users/recommendation.ts
+++ b/src/server/api/endpoints/users/recommendation.ts
@@ -32,7 +32,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 			},
 			$or: [
 				{
-					'account.last_used_at': {
+					'account.lastUsedAt': {
 						$gte: new Date(Date.now() - ms('7days'))
 					}
 				}, {
@@ -43,7 +43,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 			limit: limit,
 			skip: offset,
 			sort: {
-				followers_count: -1
+				followersCount: -1
 			}
 		});
 
diff --git a/src/server/api/endpoints/users/search.ts b/src/server/api/endpoints/users/search.ts
index 3c8157644..b03ed2f2f 100644
--- a/src/server/api/endpoints/users/search.ts
+++ b/src/server/api/endpoints/users/search.ts
@@ -41,7 +41,7 @@ async function byNative(res, rej, me, query, offset, max) {
 	const users = await User
 		.find({
 			$or: [{
-				username_lower: new RegExp(escapedQuery.replace('@', '').toLowerCase())
+				usernameLower: new RegExp(escapedQuery.replace('@', '').toLowerCase())
 			}, {
 				name: new RegExp(escapedQuery)
 			}]
diff --git a/src/server/api/endpoints/users/search_by_username.ts b/src/server/api/endpoints/users/search_by_username.ts
index 9c5e1905a..24e9c98e7 100644
--- a/src/server/api/endpoints/users/search_by_username.ts
+++ b/src/server/api/endpoints/users/search_by_username.ts
@@ -26,7 +26,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	const users = await User
 		.find({
-			username_lower: new RegExp(query.toLowerCase())
+			usernameLower: new RegExp(query.toLowerCase())
 		}, {
 			limit: limit,
 			skip: offset
diff --git a/src/server/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts
index 78df23f33..16411dddc 100644
--- a/src/server/api/endpoints/users/show.ts
+++ b/src/server/api/endpoints/users/show.ts
@@ -56,9 +56,9 @@ function webFingerAndVerify(query, verifier) {
 module.exports = (params, me) => new Promise(async (res, rej) => {
 	let user;
 
-	// Get 'user_id' parameter
-	const [userId, userIdErr] = $(params.user_id).optional.id().$;
-	if (userIdErr) return rej('invalid user_id param');
+	// Get 'userId' parameter
+	const [userId, userIdErr] = $(params.userId).optional.id().$;
+	if (userIdErr) return rej('invalid userId param');
 
 	// Get 'username' parameter
 	const [username, usernameErr] = $(params.username).optional.string().$;
@@ -69,25 +69,25 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	if (hostErr) return rej('invalid username param');
 
 	if (userId === undefined && typeof username !== 'string') {
-		return rej('user_id or pair of username and host is required');
+		return rej('userId or pair of username and host is required');
 	}
 
 	// Lookup user
 	if (typeof host === 'string') {
-		const username_lower = username.toLowerCase();
-		const host_lower_ascii = toASCII(host).toLowerCase();
-		const host_lower = toUnicode(host_lower_ascii);
+		const usernameLower = username.toLowerCase();
+		const hostLower_ascii = toASCII(host).toLowerCase();
+		const hostLower = toUnicode(hostLower_ascii);
 
-		user = await findUser({ username_lower, host_lower });
+		user = await findUser({ usernameLower, hostLower });
 
 		if (user === null) {
-			const acct_lower = `${username_lower}@${host_lower_ascii}`;
+			const acct_lower = `${usernameLower}@${hostLower_ascii}`;
 			let activityStreams;
 			let finger;
-			let followers_count;
-			let following_count;
+			let followersCount;
+			let followingCount;
 			let likes_count;
-			let posts_count;
+			let postsCount;
 
 			if (!validateUsername(username)) {
 				return rej('username validation failed');
@@ -122,7 +122,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 					activityStreams['@context'] === 'https://www.w3.org/ns/activitystreams') &&
 				activityStreams.type === 'Person' &&
 				typeof activityStreams.preferredUsername === 'string' &&
-				activityStreams.preferredUsername.toLowerCase() === username_lower &&
+				activityStreams.preferredUsername.toLowerCase() === usernameLower &&
 				isValidName(activityStreams.name) &&
 				isValidDescription(activityStreams.summary)
 			)) {
@@ -130,7 +130,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 			}
 
 			try {
-				[followers_count, following_count, likes_count, posts_count] = await Promise.all([
+				[followersCount, followingCount, likes_count, postsCount] = await Promise.all([
 					getCollectionCount(activityStreams.followers),
 					getCollectionCount(activityStreams.following),
 					getCollectionCount(activityStreams.liked),
@@ -145,21 +145,21 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 			// Create user
 			user = await User.insert({
-				avatar_id: null,
-				banner_id: null,
-				created_at: new Date(),
+				avatarId: null,
+				bannerId: null,
+				createdAt: new Date(),
 				description: summaryDOM.textContent,
-				followers_count,
-				following_count,
+				followersCount,
+				followingCount,
 				name: activityStreams.name,
-				posts_count,
+				postsCount,
 				likes_count,
 				liked_count: 0,
-				drive_capacity: 1073741824, // 1GB
+				driveCapacity: 1073741824, // 1GB
 				username: username,
-				username_lower,
+				usernameLower,
 				host: toUnicode(finger.subject.replace(/^.*?@/, '')),
-				host_lower,
+				hostLower,
 				account: {
 					uri: activityStreams.id,
 				},
@@ -182,18 +182,18 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 			User.update({ _id: user._id }, {
 				$set: {
-					avatar_id: icon._id,
-					banner_id: image._id,
+					avatarId: icon._id,
+					bannerId: image._id,
 				},
 			});
 
-			user.avatar_id = icon._id;
-			user.banner_id = icon._id;
+			user.avatarId = icon._id;
+			user.bannerId = icon._id;
 		}
 	} else {
 		const q = userId !== undefined
 			? { _id: userId }
-			: { username_lower: username.toLowerCase(), host: null };
+			: { usernameLower: username.toLowerCase(), host: null };
 
 		user = await findUser(q);
 
diff --git a/src/server/api/models/access-token.ts b/src/server/api/models/access-token.ts
index 2bf91f309..59bb09426 100644
--- a/src/server/api/models/access-token.ts
+++ b/src/server/api/models/access-token.ts
@@ -1,8 +1,16 @@
+import * as mongo from 'mongodb';
 import db from '../../../db/mongodb';
 
-const collection = db.get('access_tokens');
+const AccessToken = db.get<IAccessTokens>('accessTokens');
+AccessToken.createIndex('token');
+AccessToken.createIndex('hash');
+export default AccessToken;
 
-(collection as any).createIndex('token'); // fuck type definition
-(collection as any).createIndex('hash'); // fuck type definition
-
-export default collection as any; // fuck type definition
+export type IAccessTokens = {
+	_id: mongo.ObjectID;
+	createdAt: Date;
+	appId: mongo.ObjectID;
+	userId: mongo.ObjectID;
+	token: string;
+	hash: string;
+};
diff --git a/src/server/api/models/app.ts b/src/server/api/models/app.ts
index 17db82eca..3c17c50fd 100644
--- a/src/server/api/models/app.ts
+++ b/src/server/api/models/app.ts
@@ -5,16 +5,22 @@ import db from '../../../db/mongodb';
 import config from '../../../conf';
 
 const App = db.get<IApp>('apps');
-App.createIndex('name_id');
-App.createIndex('name_id_lower');
+App.createIndex('nameId');
+App.createIndex('nameIdLower');
 App.createIndex('secret');
 export default App;
 
 export type IApp = {
 	_id: mongo.ObjectID;
-	created_at: Date;
-	user_id: mongo.ObjectID;
+	createdAt: Date;
+	userId: mongo.ObjectID;
 	secret: string;
+	name: string;
+	nameId: string;
+	nameIdLower: string;
+	description: string;
+	permission: string;
+	callbackUrl: string;
 };
 
 export function isValidNameId(nameId: string): boolean {
@@ -70,27 +76,27 @@ export const pack = (
 	_app.id = _app._id;
 	delete _app._id;
 
-	delete _app.name_id_lower;
+	delete _app.nameIdLower;
 
 	// Visible by only owner
 	if (!opts.includeSecret) {
 		delete _app.secret;
 	}
 
-	_app.icon_url = _app.icon != null
+	_app.iconUrl = _app.icon != null
 		? `${config.drive_url}/${_app.icon}`
 		: `${config.drive_url}/app-default.jpg`;
 
 	if (me) {
 		// 既に連携しているか
 		const exist = await AccessToken.count({
-			app_id: _app.id,
-			user_id: me,
+			appId: _app.id,
+			userId: me,
 		}, {
 				limit: 1
 			});
 
-		_app.is_authorized = exist === 1;
+		_app.isAuthorized = exist === 1;
 	}
 
 	resolve(_app);
diff --git a/src/server/api/models/appdata.ts b/src/server/api/models/appdata.ts
deleted file mode 100644
index dda3c9893..000000000
--- a/src/server/api/models/appdata.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import db from '../../../db/mongodb';
-
-export default db.get('appdata') as any; // fuck type definition
diff --git a/src/server/api/models/auth-session.ts b/src/server/api/models/auth-session.ts
index a79d901df..2da40b1ea 100644
--- a/src/server/api/models/auth-session.ts
+++ b/src/server/api/models/auth-session.ts
@@ -3,11 +3,15 @@ import deepcopy = require('deepcopy');
 import db from '../../../db/mongodb';
 import { pack as packApp } from './app';
 
-const AuthSession = db.get('auth_sessions');
+const AuthSession = db.get<IAuthSession>('authSessions');
 export default AuthSession;
 
 export interface IAuthSession {
 	_id: mongo.ObjectID;
+	createdAt: Date;
+	appId: mongo.ObjectID;
+	userId: mongo.ObjectID;
+	token: string;
 }
 
 /**
@@ -24,7 +28,6 @@ export const pack = (
 	let _session: any;
 
 	// TODO: Populate session if it ID
-
 	_session = deepcopy(session);
 
 	// Me
@@ -39,7 +42,7 @@ export const pack = (
 	delete _session._id;
 
 	// Populate app
-	_session.app = await packApp(_session.app_id, me);
+	_session.app = await packApp(_session.appId, me);
 
 	resolve(_session);
 });
diff --git a/src/server/api/models/channel-watching.ts b/src/server/api/models/channel-watching.ts
index 4c6fae28d..a26b7edb9 100644
--- a/src/server/api/models/channel-watching.ts
+++ b/src/server/api/models/channel-watching.ts
@@ -1,3 +1,13 @@
+import * as mongo from 'mongodb';
 import db from '../../../db/mongodb';
 
-export default db.get('channel_watching') as any; // fuck type definition
+const ChannelWatching = db.get<IChannelWatching>('channelWatching');
+export default ChannelWatching;
+
+export interface IChannelWatching {
+	_id: mongo.ObjectID;
+	createdAt: Date;
+	deletedAt: Date;
+	channelId: mongo.ObjectID;
+	userId: mongo.ObjectID;
+}
diff --git a/src/server/api/models/channel.ts b/src/server/api/models/channel.ts
index 97999bd9e..9f94c5a8d 100644
--- a/src/server/api/models/channel.ts
+++ b/src/server/api/models/channel.ts
@@ -9,10 +9,11 @@ export default Channel;
 
 export type IChannel = {
 	_id: mongo.ObjectID;
-	created_at: Date;
+	createdAt: Date;
 	title: string;
-	user_id: mongo.ObjectID;
+	userId: mongo.ObjectID;
 	index: number;
+	watchingCount: number;
 };
 
 /**
@@ -47,7 +48,7 @@ export const pack = (
 	delete _channel._id;
 
 	// Remove needless properties
-	delete _channel.user_id;
+	delete _channel.userId;
 
 	// Me
 	const meId: mongo.ObjectID = me
@@ -61,12 +62,12 @@ export const pack = (
 	if (me) {
 		//#region Watchしているかどうか
 		const watch = await Watching.findOne({
-			user_id: meId,
-			channel_id: _channel.id,
-			deleted_at: { $exists: false }
+			userId: meId,
+			channelId: _channel.id,
+			deletedAt: { $exists: false }
 		});
 
-		_channel.is_watching = watch !== null;
+		_channel.isWatching = watch !== null;
 		//#endregion
 	}
 
diff --git a/src/server/api/models/drive-file.ts b/src/server/api/models/drive-file.ts
index 851a79a0e..04c9c54bd 100644
--- a/src/server/api/models/drive-file.ts
+++ b/src/server/api/models/drive-file.ts
@@ -4,14 +4,14 @@ import { pack as packFolder } from './drive-folder';
 import config from '../../../conf';
 import monkDb, { nativeDbConn } from '../../../db/mongodb';
 
-const DriveFile = monkDb.get<IDriveFile>('drive_files.files');
+const DriveFile = monkDb.get<IDriveFile>('driveFiles.files');
 
 export default DriveFile;
 
 const getGridFSBucket = async (): Promise<mongodb.GridFSBucket> => {
 	const db = await nativeDbConn();
 	const bucket = new mongodb.GridFSBucket(db, {
-		bucketName: 'drive_files'
+		bucketName: 'driveFiles'
 	});
 	return bucket;
 };
@@ -26,8 +26,8 @@ export type IDriveFile = {
 	contentType: string;
 	metadata: {
 		properties: any;
-		user_id: mongodb.ObjectID;
-		folder_id: mongodb.ObjectID;
+		userId: mongodb.ObjectID;
+		folderId: mongodb.ObjectID;
 	}
 };
 
@@ -79,7 +79,7 @@ export const pack = (
 	let _target: any = {};
 
 	_target.id = _file._id;
-	_target.created_at = _file.uploadDate;
+	_target.createdAt = _file.uploadDate;
 	_target.name = _file.filename;
 	_target.type = _file.contentType;
 	_target.datasize = _file.length;
@@ -92,9 +92,9 @@ export const pack = (
 	if (_target.properties == null) _target.properties = {};
 
 	if (opts.detail) {
-		if (_target.folder_id) {
+		if (_target.folderId) {
 			// Populate folder
-			_target.folder = await packFolder(_target.folder_id, {
+			_target.folder = await packFolder(_target.folderId, {
 				detail: true
 			});
 		}
diff --git a/src/server/api/models/drive-folder.ts b/src/server/api/models/drive-folder.ts
index 505556376..4ecafaa15 100644
--- a/src/server/api/models/drive-folder.ts
+++ b/src/server/api/models/drive-folder.ts
@@ -8,10 +8,10 @@ export default DriveFolder;
 
 export type IDriveFolder = {
 	_id: mongo.ObjectID;
-	created_at: Date;
+	createdAt: Date;
 	name: string;
-	user_id: mongo.ObjectID;
-	parent_id: mongo.ObjectID;
+	userId: mongo.ObjectID;
+	parentId: mongo.ObjectID;
 };
 
 export function isValidFolderName(name: string): boolean {
@@ -55,20 +55,20 @@ export const pack = (
 
 	if (opts.detail) {
 		const childFoldersCount = await DriveFolder.count({
-			parent_id: _folder.id
+			parentId: _folder.id
 		});
 
 		const childFilesCount = await DriveFile.count({
-			'metadata.folder_id': _folder.id
+			'metadata.folderId': _folder.id
 		});
 
-		_folder.folders_count = childFoldersCount;
-		_folder.files_count = childFilesCount;
+		_folder.foldersCount = childFoldersCount;
+		_folder.filesCount = childFilesCount;
 	}
 
-	if (opts.detail && _folder.parent_id) {
+	if (opts.detail && _folder.parentId) {
 		// Populate parent folder
-		_folder.parent = await pack(_folder.parent_id, {
+		_folder.parent = await pack(_folder.parentId, {
 			detail: true
 		});
 	}
diff --git a/src/server/api/models/drive-tag.ts b/src/server/api/models/drive-tag.ts
deleted file mode 100644
index d1c68365a..000000000
--- a/src/server/api/models/drive-tag.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import db from '../../../db/mongodb';
-
-export default db.get('drive_tags') as any; // fuck type definition
diff --git a/src/server/api/models/favorite.ts b/src/server/api/models/favorite.ts
index 314261764..5fb4db95a 100644
--- a/src/server/api/models/favorite.ts
+++ b/src/server/api/models/favorite.ts
@@ -1,3 +1,12 @@
+import * as mongo from 'mongodb';
 import db from '../../../db/mongodb';
 
-export default db.get('favorites') as any; // fuck type definition
+const Favorites = db.get<IFavorite>('favorites');
+export default Favorites;
+
+export type IFavorite = {
+	_id: mongo.ObjectID;
+	createdAt: Date;
+	userId: mongo.ObjectID;
+	postId: mongo.ObjectID;
+};
diff --git a/src/server/api/models/following.ts b/src/server/api/models/following.ts
index 92d7b6d31..552e94604 100644
--- a/src/server/api/models/following.ts
+++ b/src/server/api/models/following.ts
@@ -1,3 +1,13 @@
+import * as mongo from 'mongodb';
 import db from '../../../db/mongodb';
 
-export default db.get('following') as any; // fuck type definition
+const Following = db.get<IFollowing>('following');
+export default Following;
+
+export type IFollowing = {
+	_id: mongo.ObjectID;
+	createdAt: Date;
+	deletedAt: Date;
+	followeeId: mongo.ObjectID;
+	followerId: mongo.ObjectID;
+};
diff --git a/src/server/api/models/messaging-history.ts b/src/server/api/models/messaging-history.ts
index ea9f317ee..44a2adc31 100644
--- a/src/server/api/models/messaging-history.ts
+++ b/src/server/api/models/messaging-history.ts
@@ -1,3 +1,13 @@
+import * as mongo from 'mongodb';
 import db from '../../../db/mongodb';
 
-export default db.get('messaging_histories') as any; // fuck type definition
+const MessagingHistory = db.get<IMessagingHistory>('messagingHistories');
+export default MessagingHistory;
+
+export type IMessagingHistory = {
+	_id: mongo.ObjectID;
+	updatedAt: Date;
+	userId: mongo.ObjectID;
+	partnerId: mongo.ObjectID;
+	messageId: mongo.ObjectID;
+};
diff --git a/src/server/api/models/messaging-message.ts b/src/server/api/models/messaging-message.ts
index be484d635..d3a418c9a 100644
--- a/src/server/api/models/messaging-message.ts
+++ b/src/server/api/models/messaging-message.ts
@@ -5,16 +5,17 @@ import { pack as packFile } from './drive-file';
 import db from '../../../db/mongodb';
 import parse from '../common/text';
 
-const MessagingMessage = db.get<IMessagingMessage>('messaging_messages');
+const MessagingMessage = db.get<IMessagingMessage>('messagingMessages');
 export default MessagingMessage;
 
 export interface IMessagingMessage {
 	_id: mongo.ObjectID;
-	created_at: Date;
+	createdAt: Date;
 	text: string;
-	user_id: mongo.ObjectID;
-	recipient_id: mongo.ObjectID;
-	is_read: boolean;
+	userId: mongo.ObjectID;
+	recipientId: mongo.ObjectID;
+	isRead: boolean;
+	fileId: mongo.ObjectID;
 }
 
 export function isValidText(text: string): boolean {
@@ -65,16 +66,16 @@ export const pack = (
 	}
 
 	// Populate user
-	_message.user = await packUser(_message.user_id, me);
+	_message.user = await packUser(_message.userId, me);
 
-	if (_message.file_id) {
+	if (_message.fileId) {
 		// Populate file
-		_message.file = await packFile(_message.file_id);
+		_message.file = await packFile(_message.fileId);
 	}
 
 	if (opts.populateRecipient) {
 		// Populate recipient
-		_message.recipient = await packUser(_message.recipient_id, me);
+		_message.recipient = await packUser(_message.recipientId, me);
 	}
 
 	resolve(_message);
diff --git a/src/server/api/models/meta.ts b/src/server/api/models/meta.ts
index ee1ada18f..cad7f5096 100644
--- a/src/server/api/models/meta.ts
+++ b/src/server/api/models/meta.ts
@@ -1,7 +1,8 @@
 import db from '../../../db/mongodb';
 
-export default db.get('meta') as any; // fuck type definition
+const Meta = db.get<IMeta>('meta');
+export default Meta;
 
 export type IMeta = {
-	top_image: string;
+	broadcasts: any[];
 };
diff --git a/src/server/api/models/mute.ts b/src/server/api/models/mute.ts
index 02f652c30..e5385ade3 100644
--- a/src/server/api/models/mute.ts
+++ b/src/server/api/models/mute.ts
@@ -1,3 +1,13 @@
+import * as mongo from 'mongodb';
 import db from '../../../db/mongodb';
 
-export default db.get('mute') as any; // fuck type definition
+const Mute = db.get<IMute>('mute');
+export default Mute;
+
+export interface IMute {
+	_id: mongo.ObjectID;
+	createdAt: Date;
+	deletedAt: Date;
+	muterId: mongo.ObjectID;
+	muteeId: mongo.ObjectID;
+}
diff --git a/src/server/api/models/notification.ts b/src/server/api/models/notification.ts
index bcb25534d..237e2663f 100644
--- a/src/server/api/models/notification.ts
+++ b/src/server/api/models/notification.ts
@@ -9,7 +9,7 @@ export default Notification;
 
 export interface INotification {
 	_id: mongo.ObjectID;
-	created_at: Date;
+	createdAt: Date;
 
 	/**
 	 * 通知の受信者
@@ -19,7 +19,7 @@ export interface INotification {
 	/**
 	 * 通知の受信者
 	 */
-	notifiee_id: mongo.ObjectID;
+	notifieeId: mongo.ObjectID;
 
 	/**
 	 * イニシエータ(initiator)、Origin。通知を行う原因となったユーザー
@@ -29,7 +29,7 @@ export interface INotification {
 	/**
 	 * イニシエータ(initiator)、Origin。通知を行う原因となったユーザー
 	 */
-	notifier_id: mongo.ObjectID;
+	notifierId: mongo.ObjectID;
 
 	/**
 	 * 通知の種類。
@@ -46,7 +46,7 @@ export interface INotification {
 	/**
 	 * 通知が読まれたかどうか
 	 */
-	is_read: Boolean;
+	isRead: Boolean;
 }
 
 /**
@@ -75,15 +75,15 @@ export const pack = (notification: any) => new Promise<any>(async (resolve, reje
 	_notification.id = _notification._id;
 	delete _notification._id;
 
-	// Rename notifier_id to user_id
-	_notification.user_id = _notification.notifier_id;
-	delete _notification.notifier_id;
+	// Rename notifierId to userId
+	_notification.userId = _notification.notifierId;
+	delete _notification.notifierId;
 
-	const me = _notification.notifiee_id;
-	delete _notification.notifiee_id;
+	const me = _notification.notifieeId;
+	delete _notification.notifieeId;
 
 	// Populate notifier
-	_notification.user = await packUser(_notification.user_id, me);
+	_notification.user = await packUser(_notification.userId, me);
 
 	switch (_notification.type) {
 		case 'follow':
@@ -96,7 +96,7 @@ export const pack = (notification: any) => new Promise<any>(async (resolve, reje
 		case 'reaction':
 		case 'poll_vote':
 			// Populate post
-			_notification.post = await packPost(_notification.post_id, me);
+			_notification.post = await packPost(_notification.postId, me);
 			break;
 		default:
 			console.error(`Unknown type: ${_notification.type}`);
diff --git a/src/server/api/models/othello-game.ts b/src/server/api/models/othello-game.ts
index 97508e46d..ebe738815 100644
--- a/src/server/api/models/othello-game.ts
+++ b/src/server/api/models/othello-game.ts
@@ -3,17 +3,17 @@ import deepcopy = require('deepcopy');
 import db from '../../../db/mongodb';
 import { IUser, pack as packUser } from './user';
 
-const Game = db.get<IGame>('othello_games');
-export default Game;
+const OthelloGame = db.get<IOthelloGame>('othelloGames');
+export default OthelloGame;
 
-export interface IGame {
+export interface IOthelloGame {
 	_id: mongo.ObjectID;
-	created_at: Date;
-	started_at: Date;
-	user1_id: mongo.ObjectID;
-	user2_id: mongo.ObjectID;
-	user1_accepted: boolean;
-	user2_accepted: boolean;
+	createdAt: Date;
+	startedAt: Date;
+	user1Id: mongo.ObjectID;
+	user2Id: mongo.ObjectID;
+	user1Accepted: boolean;
+	user2Accepted: boolean;
 
 	/**
 	 * どちらのプレイヤーが先行(黒)か
@@ -22,9 +22,9 @@ export interface IGame {
 	 */
 	black: number;
 
-	is_started: boolean;
-	is_ended: boolean;
-	winner_id: mongo.ObjectID;
+	isStarted: boolean;
+	isEnded: boolean;
+	winnerId: mongo.ObjectID;
 	logs: Array<{
 		at: Date;
 		color: boolean;
@@ -33,9 +33,9 @@ export interface IGame {
 	settings: {
 		map: string[];
 		bw: string | number;
-		is_llotheo: boolean;
-		can_put_everywhere: boolean;
-		looped_board: boolean;
+		isLlotheo: boolean;
+		canPutEverywhere: boolean;
+		loopedBoard: boolean;
 	};
 	form1: any;
 	form2: any;
@@ -62,11 +62,11 @@ export const pack = (
 
 	// Populate the game if 'game' is ID
 	if (mongo.ObjectID.prototype.isPrototypeOf(game)) {
-		_game = await Game.findOne({
+		_game = await OthelloGame.findOne({
 			_id: game
 		});
 	} else if (typeof game === 'string') {
-		_game = await Game.findOne({
+		_game = await OthelloGame.findOne({
 			_id: new mongo.ObjectID(game)
 		});
 	} else {
@@ -97,10 +97,10 @@ export const pack = (
 	}
 
 	// Populate user
-	_game.user1 = await packUser(_game.user1_id, meId);
-	_game.user2 = await packUser(_game.user2_id, meId);
-	if (_game.winner_id) {
-		_game.winner = await packUser(_game.winner_id, meId);
+	_game.user1 = await packUser(_game.user1Id, meId);
+	_game.user2 = await packUser(_game.user2Id, meId);
+	if (_game.winnerId) {
+		_game.winner = await packUser(_game.winnerId, meId);
 	} else {
 		_game.winner = null;
 	}
diff --git a/src/server/api/models/othello-matching.ts b/src/server/api/models/othello-matching.ts
index 3c29e6a00..a294bd1ef 100644
--- a/src/server/api/models/othello-matching.ts
+++ b/src/server/api/models/othello-matching.ts
@@ -3,14 +3,14 @@ import deepcopy = require('deepcopy');
 import db from '../../../db/mongodb';
 import { IUser, pack as packUser } from './user';
 
-const Matching = db.get<IMatching>('othello_matchings');
+const Matching = db.get<IMatching>('othelloMatchings');
 export default Matching;
 
 export interface IMatching {
 	_id: mongo.ObjectID;
-	created_at: Date;
-	parent_id: mongo.ObjectID;
-	child_id: mongo.ObjectID;
+	createdAt: Date;
+	parentId: mongo.ObjectID;
+	childId: mongo.ObjectID;
 }
 
 /**
@@ -37,8 +37,8 @@ export const pack = (
 	delete _matching._id;
 
 	// Populate user
-	_matching.parent = await packUser(_matching.parent_id, meId);
-	_matching.child = await packUser(_matching.child_id, meId);
+	_matching.parent = await packUser(_matching.parentId, meId);
+	_matching.child = await packUser(_matching.childId, meId);
 
 	resolve(_matching);
 });
diff --git a/src/server/api/models/poll-vote.ts b/src/server/api/models/poll-vote.ts
index c6638ccf1..1cad95e5d 100644
--- a/src/server/api/models/poll-vote.ts
+++ b/src/server/api/models/poll-vote.ts
@@ -1,3 +1,17 @@
+<<<<<<< HEAD:src/server/api/models/poll-vote.ts
 import db from '../../../db/mongodb';
+=======
+import * as mongo from 'mongodb';
+import db from '../../db/mongodb';
+>>>>>>> refs/remotes/origin/master:src/api/models/poll-vote.ts
 
-export default db.get('poll_votes') as any; // fuck type definition
+const PollVote = db.get<IPollVote>('pollVotes');
+export default PollVote;
+
+export interface IPollVote {
+	_id: mongo.ObjectID;
+	createdAt: Date;
+	userId: mongo.ObjectID;
+	postId: mongo.ObjectID;
+	choice: number;
+}
diff --git a/src/server/api/models/post-reaction.ts b/src/server/api/models/post-reaction.ts
index 5cd122d76..f9a3f91c2 100644
--- a/src/server/api/models/post-reaction.ts
+++ b/src/server/api/models/post-reaction.ts
@@ -4,13 +4,15 @@ import db from '../../../db/mongodb';
 import Reaction from './post-reaction';
 import { pack as packUser } from './user';
 
-const PostReaction = db.get<IPostReaction>('post_reactions');
+const PostReaction = db.get<IPostReaction>('postReactions');
 export default PostReaction;
 
 export interface IPostReaction {
 	_id: mongo.ObjectID;
-	created_at: Date;
-	deleted_at: Date;
+	createdAt: Date;
+	deletedAt: Date;
+	postId: mongo.ObjectID;
+	userId: mongo.ObjectID;
 	reaction: string;
 }
 
@@ -45,7 +47,7 @@ export const pack = (
 	delete _reaction._id;
 
 	// Populate user
-	_reaction.user = await packUser(_reaction.user_id, me);
+	_reaction.user = await packUser(_reaction.userId, me);
 
 	resolve(_reaction);
 });
diff --git a/src/server/api/models/post-watching.ts b/src/server/api/models/post-watching.ts
index 9a4163c8d..abd632249 100644
--- a/src/server/api/models/post-watching.ts
+++ b/src/server/api/models/post-watching.ts
@@ -1,3 +1,12 @@
+import * as mongo from 'mongodb';
 import db from '../../../db/mongodb';
 
-export default db.get('post_watching') as any; // fuck type definition
+const PostWatching = db.get<IPostWatching>('postWatching');
+export default PostWatching;
+
+export interface IPostWatching {
+	_id: mongo.ObjectID;
+	createdAt: Date;
+	userId: mongo.ObjectID;
+	postId: mongo.ObjectID;
+}
diff --git a/src/server/api/models/post.ts b/src/server/api/models/post.ts
index 3f648e08c..0317cff3f 100644
--- a/src/server/api/models/post.ts
+++ b/src/server/api/models/post.ts
@@ -20,18 +20,20 @@ export function isValidText(text: string): boolean {
 
 export type IPost = {
 	_id: mongo.ObjectID;
-	channel_id: mongo.ObjectID;
-	created_at: Date;
-	media_ids: mongo.ObjectID[];
-	reply_id: mongo.ObjectID;
-	repost_id: mongo.ObjectID;
+	channelId: mongo.ObjectID;
+	createdAt: Date;
+	mediaIds: mongo.ObjectID[];
+	replyId: mongo.ObjectID;
+	repostId: mongo.ObjectID;
 	poll: any; // todo
 	text: string;
-	user_id: mongo.ObjectID;
-	app_id: mongo.ObjectID;
-	category: string;
-	is_category_verified: boolean;
-	via_mobile: boolean;
+	userId: mongo.ObjectID;
+	appId: mongo.ObjectID;
+	viaMobile: boolean;
+	repostCount: number;
+	repliesCount: number;
+	reactionCounts: any;
+	mentions: mongo.ObjectID[];
 	geo: {
 		latitude: number;
 		longitude: number;
@@ -102,21 +104,21 @@ export const pack = async (
 	}
 
 	// Populate user
-	_post.user = packUser(_post.user_id, meId);
+	_post.user = packUser(_post.userId, meId);
 
 	// Populate app
-	if (_post.app_id) {
-		_post.app = packApp(_post.app_id);
+	if (_post.appId) {
+		_post.app = packApp(_post.appId);
 	}
 
 	// Populate channel
-	if (_post.channel_id) {
-		_post.channel = packChannel(_post.channel_id);
+	if (_post.channelId) {
+		_post.channel = packChannel(_post.channelId);
 	}
 
 	// Populate media
-	if (_post.media_ids) {
-		_post.media = Promise.all(_post.media_ids.map(fileId =>
+	if (_post.mediaIds) {
+		_post.media = Promise.all(_post.mediaIds.map(fileId =>
 			packFile(fileId)
 		));
 	}
@@ -126,7 +128,7 @@ export const pack = async (
 		// Get previous post info
 		_post.prev = (async () => {
 			const prev = await Post.findOne({
-				user_id: _post.user_id,
+				userId: _post.userId,
 				_id: {
 					$lt: id
 				}
@@ -144,7 +146,7 @@ export const pack = async (
 		// Get next post info
 		_post.next = (async () => {
 			const next = await Post.findOne({
-				user_id: _post.user_id,
+				userId: _post.userId,
 				_id: {
 					$gt: id
 				}
@@ -159,16 +161,16 @@ export const pack = async (
 			return next ? next._id : null;
 		})();
 
-		if (_post.reply_id) {
+		if (_post.replyId) {
 			// Populate reply to post
-			_post.reply = pack(_post.reply_id, meId, {
+			_post.reply = pack(_post.replyId, meId, {
 				detail: false
 			});
 		}
 
-		if (_post.repost_id) {
+		if (_post.repostId) {
 			// Populate repost
-			_post.repost = pack(_post.repost_id, meId, {
+			_post.repost = pack(_post.repostId, meId, {
 				detail: _post.text == null
 			});
 		}
@@ -178,15 +180,15 @@ export const pack = async (
 			_post.poll = (async (poll) => {
 				const vote = await Vote
 					.findOne({
-						user_id: meId,
-						post_id: id
+						userId: meId,
+						postId: id
 					});
 
 				if (vote != null) {
 					const myChoice = poll.choices
 						.filter(c => c.id == vote.choice)[0];
 
-					myChoice.is_voted = true;
+					myChoice.isVoted = true;
 				}
 
 				return poll;
@@ -195,12 +197,12 @@ export const pack = async (
 
 		// Fetch my reaction
 		if (meId) {
-			_post.my_reaction = (async () => {
+			_post.myReaction = (async () => {
 				const reaction = await Reaction
 					.findOne({
-						user_id: meId,
-						post_id: id,
-						deleted_at: { $exists: false }
+						userId: meId,
+						postId: id,
+						deletedAt: { $exists: false }
 					});
 
 				if (reaction) {
diff --git a/src/server/api/models/signin.ts b/src/server/api/models/signin.ts
index 5cffb3c31..bec635947 100644
--- a/src/server/api/models/signin.ts
+++ b/src/server/api/models/signin.ts
@@ -7,6 +7,11 @@ export default Signin;
 
 export interface ISignin {
 	_id: mongo.ObjectID;
+	createdAt: Date;
+	userId: mongo.ObjectID;
+	ip: string;
+	headers: any;
+	success: boolean;
 }
 
 /**
diff --git a/src/server/api/models/sw-subscription.ts b/src/server/api/models/sw-subscription.ts
index 4506a982f..d3bbd75a6 100644
--- a/src/server/api/models/sw-subscription.ts
+++ b/src/server/api/models/sw-subscription.ts
@@ -1,3 +1,13 @@
+import * as mongo from 'mongodb';
 import db from '../../../db/mongodb';
 
-export default db.get('sw_subscriptions') as any; // fuck type definition
+const SwSubscription = db.get<ISwSubscription>('swSubscriptions');
+export default SwSubscription;
+
+export interface ISwSubscription {
+	_id: mongo.ObjectID;
+	userId: mongo.ObjectID;
+	endpoint: string;
+	auth: string;
+	publickey: string;
+}
diff --git a/src/server/api/models/user.ts b/src/server/api/models/user.ts
index 8e7d50baa..419ad5397 100644
--- a/src/server/api/models/user.ts
+++ b/src/server/api/models/user.ts
@@ -46,25 +46,26 @@ export type ILocalAccount = {
 	password: string;
 	token: string;
 	twitter: {
-		access_token: string;
-		access_token_secret: string;
-		user_id: string;
-		screen_name: string;
+		accessToken: string;
+		accessTokenSecret: string;
+		userId: string;
+		screenName: string;
 	};
 	line: {
-		user_id: string;
+		userId: string;
 	};
 	profile: {
 		location: string;
 		birthday: string; // 'YYYY-MM-DD'
 		tags: string[];
 	};
-	last_used_at: Date;
-	is_bot: boolean;
-	is_pro: boolean;
-	two_factor_secret: string;
-	two_factor_enabled: boolean;
-	client_settings: any;
+	lastUsedAt: Date;
+	isBot: boolean;
+	isPro: boolean;
+	twoFactorSecret: string;
+	twoFactorEnabled: boolean;
+	twoFactorTempSecret: string;
+	clientSettings: any;
 	settings: any;
 };
 
@@ -74,33 +75,33 @@ export type IRemoteAccount = {
 
 export type IUser = {
 	_id: mongo.ObjectID;
-	created_at: Date;
-	deleted_at: Date;
-	followers_count: number;
-	following_count: number;
+	createdAt: Date;
+	deletedAt: Date;
+	followersCount: number;
+	followingCount: number;
 	name: string;
-	posts_count: number;
-	drive_capacity: number;
+	postsCount: number;
+	driveCapacity: number;
 	username: string;
-	username_lower: string;
-	avatar_id: mongo.ObjectID;
-	banner_id: mongo.ObjectID;
+	usernameLower: string;
+	avatarId: mongo.ObjectID;
+	bannerId: mongo.ObjectID;
 	data: any;
 	description: string;
-	latest_post: IPost;
-	pinned_post_id: mongo.ObjectID;
-	is_suspended: boolean;
+	latestPost: IPost;
+	pinnedPostId: mongo.ObjectID;
+	isSuspended: boolean;
 	keywords: string[];
 	host: string;
-	host_lower: string;
+	hostLower: string;
 	account: ILocalAccount | IRemoteAccount;
 };
 
 export function init(user): IUser {
 	user._id = new mongo.ObjectID(user._id);
-	user.avatar_id = new mongo.ObjectID(user.avatar_id);
-	user.banner_id = new mongo.ObjectID(user.banner_id);
-	user.pinned_post_id = new mongo.ObjectID(user.pinned_post_id);
+	user.avatarId = new mongo.ObjectID(user.avatarId);
+	user.bannerId = new mongo.ObjectID(user.bannerId);
+	user.pinnedPostId = new mongo.ObjectID(user.pinnedPostId);
 	return user;
 }
 
@@ -131,7 +132,7 @@ export const pack = (
 	const fields = opts.detail ? {
 	} : {
 		'account.settings': false,
-		'account.client_settings': false,
+		'account.clientSettings': false,
 		'account.profile': false,
 		'account.keywords': false,
 		'account.domains': false
@@ -166,19 +167,19 @@ export const pack = (
 	delete _user._id;
 
 	// Remove needless properties
-	delete _user.latest_post;
+	delete _user.latestPost;
 
 	if (!_user.host) {
 		// Remove private properties
 		delete _user.account.keypair;
 		delete _user.account.password;
 		delete _user.account.token;
-		delete _user.account.two_factor_temp_secret;
-		delete _user.account.two_factor_secret;
-		delete _user.username_lower;
+		delete _user.account.twoFactorTempSecret;
+		delete _user.account.twoFactorSecret;
+		delete _user.usernameLower;
 		if (_user.account.twitter) {
-			delete _user.account.twitter.access_token;
-			delete _user.account.twitter.access_token_secret;
+			delete _user.account.twitter.accessToken;
+			delete _user.account.twitter.accessTokenSecret;
 		}
 		delete _user.account.line;
 
@@ -186,65 +187,65 @@ export const pack = (
 		if (!opts.includeSecrets) {
 			delete _user.account.email;
 			delete _user.account.settings;
-			delete _user.account.client_settings;
+			delete _user.account.clientSettings;
 		}
 
 		if (!opts.detail) {
-			delete _user.account.two_factor_enabled;
+			delete _user.account.twoFactorEnabled;
 		}
 	}
 
-	_user.avatar_url = _user.avatar_id != null
-		? `${config.drive_url}/${_user.avatar_id}`
+	_user.avatarUrl = _user.avatarId != null
+		? `${config.drive_url}/${_user.avatarId}`
 		: `${config.drive_url}/default-avatar.jpg`;
 
-	_user.banner_url = _user.banner_id != null
-		? `${config.drive_url}/${_user.banner_id}`
+	_user.bannerUrl = _user.bannerId != null
+		? `${config.drive_url}/${_user.bannerId}`
 		: null;
 
 	if (!meId || !meId.equals(_user.id) || !opts.detail) {
-		delete _user.avatar_id;
-		delete _user.banner_id;
+		delete _user.avatarId;
+		delete _user.bannerId;
 
-		delete _user.drive_capacity;
+		delete _user.driveCapacity;
 	}
 
 	if (meId && !meId.equals(_user.id)) {
 		// Whether the user is following
-		_user.is_following = (async () => {
+		_user.isFollowing = (async () => {
 			const follow = await Following.findOne({
-				follower_id: meId,
-				followee_id: _user.id,
-				deleted_at: { $exists: false }
+				followerId: meId,
+				followeeId: _user.id,
+				deletedAt: { $exists: false }
 			});
 			return follow !== null;
 		})();
 
 		// Whether the user is followed
-		_user.is_followed = (async () => {
+		_user.isFollowed = (async () => {
 			const follow2 = await Following.findOne({
-				follower_id: _user.id,
-				followee_id: meId,
-				deleted_at: { $exists: false }
+				followerId: _user.id,
+				followeeId: meId,
+				deletedAt: { $exists: false }
 			});
 			return follow2 !== null;
 		})();
 
 		// Whether the user is muted
-		_user.is_muted = (async () => {
+		_user.isMuted = (async () => {
 			const mute = await Mute.findOne({
-				muter_id: meId,
-				mutee_id: _user.id,
-				deleted_at: { $exists: false }
+				muterId: meId,
+				muteeId: _user.id,
+				deletedAt: { $exists: false }
 			});
 			return mute !== null;
 		})();
 	}
 
 	if (opts.detail) {
-		if (_user.pinned_post_id) {
+		if (_user.pinnedPostId) {
 			// Populate pinned post
-			_user.pinned_post = packPost(_user.pinned_post_id, meId, {
+			_user.pinnedPost = packPost(_user.pinnedPostId, meId, {
 				detail: true
 			});
 		}
@@ -253,17 +254,17 @@ export const pack = (
 			const myFollowingIds = await getFriends(meId);
 
 			// Get following you know count
-			_user.following_you_know_count = Following.count({
-				followee_id: { $in: myFollowingIds },
-				follower_id: _user.id,
-				deleted_at: { $exists: false }
+			_user.followingYouKnowCount = Following.count({
+				followeeId: { $in: myFollowingIds },
+				followerId: _user.id,
+				deletedAt: { $exists: false }
 			});
 
 			// Get followers you know count
-			_user.followers_you_know_count = Following.count({
-				followee_id: _user.id,
-				follower_id: { $in: myFollowingIds },
-				deleted_at: { $exists: false }
+			_user.followersYouKnowCount = Following.count({
+				followeeId: _user.id,
+				followerId: { $in: myFollowingIds },
+				deletedAt: { $exists: false }
 			});
 		}
 	}
@@ -322,7 +323,7 @@ export const packForAp = (
 		"name": _user.name,
 		"summary": _user.description,
 		"icon": [
-			`${config.drive_url}/${_user.avatar_id}`
+			`${config.drive_url}/${_user.avatarId}`
 		]
 	});
 });
diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts
index bbc990899..4b60f4c75 100644
--- a/src/server/api/private/signin.ts
+++ b/src/server/api/private/signin.ts
@@ -32,7 +32,7 @@ export default async (req: express.Request, res: express.Response) => {
 
 	// Fetch user
 	const user: IUser = await User.findOne({
-		username_lower: username.toLowerCase(),
+		usernameLower: username.toLowerCase(),
 		host: null
 	}, {
 		fields: {
@@ -54,9 +54,9 @@ export default async (req: express.Request, res: express.Response) => {
 	const same = await bcrypt.compare(password, account.password);
 
 	if (same) {
-		if (account.two_factor_enabled) {
+		if (account.twoFactorEnabled) {
 			const verified = (speakeasy as any).totp.verify({
-				secret: account.two_factor_secret,
+				secret: account.twoFactorSecret,
 				encoding: 'base32',
 				token: token
 			});
@@ -79,8 +79,8 @@ export default async (req: express.Request, res: express.Response) => {
 
 	// Append signin history
 	const record = await Signin.insert({
-		created_at: new Date(),
-		user_id: user._id,
+		createdAt: new Date(),
+		userId: user._id,
 		ip: req.ip,
 		headers: req.headers,
 		success: same
diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts
index 9f5539331..cad9752c4 100644
--- a/src/server/api/private/signup.ts
+++ b/src/server/api/private/signup.ts
@@ -64,7 +64,7 @@ export default async (req: express.Request, res: express.Response) => {
 	// Fetch exist user that same username
 	const usernameExist = await User
 		.count({
-			username_lower: username.toLowerCase(),
+			usernameLower: username.toLowerCase(),
 			host: null
 		}, {
 			limit: 1
@@ -107,21 +107,19 @@ export default async (req: express.Request, res: express.Response) => {
 
 	// Create account
 	const account: IUser = await User.insert({
-		avatar_id: null,
-		banner_id: null,
-		created_at: new Date(),
+		avatarId: null,
+		bannerId: null,
+		createdAt: new Date(),
 		description: null,
-		followers_count: 0,
-		following_count: 0,
+		followersCount: 0,
+		followingCount: 0,
 		name: name,
-		posts_count: 0,
-		likes_count: 0,
-		liked_count: 0,
-		drive_capacity: 1073741824, // 1GB
+		postsCount: 0,
+		driveCapacity: 1073741824, // 1GB
 		username: username,
-		username_lower: username.toLowerCase(),
+		usernameLower: username.toLowerCase(),
 		host: null,
-		host_lower: null,
+		hostLower: null,
 		account: {
 			keypair: generateKeypair(),
 			token: secret,
@@ -139,9 +137,9 @@ export default async (req: express.Request, res: express.Response) => {
 				weight: null
 			},
 			settings: {
-				auto_watch: true
+				autoWatch: true
 			},
-			client_settings: {
+			clientSettings: {
 				home: homeData
 			}
 		}
diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts
index a33d35975..98732e6b8 100644
--- a/src/server/api/service/github.ts
+++ b/src/server/api/service/github.ts
@@ -9,7 +9,7 @@ module.exports = async (app: express.Application) => {
 	if (config.github_bot == null) return;
 
 	const bot = await User.findOne({
-		username_lower: config.github_bot.username.toLowerCase()
+		usernameLower: config.github_bot.username.toLowerCase()
 	});
 
 	if (bot == null) {
diff --git a/src/server/api/service/twitter.ts b/src/server/api/service/twitter.ts
index 861f63ed6..bdbedc864 100644
--- a/src/server/api/service/twitter.ts
+++ b/src/server/api/service/twitter.ts
@@ -128,7 +128,7 @@ module.exports = (app: express.Application) => {
 
 				const user = await User.findOne({
 					host: null,
-					'account.twitter.user_id': result.userId
+					'account.twitter.userId': result.userId
 				});
 
 				if (user == null) {
@@ -155,10 +155,10 @@ module.exports = (app: express.Application) => {
 				}, {
 					$set: {
 						'account.twitter': {
-							access_token: result.accessToken,
-							access_token_secret: result.accessTokenSecret,
-							user_id: result.userId,
-							screen_name: result.screenName
+							accessToken: result.accessToken,
+							accessTokenSecret: result.accessTokenSecret,
+							userId: result.userId,
+							screenName: result.screenName
 						}
 					}
 				});
diff --git a/src/server/api/stream/home.ts b/src/server/api/stream/home.ts
index 1ef0f33b4..291be0824 100644
--- a/src/server/api/stream/home.ts
+++ b/src/server/api/stream/home.ts
@@ -14,10 +14,10 @@ export default async function(request: websocket.request, connection: websocket.
 	subscriber.subscribe(`misskey:user-stream:${user._id}`);
 
 	const mute = await Mute.find({
-		muter_id: user._id,
-		deleted_at: { $exists: false }
+		muterId: user._id,
+		deletedAt: { $exists: false }
 	});
-	const mutedUserIds = mute.map(m => m.mutee_id.toString());
+	const mutedUserIds = mute.map(m => m.muteeId.toString());
 
 	subscriber.on('message', async (channel, data) => {
 		switch (channel.split(':')[1]) {
@@ -26,17 +26,17 @@ export default async function(request: websocket.request, connection: websocket.
 					const x = JSON.parse(data);
 
 					if (x.type == 'post') {
-						if (mutedUserIds.indexOf(x.body.user_id) != -1) {
+						if (mutedUserIds.indexOf(x.body.userId) != -1) {
 							return;
 						}
-						if (x.body.reply != null && mutedUserIds.indexOf(x.body.reply.user_id) != -1) {
+						if (x.body.reply != null && mutedUserIds.indexOf(x.body.reply.userId) != -1) {
 							return;
 						}
-						if (x.body.repost != null && mutedUserIds.indexOf(x.body.repost.user_id) != -1) {
+						if (x.body.repost != null && mutedUserIds.indexOf(x.body.repost.userId) != -1) {
 							return;
 						}
 					} else if (x.type == 'notification') {
-						if (mutedUserIds.indexOf(x.body.user_id) != -1) {
+						if (mutedUserIds.indexOf(x.body.userId) != -1) {
 							return;
 						}
 					}
@@ -74,7 +74,7 @@ export default async function(request: websocket.request, connection: websocket.
 				// Update lastUsedAt
 				User.update({ _id: user._id }, {
 					$set: {
-						'account.last_used_at': new Date()
+						'account.lastUsedAt': new Date()
 					}
 				});
 				break;
diff --git a/src/server/api/stream/othello-game.ts b/src/server/api/stream/othello-game.ts
index 1c846f27a..e48d93cdd 100644
--- a/src/server/api/stream/othello-game.ts
+++ b/src/server/api/stream/othello-game.ts
@@ -1,7 +1,7 @@
 import * as websocket from 'websocket';
 import * as redis from 'redis';
 import * as CRC32 from 'crc-32';
-import Game, { pack } from '../models/othello-game';
+import OthelloGame, { pack } from '../models/othello-game';
 import { publishOthelloGameStream } from '../event';
 import Othello from '../../common/othello/core';
 import * as maps from '../../common/othello/maps';
@@ -60,14 +60,14 @@ export default function(request: websocket.request, connection: websocket.connec
 	});
 
 	async function updateSettings(settings) {
-		const game = await Game.findOne({ _id: gameId });
+		const game = await OthelloGame.findOne({ _id: gameId });
 
-		if (game.is_started) return;
-		if (!game.user1_id.equals(user._id) && !game.user2_id.equals(user._id)) return;
-		if (game.user1_id.equals(user._id) && game.user1_accepted) return;
-		if (game.user2_id.equals(user._id) && game.user2_accepted) return;
+		if (game.isStarted) return;
+		if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return;
+		if (game.user1Id.equals(user._id) && game.user1Accepted) return;
+		if (game.user2Id.equals(user._id) && game.user2Accepted) return;
 
-		await Game.update({ _id: gameId }, {
+		await OthelloGame.update({ _id: gameId }, {
 			$set: {
 				settings
 			}
@@ -77,34 +77,34 @@ export default function(request: websocket.request, connection: websocket.connec
 	}
 
 	async function initForm(form) {
-		const game = await Game.findOne({ _id: gameId });
+		const game = await OthelloGame.findOne({ _id: gameId });
 
-		if (game.is_started) return;
-		if (!game.user1_id.equals(user._id) && !game.user2_id.equals(user._id)) return;
+		if (game.isStarted) return;
+		if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return;
 
-		const set = game.user1_id.equals(user._id) ? {
+		const set = game.user1Id.equals(user._id) ? {
 			form1: form
 		} : {
 			form2: form
 		};
 
-		await Game.update({ _id: gameId }, {
+		await OthelloGame.update({ _id: gameId }, {
 			$set: set
 		});
 
 		publishOthelloGameStream(gameId, 'init-form', {
-			user_id: user._id,
+			userId: user._id,
 			form
 		});
 	}
 
 	async function updateForm(id, value) {
-		const game = await Game.findOne({ _id: gameId });
+		const game = await OthelloGame.findOne({ _id: gameId });
 
-		if (game.is_started) return;
-		if (!game.user1_id.equals(user._id) && !game.user2_id.equals(user._id)) return;
+		if (game.isStarted) return;
+		if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return;
 
-		const form = game.user1_id.equals(user._id) ? game.form2 : game.form1;
+		const form = game.user1Id.equals(user._id) ? game.form2 : game.form1;
 
 		const item = form.find(i => i.id == id);
 
@@ -112,18 +112,18 @@ export default function(request: websocket.request, connection: websocket.connec
 
 		item.value = value;
 
-		const set = game.user1_id.equals(user._id) ? {
+		const set = game.user1Id.equals(user._id) ? {
 			form2: form
 		} : {
 			form1: form
 		};
 
-		await Game.update({ _id: gameId }, {
+		await OthelloGame.update({ _id: gameId }, {
 			$set: set
 		});
 
 		publishOthelloGameStream(gameId, 'update-form', {
-			user_id: user._id,
+			userId: user._id,
 			id,
 			value
 		});
@@ -132,44 +132,44 @@ export default function(request: websocket.request, connection: websocket.connec
 	async function message(message) {
 		message.id = Math.random();
 		publishOthelloGameStream(gameId, 'message', {
-			user_id: user._id,
+			userId: user._id,
 			message
 		});
 	}
 
 	async function accept(accept: boolean) {
-		const game = await Game.findOne({ _id: gameId });
+		const game = await OthelloGame.findOne({ _id: gameId });
 
-		if (game.is_started) return;
+		if (game.isStarted) return;
 
 		let bothAccepted = false;
 
-		if (game.user1_id.equals(user._id)) {
-			await Game.update({ _id: gameId }, {
+		if (game.user1Id.equals(user._id)) {
+			await OthelloGame.update({ _id: gameId }, {
 				$set: {
-					user1_accepted: accept
+					user1Accepted: accept
 				}
 			});
 
 			publishOthelloGameStream(gameId, 'change-accepts', {
 				user1: accept,
-				user2: game.user2_accepted
+				user2: game.user2Accepted
 			});
 
-			if (accept && game.user2_accepted) bothAccepted = true;
-		} else if (game.user2_id.equals(user._id)) {
-			await Game.update({ _id: gameId }, {
+			if (accept && game.user2Accepted) bothAccepted = true;
+		} else if (game.user2Id.equals(user._id)) {
+			await OthelloGame.update({ _id: gameId }, {
 				$set: {
-					user2_accepted: accept
+					user2Accepted: accept
 				}
 			});
 
 			publishOthelloGameStream(gameId, 'change-accepts', {
-				user1: game.user1_accepted,
+				user1: game.user1Accepted,
 				user2: accept
 			});
 
-			if (accept && game.user1_accepted) bothAccepted = true;
+			if (accept && game.user1Accepted) bothAccepted = true;
 		} else {
 			return;
 		}
@@ -177,9 +177,9 @@ export default function(request: websocket.request, connection: websocket.connec
 		if (bothAccepted) {
 			// 3秒後、まだacceptされていたらゲーム開始
 			setTimeout(async () => {
-				const freshGame = await Game.findOne({ _id: gameId });
-				if (freshGame == null || freshGame.is_started || freshGame.is_ended) return;
-				if (!freshGame.user1_accepted || !freshGame.user2_accepted) return;
+				const freshGame = await OthelloGame.findOne({ _id: gameId });
+				if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return;
+				if (!freshGame.user1Accepted || !freshGame.user2Accepted) return;
 
 				let bw: number;
 				if (freshGame.settings.bw == 'random') {
@@ -196,10 +196,10 @@ export default function(request: websocket.request, connection: websocket.connec
 
 				const map = freshGame.settings.map != null ? freshGame.settings.map : getRandomMap();
 
-				await Game.update({ _id: gameId }, {
+				await OthelloGame.update({ _id: gameId }, {
 					$set: {
-						started_at: new Date(),
-						is_started: true,
+						startedAt: new Date(),
+						isStarted: true,
 						black: bw,
 						'settings.map': map
 					}
@@ -207,32 +207,32 @@ export default function(request: websocket.request, connection: websocket.connec
 
 				//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
 				const o = new Othello(map, {
-					isLlotheo: freshGame.settings.is_llotheo,
-					canPutEverywhere: freshGame.settings.can_put_everywhere,
-					loopedBoard: freshGame.settings.looped_board
+					isLlotheo: freshGame.settings.isLlotheo,
+					canPutEverywhere: freshGame.settings.canPutEverywhere,
+					loopedBoard: freshGame.settings.loopedBoard
 				});
 
 				if (o.isEnded) {
 					let winner;
 					if (o.winner === true) {
-						winner = freshGame.black == 1 ? freshGame.user1_id : freshGame.user2_id;
+						winner = freshGame.black == 1 ? freshGame.user1Id : freshGame.user2Id;
 					} else if (o.winner === false) {
-						winner = freshGame.black == 1 ? freshGame.user2_id : freshGame.user1_id;
+						winner = freshGame.black == 1 ? freshGame.user2Id : freshGame.user1Id;
 					} else {
 						winner = null;
 					}
 
-					await Game.update({
+					await OthelloGame.update({
 						_id: gameId
 					}, {
 						$set: {
-							is_ended: true,
-							winner_id: winner
+							isEnded: true,
+							winnerId: winner
 						}
 					});
 
 					publishOthelloGameStream(gameId, 'ended', {
-						winner_id: winner,
+						winnerId: winner,
 						game: await pack(gameId, user)
 					});
 				}
@@ -245,16 +245,16 @@ export default function(request: websocket.request, connection: websocket.connec
 
 	// 石を打つ
 	async function set(pos) {
-		const game = await Game.findOne({ _id: gameId });
+		const game = await OthelloGame.findOne({ _id: gameId });
 
-		if (!game.is_started) return;
-		if (game.is_ended) return;
-		if (!game.user1_id.equals(user._id) && !game.user2_id.equals(user._id)) return;
+		if (!game.isStarted) return;
+		if (game.isEnded) return;
+		if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return;
 
 		const o = new Othello(game.settings.map, {
-			isLlotheo: game.settings.is_llotheo,
-			canPutEverywhere: game.settings.can_put_everywhere,
-			loopedBoard: game.settings.looped_board
+			isLlotheo: game.settings.isLlotheo,
+			canPutEverywhere: game.settings.canPutEverywhere,
+			loopedBoard: game.settings.loopedBoard
 		});
 
 		game.logs.forEach(log => {
@@ -262,7 +262,7 @@ export default function(request: websocket.request, connection: websocket.connec
 		});
 
 		const myColor =
-			(game.user1_id.equals(user._id) && game.black == 1) || (game.user2_id.equals(user._id) && game.black == 2)
+			(game.user1Id.equals(user._id) && game.black == 1) || (game.user2Id.equals(user._id) && game.black == 2)
 				? true
 				: false;
 
@@ -272,9 +272,9 @@ export default function(request: websocket.request, connection: websocket.connec
 		let winner;
 		if (o.isEnded) {
 			if (o.winner === true) {
-				winner = game.black == 1 ? game.user1_id : game.user2_id;
+				winner = game.black == 1 ? game.user1Id : game.user2Id;
 			} else if (o.winner === false) {
-				winner = game.black == 1 ? game.user2_id : game.user1_id;
+				winner = game.black == 1 ? game.user2Id : game.user1Id;
 			} else {
 				winner = null;
 			}
@@ -288,13 +288,13 @@ export default function(request: websocket.request, connection: websocket.connec
 
 		const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString());
 
-		await Game.update({
+		await OthelloGame.update({
 			_id: gameId
 		}, {
 			$set: {
 				crc32,
-				is_ended: o.isEnded,
-				winner_id: winner
+				isEnded: o.isEnded,
+				winnerId: winner
 			},
 			$push: {
 				logs: log
@@ -307,16 +307,16 @@ export default function(request: websocket.request, connection: websocket.connec
 
 		if (o.isEnded) {
 			publishOthelloGameStream(gameId, 'ended', {
-				winner_id: winner,
+				winnerId: winner,
 				game: await pack(gameId, user)
 			});
 		}
 	}
 
 	async function check(crc32) {
-		const game = await Game.findOne({ _id: gameId });
+		const game = await OthelloGame.findOne({ _id: gameId });
 
-		if (!game.is_started) return;
+		if (!game.isStarted) return;
 
 		// 互換性のため
 		if (game.crc32 == null) return;
diff --git a/src/server/api/stream/othello.ts b/src/server/api/stream/othello.ts
index bd3b4a763..55c993ec8 100644
--- a/src/server/api/stream/othello.ts
+++ b/src/server/api/stream/othello.ts
@@ -18,11 +18,11 @@ export default function(request: websocket.request, connection: websocket.connec
 			case 'ping':
 				if (msg.id == null) return;
 				const matching = await Matching.findOne({
-					parent_id: user._id,
-					child_id: new mongo.ObjectID(msg.id)
+					parentId: user._id,
+					childId: new mongo.ObjectID(msg.id)
 				});
 				if (matching == null) return;
-				publishUserStream(matching.child_id, 'othello_invited', await pack(matching, matching.child_id));
+				publishUserStream(matching.childId, 'othello_invited', await pack(matching, matching.childId));
 				break;
 		}
 	});
diff --git a/src/server/api/streaming.ts b/src/server/api/streaming.ts
index 95f444e00..73f099bd8 100644
--- a/src/server/api/streaming.ts
+++ b/src/server/api/streaming.ts
@@ -110,7 +110,7 @@ function authenticate(token: string): Promise<IUser> {
 
 			// Fetch user
 			const user: IUser = await User
-				.findOne({ _id: accessToken.user_id });
+				.findOne({ _id: accessToken.userId });
 
 			resolve(user);
 		}
diff --git a/src/server/common/get-post-summary.ts b/src/server/common/get-post-summary.ts
index 6e8f65708..8d0033064 100644
--- a/src/server/common/get-post-summary.ts
+++ b/src/server/common/get-post-summary.ts
@@ -22,7 +22,7 @@ const summarize = (post: any): string => {
 	}
 
 	// 返信のとき
-	if (post.reply_id) {
+	if (post.replyId) {
 		if (post.reply) {
 			summary += ` RE: ${summarize(post.reply)}`;
 		} else {
@@ -31,7 +31,7 @@ const summarize = (post: any): string => {
 	}
 
 	// Repostのとき
-	if (post.repost_id) {
+	if (post.repostId) {
 		if (post.repost) {
 			summary += ` RP: ${summarize(post.repost)}`;
 		} else {
diff --git a/src/server/common/othello/ai/back.ts b/src/server/common/othello/ai/back.ts
index c20c6fed2..629e57113 100644
--- a/src/server/common/othello/ai/back.ts
+++ b/src/server/common/othello/ai/back.ts
@@ -44,7 +44,7 @@ process.on('message', async msg => {
 		//#region TLに投稿する
 		const game = msg.body;
 		const url = `${conf.url}/othello/${game.id}`;
-		const user = game.user1_id == id ? game.user2 : game.user1;
+		const user = game.user1Id == id ? game.user2 : game.user1;
 		const isSettai = form[0].value === 0;
 		const text = isSettai
 			? `?[${user.name}](${conf.url}/@${user.username})さんの接待を始めました!`
@@ -56,7 +56,7 @@ process.on('message', async msg => {
 			}
 		});
 
-		post = res.created_post;
+		post = res.createdPost;
 		//#endregion
 	}
 
@@ -68,23 +68,23 @@ process.on('message', async msg => {
 		});
 
 		//#region TLに投稿する
-		const user = game.user1_id == id ? game.user2 : game.user1;
+		const user = game.user1Id == id ? game.user2 : game.user1;
 		const isSettai = form[0].value === 0;
 		const text = isSettai
-			? msg.body.winner_id === null
+			? msg.body.winnerId === null
 				? `?[${user.name}](${conf.url}/@${user.username})さんに接待で引き分けました...`
-				: msg.body.winner_id == id
+				: msg.body.winnerId == id
 					? `?[${user.name}](${conf.url}/@${user.username})さんに接待で勝ってしまいました...`
 					: `?[${user.name}](${conf.url}/@${user.username})さんに接待で負けてあげました♪`
-			: msg.body.winner_id === null
+			: msg.body.winnerId === null
 				? `?[${user.name}](${conf.url}/@${user.username})さんと引き分けました~`
-				: msg.body.winner_id == id
+				: msg.body.winnerId == id
 					? `?[${user.name}](${conf.url}/@${user.username})さんに勝ちました♪`
 					: `?[${user.name}](${conf.url}/@${user.username})さんに負けました...`;
 
 		await request.post(`${conf.api_url}/posts/create`, {
 			json: { i,
-				repost_id: post.id,
+				repostId: post.id,
 				text: text
 			}
 		});
@@ -114,9 +114,9 @@ function onGameStarted(g) {
 
 	// オセロエンジン初期化
 	o = new Othello(game.settings.map, {
-		isLlotheo: game.settings.is_llotheo,
-		canPutEverywhere: game.settings.can_put_everywhere,
-		loopedBoard: game.settings.looped_board
+		isLlotheo: game.settings.isLlotheo,
+		canPutEverywhere: game.settings.canPutEverywhere,
+		loopedBoard: game.settings.loopedBoard
 	});
 
 	// 各マスの価値を計算しておく
@@ -141,7 +141,7 @@ function onGameStarted(g) {
 		return count >= 4 ? 1 : 0;
 	});
 
-	botColor = game.user1_id == id && game.black == 1 || game.user2_id == id && game.black == 2;
+	botColor = game.user1Id == id && game.black == 1 || game.user2Id == id && game.black == 2;
 
 	if (botColor) {
 		think();
@@ -188,7 +188,7 @@ function think() {
 		});
 
 		// ロセオならスコアを反転
-		if (game.settings.is_llotheo) score = -score;
+		if (game.settings.isLlotheo) score = -score;
 
 		// 接待ならスコアを反転
 		if (isSettai) score = -score;
@@ -234,7 +234,7 @@ function think() {
 
 			let score;
 
-			if (game.settings.is_llotheo) {
+			if (game.settings.isLlotheo) {
 				// 勝ちは勝ちでも、より自分の石を少なくした方が美しい勝ちだと判定する
 				score = o.winner ? base - (o.blackCount * 100) : base - (o.whiteCount * 100);
 			} else {
@@ -317,7 +317,7 @@ function think() {
 
 			let score;
 
-			if (game.settings.is_llotheo) {
+			if (game.settings.isLlotheo) {
 				// 勝ちは勝ちでも、より自分の石を少なくした方が美しい勝ちだと判定する
 				score = o.winner ? base - (o.blackCount * 100) : base - (o.whiteCount * 100);
 			} else {
diff --git a/src/server/common/othello/ai/front.ts b/src/server/common/othello/ai/front.ts
index af0b748fc..fb7a9be13 100644
--- a/src/server/common/othello/ai/front.ts
+++ b/src/server/common/othello/ai/front.ts
@@ -48,12 +48,12 @@ homeStream.on('message', message => {
 	if (msg.type == 'mention' || msg.type == 'reply') {
 		const post = msg.body;
 
-		if (post.user_id == id) return;
+		if (post.userId == id) return;
 
 		// リアクションする
 		request.post(`${conf.api_url}/posts/reactions/create`, {
 			json: { i,
-				post_id: post.id,
+				postId: post.id,
 				reaction: 'love'
 			}
 		});
@@ -62,12 +62,12 @@ homeStream.on('message', message => {
 			if (post.text.indexOf('オセロ') > -1) {
 				request.post(`${conf.api_url}/posts/create`, {
 					json: { i,
-						reply_id: post.id,
+						replyId: post.id,
 						text: '良いですよ~'
 					}
 				});
 
-				invite(post.user_id);
+				invite(post.userId);
 			}
 		}
 	}
@@ -79,12 +79,12 @@ homeStream.on('message', message => {
 			if (message.text.indexOf('オセロ') > -1) {
 				request.post(`${conf.api_url}/messaging/messages/create`, {
 					json: { i,
-						user_id: message.user_id,
+						userId: message.userId,
 						text: '良いですよ~'
 					}
 				});
 
-				invite(message.user_id);
+				invite(message.userId);
 			}
 		}
 	}
@@ -94,7 +94,7 @@ homeStream.on('message', message => {
 function invite(userId) {
 	request.post(`${conf.api_url}/othello/match`, {
 		json: { i,
-			user_id: userId
+			userId: userId
 		}
 	});
 }
@@ -225,7 +225,7 @@ async function onInviteMe(inviter) {
 	const game = await request.post(`${conf.api_url}/othello/match`, {
 		json: {
 			i,
-			user_id: inviter.id
+			userId: inviter.id
 		}
 	});
 
diff --git a/src/server/common/user/get-summary.ts b/src/server/common/user/get-summary.ts
index f9b7125e3..b314a5cef 100644
--- a/src/server/common/user/get-summary.ts
+++ b/src/server/common/user/get-summary.ts
@@ -7,7 +7,7 @@ import getAcct from './get-acct';
  */
 export default function(user: IUser): string {
 	let string = `${user.name} (@${getAcct(user)})\n` +
-		`${user.posts_count}投稿、${user.following_count}フォロー、${user.followers_count}フォロワー\n`;
+		`${user.postsCount}投稿、${user.followingCount}フォロー、${user.followersCount}フォロワー\n`;
 
 	if (user.host === null) {
 		const account = user.account as ILocalAccount;
diff --git a/src/server/web/app/auth/views/form.vue b/src/server/web/app/auth/views/form.vue
index d86ed58b3..9d9e8cdb1 100644
--- a/src/server/web/app/auth/views/form.vue
+++ b/src/server/web/app/auth/views/form.vue
@@ -7,7 +7,7 @@
 	<div class="app">
 		<section>
 			<h2>{{ app.name }}</h2>
-			<p class="nid">{{ app.name_id }}</p>
+			<p class="nid">{{ app.nameId }}</p>
 			<p class="description">{{ app.description }}</p>
 		</section>
 		<section>
diff --git a/src/server/web/app/auth/views/index.vue b/src/server/web/app/auth/views/index.vue
index 17e5cc610..e1e1b265e 100644
--- a/src/server/web/app/auth/views/index.vue
+++ b/src/server/web/app/auth/views/index.vue
@@ -14,9 +14,9 @@
 			<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>
+			<h1>{{ session.app.isAuthorized ? 'このアプリは既に連携済みです' : 'アプリケーションの連携を許可しました' }}</h1>
+			<p v-if="session.app.callbackUrl">アプリケーションに戻っています<mk-ellipsis/></p>
+			<p v-if="!session.app.callbackUrl">アプリケーションに戻って、やっていってください。</p>
 		</div>
 		<div class="error" v-if="state == 'fetch-session-error'">
 			<p>セッションが存在しません。</p>
@@ -61,7 +61,7 @@ export default Vue.extend({
 			this.fetching = false;
 
 			// 既に連携していた場合
-			if (this.session.app.is_authorized) {
+			if (this.session.app.isAuthorized) {
 				this.$root.$data.os.api('auth/accept', {
 					token: this.session.token
 				}).then(() => {
@@ -77,8 +77,8 @@ export default Vue.extend({
 	methods: {
 		accepted() {
 			this.state = 'accepted';
-			if (this.session.app.callback_url) {
-				location.href = this.session.app.callback_url + '?token=' + this.session.token;
+			if (this.session.app.callbackUrl) {
+				location.href = this.session.app.callbackUrl + '?token=' + this.session.token;
 			}
 		}
 	}
diff --git a/src/server/web/app/ch/tags/channel.tag b/src/server/web/app/ch/tags/channel.tag
index dc4b8e142..2abfb106a 100644
--- a/src/server/web/app/ch/tags/channel.tag
+++ b/src/server/web/app/ch/tags/channel.tag
@@ -5,8 +5,8 @@
 		<h1>{ channel.title }</h1>
 
 		<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>
+			<p v-if="channel.isWatching">このチャンネルをウォッチしています <a @click="unwatch">ウォッチ解除</a></p>
+			<p v-if="!channel.isWatching"><a @click="watch">このチャンネルをウォッチする</a></p>
 		</div>
 
 		<div class="share">
@@ -77,7 +77,7 @@
 
 			// チャンネル概要読み込み
 			this.$root.$data.os.api('channels/show', {
-				channel_id: this.id
+				channelId: this.id
 			}).then(channel => {
 				if (fetched) {
 					Progress.done();
@@ -96,7 +96,7 @@
 
 			// 投稿読み込み
 			this.$root.$data.os.api('channels/posts', {
-				channel_id: this.id
+				channelId: this.id
 			}).then(posts => {
 				if (fetched) {
 					Progress.done();
@@ -125,7 +125,7 @@
 			this.posts.unshift(post);
 			this.update();
 
-			if (document.hidden && this.$root.$data.os.isSignedIn && post.user_id !== this.$root.$data.os.i.id) {
+			if (document.hidden && this.$root.$data.os.isSignedIn && post.userId !== this.$root.$data.os.i.id) {
 				this.unreadCount++;
 				document.title = `(${this.unreadCount}) ${this.channel.title} | Misskey`;
 			}
@@ -140,9 +140,9 @@
 
 		this.watch = () => {
 			this.$root.$data.os.api('channels/watch', {
-				channel_id: this.id
+				channelId: this.id
 			}).then(() => {
-				this.channel.is_watching = true;
+				this.channel.isWatching = true;
 				this.update();
 			}, e => {
 				alert('error');
@@ -151,9 +151,9 @@
 
 		this.unwatch = () => {
 			this.$root.$data.os.api('channels/unwatch', {
-				channel_id: this.id
+				channelId: this.id
 			}).then(() => {
-				this.channel.is_watching = false;
+				this.channel.isWatching = false;
 				this.update();
 			}, e => {
 				alert('error');
@@ -166,8 +166,8 @@
 	<header>
 		<a class="index" @click="reply">{ post.index }:</a>
 		<a class="name" href={ _URL_ + '/@' + acct }><b>{ post.user.name }</b></a>
-		<mk-time time={ post.created_at }/>
-		<mk-time time={ post.created_at } mode="detail"/>
+		<mk-time time={ post.createdAt }/>
+		<mk-time time={ post.createdAt } mode="detail"/>
 		<span>ID:<i>{ acct }</i></span>
 	</header>
 	<div>
@@ -328,9 +328,9 @@
 
 			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,
-				channel_id: this.channel.id
+				mediaIds: files,
+				replyId: this.reply ? this.reply.id : undefined,
+				channelId: this.channel.id
 			}).then(data => {
 				this.clear();
 			}).catch(err => {
diff --git a/src/server/web/app/common/define-widget.ts b/src/server/web/app/common/define-widget.ts
index d8d29873a..27db59b5e 100644
--- a/src/server/web/app/common/define-widget.ts
+++ b/src/server/web/app/common/define-widget.ts
@@ -56,14 +56,14 @@ export default function<T extends object>(data: {
 						id: this.id,
 						data: newProps
 					}).then(() => {
-						(this as any).os.i.account.client_settings.mobile_home.find(w => w.id == this.id).data = newProps;
+						(this as any).os.i.account.clientSettings.mobile_home.find(w => w.id == this.id).data = newProps;
 					});
 				} else {
 					(this as any).api('i/update_home', {
 						id: this.id,
 						data: newProps
 					}).then(() => {
-						(this as any).os.i.account.client_settings.home.find(w => w.id == this.id).data = newProps;
+						(this as any).os.i.account.clientSettings.home.find(w => w.id == this.id).data = newProps;
 					});
 				}
 			}, {
diff --git a/src/server/web/app/common/mios.ts b/src/server/web/app/common/mios.ts
index 2c6c9988e..bcb8b6067 100644
--- a/src/server/web/app/common/mios.ts
+++ b/src/server/web/app/common/mios.ts
@@ -294,12 +294,12 @@ export default class MiOS extends EventEmitter {
 		const fetched = me => {
 			if (me) {
 				// デフォルトの設定をマージ
-				me.account.client_settings = Object.assign({
+				me.account.clientSettings = Object.assign({
 					fetchOnScroll: true,
 					showMaps: true,
 					showPostFormOnTopOfTl: false,
 					gradientWindowHeader: false
-				}, me.account.client_settings);
+				}, me.account.clientSettings);
 
 				// ローカルストレージにキャッシュ
 				localStorage.setItem('me', JSON.stringify(me));
diff --git a/src/server/web/app/common/scripts/compose-notification.ts b/src/server/web/app/common/scripts/compose-notification.ts
index e1dbd3bc1..273579cbc 100644
--- a/src/server/web/app/common/scripts/compose-notification.ts
+++ b/src/server/web/app/common/scripts/compose-notification.ts
@@ -23,42 +23,42 @@ export default function(type, data): Notification {
 			return {
 				title: `${data.user.name}さんから:`,
 				body: getPostSummary(data),
-				icon: data.user.avatar_url + '?thumbnail&size=64'
+				icon: data.user.avatarUrl + '?thumbnail&size=64'
 			};
 
 		case 'reply':
 			return {
 				title: `${data.user.name}さんから返信:`,
 				body: getPostSummary(data),
-				icon: data.user.avatar_url + '?thumbnail&size=64'
+				icon: data.user.avatarUrl + '?thumbnail&size=64'
 			};
 
 		case 'quote':
 			return {
 				title: `${data.user.name}さんが引用:`,
 				body: getPostSummary(data),
-				icon: data.user.avatar_url + '?thumbnail&size=64'
+				icon: data.user.avatarUrl + '?thumbnail&size=64'
 			};
 
 		case 'reaction':
 			return {
 				title: `${data.user.name}: ${getReactionEmoji(data.reaction)}:`,
 				body: getPostSummary(data.post),
-				icon: data.user.avatar_url + '?thumbnail&size=64'
+				icon: data.user.avatarUrl + '?thumbnail&size=64'
 			};
 
 		case 'unread_messaging_message':
 			return {
 				title: `${data.user.name}さんからメッセージ:`,
 				body: data.text, // TODO: getMessagingMessageSummary(data),
-				icon: data.user.avatar_url + '?thumbnail&size=64'
+				icon: data.user.avatarUrl + '?thumbnail&size=64'
 			};
 
 		case 'othello_invited':
 			return {
 				title: '対局への招待があります',
 				body: `${data.parent.name}さんから`,
-				icon: data.parent.avatar_url + '?thumbnail&size=64'
+				icon: data.parent.avatarUrl + '?thumbnail&size=64'
 			};
 
 		default:
diff --git a/src/server/web/app/common/scripts/parse-search-query.ts b/src/server/web/app/common/scripts/parse-search-query.ts
index 512791ecb..4f09d2b93 100644
--- a/src/server/web/app/common/scripts/parse-search-query.ts
+++ b/src/server/web/app/common/scripts/parse-search-query.ts
@@ -8,10 +8,10 @@ export default function(qs: string) {
 			const [key, value] = x.split(':');
 			switch (key) {
 				case 'user':
-					q['include_user_usernames'] = value.split(',');
+					q['includeUserUsernames'] = value.split(',');
 					break;
 				case 'exclude_user':
-					q['exclude_user_usernames'] = value.split(',');
+					q['excludeUserUsernames'] = value.split(',');
 					break;
 				case 'follow':
 					q['following'] = value == 'null' ? null : value == 'true';
diff --git a/src/server/web/app/common/scripts/streaming/home.ts b/src/server/web/app/common/scripts/streaming/home.ts
index ffcf6e536..c19861940 100644
--- a/src/server/web/app/common/scripts/streaming/home.ts
+++ b/src/server/web/app/common/scripts/streaming/home.ts
@@ -16,7 +16,7 @@ export class HomeStream extends Stream {
 		// 最終利用日時を更新するため定期的にaliveメッセージを送信
 		setInterval(() => {
 			this.send({ type: 'alive' });
-			me.account.last_used_at = new Date();
+			me.account.lastUsedAt = new Date();
 		}, 1000 * 60);
 
 		// 自分の情報が更新されたとき
diff --git a/src/server/web/app/common/views/components/autocomplete.vue b/src/server/web/app/common/views/components/autocomplete.vue
index 8afa291e3..79bd2ba02 100644
--- a/src/server/web/app/common/views/components/autocomplete.vue
+++ b/src/server/web/app/common/views/components/autocomplete.vue
@@ -2,7 +2,7 @@
 <div class="mk-autocomplete" @contextmenu.prevent="() => {}">
 	<ol class="users" ref="suggests" v-if="users.length > 0">
 		<li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1">
-			<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=32`" alt=""/>
+			<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=32`" alt=""/>
 			<span class="name">{{ user.name }}</span>
 			<span class="username">@{{ getAcct(user) }}</span>
 		</li>
diff --git a/src/server/web/app/common/views/components/messaging-room.form.vue b/src/server/web/app/common/views/components/messaging-room.form.vue
index 01886b19c..704f2016d 100644
--- a/src/server/web/app/common/views/components/messaging-room.form.vue
+++ b/src/server/web/app/common/views/components/messaging-room.form.vue
@@ -151,9 +151,9 @@ export default Vue.extend({
 		send() {
 			this.sending = true;
 			(this as any).api('messaging/messages/create', {
-				user_id: this.user.id,
+				userId: this.user.id,
 				text: this.text ? this.text : undefined,
-				file_id: this.file ? this.file.id : undefined
+				fileId: this.file ? this.file.id : undefined
 			}).then(message => {
 				this.clear();
 			}).catch(err => {
@@ -173,7 +173,7 @@ export default Vue.extend({
 			const data = JSON.parse(localStorage.getItem('message_drafts') || '{}');
 
 			data[this.draftId] = {
-				updated_at: new Date(),
+				updatedAt: new Date(),
 				data: {
 					text: this.text,
 					file: this.file
diff --git a/src/server/web/app/common/views/components/messaging-room.message.vue b/src/server/web/app/common/views/components/messaging-room.message.vue
index 5f2eb1ba8..94f87fd70 100644
--- a/src/server/web/app/common/views/components/messaging-room.message.vue
+++ b/src/server/web/app/common/views/components/messaging-room.message.vue
@@ -1,15 +1,15 @@
 <template>
 <div class="message" :data-is-me="isMe">
 	<router-link class="avatar-anchor" :to="`/@${acct}`" :title="acct" target="_blank">
-		<img class="avatar" :src="`${message.user.avatar_url}?thumbnail&size=80`" alt=""/>
+		<img class="avatar" :src="`${message.user.avatarUrl}?thumbnail&size=80`" alt=""/>
 	</router-link>
 	<div class="content">
 		<div class="balloon" :data-no-text="message.text == null">
-			<p class="read" v-if="isMe && message.is_read">%i18n:common.tags.mk-messaging-message.is-read%</p>
+			<p class="read" v-if="isMe && message.isRead">%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">
+			<div class="content" v-if="!message.isDeleted">
 				<mk-post-html class="text" v-if="message.ast" :ast="message.ast" :i="os.i"/>
 				<div class="file" v-if="message.file">
 					<a :href="message.file.url" target="_blank" :title="message.file.name">
@@ -18,14 +18,14 @@
 					</a>
 				</div>
 			</div>
-			<div class="content" v-if="message.is_deleted">
+			<div class="content" v-if="message.isDeleted">
 				<p class="is-deleted">%i18n:common.tags.mk-messaging-message.deleted%</p>
 			</div>
 		</div>
 		<div></div>
 		<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 		<footer>
-			<mk-time :time="message.created_at"/>
+			<mk-time :time="message.createdAt"/>
 			<template v-if="message.is_edited">%fa:pencil-alt%</template>
 		</footer>
 	</div>
@@ -43,7 +43,7 @@ export default Vue.extend({
 			return getAcct(this.message.user);
 		},
 		isMe(): boolean {
-			return this.message.user_id == (this as any).os.i.id;
+			return this.message.userId == (this as any).os.i.id;
 		},
 		urls(): string[] {
 			if (this.message.ast) {
diff --git a/src/server/web/app/common/views/components/messaging-room.vue b/src/server/web/app/common/views/components/messaging-room.vue
index 6ff808b61..d30c64d74 100644
--- a/src/server/web/app/common/views/components/messaging-room.vue
+++ b/src/server/web/app/common/views/components/messaging-room.vue
@@ -52,8 +52,8 @@ export default Vue.extend({
 	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;
+				const date = new Date(message.createdAt).getDate();
+				const month = new Date(message.createdAt).getMonth() + 1;
 				message._date = date;
 				message._datetext = `${month}月 ${date}日`;
 				return message;
@@ -123,9 +123,9 @@ export default Vue.extend({
 				const max = this.existMoreMessages ? 20 : 10;
 
 				(this as any).api('messaging/messages', {
-					user_id: this.user.id,
+					userId: this.user.id,
 					limit: max + 1,
-					until_id: this.existMoreMessages ? this.messages[0].id : undefined
+					untilId: this.existMoreMessages ? this.messages[0].id : undefined
 				}).then(messages => {
 					if (messages.length == max + 1) {
 						this.existMoreMessages = true;
@@ -158,7 +158,7 @@ export default Vue.extend({
 			const isBottom = this.isBottom();
 
 			this.messages.push(message);
-			if (message.user_id != (this as any).os.i.id && !document.hidden) {
+			if (message.userId != (this as any).os.i.id && !document.hidden) {
 				this.connection.send({
 					type: 'read',
 					id: message.id
@@ -170,7 +170,7 @@ export default Vue.extend({
 				this.$nextTick(() => {
 					this.scrollToBottom();
 				});
-			} else if (message.user_id != (this as any).os.i.id) {
+			} else if (message.userId != (this as any).os.i.id) {
 				// Notify
 				this.notify('%i18n:common.tags.mk-messaging-room.new-message%');
 			}
@@ -181,7 +181,7 @@ export default Vue.extend({
 			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.messages[exist].isRead = true;
 				}
 			});
 		},
@@ -223,7 +223,7 @@ export default Vue.extend({
 		onVisibilitychange() {
 			if (document.hidden) return;
 			this.messages.forEach(message => {
-				if (message.user_id !== (this as any).os.i.id && !message.is_read) {
+				if (message.userId !== (this as any).os.i.id && !message.isRead) {
 					this.connection.send({
 						type: 'read',
 						id: message.id
diff --git a/src/server/web/app/common/views/components/messaging.vue b/src/server/web/app/common/views/components/messaging.vue
index 88574b94d..8317c3738 100644
--- a/src/server/web/app/common/views/components/messaging.vue
+++ b/src/server/web/app/common/views/components/messaging.vue
@@ -13,7 +13,7 @@
 					@click="navigate(user)"
 					tabindex="-1"
 				>
-					<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=32`" alt=""/>
+					<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=32`" alt=""/>
 					<span class="name">{{ user.name }}</span>
 					<span class="username">@{{ getAcct(user) }}</span>
 				</li>
@@ -26,16 +26,16 @@
 				class="user"
 				:href="`/i/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
 				:data-is-me="isMe(message)"
-				:data-is-read="message.is_read"
+				:data-is-read="message.isRead"
 				@click.prevent="navigate(isMe(message) ? message.recipient : message.user)"
 				:key="message.id"
 			>
 				<div>
-					<img class="avatar" :src="`${isMe(message) ? message.recipient.avatar_url : message.user.avatar_url}?thumbnail&size=64`" alt=""/>
+					<img class="avatar" :src="`${isMe(message) ? message.recipient.avatarUrl : message.user.avatarUrl}?thumbnail&size=64`" alt=""/>
 					<header>
 						<span class="name">{{ isMe(message) ? message.recipient.name : message.user.name }}</span>
 						<span class="username">@{{ getAcct(isMe(message) ? message.recipient : message.user) }}</span>
-						<mk-time :time="message.created_at"/>
+						<mk-time :time="message.createdAt"/>
 					</header>
 					<div class="body">
 						<p class="text"><span class="me" v-if="isMe(message)">%i18n:common.tags.mk-messaging.you%:</span>{{ message.text }}</p>
@@ -95,19 +95,19 @@ export default Vue.extend({
 	methods: {
 		getAcct,
 		isMe(message) {
-			return message.user_id == (this as any).os.i.id;
+			return message.userId == (this as any).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)));
+				(m.recipientId == message.recipientId && m.userId == message.userId) ||
+				(m.recipientId == message.userId && m.userId == message.recipientId)));
 
 			this.messages.unshift(message);
 		},
 		onRead(ids) {
 			ids.forEach(id => {
 				const found = this.messages.find(m => m.id == id);
-				if (found) found.is_read = true;
+				if (found) found.isRead = true;
 			});
 		},
 		search() {
diff --git a/src/server/web/app/common/views/components/othello.game.vue b/src/server/web/app/common/views/components/othello.game.vue
index 414d819a5..f08742ad1 100644
--- a/src/server/web/app/common/views/components/othello.game.vue
+++ b/src/server/web/app/common/views/components/othello.game.vue
@@ -3,30 +3,30 @@
 	<header><b>{{ blackUser.name }}</b>(黒) vs <b>{{ whiteUser.name }}</b>(白)</header>
 
 	<div style="overflow: hidden">
-		<p class="turn" v-if="!iAmPlayer && !game.is_ended">{{ turnUser.name }}のターンです<mk-ellipsis/></p>
+		<p class="turn" v-if="!iAmPlayer && !game.isEnded">{{ turnUser.name }}のターンです<mk-ellipsis/></p>
 		<p class="turn" v-if="logPos != logs.length">{{ turnUser.name }}のターン</p>
-		<p class="turn1" v-if="iAmPlayer && !game.is_ended && !isMyTurn">相手のターンです<mk-ellipsis/></p>
-		<p class="turn2" v-if="iAmPlayer && !game.is_ended && isMyTurn" v-animate-css="{ classes: 'tada', iteration: 'infinite' }">あなたのターンです</p>
-		<p class="result" v-if="game.is_ended && logPos == logs.length">
-			<template v-if="game.winner"><b>{{ game.winner.name }}</b>の勝ち{{ game.settings.is_llotheo ? ' (ロセオ)' : '' }}</template>
+		<p class="turn1" v-if="iAmPlayer && !game.isEnded && !isMyTurn">相手のターンです<mk-ellipsis/></p>
+		<p class="turn2" v-if="iAmPlayer && !game.isEnded && isMyTurn" v-animate-css="{ classes: 'tada', iteration: 'infinite' }">あなたのターンです</p>
+		<p class="result" v-if="game.isEnded && logPos == logs.length">
+			<template v-if="game.winner"><b>{{ game.winner.name }}</b>の勝ち{{ game.settings.isLlotheo ? ' (ロセオ)' : '' }}</template>
 			<template v-else>引き分け</template>
 		</p>
 	</div>
 
 	<div class="board" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map[0].length }, 1fr)` }">
 		<div v-for="(stone, i) in o.board"
-			:class="{ empty: stone == null, none: o.map[i] == 'null', isEnded: game.is_ended, myTurn: !game.is_ended && isMyTurn, can: turnUser ? o.canPut(turnUser.id == blackUser.id, i) : null, prev: o.prevPos == i }"
+			:class="{ empty: stone == null, none: o.map[i] == 'null', isEnded: game.isEnded, myTurn: !game.isEnded && isMyTurn, can: turnUser ? o.canPut(turnUser.id == blackUser.id, i) : null, prev: o.prevPos == i }"
 			@click="set(i)"
 			:title="'[' + (o.transformPosToXy(i)[0] + 1) + ', ' + (o.transformPosToXy(i)[1] + 1) + '] (' + i + ')'"
 		>
-			<img v-if="stone === true" :src="`${blackUser.avatar_url}?thumbnail&size=128`" alt="">
-			<img v-if="stone === false" :src="`${whiteUser.avatar_url}?thumbnail&size=128`" alt="">
+			<img v-if="stone === true" :src="`${blackUser.avatarUrl}?thumbnail&size=128`" alt="">
+			<img v-if="stone === false" :src="`${whiteUser.avatarUrl}?thumbnail&size=128`" alt="">
 		</div>
 	</div>
 
 	<p class="status"><b>{{ logPos }}ターン目</b> 黒:{{ o.blackCount }} 白:{{ o.whiteCount }} 合計:{{ o.blackCount + o.whiteCount }}</p>
 
-	<div class="player" v-if="game.is_ended">
+	<div class="player" v-if="game.isEnded">
 		<el-button-group>
 			<el-button type="primary" @click="logPos = 0" :disabled="logPos == 0">%fa:angle-double-left%</el-button>
 			<el-button type="primary" @click="logPos--" :disabled="logPos == 0">%fa:angle-left%</el-button>
@@ -62,12 +62,12 @@ export default Vue.extend({
 	computed: {
 		iAmPlayer(): boolean {
 			if (!(this as any).os.isSignedIn) return false;
-			return this.game.user1_id == (this as any).os.i.id || this.game.user2_id == (this as any).os.i.id;
+			return this.game.user1Id == (this as any).os.i.id || this.game.user2Id == (this as any).os.i.id;
 		},
 		myColor(): Color {
 			if (!this.iAmPlayer) return null;
-			if (this.game.user1_id == (this as any).os.i.id && this.game.black == 1) return true;
-			if (this.game.user2_id == (this as any).os.i.id && this.game.black == 2) return true;
+			if (this.game.user1Id == (this as any).os.i.id && this.game.black == 1) return true;
+			if (this.game.user2Id == (this as any).os.i.id && this.game.black == 2) return true;
 			return false;
 		},
 		opColor(): Color {
@@ -97,11 +97,11 @@ export default Vue.extend({
 
 	watch: {
 		logPos(v) {
-			if (!this.game.is_ended) return;
+			if (!this.game.isEnded) return;
 			this.o = new Othello(this.game.settings.map, {
-				isLlotheo: this.game.settings.is_llotheo,
-				canPutEverywhere: this.game.settings.can_put_everywhere,
-				loopedBoard: this.game.settings.looped_board
+				isLlotheo: this.game.settings.isLlotheo,
+				canPutEverywhere: this.game.settings.canPutEverywhere,
+				loopedBoard: this.game.settings.loopedBoard
 			});
 			this.logs.forEach((log, i) => {
 				if (i < v) {
@@ -116,9 +116,9 @@ export default Vue.extend({
 		this.game = this.initGame;
 
 		this.o = new Othello(this.game.settings.map, {
-			isLlotheo: this.game.settings.is_llotheo,
-			canPutEverywhere: this.game.settings.can_put_everywhere,
-			loopedBoard: this.game.settings.looped_board
+			isLlotheo: this.game.settings.isLlotheo,
+			canPutEverywhere: this.game.settings.canPutEverywhere,
+			loopedBoard: this.game.settings.loopedBoard
 		});
 
 		this.game.logs.forEach(log => {
@@ -129,7 +129,7 @@ export default Vue.extend({
 		this.logPos = this.logs.length;
 
 		// 通信を取りこぼしてもいいように定期的にポーリングさせる
-		if (this.game.is_started && !this.game.is_ended) {
+		if (this.game.isStarted && !this.game.isEnded) {
 			this.pollingClock = setInterval(() => {
 				const crc32 = CRC32.str(this.logs.map(x => x.pos.toString()).join(''));
 				this.connection.send({
@@ -154,7 +154,7 @@ export default Vue.extend({
 
 	methods: {
 		set(pos) {
-			if (this.game.is_ended) return;
+			if (this.game.isEnded) return;
 			if (!this.iAmPlayer) return;
 			if (!this.isMyTurn) return;
 			if (!this.o.canPut(this.myColor, pos)) return;
@@ -194,16 +194,16 @@ export default Vue.extend({
 		},
 
 		checkEnd() {
-			this.game.is_ended = this.o.isEnded;
-			if (this.game.is_ended) {
+			this.game.isEnded = this.o.isEnded;
+			if (this.game.isEnded) {
 				if (this.o.winner === true) {
-					this.game.winner_id = this.game.black == 1 ? this.game.user1_id : this.game.user2_id;
+					this.game.winnerId = this.game.black == 1 ? this.game.user1Id : this.game.user2Id;
 					this.game.winner = this.game.black == 1 ? this.game.user1 : this.game.user2;
 				} else if (this.o.winner === false) {
-					this.game.winner_id = this.game.black == 1 ? this.game.user2_id : this.game.user1_id;
+					this.game.winnerId = this.game.black == 1 ? this.game.user2Id : this.game.user1Id;
 					this.game.winner = this.game.black == 1 ? this.game.user2 : this.game.user1;
 				} else {
-					this.game.winner_id = null;
+					this.game.winnerId = null;
 					this.game.winner = null;
 				}
 			}
@@ -214,9 +214,9 @@ export default Vue.extend({
 			this.game = game;
 
 			this.o = new Othello(this.game.settings.map, {
-				isLlotheo: this.game.settings.is_llotheo,
-				canPutEverywhere: this.game.settings.can_put_everywhere,
-				loopedBoard: this.game.settings.looped_board
+				isLlotheo: this.game.settings.isLlotheo,
+				canPutEverywhere: this.game.settings.canPutEverywhere,
+				loopedBoard: this.game.settings.loopedBoard
 			});
 
 			this.game.logs.forEach(log => {
diff --git a/src/server/web/app/common/views/components/othello.gameroom.vue b/src/server/web/app/common/views/components/othello.gameroom.vue
index 38a25f668..dba9ccd16 100644
--- a/src/server/web/app/common/views/components/othello.gameroom.vue
+++ b/src/server/web/app/common/views/components/othello.gameroom.vue
@@ -1,6 +1,6 @@
 <template>
 <div>
-	<x-room v-if="!g.is_started" :game="g" :connection="connection"/>
+	<x-room v-if="!g.isStarted" :game="g" :connection="connection"/>
 	<x-game v-else :init-game="g" :connection="connection"/>
 </div>
 </template>
diff --git a/src/server/web/app/common/views/components/othello.room.vue b/src/server/web/app/common/views/components/othello.room.vue
index 396541483..a32be6b74 100644
--- a/src/server/web/app/common/views/components/othello.room.vue
+++ b/src/server/web/app/common/views/components/othello.room.vue
@@ -41,9 +41,9 @@
 			<div slot="header">
 				<span>ルール</span>
 			</div>
-			<mk-switch v-model="game.settings.is_llotheo" @change="updateSettings" text="石の少ない方が勝ち(ロセオ)"/>
-			<mk-switch v-model="game.settings.looped_board" @change="updateSettings" text="ループマップ"/>
-			<mk-switch v-model="game.settings.can_put_everywhere" @change="updateSettings" text="どこでも置けるモード"/>
+			<mk-switch v-model="game.settings.isLlotheo" @change="updateSettings" text="石の少ない方が勝ち(ロセオ)"/>
+			<mk-switch v-model="game.settings.loopedBoard" @change="updateSettings" text="ループマップ"/>
+			<mk-switch v-model="game.settings.canPutEverywhere" @change="updateSettings" text="どこでも置けるモード"/>
 		</el-card>
 
 		<el-card class="bot-form" v-if="form">
@@ -116,13 +116,13 @@ export default Vue.extend({
 			return categories.filter((item, pos) => categories.indexOf(item) == pos);
 		},
 		isAccepted(): boolean {
-			if (this.game.user1_id == (this as any).os.i.id && this.game.user1_accepted) return true;
-			if (this.game.user2_id == (this as any).os.i.id && this.game.user2_accepted) return true;
+			if (this.game.user1Id == (this as any).os.i.id && this.game.user1Accepted) return true;
+			if (this.game.user2Id == (this as any).os.i.id && this.game.user2Accepted) return true;
 			return false;
 		},
 		isOpAccepted(): boolean {
-			if (this.game.user1_id != (this as any).os.i.id && this.game.user1_accepted) return true;
-			if (this.game.user2_id != (this as any).os.i.id && this.game.user2_accepted) return true;
+			if (this.game.user1Id != (this as any).os.i.id && this.game.user1Accepted) return true;
+			if (this.game.user2Id != (this as any).os.i.id && this.game.user2Accepted) return true;
 			return false;
 		}
 	},
@@ -133,8 +133,8 @@ export default Vue.extend({
 		this.connection.on('init-form', this.onInitForm);
 		this.connection.on('message', this.onMessage);
 
-		if (this.game.user1_id != (this as any).os.i.id && this.game.settings.form1) this.form = this.game.settings.form1;
-		if (this.game.user2_id != (this as any).os.i.id && this.game.settings.form2) this.form = this.game.settings.form2;
+		if (this.game.user1Id != (this as any).os.i.id && this.game.settings.form1) this.form = this.game.settings.form1;
+		if (this.game.user2Id != (this as any).os.i.id && this.game.settings.form2) this.form = this.game.settings.form2;
 	},
 
 	beforeDestroy() {
@@ -162,8 +162,8 @@ export default Vue.extend({
 		},
 
 		onChangeAccepts(accepts) {
-			this.game.user1_accepted = accepts.user1;
-			this.game.user2_accepted = accepts.user2;
+			this.game.user1Accepted = accepts.user1;
+			this.game.user2Accepted = accepts.user2;
 			this.$forceUpdate();
 		},
 
@@ -185,12 +185,12 @@ export default Vue.extend({
 		},
 
 		onInitForm(x) {
-			if (x.user_id == (this as any).os.i.id) return;
+			if (x.userId == (this as any).os.i.id) return;
 			this.form = x.form;
 		},
 
 		onMessage(x) {
-			if (x.user_id == (this as any).os.i.id) return;
+			if (x.userId == (this as any).os.i.id) return;
 			this.messages.unshift(x.message);
 		},
 
diff --git a/src/server/web/app/common/views/components/othello.vue b/src/server/web/app/common/views/components/othello.vue
index d65032234..8f7d9dfd6 100644
--- a/src/server/web/app/common/views/components/othello.vue
+++ b/src/server/web/app/common/views/components/othello.vue
@@ -31,28 +31,28 @@
 		<section v-if="invitations.length > 0">
 			<h2>対局の招待があります!:</h2>
 			<div class="invitation" v-for="i in invitations" tabindex="-1" @click="accept(i)">
-				<img :src="`${i.parent.avatar_url}?thumbnail&size=32`" alt="">
+				<img :src="`${i.parent.avatarUrl}?thumbnail&size=32`" alt="">
 				<span class="name"><b>{{ i.parent.name }}</b></span>
 				<span class="username">@{{ i.parent.username }}</span>
-				<mk-time :time="i.created_at"/>
+				<mk-time :time="i.createdAt"/>
 			</div>
 		</section>
 		<section v-if="myGames.length > 0">
 			<h2>自分の対局</h2>
 			<a class="game" v-for="g in myGames" tabindex="-1" @click.prevent="go(g)" :href="`/othello/${g.id}`">
-				<img :src="`${g.user1.avatar_url}?thumbnail&size=32`" alt="">
-				<img :src="`${g.user2.avatar_url}?thumbnail&size=32`" alt="">
+				<img :src="`${g.user1.avatarUrl}?thumbnail&size=32`" alt="">
+				<img :src="`${g.user2.avatarUrl}?thumbnail&size=32`" alt="">
 				<span><b>{{ g.user1.name }}</b> vs <b>{{ g.user2.name }}</b></span>
-				<span class="state">{{ g.is_ended ? '終了' : '進行中' }}</span>
+				<span class="state">{{ g.isEnded ? '終了' : '進行中' }}</span>
 			</a>
 		</section>
 		<section v-if="games.length > 0">
 			<h2>みんなの対局</h2>
 			<a class="game" v-for="g in games" tabindex="-1" @click.prevent="go(g)" :href="`/othello/${g.id}`">
-				<img :src="`${g.user1.avatar_url}?thumbnail&size=32`" alt="">
-				<img :src="`${g.user2.avatar_url}?thumbnail&size=32`" alt="">
+				<img :src="`${g.user1.avatarUrl}?thumbnail&size=32`" alt="">
+				<img :src="`${g.user2.avatarUrl}?thumbnail&size=32`" alt="">
 				<span><b>{{ g.user1.name }}</b> vs <b>{{ g.user2.name }}</b></span>
-				<span class="state">{{ g.is_ended ? '終了' : '進行中' }}</span>
+				<span class="state">{{ g.isEnded ? '終了' : '進行中' }}</span>
 			</a>
 		</section>
 	</div>
@@ -133,7 +133,7 @@ export default Vue.extend({
 	methods: {
 		go(game) {
 			(this as any).api('othello/games/show', {
-				game_id: game.id
+				gameId: game.id
 			}).then(game => {
 				this.matching = null;
 				this.game = game;
@@ -147,7 +147,7 @@ export default Vue.extend({
 					username
 				}).then(user => {
 					(this as any).api('othello/match', {
-						user_id: user.id
+						userId: user.id
 					}).then(res => {
 						if (res == null) {
 							this.matching = user;
@@ -164,7 +164,7 @@ export default Vue.extend({
 		},
 		accept(invitation) {
 			(this as any).api('othello/match', {
-				user_id: invitation.parent.id
+				userId: invitation.parent.id
 			}).then(game => {
 				if (game) {
 					this.matching = null;
diff --git a/src/server/web/app/common/views/components/poll.vue b/src/server/web/app/common/views/components/poll.vue
index 8156c8bc5..711d89720 100644
--- a/src/server/web/app/common/views/components/poll.vue
+++ b/src/server/web/app/common/views/components/poll.vue
@@ -4,7 +4,7 @@
 		<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>
+				<template v-if="choice.isVoted">%fa:check%</template>
 				<span>{{ choice.text }}</span>
 				<span class="votes" v-if="showResult">({{ '%i18n:common.tags.mk-poll.vote-count%'.replace('{}', choice.votes) }})</span>
 			</span>
@@ -36,7 +36,7 @@ export default Vue.extend({
 			return this.poll.choices.reduce((a, b) => a + b.votes, 0);
 		},
 		isVoted(): boolean {
-			return this.poll.choices.some(c => c.is_voted);
+			return this.poll.choices.some(c => c.isVoted);
 		}
 	},
 	created() {
@@ -47,15 +47,15 @@ export default Vue.extend({
 			this.showResult = !this.showResult;
 		},
 		vote(id) {
-			if (this.poll.choices.some(c => c.is_voted)) return;
+			if (this.poll.choices.some(c => c.isVoted)) return;
 			(this as any).api('posts/polls/vote', {
-				post_id: this.post.id,
+				postId: this.post.id,
 				choice: id
 			}).then(() => {
 				this.poll.choices.forEach(c => {
 					if (c.id == id) {
 						c.votes++;
-						Vue.set(c, 'is_voted', true);
+						Vue.set(c, 'isVoted', true);
 					}
 				});
 				this.showResult = true;
diff --git a/src/server/web/app/common/views/components/post-menu.vue b/src/server/web/app/common/views/components/post-menu.vue
index a53680e55..35116db7e 100644
--- a/src/server/web/app/common/views/components/post-menu.vue
+++ b/src/server/web/app/common/views/components/post-menu.vue
@@ -2,7 +2,7 @@
 <div class="mk-post-menu">
 	<div class="backdrop" ref="backdrop" @click="close"></div>
 	<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>
+		<button v-if="post.userId == os.i.id" @click="pin">%i18n:common.tags.mk-post-menu.pin%</button>
 	</div>
 </div>
 </template>
@@ -51,7 +51,7 @@ export default Vue.extend({
 	methods: {
 		pin() {
 			(this as any).api('i/pin', {
-				post_id: this.post.id
+				postId: this.post.id
 			}).then(() => {
 				this.$destroy();
 			});
diff --git a/src/server/web/app/common/views/components/reaction-picker.vue b/src/server/web/app/common/views/components/reaction-picker.vue
index df8100f2f..bcb6b2b96 100644
--- a/src/server/web/app/common/views/components/reaction-picker.vue
+++ b/src/server/web/app/common/views/components/reaction-picker.vue
@@ -69,7 +69,7 @@ export default Vue.extend({
 	methods: {
 		react(reaction) {
 			(this as any).api('posts/reactions/create', {
-				post_id: this.post.id,
+				postId: this.post.id,
 				reaction: reaction
 			}).then(() => {
 				if (this.cb) this.cb();
diff --git a/src/server/web/app/common/views/components/reactions-viewer.vue b/src/server/web/app/common/views/components/reactions-viewer.vue
index f6a27d913..246451008 100644
--- a/src/server/web/app/common/views/components/reactions-viewer.vue
+++ b/src/server/web/app/common/views/components/reactions-viewer.vue
@@ -20,7 +20,7 @@ export default Vue.extend({
 	props: ['post'],
 	computed: {
 		reactions(): number {
-			return this.post.reaction_counts;
+			return this.post.reactionCounts;
 		}
 	}
 });
diff --git a/src/server/web/app/common/views/components/signin.vue b/src/server/web/app/common/views/components/signin.vue
index 243468408..17154e6b3 100644
--- a/src/server/web/app/common/views/components/signin.vue
+++ b/src/server/web/app/common/views/components/signin.vue
@@ -1,12 +1,12 @@
 <template>
 <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%
+		<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.account.two_factor_enabled">
+	<label class="token" v-if="user && user.account.twoFactorEnabled">
 		<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>
@@ -43,7 +43,7 @@ export default Vue.extend({
 			(this as any).api('signin', {
 				username: this.username,
 				password: this.password,
-				token: this.user && this.user.account.two_factor_enabled ? this.token : undefined
+				token: this.user && this.user.account.twoFactorEnabled ? this.token : undefined
 			}).then(() => {
 				location.reload();
 			}).catch(() => {
diff --git a/src/server/web/app/common/views/components/signup.vue b/src/server/web/app/common/views/components/signup.vue
index c2e78aa8a..e77d849ad 100644
--- a/src/server/web/app/common/views/components/signup.vue
+++ b/src/server/web/app/common/views/components/signup.vue
@@ -2,7 +2,7 @@
 <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 @input="onChangeUsername"/>
+		<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>
diff --git a/src/server/web/app/common/views/components/twitter-setting.vue b/src/server/web/app/common/views/components/twitter-setting.vue
index 15968d20a..082d2b435 100644
--- a/src/server/web/app/common/views/components/twitter-setting.vue
+++ b/src/server/web/app/common/views/components/twitter-setting.vue
@@ -1,13 +1,13 @@
 <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.account.twitter" :title="`Twitter ID: ${os.i.account.twitter.user_id}`">%i18n:common.tags.mk-twitter-setting.connected-to%: <a :href="`https://twitter.com/${os.i.account.twitter.screen_name}`" target="_blank">@{{ os.i.account.twitter.screen_name }}</a></p>
+	<p class="account" v-if="os.i.account.twitter" :title="`Twitter ID: ${os.i.account.twitter.userId}`">%i18n:common.tags.mk-twitter-setting.connected-to%: <a :href="`https://twitter.com/${os.i.account.twitter.screenName}`" target="_blank">@{{ os.i.account.twitter.screenName }}</a></p>
 	<p>
 		<a :href="`${apiUrl}/connect/twitter`" target="_blank" @click.prevent="connect">{{ os.i.account.twitter ? '%i18n:common.tags.mk-twitter-setting.reconnect%' : '%i18n:common.tags.mk-twitter-setting.connect%' }}</a>
 		<span v-if="os.i.account.twitter"> or </span>
 		<a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="os.i.account.twitter" @click.prevent="disconnect">%i18n:common.tags.mk-twitter-setting.disconnect%</a>
 	</p>
-	<p class="id" v-if="os.i.account.twitter">Twitter ID: {{ os.i.account.twitter.user_id }}</p>
+	<p class="id" v-if="os.i.account.twitter">Twitter ID: {{ os.i.account.twitter.userId }}</p>
 </div>
 </template>
 
diff --git a/src/server/web/app/common/views/components/uploader.vue b/src/server/web/app/common/views/components/uploader.vue
index 73006b16e..c74a1edb4 100644
--- a/src/server/web/app/common/views/components/uploader.vue
+++ b/src/server/web/app/common/views/components/uploader.vue
@@ -53,7 +53,7 @@ export default Vue.extend({
 			data.append('i', (this as any).os.i.account.token);
 			data.append('file', file);
 
-			if (folder) data.append('folder_id', folder);
+			if (folder) data.append('folderId', folder);
 
 			const xhr = new XMLHttpRequest();
 			xhr.open('POST', apiUrl + '/drive/files/create', true);
diff --git a/src/server/web/app/common/views/components/welcome-timeline.vue b/src/server/web/app/common/views/components/welcome-timeline.vue
index 7586e9264..8f6199732 100644
--- a/src/server/web/app/common/views/components/welcome-timeline.vue
+++ b/src/server/web/app/common/views/components/welcome-timeline.vue
@@ -2,7 +2,7 @@
 <div class="mk-welcome-timeline">
 	<div v-for="post in posts">
 		<router-link class="avatar-anchor" :to="`/@${getAcct(post.user)}`" v-user-preview="post.user.id">
-			<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=96`" alt="avatar"/>
+			<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
 		</router-link>
 		<div class="body">
 			<header>
@@ -10,7 +10,7 @@
 				<span class="username">@{{ getAcct(post.user) }}</span>
 				<div class="info">
 					<router-link class="created-at" :to="`/@${getAcct(post.user)}/${post.id}`">
-						<mk-time :time="post.created_at"/>
+						<mk-time :time="post.createdAt"/>
 					</router-link>
 				</div>
 			</header>
diff --git a/src/server/web/app/common/views/directives/autocomplete.ts b/src/server/web/app/common/views/directives/autocomplete.ts
index 3440c4212..94635d301 100644
--- a/src/server/web/app/common/views/directives/autocomplete.ts
+++ b/src/server/web/app/common/views/directives/autocomplete.ts
@@ -77,7 +77,7 @@ class Autocomplete {
 
 		if (mentionIndex != -1 && mentionIndex > emojiIndex) {
 			const username = text.substr(mentionIndex + 1);
-			if (username != '' && username.match(/^[a-zA-Z0-9-]+$/)) {
+			if (username != '' && username.match(/^[a-zA-Z0-9_]+$/)) {
 				this.open('user', username);
 				opened = true;
 			}
diff --git a/src/server/web/app/common/views/widgets/slideshow.vue b/src/server/web/app/common/views/widgets/slideshow.vue
index e9451663e..ad32299f3 100644
--- a/src/server/web/app/common/views/widgets/slideshow.vue
+++ b/src/server/web/app/common/views/widgets/slideshow.vue
@@ -97,7 +97,7 @@ export default define({
 			this.fetching = true;
 
 			(this as any).api('drive/files', {
-				folder_id: this.props.folder,
+				folderId: this.props.folder,
 				type: 'image/*',
 				limit: 100
 			}).then(images => {
diff --git a/src/server/web/app/common/views/widgets/version.vue b/src/server/web/app/common/views/widgets/version.vue
index 5072d9b74..30b632b39 100644
--- a/src/server/web/app/common/views/widgets/version.vue
+++ b/src/server/web/app/common/views/widgets/version.vue
@@ -1,16 +1,17 @@
 <template>
-<p>ver {{ v }} (葵 aoi)</p>
+<p>ver {{ version }} ({{ codename }})</p>
 </template>
 
 <script lang="ts">
-import { version } from '../../../config';
+import { version, codename } from '../../../config';
 import define from '../../../common/define-widget';
 export default define({
 	name: 'version'
 }).extend({
 	data() {
 		return {
-			v: version
+			version,
+			codename
 		};
 	}
 });
diff --git a/src/server/web/app/config.ts b/src/server/web/app/config.ts
index 8ea6f7010..522d7ff05 100644
--- a/src/server/web/app/config.ts
+++ b/src/server/web/app/config.ts
@@ -7,13 +7,13 @@ 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;
 declare const _THEME_COLOR_: string;
 declare const _COPYRIGHT_: string;
 declare const _VERSION_: string;
+declare const _CODENAME_: string;
 declare const _LICENSE_: string;
 declare const _GOOGLE_MAPS_API_KEY_: string;
 
@@ -26,12 +26,12 @@ 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_;
 export const themeColor = _THEME_COLOR_;
 export const copyright = _COPYRIGHT_;
 export const version = _VERSION_;
+export const codename = _CODENAME_;
 export const license = _LICENSE_;
 export const googleMapsApiKey = _GOOGLE_MAPS_API_KEY_;
diff --git a/src/server/web/app/desktop/api/update-avatar.ts b/src/server/web/app/desktop/api/update-avatar.ts
index 8f748d853..36a2ffe91 100644
--- a/src/server/web/app/desktop/api/update-avatar.ts
+++ b/src/server/web/app/desktop/api/update-avatar.ts
@@ -49,7 +49,7 @@ export default (os: OS) => (cb, file = null) => {
 		}).$mount();
 		document.body.appendChild(dialog.$el);
 
-		if (folder) data.append('folder_id', folder.id);
+		if (folder) data.append('folderId', folder.id);
 
 		const xhr = new XMLHttpRequest();
 		xhr.open('POST', apiUrl + '/drive/files/create', true);
@@ -68,10 +68,10 @@ export default (os: OS) => (cb, file = null) => {
 
 	const set = file => {
 		os.api('i/update', {
-			avatar_id: file.id
+			avatarId: file.id
 		}).then(i => {
-			os.i.avatar_id = i.avatar_id;
-			os.i.avatar_url = i.avatar_url;
+			os.i.avatarId = i.avatarId;
+			os.i.avatarUrl = i.avatarUrl;
 
 			os.apis.dialog({
 				title: '%fa:info-circle%アバターを更新しました',
diff --git a/src/server/web/app/desktop/api/update-banner.ts b/src/server/web/app/desktop/api/update-banner.ts
index 9ed48b267..e66dbf016 100644
--- a/src/server/web/app/desktop/api/update-banner.ts
+++ b/src/server/web/app/desktop/api/update-banner.ts
@@ -49,7 +49,7 @@ export default (os: OS) => (cb, file = null) => {
 		}).$mount();
 		document.body.appendChild(dialog.$el);
 
-		if (folder) data.append('folder_id', folder.id);
+		if (folder) data.append('folderId', folder.id);
 
 		const xhr = new XMLHttpRequest();
 		xhr.open('POST', apiUrl + '/drive/files/create', true);
@@ -68,10 +68,10 @@ export default (os: OS) => (cb, file = null) => {
 
 	const set = file => {
 		os.api('i/update', {
-			banner_id: file.id
+			bannerId: file.id
 		}).then(i => {
-			os.i.banner_id = i.banner_id;
-			os.i.banner_url = i.banner_url;
+			os.i.bannerId = i.bannerId;
+			os.i.bannerUrl = i.bannerUrl;
 
 			os.apis.dialog({
 				title: '%fa:info-circle%バナーを更新しました',
diff --git a/src/server/web/app/desktop/views/components/activity.vue b/src/server/web/app/desktop/views/components/activity.vue
index 33b53eb70..480b956ec 100644
--- a/src/server/web/app/desktop/views/components/activity.vue
+++ b/src/server/web/app/desktop/views/components/activity.vue
@@ -43,7 +43,7 @@ export default Vue.extend({
 	},
 	mounted() {
 		(this as any).api('aggregation/users/activity', {
-			user_id: this.user.id,
+			userId: this.user.id,
 			limit: 20 * 7
 		}).then(activity => {
 			this.activity = activity;
diff --git a/src/server/web/app/desktop/views/components/drive.file.vue b/src/server/web/app/desktop/views/components/drive.file.vue
index 924ff7052..85f8361c9 100644
--- a/src/server/web/app/desktop/views/components/drive.file.vue
+++ b/src/server/web/app/desktop/views/components/drive.file.vue
@@ -9,10 +9,10 @@
 	@contextmenu.prevent.stop="onContextmenu"
 	:title="title"
 >
-	<div class="label" v-if="os.i.avatar_id == file.id"><img src="/assets/label.svg"/>
+	<div class="label" v-if="os.i.avatarId == file.id"><img src="/assets/label.svg"/>
 		<p>%i18n:desktop.tags.mk-drive-browser-file.avatar%</p>
 	</div>
-	<div class="label" v-if="os.i.banner_id == file.id"><img src="/assets/label.svg"/>
+	<div class="label" v-if="os.i.bannerId == 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: ${ background }`">
@@ -50,8 +50,8 @@ export default Vue.extend({
 			return `${this.file.name}\n${this.file.type} ${Vue.filter('bytes')(this.file.datasize)}`;
 		},
 		background(): string {
-			return this.file.properties.average_color
-				? `rgb(${this.file.properties.average_color.join(',')})`
+			return this.file.properties.avgColor
+				? `rgb(${this.file.properties.avgColor.join(',')})`
 				: 'transparent';
 		}
 	},
@@ -129,10 +129,10 @@ export default Vue.extend({
 		},
 
 		onThumbnailLoaded() {
-			if (this.file.properties.average_color) {
+			if (this.file.properties.avgColor) {
 				anime({
 					targets: this.$refs.thumbnail,
-					backgroundColor: `rgba(${this.file.properties.average_color.join(',')}, 0)`,
+					backgroundColor: `rgba(${this.file.properties.avgColor.join(',')}, 0)`,
 					duration: 100,
 					easing: 'linear'
 				});
@@ -147,7 +147,7 @@ export default Vue.extend({
 				allowEmpty: false
 			}).then(name => {
 				(this as any).api('drive/files/update', {
-					file_id: this.file.id,
+					fileId: this.file.id,
 					name: name
 				})
 			});
diff --git a/src/server/web/app/desktop/views/components/drive.folder.vue b/src/server/web/app/desktop/views/components/drive.folder.vue
index a8a9a0137..a926bf47b 100644
--- a/src/server/web/app/desktop/views/components/drive.folder.vue
+++ b/src/server/web/app/desktop/views/components/drive.folder.vue
@@ -135,8 +135,8 @@ export default Vue.extend({
 				const file = JSON.parse(driveFile);
 				this.browser.removeFile(file.id);
 				(this as any).api('drive/files/update', {
-					file_id: file.id,
-					folder_id: this.folder.id
+					fileId: file.id,
+					folderId: this.folder.id
 				});
 			}
 			//#endregion
@@ -151,8 +151,8 @@ export default Vue.extend({
 
 				this.browser.removeFolder(folder.id);
 				(this as any).api('drive/folders/update', {
-					folder_id: folder.id,
-					parent_id: this.folder.id
+					folderId: folder.id,
+					parentId: this.folder.id
 				}).then(() => {
 					// noop
 				}).catch(err => {
@@ -204,7 +204,7 @@ export default Vue.extend({
 				default: this.folder.name
 			}).then(name => {
 				(this as any).api('drive/folders/update', {
-					folder_id: this.folder.id,
+					folderId: this.folder.id,
 					name: name
 				});
 			});
diff --git a/src/server/web/app/desktop/views/components/drive.nav-folder.vue b/src/server/web/app/desktop/views/components/drive.nav-folder.vue
index dfbf116bf..d885a72f7 100644
--- a/src/server/web/app/desktop/views/components/drive.nav-folder.vue
+++ b/src/server/web/app/desktop/views/components/drive.nav-folder.vue
@@ -78,8 +78,8 @@ export default Vue.extend({
 				const file = JSON.parse(driveFile);
 				this.browser.removeFile(file.id);
 				(this as any).api('drive/files/update', {
-					file_id: file.id,
-					folder_id: this.folder ? this.folder.id : null
+					fileId: file.id,
+					folderId: this.folder ? this.folder.id : null
 				});
 			}
 			//#endregion
@@ -92,8 +92,8 @@ export default Vue.extend({
 				if (this.folder && folder.id == this.folder.id) return;
 				this.browser.removeFolder(folder.id);
 				(this as any).api('drive/folders/update', {
-					folder_id: folder.id,
-					parent_id: this.folder ? this.folder.id : null
+					folderId: folder.id,
+					parentId: this.folder ? this.folder.id : null
 				});
 			}
 			//#endregion
diff --git a/src/server/web/app/desktop/views/components/drive.vue b/src/server/web/app/desktop/views/components/drive.vue
index 0fafa8cf2..c766dfec1 100644
--- a/src/server/web/app/desktop/views/components/drive.vue
+++ b/src/server/web/app/desktop/views/components/drive.vue
@@ -160,7 +160,7 @@ export default Vue.extend({
 
 		onStreamDriveFileUpdated(file) {
 			const current = this.folder ? this.folder.id : null;
-			if (current != file.folder_id) {
+			if (current != file.folderId) {
 				this.removeFile(file);
 			} else {
 				this.addFile(file, true);
@@ -173,7 +173,7 @@ export default Vue.extend({
 
 		onStreamDriveFolderUpdated(folder) {
 			const current = this.folder ? this.folder.id : null;
-			if (current != folder.parent_id) {
+			if (current != folder.parentId) {
 				this.removeFolder(folder);
 			} else {
 				this.addFolder(folder, true);
@@ -282,8 +282,8 @@ export default Vue.extend({
 				if (this.files.some(f => f.id == file.id)) return;
 				this.removeFile(file.id);
 				(this as any).api('drive/files/update', {
-					file_id: file.id,
-					folder_id: this.folder ? this.folder.id : null
+					fileId: file.id,
+					folderId: this.folder ? this.folder.id : null
 				});
 			}
 			//#endregion
@@ -298,8 +298,8 @@ export default Vue.extend({
 				if (this.folders.some(f => f.id == folder.id)) return false;
 				this.removeFolder(folder.id);
 				(this as any).api('drive/folders/update', {
-					folder_id: folder.id,
-					parent_id: this.folder ? this.folder.id : null
+					folderId: folder.id,
+					parentId: this.folder ? this.folder.id : null
 				}).then(() => {
 					// noop
 				}).catch(err => {
@@ -332,7 +332,7 @@ export default Vue.extend({
 			}).then(url => {
 				(this as any).api('drive/files/upload_from_url', {
 					url: url,
-					folder_id: this.folder ? this.folder.id : undefined
+					folderId: this.folder ? this.folder.id : undefined
 				});
 
 				(this as any).apis.dialog({
@@ -352,7 +352,7 @@ export default Vue.extend({
 			}).then(name => {
 				(this as any).api('drive/folders/create', {
 					name: name,
-					folder_id: this.folder ? this.folder.id : undefined
+					folderId: this.folder ? this.folder.id : undefined
 				}).then(folder => {
 					this.addFolder(folder, true);
 				});
@@ -412,7 +412,7 @@ export default Vue.extend({
 			this.fetching = true;
 
 			(this as any).api('drive/folders/show', {
-				folder_id: target
+				folderId: target
 			}).then(folder => {
 				this.folder = folder;
 				this.hierarchyFolders = [];
@@ -431,7 +431,7 @@ export default Vue.extend({
 
 		addFolder(folder, unshift = false) {
 			const current = this.folder ? this.folder.id : null;
-			if (current != folder.parent_id) return;
+			if (current != folder.parentId) return;
 
 			if (this.folders.some(f => f.id == folder.id)) {
 				const exist = this.folders.map(f => f.id).indexOf(folder.id);
@@ -448,7 +448,7 @@ export default Vue.extend({
 
 		addFile(file, unshift = false) {
 			const current = this.folder ? this.folder.id : null;
-			if (current != file.folder_id) return;
+			if (current != file.folderId) return;
 
 			if (this.files.some(f => f.id == file.id)) {
 				const exist = this.files.map(f => f.id).indexOf(file.id);
@@ -514,7 +514,7 @@ export default Vue.extend({
 
 			// フォルダ一覧取得
 			(this as any).api('drive/folders', {
-				folder_id: this.folder ? this.folder.id : null,
+				folderId: this.folder ? this.folder.id : null,
 				limit: foldersMax + 1
 			}).then(folders => {
 				if (folders.length == foldersMax + 1) {
@@ -527,7 +527,7 @@ export default Vue.extend({
 
 			// ファイル一覧取得
 			(this as any).api('drive/files', {
-				folder_id: this.folder ? this.folder.id : null,
+				folderId: this.folder ? this.folder.id : null,
 				limit: filesMax + 1
 			}).then(files => {
 				if (files.length == filesMax + 1) {
@@ -557,7 +557,7 @@ export default Vue.extend({
 
 			// ファイル一覧取得
 			(this as any).api('drive/files', {
-				folder_id: this.folder ? this.folder.id : null,
+				folderId: this.folder ? this.folder.id : null,
 				limit: max + 1
 			}).then(files => {
 				if (files.length == max + 1) {
diff --git a/src/server/web/app/desktop/views/components/follow-button.vue b/src/server/web/app/desktop/views/components/follow-button.vue
index fc4f87188..9eb22b0fb 100644
--- a/src/server/web/app/desktop/views/components/follow-button.vue
+++ b/src/server/web/app/desktop/views/components/follow-button.vue
@@ -1,15 +1,15 @@
 <template>
 <button class="mk-follow-button"
-	:class="{ wait, follow: !user.is_following, unfollow: user.is_following, big: size == 'big' }"
+	:class="{ wait, follow: !user.isFollowing, unfollow: user.isFollowing, big: size == 'big' }"
 	@click="onClick"
 	:disabled="wait"
-	:title="user.is_following ? 'フォロー解除' : 'フォローする'"
+	:title="user.isFollowing ? 'フォロー解除' : 'フォローする'"
 >
-	<template v-if="!wait && user.is_following">
+	<template v-if="!wait && user.isFollowing">
 		<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="!wait && !user.isFollowing">
 		<template v-if="size == 'compact'">%fa:plus%</template>
 		<template v-if="size == 'big'">%fa:plus%フォロー</template>
 	</template>
@@ -53,23 +53,23 @@ export default Vue.extend({
 
 		onFollow(user) {
 			if (user.id == this.user.id) {
-				this.user.is_following = user.is_following;
+				this.user.isFollowing = user.isFollowing;
 			}
 		},
 
 		onUnfollow(user) {
 			if (user.id == this.user.id) {
-				this.user.is_following = user.is_following;
+				this.user.isFollowing = user.isFollowing;
 			}
 		},
 
 		onClick() {
 			this.wait = true;
-			if (this.user.is_following) {
+			if (this.user.isFollowing) {
 				(this as any).api('following/delete', {
-					user_id: this.user.id
+					userId: this.user.id
 				}).then(() => {
-					this.user.is_following = false;
+					this.user.isFollowing = false;
 				}).catch(err => {
 					console.error(err);
 				}).then(() => {
@@ -77,9 +77,9 @@ export default Vue.extend({
 				});
 			} else {
 				(this as any).api('following/create', {
-					user_id: this.user.id
+					userId: this.user.id
 				}).then(() => {
-					this.user.is_following = true;
+					this.user.isFollowing = true;
 				}).catch(err => {
 					console.error(err);
 				}).then(() => {
diff --git a/src/server/web/app/desktop/views/components/followers-window.vue b/src/server/web/app/desktop/views/components/followers-window.vue
index d41d356f9..623971fa3 100644
--- a/src/server/web/app/desktop/views/components/followers-window.vue
+++ b/src/server/web/app/desktop/views/components/followers-window.vue
@@ -1,7 +1,7 @@
 <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 }}のフォロワー
+		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ user.name }}のフォロワー
 	</span>
 	<mk-followers :user="user"/>
 </mk-window>
diff --git a/src/server/web/app/desktop/views/components/followers.vue b/src/server/web/app/desktop/views/components/followers.vue
index 4541a0007..a1b98995d 100644
--- a/src/server/web/app/desktop/views/components/followers.vue
+++ b/src/server/web/app/desktop/views/components/followers.vue
@@ -1,8 +1,8 @@
 <template>
 <mk-users-list
 	:fetch="fetch"
-	:count="user.followers_count"
-	:you-know-count="user.followers_you_know_count"
+	:count="user.followersCount"
+	:you-know-count="user.followersYouKnowCount"
 >
 	フォロワーはいないようです。
 </mk-users-list>
@@ -15,7 +15,7 @@ export default Vue.extend({
 	methods: {
 		fetch(iknow, limit, cursor, cb) {
 			(this as any).api('users/followers', {
-				user_id: this.user.id,
+				userId: this.user.id,
 				iknow: iknow,
 				limit: limit,
 				cursor: cursor ? cursor : undefined
diff --git a/src/server/web/app/desktop/views/components/following-window.vue b/src/server/web/app/desktop/views/components/following-window.vue
index c516b3b17..612847b38 100644
--- a/src/server/web/app/desktop/views/components/following-window.vue
+++ b/src/server/web/app/desktop/views/components/following-window.vue
@@ -1,7 +1,7 @@
 <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 }}のフォロー
+		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ user.name }}のフォロー
 	</span>
 	<mk-following :user="user"/>
 </mk-window>
diff --git a/src/server/web/app/desktop/views/components/following.vue b/src/server/web/app/desktop/views/components/following.vue
index e0b9f1169..b7aedda84 100644
--- a/src/server/web/app/desktop/views/components/following.vue
+++ b/src/server/web/app/desktop/views/components/following.vue
@@ -1,8 +1,8 @@
 <template>
 <mk-users-list
 	:fetch="fetch"
-	:count="user.following_count"
-	:you-know-count="user.following_you_know_count"
+	:count="user.followingCount"
+	:you-know-count="user.followingYouKnowCount"
 >
 	フォロー中のユーザーはいないようです。
 </mk-users-list>
@@ -15,7 +15,7 @@ export default Vue.extend({
 	methods: {
 		fetch(iknow, limit, cursor, cb) {
 			(this as any).api('users/following', {
-				user_id: this.user.id,
+				userId: this.user.id,
 				iknow: iknow,
 				limit: limit,
 				cursor: cursor ? cursor : undefined
diff --git a/src/server/web/app/desktop/views/components/friends-maker.vue b/src/server/web/app/desktop/views/components/friends-maker.vue
index eed15e077..fd9914b15 100644
--- a/src/server/web/app/desktop/views/components/friends-maker.vue
+++ b/src/server/web/app/desktop/views/components/friends-maker.vue
@@ -4,7 +4,7 @@
 	<div class="users" v-if="!fetching && users.length > 0">
 		<div class="user" v-for="user in users" :key="user.id">
 			<router-link class="avatar-anchor" :to="`/@${getAcct(user)}`">
-				<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=42`" alt="" v-user-preview="user.id"/>
+				<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="user.id"/>
 			</router-link>
 			<div class="body">
 				<router-link class="name" :to="`/@${getAcct(user)}`" v-user-preview="user.id">{{ user.name }}</router-link>
diff --git a/src/server/web/app/desktop/views/components/home.vue b/src/server/web/app/desktop/views/components/home.vue
index a4ce1ef94..7145ddce0 100644
--- a/src/server/web/app/desktop/views/components/home.vue
+++ b/src/server/web/app/desktop/views/components/home.vue
@@ -53,7 +53,7 @@
 			<div class="main">
 				<a @click="hint">カスタマイズのヒント</a>
 				<div>
-					<mk-post-form v-if="os.i.account.client_settings.showPostFormOnTopOfTl"/>
+					<mk-post-form v-if="os.i.account.clientSettings.showPostFormOnTopOfTl"/>
 					<mk-timeline ref="tl" @loaded="onTlLoaded"/>
 				</div>
 			</div>
@@ -63,7 +63,7 @@
 				<component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" @chosen="warp"/>
 			</div>
 			<div class="main">
-				<mk-post-form v-if="os.i.account.client_settings.showPostFormOnTopOfTl"/>
+				<mk-post-form v-if="os.i.account.clientSettings.showPostFormOnTopOfTl"/>
 				<mk-timeline ref="tl" @loaded="onTlLoaded" v-if="mode == 'timeline'"/>
 				<mk-mentions @loaded="onTlLoaded" v-if="mode == 'mentions'"/>
 			</div>
@@ -104,16 +104,16 @@ export default Vue.extend({
 		home: {
 			get(): any[] {
 				//#region 互換性のため
-				(this as any).os.i.account.client_settings.home.forEach(w => {
+				(this as any).os.i.account.clientSettings.home.forEach(w => {
 					if (w.name == 'rss-reader') w.name = 'rss';
 					if (w.name == 'user-recommendation') w.name = 'users';
 					if (w.name == 'recommended-polls') w.name = 'polls';
 				});
 				//#endregion
-				return (this as any).os.i.account.client_settings.home;
+				return (this as any).os.i.account.clientSettings.home;
 			},
 			set(value) {
-				(this as any).os.i.account.client_settings.home = value;
+				(this as any).os.i.account.clientSettings.home = value;
 			}
 		},
 		left(): any[] {
@@ -126,7 +126,7 @@ export default Vue.extend({
 	created() {
 		this.widgets.left = this.left;
 		this.widgets.right = this.right;
-		this.$watch('os.i.account.client_settings', i => {
+		this.$watch('os.i.account.clientSettings', i => {
 			this.widgets.left = this.left;
 			this.widgets.right = this.right;
 		}, {
@@ -161,17 +161,17 @@ export default Vue.extend({
 		},
 		onHomeUpdated(data) {
 			if (data.home) {
-				(this as any).os.i.account.client_settings.home = data.home;
+				(this as any).os.i.account.clientSettings.home = data.home;
 				this.widgets.left = data.home.filter(w => w.place == 'left');
 				this.widgets.right = data.home.filter(w => w.place == 'right');
 			} else {
-				const w = (this as any).os.i.account.client_settings.home.find(w => w.id == data.id);
+				const w = (this as any).os.i.account.clientSettings.home.find(w => w.id == data.id);
 				if (w != null) {
 					w.data = data.data;
 					this.$refs[w.id][0].preventSave = true;
 					this.$refs[w.id][0].props = w.data;
-					this.widgets.left = (this as any).os.i.account.client_settings.home.filter(w => w.place == 'left');
-					this.widgets.right = (this as any).os.i.account.client_settings.home.filter(w => w.place == 'right');
+					this.widgets.left = (this as any).os.i.account.clientSettings.home.filter(w => w.place == 'left');
+					this.widgets.right = (this as any).os.i.account.clientSettings.home.filter(w => w.place == 'right');
 				}
 			}
 		},
diff --git a/src/server/web/app/desktop/views/components/media-image.vue b/src/server/web/app/desktop/views/components/media-image.vue
index bc02d0f9b..51309a057 100644
--- a/src/server/web/app/desktop/views/components/media-image.vue
+++ b/src/server/web/app/desktop/views/components/media-image.vue
@@ -18,7 +18,7 @@ export default Vue.extend({
 	computed: {
 		style(): any {
 			return {
-				'background-color': this.image.properties.average_color ? `rgb(${this.image.properties.average_color.join(',')})` : 'transparent',
+				'background-color': this.image.properties.avgColor ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
 				'background-image': `url(${this.image.url}?thumbnail&size=512)`
 			};
 		}
diff --git a/src/server/web/app/desktop/views/components/mentions.vue b/src/server/web/app/desktop/views/components/mentions.vue
index 47066e813..90a92495b 100644
--- a/src/server/web/app/desktop/views/components/mentions.vue
+++ b/src/server/web/app/desktop/views/components/mentions.vue
@@ -70,7 +70,7 @@ export default Vue.extend({
 			this.moreFetching = true;
 			(this as any).api('posts/mentions', {
 				following: this.mode == 'following',
-				until_id: this.posts[this.posts.length - 1].id
+				untilId: this.posts[this.posts.length - 1].id
 			}).then(posts => {
 				this.posts = this.posts.concat(posts);
 				this.moreFetching = false;
diff --git a/src/server/web/app/desktop/views/components/notifications.vue b/src/server/web/app/desktop/views/components/notifications.vue
index b48ffc174..5e6db08c1 100644
--- a/src/server/web/app/desktop/views/components/notifications.vue
+++ b/src/server/web/app/desktop/views/components/notifications.vue
@@ -3,10 +3,10 @@
 	<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"/>
+				<mk-time :time="notification.createdAt"/>
 				<template v-if="notification.type == 'reaction'">
 					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">
-						<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
+						<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>
@@ -19,12 +19,12 @@
 					</div>
 				</template>
 				<template v-if="notification.type == 'repost'">
-					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.user_id">
-						<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
+					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">
+						<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:retweet%
-							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</router-link>
+							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ notification.post.user.name }}</router-link>
 						</p>
 						<router-link class="post-ref" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">
 							%fa:quote-left%{{ getPostSummary(notification.post.repost) }}%fa:quote-right%
@@ -32,19 +32,19 @@
 					</div>
 				</template>
 				<template v-if="notification.type == 'quote'">
-					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.user_id">
-						<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
+					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">
+						<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:quote-left%
-							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</router-link>
+							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ notification.post.user.name }}</router-link>
 						</p>
 						<router-link class="post-preview" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link>
 					</div>
 				</template>
 				<template v-if="notification.type == 'follow'">
 					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">
-						<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
+						<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:user-plus%
@@ -53,30 +53,30 @@
 					</div>
 				</template>
 				<template v-if="notification.type == 'reply'">
-					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.user_id">
-						<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
+					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">
+						<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:reply%
-							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</router-link>
+							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ notification.post.user.name }}</router-link>
 						</p>
 						<router-link class="post-preview" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link>
 					</div>
 				</template>
 				<template v-if="notification.type == 'mention'">
-					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.user_id">
-						<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
+					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">
+						<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:at%
-							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</router-link>
+							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ notification.post.user.name }}</router-link>
 						</p>
 						<a class="post-preview" :href="`/@${getAcct(notification.post.user)}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</a>
 					</div>
 				</template>
 				<template v-if="notification.type == 'poll_vote'">
 					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">
-						<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
+						<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:chart-pie%<a :href="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ notification.user.name }}</a></p>
@@ -120,8 +120,8 @@ export default Vue.extend({
 	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;
+				const date = new Date(notification.createdAt).getDate();
+				const month = new Date(notification.createdAt).getMonth() + 1;
 				notification._date = date;
 				notification._datetext = `${month}月 ${date}日`;
 				return notification;
@@ -161,7 +161,7 @@ export default Vue.extend({
 
 			(this as any).api('i/notifications', {
 				limit: max + 1,
-				until_id: this.notifications[this.notifications.length - 1].id
+				untilId: this.notifications[this.notifications.length - 1].id
 			}).then(notifications => {
 				if (notifications.length == max + 1) {
 					this.moreNotifications = true;
diff --git a/src/server/web/app/desktop/views/components/post-detail.sub.vue b/src/server/web/app/desktop/views/components/post-detail.sub.vue
index 59d8db04c..35377e7c2 100644
--- a/src/server/web/app/desktop/views/components/post-detail.sub.vue
+++ b/src/server/web/app/desktop/views/components/post-detail.sub.vue
@@ -1,17 +1,17 @@
 <template>
 <div class="sub" :title="title">
 	<router-link class="avatar-anchor" :to="`/@${acct}`">
-		<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.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="post.userId"/>
 	</router-link>
 	<div class="main">
 		<header>
 			<div class="left">
-				<router-link class="name" :to="`/@${acct}`" v-user-preview="post.user_id">{{ post.user.name }}</router-link>
+				<router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ post.user.name }}</router-link>
 				<span class="username">@{{ acct }}</span>
 			</div>
 			<div class="right">
 				<router-link class="time" :to="`/@${acct}/${post.id}`">
-					<mk-time :time="post.created_at"/>
+					<mk-time :time="post.createdAt"/>
 				</router-link>
 			</div>
 		</header>
@@ -37,7 +37,7 @@ export default Vue.extend({
 			return getAcct(this.post.user);
 		},
 		title(): string {
-			return dateStringify(this.post.created_at);
+			return dateStringify(this.post.createdAt);
 		}
 	}
 });
diff --git a/src/server/web/app/desktop/views/components/post-detail.vue b/src/server/web/app/desktop/views/components/post-detail.vue
index f09bf4cbd..7783ec62c 100644
--- a/src/server/web/app/desktop/views/components/post-detail.vue
+++ b/src/server/web/app/desktop/views/components/post-detail.vue
@@ -2,7 +2,7 @@
 <div class="mk-post-detail" :title="title">
 	<button
 		class="read-more"
-		v-if="p.reply && p.reply.reply_id && context == null"
+		v-if="p.reply && p.reply.replyId && context == null"
 		title="会話をもっと読み込む"
 		@click="fetchContext"
 		:disabled="contextFetching"
@@ -18,8 +18,8 @@
 	</div>
 	<div class="repost" v-if="isRepost">
 		<p>
-			<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="post.user_id">
-				<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=32`" alt="avatar"/>
+			<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="post.userId">
+				<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
 			</router-link>
 			%fa:retweet%
 			<router-link class="name" :href="`/@${acct}`">{{ post.user.name }}</router-link>
@@ -28,13 +28,13 @@
 	</div>
 	<article>
 		<router-link class="avatar-anchor" :to="`/@${acct}`">
-			<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.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
 		</router-link>
 		<header>
 			<router-link class="name" :to="`/@${acct}`" v-user-preview="p.user.id">{{ p.user.name }}</router-link>
 			<span class="username">@{{ acct }}</span>
 			<router-link class="time" :to="`/@${acct}/${p.id}`">
-				<mk-time :time="p.created_at"/>
+				<mk-time :time="p.createdAt"/>
 			</router-link>
 		</header>
 		<div class="body">
@@ -56,12 +56,12 @@
 		<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>
+				%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</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.repostCount > 0">{{ p.repostCount }}</p>
 			</button>
-			<button :class="{ reacted: p.my_reaction != null }" @click="react" ref="reactButton" title="リアクション">
+			<button :class="{ reacted: p.myReaction != 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">
@@ -115,21 +115,21 @@ export default Vue.extend({
 		isRepost(): boolean {
 			return (this.post.repost &&
 				this.post.text == null &&
-				this.post.media_ids == null &&
+				this.post.mediaIds == 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])
+			return this.p.reactionCounts
+				? Object.keys(this.p.reactionCounts)
+					.map(key => this.p.reactionCounts[key])
 					.reduce((a, b) => a + b)
 				: 0;
 		},
 		title(): string {
-			return dateStringify(this.p.created_at);
+			return dateStringify(this.p.createdAt);
 		},
 		urls(): string[] {
 			if (this.p.ast) {
@@ -145,7 +145,7 @@ export default Vue.extend({
 		// Get replies
 		if (!this.compact) {
 			(this as any).api('posts/replies', {
-				post_id: this.p.id,
+				postId: this.p.id,
 				limit: 8
 			}).then(replies => {
 				this.replies = replies;
@@ -154,7 +154,7 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.client_settings.showMaps : true;
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
 					const uluru = new maps.LatLng(this.p.geo.latitude, this.p.geo.longitude);
@@ -176,7 +176,7 @@ export default Vue.extend({
 
 			// Fetch context
 			(this as any).api('posts/context', {
-				post_id: this.p.reply_id
+				postId: this.p.replyId
 			}).then(context => {
 				this.contextFetching = false;
 				this.context = context.reverse();
diff --git a/src/server/web/app/desktop/views/components/post-form.vue b/src/server/web/app/desktop/views/components/post-form.vue
index 78f6d445a..11028ceb5 100644
--- a/src/server/web/app/desktop/views/components/post-form.vue
+++ b/src/server/web/app/desktop/views/components/post-form.vue
@@ -219,9 +219,9 @@ export default Vue.extend({
 
 			(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,
-				repost_id: this.repost ? this.repost.id : undefined,
+				mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
+				replyId: this.reply ? this.reply.id : undefined,
+				repostId: this.repost ? this.repost.id : undefined,
 				poll: this.poll ? (this.$refs.poll as any).get() : undefined,
 				geo: this.geo ? {
 					latitude: this.geo.latitude,
@@ -255,7 +255,7 @@ export default Vue.extend({
 			const data = JSON.parse(localStorage.getItem('drafts') || '{}');
 
 			data[this.draftId] = {
-				updated_at: new Date(),
+				updatedAt: new Date(),
 				data: {
 					text: this.text,
 					files: this.files,
diff --git a/src/server/web/app/desktop/views/components/post-preview.vue b/src/server/web/app/desktop/views/components/post-preview.vue
index 808220c0e..0ac3223be 100644
--- a/src/server/web/app/desktop/views/components/post-preview.vue
+++ b/src/server/web/app/desktop/views/components/post-preview.vue
@@ -1,14 +1,14 @@
 <template>
 <div class="mk-post-preview" :title="title">
 	<router-link class="avatar-anchor" :to="`/@${acct}`">
-		<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.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="post.userId"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`" v-user-preview="post.user_id">{{ post.user.name }}</router-link>
+			<router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ post.user.name }}</router-link>
 			<span class="username">@{{ acct }}</span>
 			<router-link class="time" :to="`/@${acct}/${post.id}`">
-				<mk-time :time="post.created_at"/>
+				<mk-time :time="post.createdAt"/>
 			</router-link>
 		</header>
 		<div class="body">
@@ -30,7 +30,7 @@ export default Vue.extend({
 			return getAcct(this.post.user);
 		},
 		title(): string {
-			return dateStringify(this.post.created_at);
+			return dateStringify(this.post.createdAt);
 		}
 	}
 });
diff --git a/src/server/web/app/desktop/views/components/posts.post.sub.vue b/src/server/web/app/desktop/views/components/posts.post.sub.vue
index 120700877..65d3017d3 100644
--- a/src/server/web/app/desktop/views/components/posts.post.sub.vue
+++ b/src/server/web/app/desktop/views/components/posts.post.sub.vue
@@ -1,14 +1,14 @@
 <template>
 <div class="sub" :title="title">
 	<router-link class="avatar-anchor" :to="`/@${acct}`">
-		<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.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="post.userId"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`" v-user-preview="post.user_id">{{ post.user.name }}</router-link>
+			<router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ post.user.name }}</router-link>
 			<span class="username">@{{ acct }}</span>
 			<router-link class="created-at" :to="`/@${acct}/${post.id}`">
-				<mk-time :time="post.created_at"/>
+				<mk-time :time="post.createdAt"/>
 			</router-link>
 		</header>
 		<div class="body">
@@ -30,7 +30,7 @@ export default Vue.extend({
 			return getAcct(this.post.user);
 		},
 		title(): string {
-			return dateStringify(this.post.created_at);
+			return dateStringify(this.post.createdAt);
 		}
 	}
 });
diff --git a/src/server/web/app/desktop/views/components/posts.post.vue b/src/server/web/app/desktop/views/components/posts.post.vue
index 6b4d3d278..c70e01911 100644
--- a/src/server/web/app/desktop/views/components/posts.post.vue
+++ b/src/server/web/app/desktop/views/components/posts.post.vue
@@ -5,30 +5,30 @@
 	</div>
 	<div class="repost" v-if="isRepost">
 		<p>
-			<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="post.user_id">
-				<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=32`" alt="avatar"/>
+			<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="post.userId">
+				<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
 			</router-link>
 			%fa:retweet%
 			<span>{{ '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('{')) }}</span>
-			<a class="name" :href="`/@${acct}`" v-user-preview="post.user_id">{{ post.user.name }}</a>
+			<a class="name" :href="`/@${acct}`" v-user-preview="post.userId">{{ post.user.name }}</a>
 			<span>{{ '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1) }}</span>
 		</p>
-		<mk-time :time="post.created_at"/>
+		<mk-time :time="post.createdAt"/>
 	</div>
 	<article>
 		<router-link class="avatar-anchor" :to="`/@${acct}`">
-			<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.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
 		</router-link>
 		<div class="main">
 			<header>
 				<router-link class="name" :to="`/@${acct}`" v-user-preview="p.user.id">{{ acct }}</router-link>
-				<span class="is-bot" v-if="p.user.host === null && p.user.account.is_bot">bot</span>
+				<span class="is-bot" v-if="p.user.host === null && p.user.account.isBot">bot</span>
 				<span class="username">@{{ acct }}</span>
 				<div class="info">
 					<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
-					<span class="mobile" v-if="p.via_mobile">%fa:mobile-alt%</span>
+					<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
 					<router-link class="created-at" :to="url">
-						<mk-time :time="p.created_at"/>
+						<mk-time :time="p.createdAt"/>
 					</router-link>
 				</div>
 			</header>
@@ -58,12 +58,12 @@
 			<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>
+					%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</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>
+					%fa:retweet%<p class="count" v-if="p.repostCount > 0">{{ p.repostCount }}</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.myReaction != 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">
@@ -122,21 +122,21 @@ export default Vue.extend({
 		isRepost(): boolean {
 			return (this.post.repost &&
 				this.post.text == null &&
-				this.post.media_ids == null &&
+				this.post.mediaIds == 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])
+			return this.p.reactionCounts
+				? Object.keys(this.p.reactionCounts)
+					.map(key => this.p.reactionCounts[key])
 					.reduce((a, b) => a + b)
 				: 0;
 		},
 		title(): string {
-			return dateStringify(this.p.created_at);
+			return dateStringify(this.p.createdAt);
 		},
 		url(): string {
 			return `/@${this.acct}/${this.p.id}`;
@@ -166,7 +166,7 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.client_settings.showMaps : true;
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
 					const uluru = new maps.LatLng(this.p.geo.latitude, this.p.geo.longitude);
@@ -216,7 +216,7 @@ export default Vue.extend({
 			const post = data.post;
 			if (post.id == this.post.id) {
 				this.$emit('update:post', post);
-			} else if (post.id == this.post.repost_id) {
+			} else if (post.id == this.post.repostId) {
 				this.post.repost = post;
 			}
 		},
diff --git a/src/server/web/app/desktop/views/components/posts.vue b/src/server/web/app/desktop/views/components/posts.vue
index ffceff876..5031667c7 100644
--- a/src/server/web/app/desktop/views/components/posts.vue
+++ b/src/server/web/app/desktop/views/components/posts.vue
@@ -30,8 +30,8 @@ export default Vue.extend({
 	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;
+				const date = new Date(post.createdAt).getDate();
+				const month = new Date(post.createdAt).getMonth() + 1;
 				post._date = date;
 				post._datetext = `${month}月 ${date}日`;
 				return post;
diff --git a/src/server/web/app/desktop/views/components/repost-form.vue b/src/server/web/app/desktop/views/components/repost-form.vue
index f2774b817..3a5e3a7c5 100644
--- a/src/server/web/app/desktop/views/components/repost-form.vue
+++ b/src/server/web/app/desktop/views/components/repost-form.vue
@@ -29,7 +29,7 @@ export default Vue.extend({
 		ok() {
 			this.wait = true;
 			(this as any).api('posts/create', {
-				repost_id: this.post.id
+				repostId: this.post.id
 			}).then(data => {
 				this.$emit('posted');
 				(this as any).apis.notify('%i18n:desktop.tags.mk-repost-form.success%');
diff --git a/src/server/web/app/desktop/views/components/settings.2fa.vue b/src/server/web/app/desktop/views/components/settings.2fa.vue
index 85f2d6ba5..b8dd1dfd9 100644
--- a/src/server/web/app/desktop/views/components/settings.2fa.vue
+++ b/src/server/web/app/desktop/views/components/settings.2fa.vue
@@ -2,8 +2,8 @@
 <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 && !os.i.account.two_factor_enabled"><button @click="register" class="ui primary">%i18n:desktop.tags.mk-2fa-setting.register%</button></p>
-	<template v-if="os.i.account.two_factor_enabled">
+	<p v-if="!data && !os.i.account.twoFactorEnabled"><button @click="register" class="ui primary">%i18n:desktop.tags.mk-2fa-setting.register%</button></p>
+	<template v-if="os.i.account.twoFactorEnabled">
 		<p>%i18n:desktop.tags.mk-2fa-setting.already-registered%</p>
 		<button @click="unregister" class="ui">%i18n:desktop.tags.mk-2fa-setting.unregister%</button>
 	</template>
@@ -54,7 +54,7 @@ export default Vue.extend({
 					password: password
 				}).then(() => {
 					(this as any).apis.notify('%i18n:desktop.tags.mk-2fa-setting.unregistered%');
-					(this as any).os.i.account.two_factor_enabled = false;
+					(this as any).os.i.account.twoFactorEnabled = false;
 				});
 			});
 		},
@@ -64,7 +64,7 @@ export default Vue.extend({
 				token: this.token
 			}).then(() => {
 				(this as any).apis.notify('%i18n:desktop.tags.mk-2fa-setting.success%');
-				(this as any).os.i.account.two_factor_enabled = true;
+				(this as any).os.i.account.twoFactorEnabled = true;
 			}).catch(() => {
 				(this as any).apis.notify('%i18n:desktop.tags.mk-2fa-setting.failed%');
 			});
diff --git a/src/server/web/app/desktop/views/components/settings.password.vue b/src/server/web/app/desktop/views/components/settings.password.vue
index be3f0370d..f883b5406 100644
--- a/src/server/web/app/desktop/views/components/settings.password.vue
+++ b/src/server/web/app/desktop/views/components/settings.password.vue
@@ -33,8 +33,8 @@ export default Vue.extend({
 							return;
 						}
 						(this as any).api('i/change_password', {
-							current_password: currentPassword,
-							new_password: newPassword
+							currentPasword: currentPassword,
+							newPassword: newPassword
 						}).then(() => {
 							(this as any).apis.notify('%i18n:desktop.tags.mk-password-setting.changed%');
 						});
diff --git a/src/server/web/app/desktop/views/components/settings.profile.vue b/src/server/web/app/desktop/views/components/settings.profile.vue
index 67a211c79..ba86286f8 100644
--- a/src/server/web/app/desktop/views/components/settings.profile.vue
+++ b/src/server/web/app/desktop/views/components/settings.profile.vue
@@ -2,7 +2,7 @@
 <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"/>
+		<img class="avatar" :src="`${os.i.avatarUrl}?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">
@@ -24,7 +24,7 @@
 	<button class="ui primary" @click="save">%i18n:desktop.tags.mk-profile-setting.save%</button>
 	<section>
 		<h2>その他</h2>
-		<mk-switch v-model="os.i.account.is_bot" @change="onChangeIsBot" text="このアカウントはbotです"/>
+		<mk-switch v-model="os.i.account.isBot" @change="onChangeIsBot" text="このアカウントはbotです"/>
 	</section>
 </div>
 </template>
@@ -63,7 +63,7 @@ export default Vue.extend({
 		},
 		onChangeIsBot() {
 			(this as any).api('i/update', {
-				is_bot: (this as any).os.i.account.is_bot
+				isBot: (this as any).os.i.account.isBot
 			});
 		}
 	}
diff --git a/src/server/web/app/desktop/views/components/settings.signins.vue b/src/server/web/app/desktop/views/components/settings.signins.vue
index ddc567f06..a414c95c2 100644
--- a/src/server/web/app/desktop/views/components/settings.signins.vue
+++ b/src/server/web/app/desktop/views/components/settings.signins.vue
@@ -6,7 +6,7 @@
 			<template v-if="signin.success">%fa:check%</template>
 			<template v-else>%fa:times%</template>
 			<span class="ip">{{ signin.ip }}</span>
-			<mk-time :time="signin.created_at"/>
+			<mk-time :time="signin.createdAt"/>
 		</header>
 		<div class="headers" v-show="signin._show">
 			<tree-view :data="signin.headers"/>
diff --git a/src/server/web/app/desktop/views/components/settings.vue b/src/server/web/app/desktop/views/components/settings.vue
index 3e6a477ce..fd82c171c 100644
--- a/src/server/web/app/desktop/views/components/settings.vue
+++ b/src/server/web/app/desktop/views/components/settings.vue
@@ -20,7 +20,7 @@
 
 		<section class="web" v-show="page == 'web'">
 			<h1>動作</h1>
-			<mk-switch v-model="os.i.account.client_settings.fetchOnScroll" @change="onChangeFetchOnScroll" text="スクロールで自動読み込み">
+			<mk-switch v-model="os.i.account.clientSettings.fetchOnScroll" @change="onChangeFetchOnScroll" text="スクロールで自動読み込み">
 				<span>ページを下までスクロールしたときに自動で追加のコンテンツを読み込みます。</span>
 			</mk-switch>
 			<mk-switch v-model="autoPopout" text="ウィンドウの自動ポップアウト">
@@ -33,11 +33,11 @@
 			<div class="div">
 				<button class="ui button" @click="customizeHome">ホームをカスタマイズ</button>
 			</div>
-			<mk-switch v-model="os.i.account.client_settings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="タイムライン上部に投稿フォームを表示する"/>
-			<mk-switch v-model="os.i.account.client_settings.showMaps" @change="onChangeShowMaps" text="マップの自動展開">
+			<mk-switch v-model="os.i.account.clientSettings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="タイムライン上部に投稿フォームを表示する"/>
+			<mk-switch v-model="os.i.account.clientSettings.showMaps" @change="onChangeShowMaps" text="マップの自動展開">
 				<span>位置情報が添付された投稿のマップを自動的に展開します。</span>
 			</mk-switch>
-			<mk-switch v-model="os.i.account.client_settings.gradientWindowHeader" @change="onChangeGradientWindowHeader" text="ウィンドウのタイトルバーにグラデーションを使用"/>
+			<mk-switch v-model="os.i.account.clientSettings.gradientWindowHeader" @change="onChangeGradientWindowHeader" text="ウィンドウのタイトルバーにグラデーションを使用"/>
 		</section>
 
 		<section class="web" v-show="page == 'web'">
@@ -57,7 +57,7 @@
 
 		<section class="web" v-show="page == 'web'">
 			<h1>モバイル</h1>
-			<mk-switch v-model="os.i.account.client_settings.disableViaMobile" @change="onChangeDisableViaMobile" text="「モバイルからの投稿」フラグを付けない"/>
+			<mk-switch v-model="os.i.account.clientSettings.disableViaMobile" @change="onChangeDisableViaMobile" text="「モバイルからの投稿」フラグを付けない"/>
 		</section>
 
 		<section class="web" v-show="page == 'web'">
@@ -86,7 +86,7 @@
 
 		<section class="notification" v-show="page == 'notification'">
 			<h1>通知</h1>
-			<mk-switch v-model="os.i.account.settings.auto_watch" @change="onChangeAutoWatch" text="投稿の自動ウォッチ">
+			<mk-switch v-model="os.i.account.settings.autoWatch" @change="onChangeAutoWatch" text="投稿の自動ウォッチ">
 				<span>リアクションしたり返信したりした投稿に関する通知を自動的に受け取るようにします。</span>
 			</mk-switch>
 		</section>
@@ -283,7 +283,7 @@ export default Vue.extend({
 		},
 		onChangeAutoWatch(v) {
 			(this as any).api('i/update', {
-				auto_watch: v
+				autoWatch: v
 			});
 		},
 		onChangeShowPostFormOnTopOfTl(v) {
diff --git a/src/server/web/app/desktop/views/components/sub-post-content.vue b/src/server/web/app/desktop/views/components/sub-post-content.vue
index 8c8f42c80..f13822331 100644
--- a/src/server/web/app/desktop/views/components/sub-post-content.vue
+++ b/src/server/web/app/desktop/views/components/sub-post-content.vue
@@ -1,9 +1,9 @@
 <template>
 <div class="mk-sub-post-content">
 	<div class="body">
-		<a class="reply" v-if="post.reply_id">%fa:reply%</a>
+		<a class="reply" v-if="post.replyId">%fa:reply%</a>
 		<mk-post-html :ast="post.ast" :i="os.i"/>
-		<a class="rp" v-if="post.repost_id" :href="`/post:${post.repost_id}`">RP: ...</a>
+		<a class="rp" v-if="post.repostId" :href="`/post:${post.repostId}`">RP: ...</a>
 		<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 	</div>
 	<details v-if="post.media">
diff --git a/src/server/web/app/desktop/views/components/timeline.vue b/src/server/web/app/desktop/views/components/timeline.vue
index 47a9688b6..65b4bd1c7 100644
--- a/src/server/web/app/desktop/views/components/timeline.vue
+++ b/src/server/web/app/desktop/views/components/timeline.vue
@@ -34,7 +34,7 @@ export default Vue.extend({
 	},
 	computed: {
 		alone(): boolean {
-			return (this as any).os.i.following_count == 0;
+			return (this as any).os.i.followingCount == 0;
 		}
 	},
 	mounted() {
@@ -65,7 +65,7 @@ export default Vue.extend({
 
 			(this as any).api('posts/timeline', {
 				limit: 11,
-				until_date: this.date ? this.date.getTime() : undefined
+				untilDate: this.date ? this.date.getTime() : undefined
 			}).then(posts => {
 				if (posts.length == 11) {
 					posts.pop();
@@ -82,7 +82,7 @@ export default Vue.extend({
 			this.moreFetching = true;
 			(this as any).api('posts/timeline', {
 				limit: 11,
-				until_id: this.posts[this.posts.length - 1].id
+				untilId: this.posts[this.posts.length - 1].id
 			}).then(posts => {
 				if (posts.length == 11) {
 					posts.pop();
@@ -107,7 +107,7 @@ export default Vue.extend({
 			this.fetch();
 		},
 		onScroll() {
-			if ((this as any).os.i.account.client_settings.fetchOnScroll !== false) {
+			if ((this as any).os.i.account.clientSettings.fetchOnScroll !== false) {
 				const current = window.scrollY + window.innerHeight;
 				if (current > document.body.offsetHeight - 8) this.more();
 			}
diff --git a/src/server/web/app/desktop/views/components/ui.header.account.vue b/src/server/web/app/desktop/views/components/ui.header.account.vue
index 19b9d7779..ec4635f33 100644
--- a/src/server/web/app/desktop/views/components/ui.header.account.vue
+++ b/src/server/web/app/desktop/views/components/ui.header.account.vue
@@ -2,7 +2,7 @@
 <div class="account">
 	<button class="header" :data-active="isOpen" @click="toggle">
 		<span class="username">{{ os.i.username }}<template v-if="!isOpen">%fa:angle-down%</template><template v-if="isOpen">%fa:angle-up%</template></span>
-		<img class="avatar" :src="`${ os.i.avatar_url }?thumbnail&size=64`" alt="avatar"/>
+		<img class="avatar" :src="`${ os.i.avatarUrl }?thumbnail&size=64`" alt="avatar"/>
 	</button>
 	<transition name="zoom-in-top">
 		<div class="menu" v-if="isOpen">
diff --git a/src/server/web/app/desktop/views/components/ui.header.vue b/src/server/web/app/desktop/views/components/ui.header.vue
index 8af0e2fbe..7e337d2ae 100644
--- a/src/server/web/app/desktop/views/components/ui.header.vue
+++ b/src/server/web/app/desktop/views/components/ui.header.vue
@@ -44,9 +44,9 @@ export default Vue.extend({
 	},
 	mounted() {
 		if ((this as any).os.isSignedIn) {
-			const ago = (new Date().getTime() - new Date((this as any).os.i.account.last_used_at).getTime()) / 1000
+			const ago = (new Date().getTime() - new Date((this as any).os.i.account.lastUsedAt).getTime()) / 1000
 			const isHisasiburi = ago >= 3600;
-			(this as any).os.i.account.last_used_at = new Date();
+			(this as any).os.i.account.lastUsedAt = new Date();
 			if (isHisasiburi) {
 				(this.$refs.welcomeback as any).style.display = 'block';
 				(this.$refs.main as any).style.overflow = 'hidden';
diff --git a/src/server/web/app/desktop/views/components/user-preview.vue b/src/server/web/app/desktop/views/components/user-preview.vue
index 24d613f12..8c86b2efe 100644
--- a/src/server/web/app/desktop/views/components/user-preview.vue
+++ b/src/server/web/app/desktop/views/components/user-preview.vue
@@ -1,9 +1,9 @@
 <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>
+		<div class="banner" :style="u.bannerUrl ? `background-image: url(${u.bannerUrl}?thumbnail&size=512)` : ''"></div>
 		<router-link class="avatar" :to="`/@${acct}`">
-			<img :src="`${u.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+			<img :src="`${u.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="title">
 			<router-link class="name" :to="`/@${acct}`">{{ u.name }}</router-link>
@@ -12,13 +12,13 @@
 		<div class="description">{{ u.description }}</div>
 		<div class="status">
 			<div>
-				<p>投稿</p><a>{{ u.posts_count }}</a>
+				<p>投稿</p><a>{{ u.postsCount }}</a>
 			</div>
 			<div>
-				<p>フォロー</p><a>{{ u.following_count }}</a>
+				<p>フォロー</p><a>{{ u.followingCount }}</a>
 			</div>
 			<div>
-				<p>フォロワー</p><a>{{ u.followers_count }}</a>
+				<p>フォロワー</p><a>{{ u.followersCount }}</a>
 			</div>
 		</div>
 		<mk-follow-button v-if="os.isSignedIn && user.id != os.i.id" :user="u"/>
@@ -58,7 +58,7 @@ export default Vue.extend({
 		} else {
 			const query = this.user[0] == '@' ?
 				parseAcct(this.user[0].substr(1)) :
-				{ user_id: this.user[0] };
+				{ userId: this.user[0] };
 
 			(this as any).api('users/show', query).then(user => {
 				this.u = user;
diff --git a/src/server/web/app/desktop/views/components/users-list.item.vue b/src/server/web/app/desktop/views/components/users-list.item.vue
index e02d1311d..d2bfc117d 100644
--- a/src/server/web/app/desktop/views/components/users-list.item.vue
+++ b/src/server/web/app/desktop/views/components/users-list.item.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="root item">
 	<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="user.id">
-		<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 	</router-link>
 	<div class="main">
 		<header>
@@ -9,7 +9,7 @@
 			<span class="username">@{{ acct }}</span>
 		</header>
 		<div class="body">
-			<p class="followed" v-if="user.is_followed">フォローされています</p>
+			<p class="followed" v-if="user.isFollowed">フォローされています</p>
 			<div class="description">{{ user.description }}</div>
 		</div>
 	</div>
diff --git a/src/server/web/app/desktop/views/components/widget-container.vue b/src/server/web/app/desktop/views/components/widget-container.vue
index dd42be63b..68c5bcb8d 100644
--- a/src/server/web/app/desktop/views/components/widget-container.vue
+++ b/src/server/web/app/desktop/views/components/widget-container.vue
@@ -24,8 +24,8 @@ export default Vue.extend({
 	computed: {
 		withGradient(): boolean {
 			return (this as any).os.isSignedIn
-				? (this as any).os.i.account.client_settings.gradientWindowHeader != null
-					? (this as any).os.i.account.client_settings.gradientWindowHeader
+				? (this as any).os.i.account.clientSettings.gradientWindowHeader != null
+					? (this as any).os.i.account.clientSettings.gradientWindowHeader
 					: false
 				: false;
 		}
diff --git a/src/server/web/app/desktop/views/components/window.vue b/src/server/web/app/desktop/views/components/window.vue
index 75f725d4b..48dc46feb 100644
--- a/src/server/web/app/desktop/views/components/window.vue
+++ b/src/server/web/app/desktop/views/components/window.vue
@@ -92,8 +92,8 @@ export default Vue.extend({
 		},
 		withGradient(): boolean {
 			return (this as any).os.isSignedIn
-				? (this as any).os.i.account.client_settings.gradientWindowHeader != null
-					? (this as any).os.i.account.client_settings.gradientWindowHeader
+				? (this as any).os.i.account.clientSettings.gradientWindowHeader != null
+					? (this as any).os.i.account.clientSettings.gradientWindowHeader
 					: false
 				: false;
 		}
diff --git a/src/server/web/app/desktop/views/pages/home.vue b/src/server/web/app/desktop/views/pages/home.vue
index e1464bab1..69e134f79 100644
--- a/src/server/web/app/desktop/views/pages/home.vue
+++ b/src/server/web/app/desktop/views/pages/home.vue
@@ -45,7 +45,7 @@ export default Vue.extend({
 		},
 
 		onStreamPost(post) {
-			if (document.hidden && post.user_id != (this as any).os.i.id) {
+			if (document.hidden && post.userId != (this as any).os.i.id) {
 				this.unreadCount++;
 				document.title = `(${this.unreadCount}) ${getPostSummary(post)}`;
 			}
diff --git a/src/server/web/app/desktop/views/pages/othello.vue b/src/server/web/app/desktop/views/pages/othello.vue
index 160dd9a35..0d8e987dd 100644
--- a/src/server/web/app/desktop/views/pages/othello.vue
+++ b/src/server/web/app/desktop/views/pages/othello.vue
@@ -34,7 +34,7 @@ export default Vue.extend({
 			this.fetching = true;
 
 			(this as any).api('othello/games/show', {
-				game_id: this.$route.params.game
+				gameId: this.$route.params.game
 			}).then(game => {
 				this.game = game;
 				this.fetching = false;
diff --git a/src/server/web/app/desktop/views/pages/post.vue b/src/server/web/app/desktop/views/pages/post.vue
index c7b8729b7..dbd707e04 100644
--- a/src/server/web/app/desktop/views/pages/post.vue
+++ b/src/server/web/app/desktop/views/pages/post.vue
@@ -31,7 +31,7 @@ export default Vue.extend({
 			this.fetching = true;
 
 			(this as any).api('posts/show', {
-				post_id: this.$route.params.post
+				postId: this.$route.params.post
 			}).then(post => {
 				this.post = post;
 				this.fetching = false;
diff --git a/src/server/web/app/desktop/views/pages/user/user.followers-you-know.vue b/src/server/web/app/desktop/views/pages/user/user.followers-you-know.vue
index 80b38e8ac..d0dab6c3d 100644
--- a/src/server/web/app/desktop/views/pages/user/user.followers-you-know.vue
+++ b/src/server/web/app/desktop/views/pages/user/user.followers-you-know.vue
@@ -4,7 +4,7 @@
 	<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="`/@${getAcct(user)}`" :key="user.id">
-		<img :src="`${user.avatar_url}?thumbnail&size=64`" :alt="user.name" v-user-preview="user.id"/>
+		<img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="user.name" v-user-preview="user.id"/>
 	</router-link>
 	</div>
 	<p class="empty" v-if="!fetching && users.length == 0">%i18n:desktop.tags.mk-user.followers-you-know.no-users%</p>
@@ -28,7 +28,7 @@ export default Vue.extend({
 	},
 	mounted() {
 		(this as any).api('users/followers', {
-			user_id: this.user.id,
+			userId: this.user.id,
 			iknow: true,
 			limit: 16
 		}).then(x => {
diff --git a/src/server/web/app/desktop/views/pages/user/user.friends.vue b/src/server/web/app/desktop/views/pages/user/user.friends.vue
index 57e6def27..3ec30fb43 100644
--- a/src/server/web/app/desktop/views/pages/user/user.friends.vue
+++ b/src/server/web/app/desktop/views/pages/user/user.friends.vue
@@ -5,7 +5,7 @@
 	<template v-if="!fetching && users.length != 0">
 		<div class="user" v-for="friend in users">
 			<router-link class="avatar-anchor" :to="`/@${getAcct(friend)}`">
-				<img class="avatar" :src="`${friend.avatar_url}?thumbnail&size=42`" alt="" v-user-preview="friend.id"/>
+				<img class="avatar" :src="`${friend.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="friend.id"/>
 			</router-link>
 			<div class="body">
 				<router-link class="name" :to="`/@${getAcct(friend)}`" v-user-preview="friend.id">{{ friend.name }}</router-link>
@@ -35,7 +35,7 @@ export default Vue.extend({
 	},
 	mounted() {
 		(this as any).api('users/get_frequently_replied_users', {
-			user_id: this.user.id,
+			userId: this.user.id,
 			limit: 4
 		}).then(docs => {
 			this.users = docs.map(doc => doc.user);
diff --git a/src/server/web/app/desktop/views/pages/user/user.header.vue b/src/server/web/app/desktop/views/pages/user/user.header.vue
index 3522e76bd..54f431fd2 100644
--- a/src/server/web/app/desktop/views/pages/user/user.header.vue
+++ b/src/server/web/app/desktop/views/pages/user/user.header.vue
@@ -1,11 +1,11 @@
 <template>
-<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 class="header" :data-is-dark-background="user.bannerUrl != null">
+	<div class="banner-container" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=2048)` : ''">
+		<div class="banner" ref="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?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"/>
+		<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=150`" alt="avatar"/>
 		<div class="title">
 			<p class="name">{{ user.name }}</p>
 			<p class="username">@{{ acct }}</p>
@@ -59,7 +59,7 @@ export default Vue.extend({
 			if (!(this as any).os.isSignedIn || (this as any).os.i.id != this.user.id) return;
 
 			(this as any).apis.updateBanner((this as any).os.i, i => {
-				this.user.banner_url = i.banner_url;
+				this.user.bannerUrl = i.bannerUrl;
 			});
 		}
 	}
diff --git a/src/server/web/app/desktop/views/pages/user/user.home.vue b/src/server/web/app/desktop/views/pages/user/user.home.vue
index 2483a6c72..071c9bb61 100644
--- a/src/server/web/app/desktop/views/pages/user/user.home.vue
+++ b/src/server/web/app/desktop/views/pages/user/user.home.vue
@@ -5,16 +5,16 @@
 			<x-profile :user="user"/>
 			<x-photos :user="user"/>
 			<x-followers-you-know v-if="os.isSignedIn && os.i.id != user.id" :user="user"/>
-			<p v-if="user.host === null">%i18n:desktop.tags.mk-user.last-used-at%: <b><mk-time :time="user.account.last_used_at"/></b></p>
+			<p v-if="user.host === null">%i18n:desktop.tags.mk-user.last-used-at%: <b><mk-time :time="user.account.lastUsedAt"/></b></p>
 		</div>
 	</div>
 	<main>
-		<mk-post-detail v-if="user.pinned_post" :post="user.pinned_post" :compact="true"/>
+		<mk-post-detail v-if="user.pinnedPost" :post="user.pinnedPost" :compact="true"/>
 		<x-timeline class="timeline" ref="tl" :user="user"/>
 	</main>
 	<div>
 		<div ref="right">
-			<mk-calendar @chosen="warp" :start="new Date(user.created_at)"/>
+			<mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/>
 			<mk-activity :user="user"/>
 			<x-friends :user="user"/>
 			<div class="nav"><mk-nav/></div>
diff --git a/src/server/web/app/desktop/views/pages/user/user.photos.vue b/src/server/web/app/desktop/views/pages/user/user.photos.vue
index db29a9945..1ff79b4ae 100644
--- a/src/server/web/app/desktop/views/pages/user/user.photos.vue
+++ b/src/server/web/app/desktop/views/pages/user/user.photos.vue
@@ -23,8 +23,8 @@ export default Vue.extend({
 	},
 	mounted() {
 		(this as any).api('users/posts', {
-			user_id: this.user.id,
-			with_media: true,
+			userId: this.user.id,
+			withMedia: true,
 			limit: 9
 		}).then(posts => {
 			posts.forEach(post => {
diff --git a/src/server/web/app/desktop/views/pages/user/user.profile.vue b/src/server/web/app/desktop/views/pages/user/user.profile.vue
index b51aae18f..f5562d091 100644
--- a/src/server/web/app/desktop/views/pages/user/user.profile.vue
+++ b/src/server/web/app/desktop/views/pages/user/user.profile.vue
@@ -2,21 +2,21 @@
 <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>
-		<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>
+		<p class="followed" v-if="user.isFollowed">%i18n:desktop.tags.mk-user.follows-you%</p>
+		<p v-if="user.isMuted">%i18n:desktop.tags.mk-user.muted% <a @click="unmute">%i18n:desktop.tags.mk-user.unmute%</a></p>
+		<p v-if="!user.isMuted"><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.host === null && user.account.profile.birthday">
 		<p>%fa:birthday-cake%{{ user.account.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳)</p>
 	</div>
 	<div class="twitter" v-if="user.host === null && user.account.twitter">
-		<p>%fa:B twitter%<a :href="`https://twitter.com/${user.account.twitter.screen_name}`" target="_blank">@{{ user.account.twitter.screen_name }}</a></p>
+		<p>%fa:B twitter%<a :href="`https://twitter.com/${user.account.twitter.screenName}`" target="_blank">@{{ user.account.twitter.screenName }}</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>
+		<p class="posts-count">%fa:angle-right%<a>{{ user.postsCount }}</a><b>投稿</b></p>
+		<p class="following">%fa:angle-right%<a @click="showFollowing">{{ user.followingCount }}</a>人を<b>フォロー</b></p>
+		<p class="followers">%fa:angle-right%<a @click="showFollowers">{{ user.followersCount }}</a>人の<b>フォロワー</b></p>
 	</div>
 </div>
 </template>
@@ -49,9 +49,9 @@ export default Vue.extend({
 
 		mute() {
 			(this as any).api('mute/create', {
-				user_id: this.user.id
+				userId: this.user.id
 			}).then(() => {
-				this.user.is_muted = true;
+				this.user.isMuted = true;
 			}, () => {
 				alert('error');
 			});
@@ -59,9 +59,9 @@ export default Vue.extend({
 
 		unmute() {
 			(this as any).api('mute/delete', {
-				user_id: this.user.id
+				userId: this.user.id
 			}).then(() => {
-				this.user.is_muted = false;
+				this.user.isMuted = false;
 			}, () => {
 				alert('error');
 			});
diff --git a/src/server/web/app/desktop/views/pages/user/user.timeline.vue b/src/server/web/app/desktop/views/pages/user/user.timeline.vue
index 60eef8951..134ad423c 100644
--- a/src/server/web/app/desktop/views/pages/user/user.timeline.vue
+++ b/src/server/web/app/desktop/views/pages/user/user.timeline.vue
@@ -61,8 +61,8 @@ export default Vue.extend({
 		},
 		fetch(cb?) {
 			(this as any).api('users/posts', {
-				user_id: this.user.id,
-				until_date: this.date ? this.date.getTime() : undefined,
+				userId: this.user.id,
+				untilDate: this.date ? this.date.getTime() : undefined,
 				with_replies: this.mode == 'with-replies'
 			}).then(posts => {
 				this.posts = posts;
@@ -74,9 +74,9 @@ export default Vue.extend({
 			if (this.moreFetching || this.fetching || this.posts.length == 0) return;
 			this.moreFetching = true;
 			(this as any).api('users/posts', {
-				user_id: this.user.id,
+				userId: this.user.id,
 				with_replies: this.mode == 'with-replies',
-				until_id: this.posts[this.posts.length - 1].id
+				untilId: this.posts[this.posts.length - 1].id
 			}).then(posts => {
 				this.moreFetching = false;
 				this.posts = this.posts.concat(posts);
diff --git a/src/server/web/app/desktop/views/pages/welcome.vue b/src/server/web/app/desktop/views/pages/welcome.vue
index 927ddf575..34c28854b 100644
--- a/src/server/web/app/desktop/views/pages/welcome.vue
+++ b/src/server/web/app/desktop/views/pages/welcome.vue
@@ -9,7 +9,7 @@
 					<p><button class="signup" @click="signup">はじめる</button><button class="signin" @click="signin">ログイン</button></p>
 					<div class="users">
 						<router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="`/@${getAcct(user)}`" v-user-preview="user.id">
-							<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+							<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 						</router-link>
 					</div>
 				</div>
diff --git a/src/server/web/app/desktop/views/widgets/channel.channel.form.vue b/src/server/web/app/desktop/views/widgets/channel.channel.form.vue
index 392ba5924..aaf327f1e 100644
--- a/src/server/web/app/desktop/views/widgets/channel.channel.form.vue
+++ b/src/server/web/app/desktop/views/widgets/channel.channel.form.vue
@@ -30,8 +30,8 @@ export default Vue.extend({
 
 			(this as any).api('posts/create', {
 				text: this.text,
-				reply_id: reply ? reply.id : undefined,
-				channel_id: (this.$parent as any).channel.id
+				replyId: reply ? reply.id : undefined,
+				channelId: (this.$parent as any).channel.id
 			}).then(data => {
 				this.text = '';
 			}).catch(err => {
diff --git a/src/server/web/app/desktop/views/widgets/channel.channel.vue b/src/server/web/app/desktop/views/widgets/channel.channel.vue
index de5885bfc..e9fb9e3fd 100644
--- a/src/server/web/app/desktop/views/widgets/channel.channel.vue
+++ b/src/server/web/app/desktop/views/widgets/channel.channel.vue
@@ -44,7 +44,7 @@ export default Vue.extend({
 			this.fetching = true;
 
 			(this as any).api('channels/posts', {
-				channel_id: this.channel.id
+				channelId: this.channel.id
 			}).then(posts => {
 				this.posts = posts;
 				this.fetching = false;
diff --git a/src/server/web/app/desktop/views/widgets/channel.vue b/src/server/web/app/desktop/views/widgets/channel.vue
index fc143bb1d..c9b62dfea 100644
--- a/src/server/web/app/desktop/views/widgets/channel.vue
+++ b/src/server/web/app/desktop/views/widgets/channel.vue
@@ -48,7 +48,7 @@ export default define({
 			this.fetching = true;
 
 			(this as any).api('channels/show', {
-				channel_id: this.props.channel
+				channelId: this.props.channel
 			}).then(channel => {
 				this.channel = channel;
 				this.fetching = false;
diff --git a/src/server/web/app/desktop/views/widgets/profile.vue b/src/server/web/app/desktop/views/widgets/profile.vue
index 394010619..83cd67b50 100644
--- a/src/server/web/app/desktop/views/widgets/profile.vue
+++ b/src/server/web/app/desktop/views/widgets/profile.vue
@@ -4,12 +4,12 @@
 	:data-melt="props.design == 2"
 >
 	<div class="banner"
-		:style="os.i.banner_url ? `background-image: url(${os.i.banner_url}?thumbnail&size=256)` : ''"
+		:style="os.i.bannerUrl ? `background-image: url(${os.i.bannerUrl}?thumbnail&size=256)` : ''"
 		title="クリックでバナー編集"
 		@click="os.apis.updateBanner"
 	></div>
 	<img class="avatar"
-		:src="`${os.i.avatar_url}?thumbnail&size=96`"
+		:src="`${os.i.avatarUrl}?thumbnail&size=96`"
 		@click="os.apis.updateAvatar"
 		alt="avatar"
 		title="クリックでアバター編集"
diff --git a/src/server/web/app/desktop/views/widgets/users.vue b/src/server/web/app/desktop/views/widgets/users.vue
index 10e3c529e..7b8944112 100644
--- a/src/server/web/app/desktop/views/widgets/users.vue
+++ b/src/server/web/app/desktop/views/widgets/users.vue
@@ -8,7 +8,7 @@
 	<template v-else-if="users.length != 0">
 		<div class="user" v-for="_user in users">
 			<router-link class="avatar-anchor" :to="`/@${getAcct(_user)}`">
-				<img class="avatar" :src="`${_user.avatar_url}?thumbnail&size=42`" alt="" v-user-preview="_user.id"/>
+				<img class="avatar" :src="`${_user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="_user.id"/>
 			</router-link>
 			<div class="body">
 				<router-link class="name" :to="`/@${getAcct(_user)}`" v-user-preview="_user.id">{{ _user.name }}</router-link>
diff --git a/src/server/web/app/dev/views/app.vue b/src/server/web/app/dev/views/app.vue
index 2c2a3c83c..a35b032b7 100644
--- a/src/server/web/app/dev/views/app.vue
+++ b/src/server/web/app/dev/views/app.vue
@@ -28,7 +28,7 @@ export default Vue.extend({
 		fetch() {
 			this.fetching = true;
 			(this as any).api('app/show', {
-				app_id: this.$route.params.id
+				appId: this.$route.params.id
 			}).then(app => {
 				this.app = app;
 				this.fetching = false;
diff --git a/src/server/web/app/dev/views/new-app.vue b/src/server/web/app/dev/views/new-app.vue
index 344e8468f..e407ca00d 100644
--- a/src/server/web/app/dev/views/new-app.vue
+++ b/src/server/web/app/dev/views/new-app.vue
@@ -6,12 +6,12 @@
 				<b-form-input v-model="name" type="text" placeholder="ex) Misskey for iOS" autocomplete="off" required/>
 			</b-form-group>
 			<b-form-group label="ID" description="あなたのアプリのID。">
-				<b-input v-model="nid" type="text" pattern="^[a-zA-Z0-9-]{3,30}$" placeholder="ex) misskey-for-ios" autocomplete="off" required/>
+				<b-input v-model="nid" type="text" pattern="^[a-zA-Z0-9_]{3,30}$" placeholder="ex) misskey-for-ios" autocomplete="off" required/>
 				<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 == '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>
 			</b-form-group>
@@ -77,8 +77,8 @@ export default Vue.extend({
 
 			this.nidState = 'wait';
 
-			(this as any).api('app/name_id/available', {
-				name_id: this.nid
+			(this as any).api('app/nameId/available', {
+				nameId: this.nid
 			}).then(result => {
 				this.nidState = result.available ? 'ok' : 'unavailable';
 			}).catch(err => {
@@ -90,9 +90,9 @@ export default Vue.extend({
 		onSubmit() {
 			(this as any).api('app/create', {
 				name: this.name,
-				name_id: this.nid,
+				nameId: this.nid,
 				description: this.description,
-				callback_url: this.cb,
+				callbackUrl: this.cb,
 				permission: this.permission
 			}).then(() => {
 				location.href = '/apps';
diff --git a/src/server/web/app/init.ts b/src/server/web/app/init.ts
index 521dade86..3e5c38961 100644
--- a/src/server/web/app/init.ts
+++ b/src/server/web/app/init.ts
@@ -14,7 +14,7 @@ import ElementLocaleJa from 'element-ui/lib/locale/lang/ja';
 import App from './app.vue';
 import checkForUpdate from './common/scripts/check-for-update';
 import MiOS, { API } from './common/mios';
-import { version, hostname, lang } from './config';
+import { version, codename, hostname, lang } from './config';
 
 let elementLocale;
 switch (lang) {
@@ -51,7 +51,7 @@ Vue.mixin({
  * APP ENTRY POINT!
  */
 
-console.info(`Misskey v${version} (葵 aoi)`);
+console.info(`Misskey v${version} (${codename})`);
 console.info(
 	'%cここにコードを入力したり張り付けたりしないでください。アカウントが不正利用される可能性があります。',
 	'color: red; background: yellow; font-size: 16px; font-weight: bold;');
diff --git a/src/server/web/app/mobile/api/post.ts b/src/server/web/app/mobile/api/post.ts
index 9b78ce10c..841103fee 100644
--- a/src/server/web/app/mobile/api/post.ts
+++ b/src/server/web/app/mobile/api/post.ts
@@ -18,7 +18,7 @@ export default (os) => (opts) => {
 		const text = window.prompt(`「${getPostSummary(o.repost)}」をRepost`);
 		if (text == null) return;
 		os.api('posts/create', {
-			repost_id: o.repost.id,
+			repostId: o.repost.id,
 			text: text == '' ? undefined : text
 		});
 	} else {
diff --git a/src/server/web/app/mobile/views/components/activity.vue b/src/server/web/app/mobile/views/components/activity.vue
index b50044b3d..2e44017e7 100644
--- a/src/server/web/app/mobile/views/components/activity.vue
+++ b/src/server/web/app/mobile/views/components/activity.vue
@@ -29,7 +29,7 @@ export default Vue.extend({
 	},
 	mounted() {
 		(this as any).api('aggregation/users/activity', {
-			user_id: this.user.id,
+			userId: this.user.id,
 			limit: 30
 		}).then(data => {
 			data.forEach(d => d.total = d.posts + d.replies + d.reposts);
diff --git a/src/server/web/app/mobile/views/components/drive.file-detail.vue b/src/server/web/app/mobile/views/components/drive.file-detail.vue
index e41ebbb45..f3274f677 100644
--- a/src/server/web/app/mobile/views/components/drive.file-detail.vue
+++ b/src/server/web/app/mobile/views/components/drive.file-detail.vue
@@ -29,7 +29,7 @@
 			<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>
+			<span class="created-at" @click="showCreatedAt">%fa:R clock%<mk-time :time="file.createdAt"/></span>
 		</div>
 	</div>
 	<div class="menu">
@@ -86,8 +86,8 @@ export default Vue.extend({
 			return this.file.type.split('/')[0];
 		},
 		style(): any {
-			return this.file.properties.average_color ? {
-				'background-color': `rgb(${ this.file.properties.average_color.join(',') })`
+			return this.file.properties.avgColor ? {
+				'background-color': `rgb(${ this.file.properties.avgColor.join(',') })`
 			} : {};
 		}
 	},
@@ -96,7 +96,7 @@ export default Vue.extend({
 			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,
+				fileId: this.file.id,
 				name: name
 			}).then(() => {
 				this.browser.cf(this.file, true);
@@ -105,15 +105,15 @@ export default Vue.extend({
 		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
+					fileId: this.file.id,
+					folderId: folder == null ? null : folder.id
 				}).then(() => {
 					this.browser.cf(this.file, true);
 				});
 			});
 		},
 		showCreatedAt() {
-			alert(new Date(this.file.created_at).toLocaleString());
+			alert(new Date(this.file.createdAt).toLocaleString());
 		},
 		onImageLoaded() {
 			const self = this;
diff --git a/src/server/web/app/mobile/views/components/drive.file.vue b/src/server/web/app/mobile/views/components/drive.file.vue
index db7381628..7d1957042 100644
--- a/src/server/web/app/mobile/views/components/drive.file.vue
+++ b/src/server/web/app/mobile/views/components/drive.file.vue
@@ -19,7 +19,7 @@
 				<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"/>
+					%fa:R clock%<mk-time :time="file.createdAt"/>
 				</p>
 			</footer>
 		</div>
@@ -42,7 +42,7 @@ export default Vue.extend({
 		},
 		thumbnail(): any {
 			return {
-				'background-color': this.file.properties.average_color ? `rgb(${this.file.properties.average_color.join(',')})` : 'transparent',
+				'background-color': this.file.properties.avgColor ? `rgb(${this.file.properties.avgColor.join(',')})` : 'transparent',
 				'background-image': `url(${this.file.url}?thumbnail&size=128)`
 			};
 		}
diff --git a/src/server/web/app/mobile/views/components/drive.vue b/src/server/web/app/mobile/views/components/drive.vue
index 696c63e2a..ff5366a0a 100644
--- a/src/server/web/app/mobile/views/components/drive.vue
+++ b/src/server/web/app/mobile/views/components/drive.vue
@@ -19,10 +19,10 @@
 	<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 && (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.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 v-if="folder != null && (folder.foldersCount > 0 || folder.filesCount > 0)">
+				<template v-if="folder.foldersCount > 0">{{ folder.foldersCount }} %i18n:mobile.tags.mk-drive.folder-count%</template>
+				<template v-if="folder.foldersCount > 0 && folder.filesCount > 0">%i18n:mobile.tags.mk-drive.count-separator%</template>
+				<template v-if="folder.filesCount > 0">{{ folder.filesCount }} %i18n:mobile.tags.mk-drive.file-count%</template>
 			</p>
 		</div>
 		<div class="folders" v-if="folders.length > 0">
@@ -129,7 +129,7 @@ export default Vue.extend({
 
 		onStreamDriveFileUpdated(file) {
 			const current = this.folder ? this.folder.id : null;
-			if (current != file.folder_id) {
+			if (current != file.folderId) {
 				this.removeFile(file);
 			} else {
 				this.addFile(file, true);
@@ -142,7 +142,7 @@ export default Vue.extend({
 
 		onStreamDriveFolderUpdated(folder) {
 			const current = this.folder ? this.folder.id : null;
-			if (current != folder.parent_id) {
+			if (current != folder.parentId) {
 				this.removeFolder(folder);
 			} else {
 				this.addFolder(folder, true);
@@ -167,7 +167,7 @@ export default Vue.extend({
 			this.fetching = true;
 
 			(this as any).api('drive/folders/show', {
-				folder_id: target
+				folderId: target
 			}).then(folder => {
 				this.folder = folder;
 				this.hierarchyFolders = [];
@@ -182,7 +182,7 @@ export default Vue.extend({
 		addFolder(folder, unshift = false) {
 			const current = this.folder ? this.folder.id : null;
 			// 追加しようとしているフォルダが、今居る階層とは違う階層のものだったら中断
-			if (current != folder.parent_id) return;
+			if (current != folder.parentId) return;
 
 			// 追加しようとしているフォルダを既に所有してたら中断
 			if (this.folders.some(f => f.id == folder.id)) return;
@@ -197,7 +197,7 @@ export default Vue.extend({
 		addFile(file, unshift = false) {
 			const current = this.folder ? this.folder.id : null;
 			// 追加しようとしているファイルが、今居る階層とは違う階層のものだったら中断
-			if (current != file.folder_id) return;
+			if (current != file.folderId) return;
 
 			if (this.files.some(f => f.id == file.id)) {
 				const exist = this.files.map(f => f.id).indexOf(file.id);
@@ -262,7 +262,7 @@ export default Vue.extend({
 
 			// フォルダ一覧取得
 			(this as any).api('drive/folders', {
-				folder_id: this.folder ? this.folder.id : null,
+				folderId: this.folder ? this.folder.id : null,
 				limit: foldersMax + 1
 			}).then(folders => {
 				if (folders.length == foldersMax + 1) {
@@ -275,7 +275,7 @@ export default Vue.extend({
 
 			// ファイル一覧取得
 			(this as any).api('drive/files', {
-				folder_id: this.folder ? this.folder.id : null,
+				folderId: this.folder ? this.folder.id : null,
 				limit: filesMax + 1
 			}).then(files => {
 				if (files.length == filesMax + 1) {
@@ -318,9 +318,9 @@ export default Vue.extend({
 
 			// ファイル一覧取得
 			(this as any).api('drive/files', {
-				folder_id: this.folder ? this.folder.id : null,
+				folderId: this.folder ? this.folder.id : null,
 				limit: max + 1,
-				until_id: this.files[this.files.length - 1].id
+				untilId: this.files[this.files.length - 1].id
 			}).then(files => {
 				if (files.length == max + 1) {
 					this.moreFiles = true;
@@ -357,7 +357,7 @@ export default Vue.extend({
 			this.fetching = true;
 
 			(this as any).api('drive/files/show', {
-				file_id: file
+				fileId: file
 			}).then(file => {
 				this.file = file;
 				this.folder = null;
@@ -405,7 +405,7 @@ export default Vue.extend({
 			if (name == null || name == '') return;
 			(this as any).api('drive/folders/create', {
 				name: name,
-				parent_id: this.folder ? this.folder.id : undefined
+				parentId: this.folder ? this.folder.id : undefined
 			}).then(folder => {
 				this.addFolder(folder, true);
 			});
@@ -420,7 +420,7 @@ export default Vue.extend({
 			if (name == null || name == '') return;
 			(this as any).api('drive/folders/update', {
 				name: name,
-				folder_id: this.folder.id
+				folderId: this.folder.id
 			}).then(folder => {
 				this.cd(folder);
 			});
@@ -433,8 +433,8 @@ export default Vue.extend({
 			}
 			(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
+					parentId: folder ? folder.id : null,
+					folderId: this.folder.id
 				}).then(folder => {
 					this.cd(folder);
 				});
@@ -446,7 +446,7 @@ export default Vue.extend({
 			if (url == null || url == '') return;
 			(this as any).api('drive/files/upload_from_url', {
 				url: url,
-				folder_id: this.folder ? this.folder.id : undefined
+				folderId: this.folder ? this.folder.id : undefined
 			});
 			alert('アップロードをリクエストしました。アップロードが完了するまで時間がかかる場合があります。');
 		},
diff --git a/src/server/web/app/mobile/views/components/follow-button.vue b/src/server/web/app/mobile/views/components/follow-button.vue
index fb6eaa39c..43c69d4e0 100644
--- a/src/server/web/app/mobile/views/components/follow-button.vue
+++ b/src/server/web/app/mobile/views/components/follow-button.vue
@@ -1,13 +1,13 @@
 <template>
 <button class="mk-follow-button"
-	:class="{ wait: wait, follow: !user.is_following, unfollow: user.is_following }"
+	:class="{ wait: wait, follow: !user.isFollowing, unfollow: user.isFollowing }"
 	@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 && user.isFollowing">%fa:minus%</template>
+	<template v-if="!wait && !user.isFollowing">%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%' }}
+	{{ user.isFollowing ? '%i18n:mobile.tags.mk-follow-button.unfollow%' : '%i18n:mobile.tags.mk-follow-button.follow%' }}
 </button>
 </template>
 
@@ -43,23 +43,23 @@ export default Vue.extend({
 
 		onFollow(user) {
 			if (user.id == this.user.id) {
-				this.user.is_following = user.is_following;
+				this.user.isFollowing = user.isFollowing;
 			}
 		},
 
 		onUnfollow(user) {
 			if (user.id == this.user.id) {
-				this.user.is_following = user.is_following;
+				this.user.isFollowing = user.isFollowing;
 			}
 		},
 
 		onClick() {
 			this.wait = true;
-			if (this.user.is_following) {
+			if (this.user.isFollowing) {
 				(this as any).api('following/delete', {
-					user_id: this.user.id
+					userId: this.user.id
 				}).then(() => {
-					this.user.is_following = false;
+					this.user.isFollowing = false;
 				}).catch(err => {
 					console.error(err);
 				}).then(() => {
@@ -67,9 +67,9 @@ export default Vue.extend({
 				});
 			} else {
 				(this as any).api('following/create', {
-					user_id: this.user.id
+					userId: this.user.id
 				}).then(() => {
-					this.user.is_following = true;
+					this.user.isFollowing = true;
 				}).catch(err => {
 					console.error(err);
 				}).then(() => {
diff --git a/src/server/web/app/mobile/views/components/media-image.vue b/src/server/web/app/mobile/views/components/media-image.vue
index faf8bad48..cfc213498 100644
--- a/src/server/web/app/mobile/views/components/media-image.vue
+++ b/src/server/web/app/mobile/views/components/media-image.vue
@@ -10,7 +10,7 @@ export default Vue.extend({
 	computed: {
 		style(): any {
 			return {
-				'background-color': this.image.properties.average_color ? `rgb(${this.image.properties.average_color.join(',')})` : 'transparent',
+				'background-color': this.image.properties.avgColor ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
 				'background-image': `url(${this.image.url}?thumbnail&size=512)`
 			};
 		}
diff --git a/src/server/web/app/mobile/views/components/notification-preview.vue b/src/server/web/app/mobile/views/components/notification-preview.vue
index 47df626fa..fce9ed82f 100644
--- a/src/server/web/app/mobile/views/components/notification-preview.vue
+++ b/src/server/web/app/mobile/views/components/notification-preview.vue
@@ -1,7 +1,7 @@
 <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"/>
+		<img class="avatar" :src="`${notification.user.avatarUrl}?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>
@@ -9,7 +9,7 @@
 	</template>
 
 	<template v-if="notification.type == 'repost'">
-		<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		<img class="avatar" :src="`${notification.post.user.avatarUrl}?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>
@@ -17,7 +17,7 @@
 	</template>
 
 	<template v-if="notification.type == 'quote'">
-		<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		<img class="avatar" :src="`${notification.post.user.avatarUrl}?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>
@@ -25,14 +25,14 @@
 	</template>
 
 	<template v-if="notification.type == 'follow'">
-		<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		<img class="avatar" :src="`${notification.user.avatarUrl}?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"/>
+		<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
 			<p>%fa:reply%{{ notification.post.user.name }}</p>
 			<p class="post-preview">{{ getPostSummary(notification.post) }}</p>
@@ -40,7 +40,7 @@
 	</template>
 
 	<template v-if="notification.type == 'mention'">
-		<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
 			<p>%fa:at%{{ notification.post.user.name }}</p>
 			<p class="post-preview">{{ getPostSummary(notification.post) }}</p>
@@ -48,7 +48,7 @@
 	</template>
 
 	<template v-if="notification.type == 'poll_vote'">
-		<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		<img class="avatar" :src="`${notification.user.avatarUrl}?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>
diff --git a/src/server/web/app/mobile/views/components/notification.vue b/src/server/web/app/mobile/views/components/notification.vue
index 150ac0fd8..e221fb3ac 100644
--- a/src/server/web/app/mobile/views/components/notification.vue
+++ b/src/server/web/app/mobile/views/components/notification.vue
@@ -1,9 +1,9 @@
 <template>
 <div class="mk-notification">
 	<div class="notification reaction" v-if="notification.type == 'reaction'">
-		<mk-time :time="notification.created_at"/>
+		<mk-time :time="notification.createdAt"/>
 		<router-link class="avatar-anchor" :to="`/@${acct}`">
-			<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+			<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="text">
 			<p>
@@ -18,9 +18,9 @@
 	</div>
 
 	<div class="notification repost" v-if="notification.type == 'repost'">
-		<mk-time :time="notification.created_at"/>
+		<mk-time :time="notification.createdAt"/>
 		<router-link class="avatar-anchor" :to="`/@${acct}`">
-			<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+			<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="text">
 			<p>
@@ -38,9 +38,9 @@
 	</template>
 
 	<div class="notification follow" v-if="notification.type == 'follow'">
-		<mk-time :time="notification.created_at"/>
+		<mk-time :time="notification.createdAt"/>
 		<router-link class="avatar-anchor" :to="`/@${acct}`">
-			<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+			<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="text">
 			<p>
@@ -59,9 +59,9 @@
 	</template>
 
 	<div class="notification poll_vote" v-if="notification.type == 'poll_vote'">
-		<mk-time :time="notification.created_at"/>
+		<mk-time :time="notification.createdAt"/>
 		<router-link class="avatar-anchor" :to="`/@${acct}`">
-			<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+			<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="text">
 			<p>
diff --git a/src/server/web/app/mobile/views/components/notifications.vue b/src/server/web/app/mobile/views/components/notifications.vue
index 1cd6e2bc1..d68b990df 100644
--- a/src/server/web/app/mobile/views/components/notifications.vue
+++ b/src/server/web/app/mobile/views/components/notifications.vue
@@ -34,8 +34,8 @@ export default Vue.extend({
 	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;
+				const date = new Date(notification.createdAt).getDate();
+				const month = new Date(notification.createdAt).getMonth() + 1;
 				notification._date = date;
 				notification._datetext = `${month}月 ${date}日`;
 				return notification;
@@ -75,7 +75,7 @@ export default Vue.extend({
 
 			(this as any).api('i/notifications', {
 				limit: max + 1,
-				until_id: this.notifications[this.notifications.length - 1].id
+				untilId: this.notifications[this.notifications.length - 1].id
 			}).then(notifications => {
 				if (notifications.length == max + 1) {
 					this.moreNotifications = true;
diff --git a/src/server/web/app/mobile/views/components/post-card.vue b/src/server/web/app/mobile/views/components/post-card.vue
index 8ca7550c2..10dfd9241 100644
--- a/src/server/web/app/mobile/views/components/post-card.vue
+++ b/src/server/web/app/mobile/views/components/post-card.vue
@@ -7,7 +7,7 @@
 		<div>
 			{{ text }}
 		</div>
-		<mk-time :time="post.created_at"/>
+		<mk-time :time="post.createdAt"/>
 	</a>
 </div>
 </template>
diff --git a/src/server/web/app/mobile/views/components/post-detail.sub.vue b/src/server/web/app/mobile/views/components/post-detail.sub.vue
index 6906cf570..db7567834 100644
--- a/src/server/web/app/mobile/views/components/post-detail.sub.vue
+++ b/src/server/web/app/mobile/views/components/post-detail.sub.vue
@@ -1,14 +1,14 @@
 <template>
 <div class="root sub">
 	<router-link class="avatar-anchor" :to="`/@${acct}`">
-		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 	</router-link>
 	<div class="main">
 		<header>
 			<router-link class="name" :to="`/@${acct}`">{{ post.user.name }}</router-link>
 			<span class="username">@{{ acct }}</span>
 			<router-link class="time" :to="`/@${acct}/${post.id}`">
-				<mk-time :time="post.created_at"/>
+				<mk-time :time="post.createdAt"/>
 			</router-link>
 		</header>
 		<div class="body">
diff --git a/src/server/web/app/mobile/views/components/post-detail.vue b/src/server/web/app/mobile/views/components/post-detail.vue
index b5c915830..29993c79e 100644
--- a/src/server/web/app/mobile/views/components/post-detail.vue
+++ b/src/server/web/app/mobile/views/components/post-detail.vue
@@ -2,7 +2,7 @@
 <div class="mk-post-detail">
 	<button
 		class="more"
-		v-if="p.reply && p.reply.reply_id && context == null"
+		v-if="p.reply && p.reply.replyId && context == null"
 		@click="fetchContext"
 		:disabled="fetchingContext"
 	>
@@ -18,7 +18,7 @@
 	<div class="repost" v-if="isRepost">
 		<p>
 			<router-link class="avatar-anchor" :to="`/@${acct}`">
-				<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=32`" alt="avatar"/>
+				<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
 			</router-link>
 			%fa:retweet%
 			<router-link class="name" :to="`/@${acct}`">
@@ -30,7 +30,7 @@
 	<article>
 		<header>
 			<router-link class="avatar-anchor" :to="`/@${pAcct}`">
-				<img class="avatar" :src="`${p.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+				<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 			</router-link>
 			<div>
 				<router-link class="name" :to="`/@${pAcct}`">{{ p.user.name }}</router-link>
@@ -54,17 +54,17 @@
 			</div>
 		</div>
 		<router-link class="time" :to="`/@${pAcct}/${p.id}`">
-			<mk-time :time="p.created_at" mode="detail"/>
+			<mk-time :time="p.createdAt" mode="detail"/>
 		</router-link>
 		<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>
+				%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</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.repostCount > 0">{{ p.repostCount }}</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.myReaction != 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">
@@ -115,16 +115,16 @@ export default Vue.extend({
 		isRepost(): boolean {
 			return (this.post.repost &&
 				this.post.text == null &&
-				this.post.media_ids == null &&
+				this.post.mediaIds == 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])
+			return this.p.reactionCounts
+				? Object.keys(this.p.reactionCounts)
+					.map(key => this.p.reactionCounts[key])
 					.reduce((a, b) => a + b)
 				: 0;
 		},
@@ -142,7 +142,7 @@ export default Vue.extend({
 		// Get replies
 		if (!this.compact) {
 			(this as any).api('posts/replies', {
-				post_id: this.p.id,
+				postId: this.p.id,
 				limit: 8
 			}).then(replies => {
 				this.replies = replies;
@@ -151,7 +151,7 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.client_settings.showMaps : true;
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
 					const uluru = new maps.LatLng(this.p.geo.latitude, this.p.geo.longitude);
@@ -173,7 +173,7 @@ export default Vue.extend({
 
 			// Fetch context
 			(this as any).api('posts/context', {
-				post_id: this.p.reply_id
+				postId: this.p.replyId
 			}).then(context => {
 				this.contextFetching = false;
 				this.context = context.reverse();
diff --git a/src/server/web/app/mobile/views/components/post-form.vue b/src/server/web/app/mobile/views/components/post-form.vue
index 2aa3c6f6c..929dc5933 100644
--- a/src/server/web/app/mobile/views/components/post-form.vue
+++ b/src/server/web/app/mobile/views/components/post-form.vue
@@ -111,11 +111,11 @@ export default Vue.extend({
 		},
 		post() {
 			this.posting = true;
-			const viaMobile = (this as any).os.i.account.client_settings.disableViaMobile !== true;
+			const viaMobile = (this as any).os.i.account.clientSettings.disableViaMobile !== 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,
+				mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
+				replyId: this.reply ? this.reply.id : undefined,
 				poll: this.poll ? (this.$refs.poll as any).get() : undefined,
 				geo: this.geo ? {
 					latitude: this.geo.latitude,
@@ -126,7 +126,7 @@ export default Vue.extend({
 					heading: isNaN(this.geo.heading) ? null : this.geo.heading,
 					speed: this.geo.speed,
 				} : null,
-				via_mobile: viaMobile
+				viaMobile: viaMobile
 			}).then(data => {
 				this.$emit('post');
 				this.$destroy();
diff --git a/src/server/web/app/mobile/views/components/post-preview.vue b/src/server/web/app/mobile/views/components/post-preview.vue
index 0bd0a355b..a6141dc8e 100644
--- a/src/server/web/app/mobile/views/components/post-preview.vue
+++ b/src/server/web/app/mobile/views/components/post-preview.vue
@@ -1,14 +1,14 @@
 <template>
 <div class="mk-post-preview">
 	<router-link class="avatar-anchor" :to="`/@${acct}`">
-		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 	</router-link>
 	<div class="main">
 		<header>
 			<router-link class="name" :to="`/@${acct}`">{{ post.user.name }}</router-link>
 			<span class="username">@{{ acct }}</span>
 			<router-link class="time" :to="`/@${acct}/${post.id}`">
-				<mk-time :time="post.created_at"/>
+				<mk-time :time="post.createdAt"/>
 			</router-link>
 		</header>
 		<div class="body">
diff --git a/src/server/web/app/mobile/views/components/post.sub.vue b/src/server/web/app/mobile/views/components/post.sub.vue
index b6ee7c1e0..adf444a2d 100644
--- a/src/server/web/app/mobile/views/components/post.sub.vue
+++ b/src/server/web/app/mobile/views/components/post.sub.vue
@@ -1,14 +1,14 @@
 <template>
 <div class="sub">
 	<router-link class="avatar-anchor" :to="`/@${acct}`">
-		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=96`" alt="avatar"/>
+		<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
 	</router-link>
 	<div class="main">
 		<header>
 			<router-link class="name" :to="`/@${acct}`">{{ post.user.name }}</router-link>
 			<span class="username">@{{ acct }}</span>
 			<router-link class="created-at" :to="`/@${acct}/${post.id}`">
-				<mk-time :time="post.created_at"/>
+				<mk-time :time="post.createdAt"/>
 			</router-link>
 		</header>
 		<div class="body">
diff --git a/src/server/web/app/mobile/views/components/post.vue b/src/server/web/app/mobile/views/components/post.vue
index e5bc96479..66c595f4e 100644
--- a/src/server/web/app/mobile/views/components/post.vue
+++ b/src/server/web/app/mobile/views/components/post.vue
@@ -6,28 +6,28 @@
 	<div class="repost" v-if="isRepost">
 		<p>
 			<router-link class="avatar-anchor" :to="`/@${acct}`">
-				<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+				<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 			</router-link>
 			%fa:retweet%
 			<span>{{ '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('{')) }}</span>
 			<router-link class="name" :to="`/@${acct}`">{{ post.user.name }}</router-link>
 			<span>{{ '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr('%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1) }}</span>
 		</p>
-		<mk-time :time="post.created_at"/>
+		<mk-time :time="post.createdAt"/>
 	</div>
 	<article>
 		<router-link class="avatar-anchor" :to="`/@${pAcct}`">
-			<img class="avatar" :src="`${p.user.avatar_url}?thumbnail&size=96`" alt="avatar"/>
+			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
 		</router-link>
 		<div class="main">
 			<header>
 				<router-link class="name" :to="`/@${pAcct}`">{{ p.user.name }}</router-link>
-				<span class="is-bot" v-if="p.user.host === null && p.user.account.is_bot">bot</span>
+				<span class="is-bot" v-if="p.user.host === null && p.user.account.isBot">bot</span>
 				<span class="username">@{{ pAcct }}</span>
 				<div class="info">
-					<span class="mobile" v-if="p.via_mobile">%fa:mobile-alt%</span>
+					<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
 					<router-link class="created-at" :to="url">
-						<mk-time :time="p.created_at"/>
+						<mk-time :time="p.createdAt"/>
 					</router-link>
 				</div>
 			</header>
@@ -58,12 +58,12 @@
 			<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>
+					%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</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.repostCount > 0">{{ p.repostCount }}</p>
 				</button>
-				<button :class="{ reacted: p.my_reaction != null }" @click="react" ref="reactButton">
+				<button :class="{ reacted: p.myReaction != 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">
@@ -103,16 +103,16 @@ export default Vue.extend({
 		isRepost(): boolean {
 			return (this.post.repost &&
 				this.post.text == null &&
-				this.post.media_ids == null &&
+				this.post.mediaIds == 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])
+			return this.p.reactionCounts
+				? Object.keys(this.p.reactionCounts)
+					.map(key => this.p.reactionCounts[key])
 					.reduce((a, b) => a + b)
 				: 0;
 		},
@@ -144,7 +144,7 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.client_settings.showMaps : true;
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
 					const uluru = new maps.LatLng(this.p.geo.latitude, this.p.geo.longitude);
@@ -194,7 +194,7 @@ export default Vue.extend({
 			const post = data.post;
 			if (post.id == this.post.id) {
 				this.$emit('update:post', post);
-			} else if (post.id == this.post.repost_id) {
+			} else if (post.id == this.post.repostId) {
 				this.post.repost = post;
 			}
 		},
diff --git a/src/server/web/app/mobile/views/components/posts.vue b/src/server/web/app/mobile/views/components/posts.vue
index 7e71fa098..4695f1bea 100644
--- a/src/server/web/app/mobile/views/components/posts.vue
+++ b/src/server/web/app/mobile/views/components/posts.vue
@@ -28,8 +28,8 @@ export default Vue.extend({
 	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;
+				const date = new Date(post.createdAt).getDate();
+				const month = new Date(post.createdAt).getMonth() + 1;
 				post._date = date;
 				post._datetext = `${month}月 ${date}日`;
 				return post;
diff --git a/src/server/web/app/mobile/views/components/sub-post-content.vue b/src/server/web/app/mobile/views/components/sub-post-content.vue
index 389fc420e..b95883de7 100644
--- a/src/server/web/app/mobile/views/components/sub-post-content.vue
+++ b/src/server/web/app/mobile/views/components/sub-post-content.vue
@@ -1,9 +1,9 @@
 <template>
 <div class="mk-sub-post-content">
 	<div class="body">
-		<a class="reply" v-if="post.reply_id">%fa:reply%</a>
+		<a class="reply" v-if="post.replyId">%fa:reply%</a>
 		<mk-post-html v-if="post.ast" :ast="post.ast" :i="os.i"/>
-		<a class="rp" v-if="post.repost_id">RP: ...</a>
+		<a class="rp" v-if="post.repostId">RP: ...</a>
 	</div>
 	<details v-if="post.media">
 		<summary>({{ post.media.length }}個のメディア)</summary>
diff --git a/src/server/web/app/mobile/views/components/timeline.vue b/src/server/web/app/mobile/views/components/timeline.vue
index c0e766523..7b5948faf 100644
--- a/src/server/web/app/mobile/views/components/timeline.vue
+++ b/src/server/web/app/mobile/views/components/timeline.vue
@@ -41,7 +41,7 @@ export default Vue.extend({
 	},
 	computed: {
 		alone(): boolean {
-			return (this as any).os.i.following_count == 0;
+			return (this as any).os.i.followingCount == 0;
 		}
 	},
 	mounted() {
@@ -65,7 +65,7 @@ export default Vue.extend({
 			this.fetching = true;
 			(this as any).api('posts/timeline', {
 				limit: limit + 1,
-				until_date: this.date ? (this.date as any).getTime() : undefined
+				untilDate: this.date ? (this.date as any).getTime() : undefined
 			}).then(posts => {
 				if (posts.length == limit + 1) {
 					posts.pop();
@@ -81,7 +81,7 @@ export default Vue.extend({
 			this.moreFetching = true;
 			(this as any).api('posts/timeline', {
 				limit: limit + 1,
-				until_id: this.posts[this.posts.length - 1].id
+				untilId: this.posts[this.posts.length - 1].id
 			}).then(posts => {
 				if (posts.length == limit + 1) {
 					posts.pop();
diff --git a/src/server/web/app/mobile/views/components/ui.header.vue b/src/server/web/app/mobile/views/components/ui.header.vue
index 66e10a0f8..2bf47a90a 100644
--- a/src/server/web/app/mobile/views/components/ui.header.vue
+++ b/src/server/web/app/mobile/views/components/ui.header.vue
@@ -57,9 +57,9 @@ export default Vue.extend({
 				}
 			});
 
-			const ago = (new Date().getTime() - new Date((this as any).os.i.account.last_used_at).getTime()) / 1000
+			const ago = (new Date().getTime() - new Date((this as any).os.i.account.lastUsedAt).getTime()) / 1000
 			const isHisasiburi = ago >= 3600;
-			(this as any).os.i.account.last_used_at = new Date();
+			(this as any).os.i.account.lastUsedAt = new Date();
 			if (isHisasiburi) {
 				(this.$refs.welcomeback as any).style.display = 'block';
 				(this.$refs.main as any).style.overflow = 'hidden';
diff --git a/src/server/web/app/mobile/views/components/ui.nav.vue b/src/server/web/app/mobile/views/components/ui.nav.vue
index 760a5b518..a923774a7 100644
--- a/src/server/web/app/mobile/views/components/ui.nav.vue
+++ b/src/server/web/app/mobile/views/components/ui.nav.vue
@@ -10,7 +10,7 @@
 	<transition name="nav">
 		<div class="body" v-if="isOpen">
 			<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"/>
+				<img class="avatar" :src="`${os.i.avatarUrl}?thumbnail&size=128`" alt="avatar"/>
 				<p class="name">{{ os.i.name }}</p>
 			</router-link>
 			<div class="links">
diff --git a/src/server/web/app/mobile/views/components/user-card.vue b/src/server/web/app/mobile/views/components/user-card.vue
index 5a7309cfd..ffa110051 100644
--- a/src/server/web/app/mobile/views/components/user-card.vue
+++ b/src/server/web/app/mobile/views/components/user-card.vue
@@ -1,8 +1,8 @@
 <template>
 <div class="mk-user-card">
-	<header :style="user.banner_url ? `background-image: url(${user.banner_url}?thumbnail&size=1024)` : ''">
+	<header :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=1024)` : ''">
 		<a :href="`/@${acct}`">
-			<img :src="`${user.avatar_url}?thumbnail&size=200`" alt="avatar"/>
+			<img :src="`${user.avatarUrl}?thumbnail&size=200`" alt="avatar"/>
 		</a>
 	</header>
 	<a class="name" :href="`/@${acct}`" target="_blank">{{ user.name }}</a>
diff --git a/src/server/web/app/mobile/views/components/user-preview.vue b/src/server/web/app/mobile/views/components/user-preview.vue
index be80582ca..e51e4353d 100644
--- a/src/server/web/app/mobile/views/components/user-preview.vue
+++ b/src/server/web/app/mobile/views/components/user-preview.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mk-user-preview">
 	<router-link class="avatar-anchor" :to="`/@${acct}`">
-		<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 	</router-link>
 	<div class="main">
 		<header>
diff --git a/src/server/web/app/mobile/views/components/user-timeline.vue b/src/server/web/app/mobile/views/components/user-timeline.vue
index 39f959187..bd3e3d0c8 100644
--- a/src/server/web/app/mobile/views/components/user-timeline.vue
+++ b/src/server/web/app/mobile/views/components/user-timeline.vue
@@ -33,8 +33,8 @@ export default Vue.extend({
 	},
 	mounted() {
 		(this as any).api('users/posts', {
-			user_id: this.user.id,
-			with_media: this.withMedia,
+			userId: this.user.id,
+			withMedia: this.withMedia,
 			limit: limit + 1
 		}).then(posts => {
 			if (posts.length == limit + 1) {
@@ -50,10 +50,10 @@ export default Vue.extend({
 		more() {
 			this.moreFetching = true;
 			(this as any).api('users/posts', {
-				user_id: this.user.id,
-				with_media: this.withMedia,
+				userId: this.user.id,
+				withMedia: this.withMedia,
 				limit: limit + 1,
-				until_id: this.posts[this.posts.length - 1].id
+				untilId: this.posts[this.posts.length - 1].id
 			}).then(posts => {
 				if (posts.length == limit + 1) {
 					posts.pop();
diff --git a/src/server/web/app/mobile/views/pages/followers.vue b/src/server/web/app/mobile/views/pages/followers.vue
index 1edf4e38a..8c058eb4e 100644
--- a/src/server/web/app/mobile/views/pages/followers.vue
+++ b/src/server/web/app/mobile/views/pages/followers.vue
@@ -1,14 +1,14 @@
 <template>
 <mk-ui>
 	<template slot="header" v-if="!fetching">
-		<img :src="`${user.avatar_url}?thumbnail&size=64`" alt="">
+		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt="">
 		{{ '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name) }}
 	</template>
 	<mk-users-list
 		v-if="!fetching"
 		:fetch="fetchUsers"
-		:count="user.followers_count"
-		:you-know-count="user.followers_you_know_count"
+		:count="user.followersCount"
+		:you-know-count="user.followersYouKnowCount"
 		@loaded="onLoaded"
 	>
 		%i18n:mobile.tags.mk-user-followers.no-users%
@@ -54,7 +54,7 @@ export default Vue.extend({
 		},
 		fetchUsers(iknow, limit, cursor, cb) {
 			(this as any).api('users/followers', {
-				user_id: this.user.id,
+				userId: this.user.id,
 				iknow: iknow,
 				limit: limit,
 				cursor: cursor ? cursor : undefined
diff --git a/src/server/web/app/mobile/views/pages/following.vue b/src/server/web/app/mobile/views/pages/following.vue
index 0dd171cce..a73c9d171 100644
--- a/src/server/web/app/mobile/views/pages/following.vue
+++ b/src/server/web/app/mobile/views/pages/following.vue
@@ -1,14 +1,14 @@
 <template>
 <mk-ui>
 	<template slot="header" v-if="!fetching">
-		<img :src="`${user.avatar_url}?thumbnail&size=64`" alt="">
+		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt="">
 		{{ '%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"
+		:count="user.followingCount"
+		:you-know-count="user.followingYouKnowCount"
 		@loaded="onLoaded"
 	>
 		%i18n:mobile.tags.mk-user-following.no-users%
@@ -54,7 +54,7 @@ export default Vue.extend({
 		},
 		fetchUsers(iknow, limit, cursor, cb) {
 			(this as any).api('users/following', {
-				user_id: this.user.id,
+				userId: this.user.id,
 				iknow: iknow,
 				limit: limit,
 				cursor: cursor ? cursor : undefined
diff --git a/src/server/web/app/mobile/views/pages/home.vue b/src/server/web/app/mobile/views/pages/home.vue
index b110fc409..be9101aa7 100644
--- a/src/server/web/app/mobile/views/pages/home.vue
+++ b/src/server/web/app/mobile/views/pages/home.vue
@@ -82,8 +82,8 @@ export default Vue.extend({
 		};
 	},
 	created() {
-		if ((this as any).os.i.account.client_settings.mobile_home == null) {
-			Vue.set((this as any).os.i.account.client_settings, 'mobile_home', [{
+		if ((this as any).os.i.account.clientSettings.mobile_home == null) {
+			Vue.set((this as any).os.i.account.clientSettings, 'mobile_home', [{
 				name: 'calendar',
 				id: 'a', data: {}
 			}, {
@@ -105,14 +105,14 @@ export default Vue.extend({
 				name: 'version',
 				id: 'g', data: {}
 			}]);
-			this.widgets = (this as any).os.i.account.client_settings.mobile_home;
+			this.widgets = (this as any).os.i.account.clientSettings.mobile_home;
 			this.saveHome();
 		} else {
-			this.widgets = (this as any).os.i.account.client_settings.mobile_home;
+			this.widgets = (this as any).os.i.account.clientSettings.mobile_home;
 		}
 
-		this.$watch('os.i.account.client_settings', i => {
-			this.widgets = (this as any).os.i.account.client_settings.mobile_home;
+		this.$watch('os.i.account.clientSettings', i => {
+			this.widgets = (this as any).os.i.account.clientSettings.mobile_home;
 		}, {
 			deep: true
 		});
@@ -144,7 +144,7 @@ export default Vue.extend({
 			Progress.done();
 		},
 		onStreamPost(post) {
-			if (document.hidden && post.user_id !== (this as any).os.i.id) {
+			if (document.hidden && post.userId !== (this as any).os.i.id) {
 				this.unreadCount++;
 				document.title = `(${this.unreadCount}) ${getPostSummary(post)}`;
 			}
@@ -157,15 +157,15 @@ export default Vue.extend({
 		},
 		onHomeUpdated(data) {
 			if (data.home) {
-				(this as any).os.i.account.client_settings.mobile_home = data.home;
+				(this as any).os.i.account.clientSettings.mobile_home = data.home;
 				this.widgets = data.home;
 			} else {
-				const w = (this as any).os.i.account.client_settings.mobile_home.find(w => w.id == data.id);
+				const w = (this as any).os.i.account.clientSettings.mobile_home.find(w => w.id == data.id);
 				if (w != null) {
 					w.data = data.data;
 					this.$refs[w.id][0].preventSave = true;
 					this.$refs[w.id][0].props = w.data;
-					this.widgets = (this as any).os.i.account.client_settings.mobile_home;
+					this.widgets = (this as any).os.i.account.clientSettings.mobile_home;
 				}
 			}
 		},
@@ -194,7 +194,7 @@ export default Vue.extend({
 			this.saveHome();
 		},
 		saveHome() {
-			(this as any).os.i.account.client_settings.mobile_home = this.widgets;
+			(this as any).os.i.account.clientSettings.mobile_home = this.widgets;
 			(this as any).api('i/update_mobile_home', {
 				home: this.widgets
 			});
diff --git a/src/server/web/app/mobile/views/pages/notifications.vue b/src/server/web/app/mobile/views/pages/notifications.vue
index 3dcfb2f38..6d45e22a9 100644
--- a/src/server/web/app/mobile/views/pages/notifications.vue
+++ b/src/server/web/app/mobile/views/pages/notifications.vue
@@ -22,7 +22,7 @@ export default Vue.extend({
 			const ok = window.confirm('%i18n:mobile.tags.mk-notifications-page.read-all%');
 			if (!ok) return;
 
-			(this as any).api('notifications/mark_as_read_all');
+			(this as any).api('notifications/markAsRead_all');
 		},
 		onFetched() {
 			Progress.done();
diff --git a/src/server/web/app/mobile/views/pages/othello.vue b/src/server/web/app/mobile/views/pages/othello.vue
index b110bf309..e04e583c2 100644
--- a/src/server/web/app/mobile/views/pages/othello.vue
+++ b/src/server/web/app/mobile/views/pages/othello.vue
@@ -34,7 +34,7 @@ export default Vue.extend({
 			this.fetching = true;
 
 			(this as any).api('othello/games/show', {
-				game_id: this.$route.params.game
+				gameId: this.$route.params.game
 			}).then(game => {
 				this.game = game;
 				this.fetching = false;
diff --git a/src/server/web/app/mobile/views/pages/post.vue b/src/server/web/app/mobile/views/pages/post.vue
index 2ed2ebfcf..49a4bfd9d 100644
--- a/src/server/web/app/mobile/views/pages/post.vue
+++ b/src/server/web/app/mobile/views/pages/post.vue
@@ -38,7 +38,7 @@ export default Vue.extend({
 			this.fetching = true;
 
 			(this as any).api('posts/show', {
-				post_id: this.$route.params.post
+				postId: this.$route.params.post
 			}).then(post => {
 				this.post = post;
 				this.fetching = false;
diff --git a/src/server/web/app/mobile/views/pages/profile-setting.vue b/src/server/web/app/mobile/views/pages/profile-setting.vue
index 941165c99..15f9bc9b6 100644
--- a/src/server/web/app/mobile/views/pages/profile-setting.vue
+++ b/src/server/web/app/mobile/views/pages/profile-setting.vue
@@ -4,8 +4,8 @@
 	<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 :style="os.i.bannerUrl ? `background-image: url(${os.i.bannerUrl}?thumbnail&size=1024)` : ''" @click="setBanner">
+				<img :src="`${os.i.avatarUrl}?thumbnail&size=200`" alt="avatar" @click="setAvatar"/>
 			</div>
 			<label>
 				<p>%i18n:mobile.tags.mk-profile-setting.name%</p>
@@ -69,7 +69,7 @@ export default Vue.extend({
 				this.avatarSaving = true;
 
 				(this as any).api('i/update', {
-					avatar_id: file.id
+					avatarId: file.id
 				}).then(() => {
 					this.avatarSaving = false;
 					alert('%i18n:mobile.tags.mk-profile-setting.avatar-saved%');
@@ -83,7 +83,7 @@ export default Vue.extend({
 				this.bannerSaving = true;
 
 				(this as any).api('i/update', {
-					banner_id: file.id
+					bannerId: file.id
 				}).then(() => {
 					this.bannerSaving = false;
 					alert('%i18n:mobile.tags.mk-profile-setting.banner-saved%');
diff --git a/src/server/web/app/mobile/views/pages/settings.vue b/src/server/web/app/mobile/views/pages/settings.vue
index 3250999e1..a945a21c5 100644
--- a/src/server/web/app/mobile/views/pages/settings.vue
+++ b/src/server/web/app/mobile/views/pages/settings.vue
@@ -12,19 +12,20 @@
 		<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>
+		<p><small>ver {{ version }} ({{ codename }})</small></p>
 	</div>
 </mk-ui>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-import { version } from '../../../config';
+import { version, codename } from '../../../config';
 
 export default Vue.extend({
 	data() {
 		return {
-			v: version
+			version,
+			codename
 		};
 	},
 	mounted() {
diff --git a/src/server/web/app/mobile/views/pages/user.vue b/src/server/web/app/mobile/views/pages/user.vue
index 7ff897e42..114decb8e 100644
--- a/src/server/web/app/mobile/views/pages/user.vue
+++ b/src/server/web/app/mobile/views/pages/user.vue
@@ -3,18 +3,18 @@
 	<span slot="header" v-if="!fetching">%fa:user% {{ user.name }}</span>
 	<main v-if="!fetching">
 		<header>
-			<div class="banner" :style="user.banner_url ? `background-image: url(${user.banner_url}?thumbnail&size=1024)` : ''"></div>
+			<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=1024)` : ''"></div>
 			<div class="body">
 				<div class="top">
 					<a class="avatar">
-						<img :src="`${user.avatar_url}?thumbnail&size=200`" alt="avatar"/>
+						<img :src="`${user.avatarUrl}?thumbnail&size=200`" alt="avatar"/>
 					</a>
 					<mk-follow-button v-if="os.isSignedIn && os.i.id != user.id" :user="user"/>
 				</div>
 				<div class="title">
 					<h1>{{ user.name }}</h1>
 					<span class="username">@{{ acct }}</span>
-					<span class="followed" v-if="user.is_followed">%i18n:mobile.tags.mk-user.follows-you%</span>
+					<span class="followed" v-if="user.isFollowed">%i18n:mobile.tags.mk-user.follows-you%</span>
 				</div>
 				<div class="description">{{ user.description }}</div>
 				<div class="info">
@@ -27,15 +27,15 @@
 				</div>
 				<div class="status">
 					<a>
-						<b>{{ user.posts_count | number }}</b>
+						<b>{{ user.postsCount | number }}</b>
 						<i>%i18n:mobile.tags.mk-user.posts%</i>
 					</a>
 					<a :href="`@${acct}/following`">
-						<b>{{ user.following_count | number }}</b>
+						<b>{{ user.followingCount | number }}</b>
 						<i>%i18n:mobile.tags.mk-user.following%</i>
 					</a>
 					<a :href="`@${acct}/followers`">
-						<b>{{ user.followers_count | number }}</b>
+						<b>{{ user.followersCount | number }}</b>
 						<i>%i18n:mobile.tags.mk-user.followers%</i>
 					</a>
 				</div>
diff --git a/src/server/web/app/mobile/views/pages/user/home.followers-you-know.vue b/src/server/web/app/mobile/views/pages/user/home.followers-you-know.vue
index 1a2b8f708..8c84d2dbb 100644
--- a/src/server/web/app/mobile/views/pages/user/home.followers-you-know.vue
+++ b/src/server/web/app/mobile/views/pages/user/home.followers-you-know.vue
@@ -3,7 +3,7 @@
 	<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="`/@${getAcct(user)}`">
-			<img :src="`${user.avatar_url}?thumbnail&size=64`" :alt="user.name"/>
+			<img :src="`${user.avatarUrl}?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>
@@ -27,7 +27,7 @@ export default Vue.extend({
 	},
 	mounted() {
 		(this as any).api('users/followers', {
-			user_id: this.user.id,
+			userId: this.user.id,
 			iknow: true,
 			limit: 30
 		}).then(res => {
diff --git a/src/server/web/app/mobile/views/pages/user/home.friends.vue b/src/server/web/app/mobile/views/pages/user/home.friends.vue
index b37f1a2fe..469781abb 100644
--- a/src/server/web/app/mobile/views/pages/user/home.friends.vue
+++ b/src/server/web/app/mobile/views/pages/user/home.friends.vue
@@ -20,7 +20,7 @@ export default Vue.extend({
 	},
 	mounted() {
 		(this as any).api('users/get_frequently_replied_users', {
-			user_id: this.user.id
+			userId: this.user.id
 		}).then(res => {
 			this.users = res.map(x => x.user);
 			this.fetching = false;
diff --git a/src/server/web/app/mobile/views/pages/user/home.photos.vue b/src/server/web/app/mobile/views/pages/user/home.photos.vue
index f12f59a40..f703f8a74 100644
--- a/src/server/web/app/mobile/views/pages/user/home.photos.vue
+++ b/src/server/web/app/mobile/views/pages/user/home.photos.vue
@@ -29,8 +29,8 @@ export default Vue.extend({
 	},
 	mounted() {
 		(this as any).api('users/posts', {
-			user_id: this.user.id,
-			with_media: true,
+			userId: this.user.id,
+			withMedia: true,
 			limit: 6
 		}).then(posts => {
 			posts.forEach(post => {
diff --git a/src/server/web/app/mobile/views/pages/user/home.posts.vue b/src/server/web/app/mobile/views/pages/user/home.posts.vue
index 70b20ce94..654f7f63e 100644
--- a/src/server/web/app/mobile/views/pages/user/home.posts.vue
+++ b/src/server/web/app/mobile/views/pages/user/home.posts.vue
@@ -20,7 +20,7 @@ export default Vue.extend({
 	},
 	mounted() {
 		(this as any).api('users/posts', {
-			user_id: this.user.id
+			userId: this.user.id
 		}).then(posts => {
 			this.posts = posts;
 			this.fetching = false;
diff --git a/src/server/web/app/mobile/views/pages/user/home.vue b/src/server/web/app/mobile/views/pages/user/home.vue
index e3def6151..1afcd1f5b 100644
--- a/src/server/web/app/mobile/views/pages/user/home.vue
+++ b/src/server/web/app/mobile/views/pages/user/home.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="root home">
-	<mk-post-detail v-if="user.pinned_post" :post="user.pinned_post" :compact="true"/>
+	<mk-post-detail v-if="user.pinnedPost" :post="user.pinnedPost" :compact="true"/>
 	<section class="recent-posts">
 		<h2>%fa:R comments%%i18n:mobile.tags.mk-user-overview.recent-posts%</h2>
 		<div>
@@ -31,7 +31,7 @@
 			<x-followers-you-know :user="user"/>
 		</div>
 	</section>
-	<p v-if="user.host === null">%i18n:mobile.tags.mk-user-overview.last-used-at%: <b><mk-time :time="user.account.last_used_at"/></b></p>
+	<p v-if="user.host === null">%i18n:mobile.tags.mk-user-overview.last-used-at%: <b><mk-time :time="user.account.lastUsedAt"/></b></p>
 </div>
 </template>
 
diff --git a/src/server/web/app/mobile/views/pages/welcome.vue b/src/server/web/app/mobile/views/pages/welcome.vue
index 3384ee699..17cdf9306 100644
--- a/src/server/web/app/mobile/views/pages/welcome.vue
+++ b/src/server/web/app/mobile/views/pages/welcome.vue
@@ -6,9 +6,9 @@
 		<p>%fa:lock% ログイン</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="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.account.two_factor_enabled" v-model="token" type="number" placeholder="トークン" required/>
+				<input v-if="user && user.account.twoFactorEnabled" v-model="token" type="number" placeholder="トークン" required/>
 				<button type="submit" :disabled="signing">{{ signing ? 'ログインしています' : 'ログイン' }}</button>
 			</form>
 			<div>
@@ -22,7 +22,7 @@
 	</div>
 	<div class="users">
 		<router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="`/@${user.username}`">
-			<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+			<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 	</div>
 	<footer>
@@ -70,7 +70,7 @@ export default Vue.extend({
 			(this as any).api('signin', {
 				username: this.username,
 				password: this.password,
-				token: this.user && this.user.account.two_factor_enabled ? this.token : undefined
+				token: this.user && this.user.account.twoFactorEnabled ? this.token : undefined
 			}).then(() => {
 				location.reload();
 			}).catch(() => {
diff --git a/src/server/web/app/mobile/views/widgets/profile.vue b/src/server/web/app/mobile/views/widgets/profile.vue
index 1c9d038b4..f1d283e45 100644
--- a/src/server/web/app/mobile/views/widgets/profile.vue
+++ b/src/server/web/app/mobile/views/widgets/profile.vue
@@ -2,10 +2,10 @@
 <div class="mkw-profile">
 	<mk-widget-container>
 		<div :class="$style.banner"
-			:style="os.i.banner_url ? `background-image: url(${os.i.banner_url}?thumbnail&size=256)` : ''"
+			:style="os.i.bannerUrl ? `background-image: url(${os.i.bannerUrl}?thumbnail&size=256)` : ''"
 		></div>
 		<img :class="$style.avatar"
-			:src="`${os.i.avatar_url}?thumbnail&size=96`"
+			:src="`${os.i.avatarUrl}?thumbnail&size=96`"
 			alt="avatar"
 		/>
 		<router-link :class="$style.name" :to="`/@${os.i.username}`">{{ os.i.name }}</router-link>
diff --git a/src/server/web/app/stats/tags/index.tag b/src/server/web/app/stats/tags/index.tag
index 4b167ccbc..63fdd2404 100644
--- a/src/server/web/app/stats/tags/index.tag
+++ b/src/server/web/app/stats/tags/index.tag
@@ -57,7 +57,7 @@
 </mk-index>
 
 <mk-posts>
-	<h2>%i18n:stats.posts-count% <b>{ stats.posts_count }</b></h2>
+	<h2>%i18n:stats.posts-count% <b>{ stats.postsCount }</b></h2>
 	<mk-posts-chart v-if="!initializing" data={ data }/>
 	<style lang="stylus" scoped>
 		:scope
@@ -83,7 +83,7 @@
 </mk-posts>
 
 <mk-users>
-	<h2>%i18n:stats.users-count% <b>{ stats.users_count }</b></h2>
+	<h2>%i18n:stats.users-count% <b>{ stats.usersCount }</b></h2>
 	<mk-users-chart v-if="!initializing" data={ data }/>
 	<style lang="stylus" scoped>
 		:scope
diff --git a/src/server/web/docs/api.ja.pug b/src/server/web/docs/api.ja.pug
index 2bb08f7f3..665cfdc4b 100644
--- a/src/server/web/docs/api.ja.pug
+++ b/src/server/web/docs/api.ja.pug
@@ -54,7 +54,7 @@ section
 		h3 2.ユーザーに認証させる
 		p あなたのアプリを使ってもらうには、ユーザーにアカウントへのアクセスの許可をもらう必要があります。
 		p
-			| 認証セッションを開始するには、#{common.config.api_url}/auth/session/generate へパラメータに app_secret としてシークレットキーを含めたリクエストを送信します。
+			| 認証セッションを開始するには、#{common.config.api_url}/auth/session/generate へパラメータに appSecret としてシークレットキーを含めたリクエストを送信します。
 			| リクエスト形式はJSONで、メソッドはPOSTです。
 			| レスポンスとして認証セッションのトークンや認証フォームのURLが取得できるので、認証フォームのURLをブラウザで表示し、ユーザーにフォームを提示してください。
 
@@ -76,7 +76,7 @@ section
 					th 説明
 			tbody
 				tr
-					td app_secret
+					td appSecret
 					td string
 					td あなたのアプリのシークレットキー
 				tr
diff --git a/src/server/web/docs/api/endpoints/posts/create.yaml b/src/server/web/docs/api/endpoints/posts/create.yaml
index 5e2307dab..11d9f40c5 100644
--- a/src/server/web/docs/api/endpoints/posts/create.yaml
+++ b/src/server/web/docs/api/endpoints/posts/create.yaml
@@ -11,19 +11,19 @@ params:
     desc:
       ja: "投稿の本文"
       en: "The text of your post"
-  - name: "media_ids"
+  - name: "mediaIds"
     type: "id(DriveFile)[]"
     optional: true
     desc:
       ja: "添付するメディア(1~4つ)"
       en: "Media you want to attach (1~4)"
-  - name: "reply_id"
+  - name: "replyId"
     type: "id(Post)"
     optional: true
     desc:
       ja: "返信する投稿"
       en: "The post you want to reply"
-  - name: "repost_id"
+  - name: "repostId"
     type: "id(Post)"
     optional: true
     desc:
@@ -45,7 +45,7 @@ params:
           en: "Choices of a poll"
 
 res:
-  - name: "created_post"
+  - name: "createdPost"
     type: "entity(Post)"
     optional: false
     desc:
diff --git a/src/server/web/docs/api/endpoints/posts/timeline.yaml b/src/server/web/docs/api/endpoints/posts/timeline.yaml
index 01976b061..9c44dd736 100644
--- a/src/server/web/docs/api/endpoints/posts/timeline.yaml
+++ b/src/server/web/docs/api/endpoints/posts/timeline.yaml
@@ -10,22 +10,22 @@ params:
     optional: true
     desc:
       ja: "取得する最大の数"
-  - name: "since_id"
+  - name: "sinceId"
     type: "id(Post)"
     optional: true
     desc:
       ja: "指定すると、この投稿を基点としてより新しい投稿を取得します"
-  - name: "until_id"
+  - name: "untilId"
     type: "id(Post)"
     optional: true
     desc:
       ja: "指定すると、この投稿を基点としてより古い投稿を取得します"
-  - name: "since_date"
+  - name: "sinceDate"
     type: "number"
     optional: true
     desc:
       ja: "指定した時間を基点としてより新しい投稿を取得します。数値は、1970 年 1 月 1 日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。"
-  - name: "until_date"
+  - name: "untilDate"
     type: "number"
     optional: true
     desc:
diff --git a/src/server/web/docs/api/entities/drive-file.yaml b/src/server/web/docs/api/entities/drive-file.yaml
index 2ebbb089a..02ab0d608 100644
--- a/src/server/web/docs/api/entities/drive-file.yaml
+++ b/src/server/web/docs/api/entities/drive-file.yaml
@@ -11,13 +11,13 @@ props:
     desc:
       ja: "ファイルID"
       en: "The ID of this file"
-  - name: "created_at"
+  - name: "createdAt"
     type: "date"
     optional: false
     desc:
       ja: "アップロード日時"
       en: "The upload date of this file"
-  - name: "user_id"
+  - name: "userId"
     type: "id(User)"
     optional: false
     desc:
@@ -59,7 +59,7 @@ props:
     desc:
       ja: "ファイルのURL"
       en: "The URL of this file"
-  - name: "folder_id"
+  - name: "folderId"
     type: "id(DriveFolder)"
     optional: true
     desc:
diff --git a/src/server/web/docs/api/entities/post.yaml b/src/server/web/docs/api/entities/post.yaml
index f78026314..71b6a6412 100644
--- a/src/server/web/docs/api/entities/post.yaml
+++ b/src/server/web/docs/api/entities/post.yaml
@@ -11,13 +11,13 @@ props:
     desc:
       ja: "投稿ID"
       en: "The ID of this post"
-  - name: "created_at"
+  - name: "createdAt"
     type: "date"
     optional: false
     desc:
       ja: "投稿日時"
       en: "The posted date of this post"
-  - name: "via_mobile"
+  - name: "viaMobile"
     type: "boolean"
     optional: true
     desc:
@@ -29,7 +29,7 @@ props:
     desc:
       ja: "投稿の本文"
       en: "The text of this post"
-  - name: "media_ids"
+  - name: "mediaIds"
     type: "id(DriveFile)[]"
     optional: true
     desc:
@@ -41,7 +41,7 @@ props:
     desc:
       ja: "添付されているメディア"
       en: "The attached media"
-  - name: "user_id"
+  - name: "userId"
     type: "id(User)"
     optional: false
     desc:
@@ -53,18 +53,18 @@ props:
     desc:
       ja: "投稿者"
       en: "The author of this post"
-  - name: "my_reaction"
+  - name: "myReaction"
     type: "string"
     optional: true
     desc:
       ja: "この投稿に対する自分の<a href='/docs/api/reactions'>リアクション</a>"
       en: "The your <a href='/docs/api/reactions'>reaction</a> of this post"
-  - name: "reaction_counts"
+  - name: "reactionCounts"
     type: "object"
     optional: false
     desc:
       ja: "<a href='/docs/api/reactions'>リアクション</a>をキーとし、この投稿に対するそのリアクションの数を値としたオブジェクト"
-  - name: "reply_id"
+  - name: "replyId"
     type: "id(Post)"
     optional: true
     desc:
@@ -76,7 +76,7 @@ props:
     desc:
       ja: "返信した投稿"
       en: "The replyed post"
-  - name: "repost_id"
+  - name: "repostId"
     type: "id(Post)"
     optional: true
     desc:
@@ -110,7 +110,7 @@ props:
             desc:
               ja: "選択肢ID"
               en: "The ID of this choice"
-          - name: "is_voted"
+          - name: "isVoted"
             type: "boolean"
             optional: true
             desc:
diff --git a/src/server/web/docs/api/entities/user.yaml b/src/server/web/docs/api/entities/user.yaml
index a451a4080..a1fae1482 100644
--- a/src/server/web/docs/api/entities/user.yaml
+++ b/src/server/web/docs/api/entities/user.yaml
@@ -11,7 +11,7 @@ props:
     desc:
       ja: "ユーザーID"
       en: "The ID of this user"
-  - name: "created_at"
+  - name: "createdAt"
     type: "date"
     optional: false
     desc:
@@ -29,77 +29,77 @@ props:
     desc:
       ja: "アカウントの説明(自己紹介)"
       en: "The description of this user"
-  - name: "avatar_id"
+  - name: "avatarId"
     type: "id(DriveFile)"
     optional: true
     desc:
       ja: "アバターのID"
       en: "The ID of the avatar of this user"
-  - name: "avatar_url"
+  - name: "avatarUrl"
     type: "string"
     optional: false
     desc:
       ja: "アバターのURL"
       en: "The URL of the avatar of this user"
-  - name: "banner_id"
+  - name: "bannerId"
     type: "id(DriveFile)"
     optional: true
     desc:
       ja: "バナーのID"
       en: "The ID of the banner of this user"
-  - name: "banner_url"
+  - name: "bannerUrl"
     type: "string"
     optional: false
     desc:
       ja: "バナーのURL"
       en: "The URL of the banner of this user"
-  - name: "followers_count"
+  - name: "followersCount"
     type: "number"
     optional: false
     desc:
       ja: "フォロワーの数"
       en: "The number of the followers for this user"
-  - name: "following_count"
+  - name: "followingCount"
     type: "number"
     optional: false
     desc:
       ja: "フォローしているユーザーの数"
       en: "The number of the following users for this user"
-  - name: "is_following"
+  - name: "isFollowing"
     type: "boolean"
     optional: true
     desc:
       ja: "自分がこのユーザーをフォローしているか"
-  - name: "is_followed"
+  - name: "isFollowed"
     type: "boolean"
     optional: true
     desc:
       ja: "自分がこのユーザーにフォローされているか"
-  - name: "is_muted"
+  - name: "isMuted"
     type: "boolean"
     optional: true
     desc:
       ja: "自分がこのユーザーをミュートしているか"
       en: "Whether you muted this user"
-  - name: "posts_count"
+  - name: "postsCount"
     type: "number"
     optional: false
     desc:
       ja: "投稿の数"
       en: "The number of the posts of this user"
-  - name: "pinned_post"
+  - name: "pinnedPost"
     type: "entity(Post)"
     optional: true
     desc:
       ja: "ピン留めされた投稿"
       en: "The pinned post of this user"
-  - name: "pinned_post_id"
+  - name: "pinnedPostId"
     type: "id(Post)"
     optional: true
     desc:
       ja: "ピン留めされた投稿のID"
       en: "The ID of the pinned post of this user"
-  - name: "drive_capacity"
+  - name: "driveCapacity"
     type: "number"
     optional: false
     desc:
@@ -119,13 +119,13 @@ props:
       en: "The account of this user on this server"
     defName: "account"
     def:
-      - name: "last_used_at"
+      - name: "lastUsedAt"
         type: "date"
         optional: false
         desc:
           ja: "最終利用日時"
           en: "The last used date of this user"
-      - name: "is_bot"
+      - name: "isBot"
         type: "boolean"
         optional: true
         desc:
@@ -139,13 +139,13 @@ props:
           en: "The info of the connected twitter account of this user"
         defName: "twitter"
         def:
-          - name: "user_id"
+          - name: "userId"
             type: "string"
             optional: false
             desc:
               ja: "ユーザーID"
               en: "The user ID"
-          - name: "screen_name"
+          - name: "screenName"
             type: "string"
             optional: false
             desc:
diff --git a/src/tools/analysis/extract-user-domains.ts b/src/tools/analysis/extract-user-domains.ts
index ba472b89a..1aa456db8 100644
--- a/src/tools/analysis/extract-user-domains.ts
+++ b/src/tools/analysis/extract-user-domains.ts
@@ -50,7 +50,7 @@ function extractDomainsOne(id) {
 
 		// Fetch recent posts
 		const recentPosts = await Post.find({
-			user_id: id,
+			userId: id,
 			text: {
 				$exists: true
 			}
diff --git a/src/tools/analysis/extract-user-keywords.ts b/src/tools/analysis/extract-user-keywords.ts
index 4fa9b384e..9b0691b7d 100644
--- a/src/tools/analysis/extract-user-keywords.ts
+++ b/src/tools/analysis/extract-user-keywords.ts
@@ -96,7 +96,7 @@ function extractKeywordsOne(id) {
 
 		// Fetch recent posts
 		const recentPosts = await Post.find({
-			user_id: id,
+			userId: id,
 			text: {
 				$exists: true
 			}
diff --git a/src/tools/analysis/predict-user-interst.ts b/src/tools/analysis/predict-user-interst.ts
index 6599fb220..a101f2010 100644
--- a/src/tools/analysis/predict-user-interst.ts
+++ b/src/tools/analysis/predict-user-interst.ts
@@ -6,7 +6,7 @@ export async function predictOne(id) {
 
 	// TODO: repostなども含める
 	const recentPosts = await Post.find({
-		user_id: id,
+		userId: id,
 		category: {
 			$exists: true
 		}
diff --git a/swagger.js b/swagger.js
index 0cfd2fff0..ebd7a356e 100644
--- a/swagger.js
+++ b/swagger.js
@@ -23,7 +23,7 @@ const defaultSwagger = {
   "swagger": "2.0",
   "info": {
     "title": "Misskey API",
-    "version": "aoi"
+    "version": "nighthike"
   },
   "host": "api.misskey.xyz",
   "schemes": [
@@ -83,7 +83,7 @@ const defaultSwagger = {
           "type": "string",
           "description": "アバターに設定しているドライブのファイルのID"
         },
-        "avatar_url": {
+        "avatarUrl": {
           "type": "string",
           "description": "アバターURL"
         },
@@ -91,7 +91,7 @@ const defaultSwagger = {
           "type": "string",
           "description": "バナーに設定しているドライブのファイルのID"
         },
-        "banner_url": {
+        "bannerUrl": {
           "type": "string",
           "description": "バナーURL"
         },
@@ -218,8 +218,8 @@ options.apis = files.map(c => {return `${apiRoot}/${c}`;});
 if(fs.existsSync('.config/config.yml')){
   var config = yaml.safeLoad(fs.readFileSync('./.config/config.yml', 'utf8'));
   options.swaggerDefinition.host = `api.${config.url.match(/\:\/\/(.+)$/)[1]}`;
-  options.swaggerDefinition.schemes = config.https.enable ? 
-                                      ['https'] : 
+  options.swaggerDefinition.schemes = config.https.enable ?
+                                      ['https'] :
                                       ['http'];
 }
 
diff --git a/tools/migration/node.1522066477.user-account-keypair.js b/tools/migration/nighthike/1.js
similarity index 79%
rename from tools/migration/node.1522066477.user-account-keypair.js
rename to tools/migration/nighthike/1.js
index c413e3db1..d7e011c5b 100644
--- a/tools/migration/node.1522066477.user-account-keypair.js
+++ b/tools/migration/nighthike/1.js
@@ -1,13 +1,13 @@
 // for Node.js interpret
 
-const { default: User } = require('../../built/api/models/user');
-const { generate } = require('../../built/crypto_key');
+const { default: User } = require('../../../built/api/models/user');
 const { default: zip } = require('@prezzemolo/zip')
 
 const migrate = async (user) => {
 	const result = await User.update(user._id, {
 		$set: {
-			'account.keypair': generate()
+			'username': user.username.replace(/\-/g, '_'),
+			'username_lower': user.username_lower.replace(/\-/g, '_')
 		}
 	});
 	return result.ok === 1;
diff --git a/tools/migration/nighthike/2.js b/tools/migration/nighthike/2.js
new file mode 100644
index 000000000..8fb5bbb08
--- /dev/null
+++ b/tools/migration/nighthike/2.js
@@ -0,0 +1,39 @@
+// for Node.js interpret
+
+const { default: App } = require('../../../built/api/models/app');
+const { default: zip } = require('@prezzemolo/zip')
+
+const migrate = async (app) => {
+	const result = await User.update(app._id, {
+		$set: {
+			'name_id': app.name_id.replace(/\-/g, '_'),
+			'name_id_lower': app.name_id_lower.replace(/\-/g, '_')
+		}
+	});
+	return result.ok === 1;
+}
+
+async function main() {
+	const count = await App.count({});
+
+	const dop = Number.parseInt(process.argv[2]) || 5
+	const idop = ((count - (count % dop)) / dop) + 1
+
+	return zip(
+		1,
+		async (time) => {
+			console.log(`${time} / ${idop}`)
+			const doc = await App.find({}, {
+				limit: dop, skip: time * dop
+			})
+			return Promise.all(doc.map(migrate))
+		},
+		idop
+	).then(a => {
+		const rv = []
+		a.forEach(e => rv.push(...e))
+		return rv
+	})
+}
+
+main().then(console.dir).catch(console.error)
diff --git a/tools/migration/nighthike/3.js b/tools/migration/nighthike/3.js
new file mode 100644
index 000000000..cc0603d9e
--- /dev/null
+++ b/tools/migration/nighthike/3.js
@@ -0,0 +1,73 @@
+// for Node.js interpret
+
+const { default: User } = require('../../../built/api/models/user');
+const { generate } = require('../../../built/crypto_key');
+const { default: zip } = require('@prezzemolo/zip')
+
+const migrate = async (user) => {
+	const result = await User.update(user._id, {
+		$unset: {
+			email: '',
+			links: '',
+			password: '',
+			token: '',
+			twitter: '',
+			line: '',
+			profile: '',
+			last_used_at: '',
+			is_bot: '',
+			is_pro: '',
+			two_factor_secret: '',
+			two_factor_enabled: '',
+			client_settings: '',
+			settings: ''
+		},
+		$set: {
+			host: null,
+			host_lower: null,
+			account: {
+				email: user.email,
+				links: user.links,
+				password: user.password,
+				token: user.token,
+				twitter: user.twitter,
+				line: user.line,
+				profile: user.profile,
+				last_used_at: user.last_used_at,
+				is_bot: user.is_bot,
+				is_pro: user.is_pro,
+				two_factor_secret: user.two_factor_secret,
+				two_factor_enabled: user.two_factor_enabled,
+				client_settings: user.client_settings,
+				settings: user.settings,
+				keypair: generate()
+			}
+		}
+	});
+	return result.ok === 1;
+}
+
+async function main() {
+	const count = await User.count({});
+
+	const dop = Number.parseInt(process.argv[2]) || 5
+	const idop = ((count - (count % dop)) / dop) + 1
+
+	return zip(
+		1,
+		async (time) => {
+			console.log(`${time} / ${idop}`)
+			const doc = await User.find({}, {
+				limit: dop, skip: time * dop
+			})
+			return Promise.all(doc.map(migrate))
+		},
+		idop
+	).then(a => {
+		const rv = []
+		a.forEach(e => rv.push(...e))
+		return rv
+	})
+}
+
+main().then(console.dir).catch(console.error)
diff --git a/tools/migration/nighthike/4.js b/tools/migration/nighthike/4.js
new file mode 100644
index 000000000..2e252b7f4
--- /dev/null
+++ b/tools/migration/nighthike/4.js
@@ -0,0 +1,232 @@
+// このスクリプトを走らせる前か後に notifications コレクションはdropしてください
+
+db.access_tokens.renameCollection('accessTokens');
+db.accessTokens.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		app_id: 'appId',
+		user_id: 'userId',
+	}
+}, false, true);
+
+db.apps.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		user_id: 'userId',
+		name_id: 'nameId',
+		name_id_lower: 'nameIdLower',
+		callback_url: 'callbackUrl',
+	}
+}, false, true);
+
+db.auth_sessions.renameCollection('authSessions');
+db.authSessions.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		app_id: 'appId',
+		user_id: 'userId',
+	}
+}, false, true);
+
+db.channel_watching.renameCollection('channelWatching');
+db.channelWatching.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		deleted_at: 'deletedAt',
+		channel_id: 'channelId',
+		user_id: 'userId',
+	}
+}, false, true);
+
+db.channels.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		user_id: 'userId',
+		watching_count: 'watchingCount'
+	}
+}, false, true);
+
+db.drive_files.files.renameCollection('driveFiles.files');
+db.drive_files.chunks.renameCollection('driveFiles.chunks');
+db.driveFiles.files.update({}, {
+	$rename: {
+		'metadata.user_id': 'metadata.userId',
+		'metadata.folder_id': 'metadata.folderId',
+		'metadata.properties.average_color': 'metadata.properties.avgColor'
+	}
+}, false, true);
+
+db.drive_folders.renameCollection('driveFolders');
+db.driveFolders.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		user_id: 'userId',
+		parent_id: 'parentId',
+	}
+}, false, true);
+
+db.favorites.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		user_id: 'userId',
+		post_id: 'postId',
+	}
+}, false, true);
+
+db.following.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		deleted_at: 'deletedAt',
+		followee_id: 'followeeId',
+		follower_id: 'followerId',
+	}
+}, false, true);
+
+db.messaging_histories.renameCollection('messagingHistories');
+db.messagingHistories.update({}, {
+	$rename: {
+		updated_at: 'updatedAt',
+		user_id: 'userId',
+		partner: 'partnerId',
+		message: 'messageId',
+	}
+}, false, true);
+
+db.messaging_messages.renameCollection('messagingMessages');
+db.messagingMessages.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		user_id: 'userId',
+		recipient_id: 'recipientId',
+		file_id: 'fileId',
+		is_read: 'isRead'
+	}
+}, false, true);
+
+db.mute.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		deleted_at: 'deletedAt',
+		mutee_id: 'muteeId',
+		muter_id: 'muterId',
+	}
+}, false, true);
+
+db.othello_games.renameCollection('othelloGames');
+db.othelloGames.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		started_at: 'startedAt',
+		is_started: 'isStarted',
+		is_ended: 'isEnded',
+		user1_id: 'user1Id',
+		user2_id: 'user2Id',
+		user1_accepted: 'user1Accepted',
+		user2_accepted: 'user2Accepted',
+		winner_id: 'winnerId',
+		'settings.is_llotheo': 'settings.isLlotheo',
+		'settings.can_put_everywhere': 'settings.canPutEverywhere',
+		'settings.looped_board': 'settings.loopedBoard',
+	}
+}, false, true);
+
+db.othello_matchings.renameCollection('othelloMatchings');
+db.othelloMatchings.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		parent_id: 'parentId',
+		child_id: 'childId'
+	}
+}, false, true);
+
+db.poll_votes.renameCollection('pollVotes');
+db.pollVotes.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		user_id: 'userId',
+		post_id: 'postId'
+	}
+}, false, true);
+
+db.post_reactions.renameCollection('postReactions');
+db.postReactions.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		user_id: 'userId',
+		post_id: 'postId'
+	}
+}, false, true);
+
+db.post_watching.renameCollection('postWatching');
+db.postWatching.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		user_id: 'userId',
+		post_id: 'postId'
+	}
+}, false, true);
+
+db.posts.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		channel_id: 'channelId',
+		user_id: 'userId',
+		app_id: 'appId',
+		media_ids: 'mediaIds',
+		reply_id: 'replyId',
+		repost_id: 'repostId',
+		via_mobile: 'viaMobile',
+		reaction_counts: 'reactionCounts',
+		replies_count: 'repliesCount',
+		repost_count: 'repostCount',
+		'_reply.user_id': '_reply.userId',
+		'_repost.user_id': '_repost.userId',
+	}
+}, false, true);
+
+db.signin.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		user_id: 'userId',
+	}
+}, false, true);
+
+db.sw_subscriptions.renameCollection('swSubscriptions');
+db.swSubscriptions.update({}, {
+	$rename: {
+		user_id: 'userId',
+	}
+}, false, true);
+
+db.users.update({}, {
+	$rename: {
+		created_at: 'createdAt',
+		deleted_at: 'deletedAt',
+		followers_count: 'followersCount',
+		following_count: 'followingCount',
+		posts_count: 'postsCount',
+		drive_capacity: 'driveCapacity',
+		username_lower: 'usernameLower',
+		avatar_id: 'avatarId',
+		banner_id: 'bannerId',
+		pinned_post_id: 'pinnedPostId',
+		is_suspended: 'isSuspended',
+		host_lower: 'hostLower',
+		'account.last_used_at': 'account.lastUsedAt',
+		'account.is_bot': 'account.isBot',
+		'account.is_pro': 'account.isPro',
+		'account.two_factor_secret': 'account.twoFactorSecret',
+		'account.two_factor_enabled': 'account.twoFactorEnabled',
+		'account.client_settings': 'account.clientSettings'
+	},
+	$unset: {
+		likes_count: '',
+		liked_count: '',
+		latest_post: '',
+		'account.twitter.access_token': '',
+		'account.twitter.access_token_secret': '',
+		'account.twitter.user_id': '',
+		'account.twitter.screen_name': '',
+		'account.line.user_id': ''
+	}
+}, false, true);
diff --git a/tools/migration/shell.1522038492.user-account.js b/tools/migration/shell.1522038492.user-account.js
deleted file mode 100644
index 056c29e8e..000000000
--- a/tools/migration/shell.1522038492.user-account.js
+++ /dev/null
@@ -1,41 +0,0 @@
-db.users.dropIndex({ token: 1 });
-
-db.users.find({}).forEach(function(user) {
-	print(user._id);
-	db.users.update({ _id: user._id }, {
-		$unset: {
-			email: '',
-			links: '',
-			password: '',
-			token: '',
-			twitter: '',
-			line: '',
-			profile: '',
-			last_used_at: '',
-			is_bot: '',
-			is_pro: '',
-			two_factor_secret: '',
-			two_factor_enabled: '',
-			client_settings: '',
-			settings: ''
-		},
-		$set: {
-			account: {
-				email: user.email,
-				links: user.links,
-				password: user.password,
-				token: user.token,
-				twitter: user.twitter,
-				line: user.line,
-				profile: user.profile,
-				last_used_at: user.last_used_at,
-				is_bot: user.is_bot,
-				is_pro: user.is_pro,
-				two_factor_secret: user.two_factor_secret,
-				two_factor_enabled: user.two_factor_enabled,
-				client_settings: user.client_settings,
-				settings: user.settings
-			}
-		}
-	}, false, false);
-});
diff --git a/tools/migration/shell.1522116709.user-host.js b/tools/migration/shell.1522116709.user-host.js
deleted file mode 100644
index b354709a6..000000000
--- a/tools/migration/shell.1522116709.user-host.js
+++ /dev/null
@@ -1 +0,0 @@
-db.users.update({ }, { $set: { host: null } }, { multi: true });
diff --git a/tools/migration/shell.1522116710.user-host_lower.js b/tools/migration/shell.1522116710.user-host_lower.js
deleted file mode 100644
index 31ec6c468..000000000
--- a/tools/migration/shell.1522116710.user-host_lower.js
+++ /dev/null
@@ -1 +0,0 @@
-db.users.update({ }, { $set: { host_lower: null } }, { multi: true });
diff --git a/webpack.config.ts b/webpack.config.ts
index 6f16fcbfa..53e3d2630 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -20,6 +20,7 @@ import { licenseHtml } from './src/build/license';
 import locales from './locales';
 const meta = require('./package.json');
 const version = meta.version;
+const codename = meta.codename;
 
 //#region Replacer definitions
 global['faReplacement'] = faReplacement;
@@ -76,13 +77,13 @@ module.exports = entries.map(x => {
 		_THEME_COLOR_: constants.themeColor,
 		_COPYRIGHT_: constants.copyright,
 		_VERSION_: version,
+		_CODENAME_: codename,
 		_STATUS_URL_: config.status_url,
 		_STATS_URL_: config.stats_url,
 		_DOCS_URL_: config.docs_url,
 		_API_URL_: config.api_url,
 		_WS_URL_: config.ws_url,
 		_DEV_URL_: config.dev_url,
-		_CH_URL_: config.ch_url,
 		_LANG_: lang,
 		_HOST_: config.host,
 		_HOSTNAME_: config.hostname,

From 64e4057850ded7312363281dfe5ae721273eb342 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 29 Mar 2018 14:53:48 +0900
Subject: [PATCH 0926/1250] oops

---
 src/server/api/endpoints.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/server/api/endpoints.ts b/src/server/api/endpoints.ts
index 979d8ac29..c7100bd03 100644
--- a/src/server/api/endpoints.ts
+++ b/src/server/api/endpoints.ts
@@ -289,7 +289,7 @@ const endpoints: Endpoint[] = [
 		kind: 'notification-write'
 	},
 	{
-		name: 'notifications/markAsRead_all',
+		name: 'notifications/mark_as_read_all',
 		withCredential: true,
 		kind: 'notification-write'
 	},

From 7d2e176aa145aa4d8475442a9919046828ebb227 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 29 Mar 2018 14:59:06 +0900
Subject: [PATCH 0927/1250] Use @type

---
 package.json     | 3 ++-
 src/parse-opt.ts | 2 +-
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index 5d11bf330..743994aed 100644
--- a/package.json
+++ b/package.json
@@ -65,6 +65,7 @@
 		"@types/ms": "0.7.30",
 		"@types/multer": "1.3.6",
 		"@types/node": "9.6.0",
+		"@types/nopt": "3.0.29",
 		"@types/proxy-addr": "2.0.0",
 		"@types/pug": "2.0.4",
 		"@types/qrcode": "0.8.1",
@@ -149,10 +150,10 @@
 		"morgan": "1.9.0",
 		"ms": "2.1.1",
 		"multer": "1.3.0",
-		"nopt": "4.0.1",
 		"nan": "2.10.0",
 		"node-sass": "4.8.3",
 		"node-sass-json-importer": "3.1.6",
+		"nopt": "4.0.1",
 		"nprogress": "0.2.0",
 		"object-assign-deep": "0.3.1",
 		"on-build-webpack": "0.1.0",
diff --git a/src/parse-opt.ts b/src/parse-opt.ts
index 61dc60c6c..031b1a6fe 100644
--- a/src/parse-opt.ts
+++ b/src/parse-opt.ts
@@ -1,4 +1,4 @@
-const nopt = require('nopt');
+import * as nopt from 'nopt';
 
 export default (vector, index) => {
 	const parsed = nopt({

From 308dedf5e4df45e6599c0d8084824f20c051a883 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 29 Mar 2018 15:23:15 +0900
Subject: [PATCH 0928/1250] #1253

---
 src/server/api/endpoints/posts/create.ts      |  5 +-
 src/server/api/models/post.ts                 |  4 +-
 .../desktop/views/components/post-detail.vue  |  4 +-
 .../desktop/views/components/post-form.vue    |  3 +-
 .../desktop/views/components/posts.post.vue   |  4 +-
 .../mobile/views/components/post-detail.vue   |  4 +-
 .../app/mobile/views/components/post-form.vue |  3 +-
 .../web/app/mobile/views/components/post.vue  |  4 +-
 src/server/web/docs/api/entities/post.yaml    | 11 ++---
 tools/migration/nighthike/5.js                | 47 +++++++++++++++++++
 10 files changed, 65 insertions(+), 24 deletions(-)
 create mode 100644 tools/migration/nighthike/5.js

diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts
index 390370ad4..33042a51a 100644
--- a/src/server/api/endpoints/posts/create.ts
+++ b/src/server/api/endpoints/posts/create.ts
@@ -43,8 +43,9 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 
 	// Get 'geo' parameter
 	const [geo, geoErr] = $(params.geo).optional.nullable.strict.object()
-		.have('latitude', $().number().range(-90, 90))
-		.have('longitude', $().number().range(-180, 180))
+		.have('coordinates', $().array().length(2)
+			.item(0, $().number().range(-180, 180))
+			.item(1, $().number().range(-90, 90)))
 		.have('altitude', $().nullable.number())
 		.have('accuracy', $().nullable.number())
 		.have('altitudeAccuracy', $().nullable.number())
diff --git a/src/server/api/models/post.ts b/src/server/api/models/post.ts
index 0317cff3f..1bf4e0905 100644
--- a/src/server/api/models/post.ts
+++ b/src/server/api/models/post.ts
@@ -35,8 +35,7 @@ export type IPost = {
 	reactionCounts: any;
 	mentions: mongo.ObjectID[];
 	geo: {
-		latitude: number;
-		longitude: number;
+		coordinates: number[];
 		altitude: number;
 		accuracy: number;
 		altitudeAccuracy: number;
@@ -97,6 +96,7 @@ export const pack = async (
 	delete _post._id;
 
 	delete _post.mentions;
+	if (_post.geo) delete _post.geo.type;
 
 	// Parse text
 	if (_post.text) {
diff --git a/src/server/web/app/desktop/views/components/post-detail.vue b/src/server/web/app/desktop/views/components/post-detail.vue
index 7783ec62c..5c7a7dfdb 100644
--- a/src/server/web/app/desktop/views/components/post-detail.vue
+++ b/src/server/web/app/desktop/views/components/post-detail.vue
@@ -47,7 +47,7 @@
 			<div class="tags" v-if="p.tags && p.tags.length > 0">
 				<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
 			</div>
-			<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.latitude},${p.geo.longitude}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
+			<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
 			<div class="map" v-if="p.geo" ref="map"></div>
 			<div class="repost" v-if="p.repost">
 				<mk-post-preview :post="p.repost"/>
@@ -157,7 +157,7 @@ export default Vue.extend({
 			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
-					const uluru = new maps.LatLng(this.p.geo.latitude, this.p.geo.longitude);
+					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
 					const map = new maps.Map(this.$refs.map, {
 						center: uluru,
 						zoom: 15
diff --git a/src/server/web/app/desktop/views/components/post-form.vue b/src/server/web/app/desktop/views/components/post-form.vue
index 11028ceb5..1c83a38b6 100644
--- a/src/server/web/app/desktop/views/components/post-form.vue
+++ b/src/server/web/app/desktop/views/components/post-form.vue
@@ -224,8 +224,7 @@ export default Vue.extend({
 				repostId: this.repost ? this.repost.id : undefined,
 				poll: this.poll ? (this.$refs.poll as any).get() : undefined,
 				geo: this.geo ? {
-					latitude: this.geo.latitude,
-					longitude: this.geo.longitude,
+					coordinates: [this.geo.longitude, this.geo.latitude],
 					altitude: this.geo.altitude,
 					accuracy: this.geo.accuracy,
 					altitudeAccuracy: this.geo.altitudeAccuracy,
diff --git a/src/server/web/app/desktop/views/components/posts.post.vue b/src/server/web/app/desktop/views/components/posts.post.vue
index c70e01911..37c6e6304 100644
--- a/src/server/web/app/desktop/views/components/posts.post.vue
+++ b/src/server/web/app/desktop/views/components/posts.post.vue
@@ -48,7 +48,7 @@
 				<div class="tags" v-if="p.tags && p.tags.length > 0">
 					<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
 				</div>
-				<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.latitude},${p.geo.longitude}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
+				<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
 				<div class="map" v-if="p.geo" ref="map"></div>
 				<div class="repost" v-if="p.repost">
 					<mk-post-preview :post="p.repost"/>
@@ -169,7 +169,7 @@ export default Vue.extend({
 			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
-					const uluru = new maps.LatLng(this.p.geo.latitude, this.p.geo.longitude);
+					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
 					const map = new maps.Map(this.$refs.map, {
 						center: uluru,
 						zoom: 15
diff --git a/src/server/web/app/mobile/views/components/post-detail.vue b/src/server/web/app/mobile/views/components/post-detail.vue
index 29993c79e..f0af1a61a 100644
--- a/src/server/web/app/mobile/views/components/post-detail.vue
+++ b/src/server/web/app/mobile/views/components/post-detail.vue
@@ -47,7 +47,7 @@
 			</div>
 			<mk-poll v-if="p.poll" :post="p"/>
 			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
-			<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.latitude},${p.geo.longitude}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
+			<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
 			<div class="map" v-if="p.geo" ref="map"></div>
 			<div class="repost" v-if="p.repost">
 				<mk-post-preview :post="p.repost"/>
@@ -154,7 +154,7 @@ export default Vue.extend({
 			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
-					const uluru = new maps.LatLng(this.p.geo.latitude, this.p.geo.longitude);
+					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
 					const map = new maps.Map(this.$refs.map, {
 						center: uluru,
 						zoom: 15
diff --git a/src/server/web/app/mobile/views/components/post-form.vue b/src/server/web/app/mobile/views/components/post-form.vue
index 929dc5933..5b78a2571 100644
--- a/src/server/web/app/mobile/views/components/post-form.vue
+++ b/src/server/web/app/mobile/views/components/post-form.vue
@@ -118,8 +118,7 @@ export default Vue.extend({
 				replyId: this.reply ? this.reply.id : undefined,
 				poll: this.poll ? (this.$refs.poll as any).get() : undefined,
 				geo: this.geo ? {
-					latitude: this.geo.latitude,
-					longitude: this.geo.longitude,
+					coordinates: [this.geo.longitude, this.geo.latitude],
 					altitude: this.geo.altitude,
 					accuracy: this.geo.accuracy,
 					altitudeAccuracy: this.geo.altitudeAccuracy,
diff --git a/src/server/web/app/mobile/views/components/post.vue b/src/server/web/app/mobile/views/components/post.vue
index 66c595f4e..a01eb7669 100644
--- a/src/server/web/app/mobile/views/components/post.vue
+++ b/src/server/web/app/mobile/views/components/post.vue
@@ -48,7 +48,7 @@
 					<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
 				</div>
 				<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
-				<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.latitude},${p.geo.longitude}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
+				<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
 				<div class="map" v-if="p.geo" ref="map"></div>
 				<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
 				<div class="repost" v-if="p.repost">
@@ -147,7 +147,7 @@ export default Vue.extend({
 			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
-					const uluru = new maps.LatLng(this.p.geo.latitude, this.p.geo.longitude);
+					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
 					const map = new maps.Map(this.$refs.map, {
 						center: uluru,
 						zoom: 15
diff --git a/src/server/web/docs/api/entities/post.yaml b/src/server/web/docs/api/entities/post.yaml
index 71b6a6412..74d7973e3 100644
--- a/src/server/web/docs/api/entities/post.yaml
+++ b/src/server/web/docs/api/entities/post.yaml
@@ -136,16 +136,11 @@ props:
       en: "Geo location"
     defName: "geo"
     def:
-      - name: "latitude"
-        type: "number"
+      - name: "coordinates"
+        type: "number[]"
         optional: false
         desc:
-          ja: "緯度。-90〜90で表す。"
-      - name: "longitude"
-        type: "number"
-        optional: false
-        desc:
-          ja: "経度。-180〜180で表す。"
+          ja: "座標。最初に経度:-180〜180で表す。最後に緯度:-90〜90で表す。"
       - name: "altitude"
         type: "number"
         optional: false
diff --git a/tools/migration/nighthike/5.js b/tools/migration/nighthike/5.js
new file mode 100644
index 000000000..fb72b6907
--- /dev/null
+++ b/tools/migration/nighthike/5.js
@@ -0,0 +1,47 @@
+// for Node.js interpret
+
+const { default: Post } = require('../../../built/api/models/post');
+const { default: zip } = require('@prezzemolo/zip')
+
+const migrate = async (post) => {
+	const result = await Post.update(post._id, {
+		$set: {
+			'geo.type': 'Point',
+			'geo.coordinates': [post.geo.longitude, post.geo.latitude]
+		},
+		$unset: {
+			'geo.longitude': '',
+			'geo.latitude': '',
+		}
+	});
+	return result.ok === 1;
+}
+
+async function main() {
+	const count = await Post.count({
+		'geo': { $ne: null }
+	});
+
+	const dop = Number.parseInt(process.argv[2]) || 5
+	const idop = ((count - (count % dop)) / dop) + 1
+
+	return zip(
+		1,
+		async (time) => {
+			console.log(`${time} / ${idop}`)
+			const doc = await Post.find({
+				'geo': { $ne: null }
+			}, {
+				limit: dop, skip: time * dop
+			})
+			return Promise.all(doc.map(migrate))
+		},
+		idop
+	).then(a => {
+		const rv = []
+		a.forEach(e => rv.push(...e))
+		return rv
+	})
+}
+
+main().then(console.dir).catch(console.error)

From 70455b8ab3b881561e1fd8301b533668c4f486e0 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 29 Mar 2018 16:35:29 +0900
Subject: [PATCH 0929/1250] Update .travis.yml

---
 .travis.yml | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index aa1410c6e..d2552bb46 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -36,8 +36,7 @@ before_script:
   # --only=dev オプションを付けてそれらもインストールされるようにする:
   - npm install --only=dev
 
-  # 設定ファイルを設定
-  - mkdir ./.config
+  # 設定ファイルを配置
   - cp ./.travis/default.yml ./.config
   - cp ./.travis/test.yml ./.config
 

From 40c10a00d85a2c6f58faa8663837ad2909089088 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 29 Mar 2018 20:32:18 +0900
Subject: [PATCH 0930/1250] =?UTF-8?q?=E6=95=B4=E7=90=86=E3=81=97=E3=81=9F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 gulpfile.ts                                   |  31 +-
 src/{server/web => client}/app/animation.styl |   0
 src/{server/web => client}/app/app.styl       |   0
 src/{server/web => client}/app/app.vue        |   0
 .../web => client}/app/auth/assets/logo.svg   |   0
 src/{server/web => client}/app/auth/script.ts |   0
 .../web => client}/app/auth/style.styl        |   0
 .../web => client}/app/auth/views/form.vue    |   0
 .../web => client}/app/auth/views/index.vue   |   0
 src/{server/web => client}/app/base.pug       |   6 +-
 src/{server/web => client}/app/boot.js        |   0
 src/{server/web => client}/app/ch/script.ts   |   0
 src/{server/web => client}/app/ch/style.styl  |   0
 .../web => client}/app/ch/tags/channel.tag    |   0
 .../web => client}/app/ch/tags/header.tag     |   0
 .../web => client}/app/ch/tags/index.tag      |   0
 .../web => client}/app/ch/tags/index.ts       |   0
 .../app/common/define-widget.ts               |   0
 src/{server/web => client}/app/common/mios.ts |   0
 .../app/common/scripts/check-for-update.ts    |   0
 .../common/scripts/compose-notification.ts    |   0
 .../app/common/scripts/contains.ts            |   0
 .../app/common/scripts/copy-to-clipboard.ts   |   0
 .../app/common/scripts/date-stringify.ts      |   0
 .../app/common/scripts/fuck-ad-block.ts       |   0
 .../web => client}/app/common/scripts/gcd.ts  |   0
 .../app/common/scripts/get-kao.ts             |   0
 .../app/common/scripts/get-median.ts          |   0
 .../app/common/scripts/loading.ts             |   0
 .../app/common/scripts/parse-search-query.ts  |   0
 .../app/common/scripts/streaming/channel.ts   |   0
 .../app/common/scripts/streaming/drive.ts     |   0
 .../app/common/scripts/streaming/home.ts      |   0
 .../scripts/streaming/messaging-index.ts      |   0
 .../app/common/scripts/streaming/messaging.ts |   0
 .../common/scripts/streaming/othello-game.ts  |   0
 .../app/common/scripts/streaming/othello.ts   |   0
 .../app/common/scripts/streaming/requests.ts  |   0
 .../app/common/scripts/streaming/server.ts    |   0
 .../scripts/streaming/stream-manager.ts       |   0
 .../app/common/scripts/streaming/stream.ts    |   0
 .../common/views/components/autocomplete.vue  |   0
 .../connect-failed.troubleshooter.vue         |   0
 .../views/components/connect-failed.vue       |   0
 .../app/common/views/components/ellipsis.vue  |   0
 .../views/components/file-type-icon.vue       |   0
 .../app/common/views/components/forkit.vue    |   0
 .../app/common/views/components/index.ts      |   0
 .../common/views/components/media-list.vue    |   0
 .../views/components/messaging-room.form.vue  |   0
 .../components/messaging-room.message.vue     |   0
 .../views/components/messaging-room.vue       |   0
 .../app/common/views/components/messaging.vue |   0
 .../app/common/views/components/nav.vue       |   0
 .../common/views/components/othello.game.vue  |   0
 .../views/components/othello.gameroom.vue     |   0
 .../common/views/components/othello.room.vue  |   0
 .../app/common/views/components/othello.vue   |   0
 .../common/views/components/poll-editor.vue   |   0
 .../app/common/views/components/poll.vue      |   0
 .../app/common/views/components/post-html.ts  |   0
 .../app/common/views/components/post-menu.vue |   0
 .../common/views/components/reaction-icon.vue |   0
 .../views/components/reaction-picker.vue      |   0
 .../views/components/reactions-viewer.vue     |   0
 .../app/common/views/components/signin.vue    |   0
 .../app/common/views/components/signup.vue    |   0
 .../views/components/special-message.vue      |   0
 .../views/components/stream-indicator.vue     |   0
 .../app/common/views/components/switch.vue    |   0
 .../app/common/views/components/time.vue      |   0
 .../app/common/views/components/timer.vue     |   0
 .../views/components/twitter-setting.vue      |   0
 .../app/common/views/components/uploader.vue  |   0
 .../common/views/components/url-preview.vue   |   0
 .../app/common/views/components/url.vue       |   0
 .../views/components/welcome-timeline.vue     |   0
 .../common/views/directives/autocomplete.ts   |   0
 .../app/common/views/directives/index.ts      |   0
 .../app/common/views/filters/bytes.ts         |   0
 .../app/common/views/filters/index.ts         |   0
 .../app/common/views/filters/number.ts        |   0
 .../app/common/views/widgets/access-log.vue   |   0
 .../app/common/views/widgets/broadcast.vue    |   0
 .../app/common/views/widgets/calendar.vue     |   0
 .../app/common/views/widgets/donation.vue     |   0
 .../app/common/views/widgets/index.ts         |   0
 .../app/common/views/widgets/nav.vue          |   0
 .../app/common/views/widgets/photo-stream.vue |   0
 .../app/common/views/widgets/rss.vue          |   0
 .../views/widgets/server.cpu-memory.vue       |   0
 .../app/common/views/widgets/server.cpu.vue   |   0
 .../app/common/views/widgets/server.disk.vue  |   0
 .../app/common/views/widgets/server.info.vue  |   0
 .../common/views/widgets/server.memory.vue    |   0
 .../app/common/views/widgets/server.pie.vue   |   0
 .../common/views/widgets/server.uptimes.vue   |   0
 .../app/common/views/widgets/server.vue       |   0
 .../app/common/views/widgets/slideshow.vue    |   0
 .../app/common/views/widgets/tips.vue         |   0
 .../app/common/views/widgets/version.vue      |   0
 src/{server/web => client}/app/config.ts      |   0
 .../app/desktop/api/choose-drive-file.ts      |   0
 .../app/desktop/api/choose-drive-folder.ts    |   0
 .../app/desktop/api/contextmenu.ts            |   0
 .../web => client}/app/desktop/api/dialog.ts  |   0
 .../web => client}/app/desktop/api/input.ts   |   0
 .../web => client}/app/desktop/api/notify.ts  |   0
 .../web => client}/app/desktop/api/post.ts    |   0
 .../app/desktop/api/update-avatar.ts          |   0
 .../app/desktop/api/update-banner.ts          |   0
 .../app/desktop/assets/grid.svg               |   0
 .../app/desktop/assets/header-logo-white.svg  |   0
 .../app/desktop/assets/header-logo.svg        |   0
 .../app/desktop/assets/index.jpg              |   0
 .../app/desktop/assets/remove.png             |   0
 .../web => client}/app/desktop/script.ts      |   0
 .../web => client}/app/desktop/style.styl     |   0
 .../web => client}/app/desktop/ui.styl        |   0
 .../views/components/activity.calendar.vue    |   0
 .../views/components/activity.chart.vue       |   0
 .../app/desktop/views/components/activity.vue |   0
 .../desktop/views/components/analog-clock.vue |   0
 .../app/desktop/views/components/calendar.vue |   0
 .../choose-file-from-drive-window.vue         |   0
 .../choose-folder-from-drive-window.vue       |   0
 .../views/components/context-menu.menu.vue    |   0
 .../desktop/views/components/context-menu.vue |   0
 .../desktop/views/components/crop-window.vue  |   0
 .../app/desktop/views/components/dialog.vue   |   0
 .../desktop/views/components/drive-window.vue |   0
 .../desktop/views/components/drive.file.vue   |   0
 .../desktop/views/components/drive.folder.vue |   0
 .../views/components/drive.nav-folder.vue     |   0
 .../app/desktop/views/components/drive.vue    |   0
 .../views/components/ellipsis-icon.vue        |   0
 .../views/components/follow-button.vue        |   0
 .../views/components/followers-window.vue     |   0
 .../desktop/views/components/followers.vue    |   0
 .../views/components/following-window.vue     |   0
 .../desktop/views/components/following.vue    |   0
 .../views/components/friends-maker.vue        |   0
 .../desktop/views/components/game-window.vue  |   0
 .../app/desktop/views/components/home.vue     |   0
 .../app/desktop/views/components/index.ts     |   0
 .../desktop/views/components/input-dialog.vue |   0
 .../views/components/media-image-dialog.vue   |   0
 .../desktop/views/components/media-image.vue  |   0
 .../views/components/media-video-dialog.vue   |   0
 .../desktop/views/components/media-video.vue  |   0
 .../app/desktop/views/components/mentions.vue |   0
 .../components/messaging-room-window.vue      |   0
 .../views/components/messaging-window.vue     |   0
 .../views/components/notifications.vue        |   0
 .../views/components/post-detail.sub.vue      |   0
 .../desktop/views/components/post-detail.vue  |   0
 .../views/components/post-form-window.vue     |   0
 .../desktop/views/components/post-form.vue    |   0
 .../desktop/views/components/post-preview.vue |   0
 .../views/components/posts.post.sub.vue       |   0
 .../desktop/views/components/posts.post.vue   |   0
 .../app/desktop/views/components/posts.vue    |   0
 .../views/components/progress-dialog.vue      |   0
 .../views/components/repost-form-window.vue   |   0
 .../desktop/views/components/repost-form.vue  |   0
 .../views/components/settings-window.vue      |   0
 .../desktop/views/components/settings.2fa.vue |   0
 .../desktop/views/components/settings.api.vue |   0
 .../views/components/settings.apps.vue        |   0
 .../views/components/settings.drive.vue       |   0
 .../views/components/settings.mute.vue        |   0
 .../views/components/settings.password.vue    |   0
 .../views/components/settings.profile.vue     |   0
 .../views/components/settings.signins.vue     |   0
 .../app/desktop/views/components/settings.vue |   0
 .../views/components/sub-post-content.vue     |   0
 .../desktop/views/components/taskmanager.vue  |   0
 .../app/desktop/views/components/timeline.vue |   0
 .../views/components/ui-notification.vue      |   0
 .../views/components/ui.header.account.vue    |   0
 .../views/components/ui.header.clock.vue      |   0
 .../views/components/ui.header.nav.vue        |   0
 .../components/ui.header.notifications.vue    |   0
 .../views/components/ui.header.post.vue       |   0
 .../views/components/ui.header.search.vue     |   0
 .../desktop/views/components/ui.header.vue    |   0
 .../app/desktop/views/components/ui.vue       |   0
 .../desktop/views/components/user-preview.vue |   0
 .../views/components/users-list.item.vue      |   0
 .../desktop/views/components/users-list.vue   |   0
 .../views/components/widget-container.vue     |   0
 .../app/desktop/views/components/window.vue   |   0
 .../app/desktop/views/directives/index.ts     |   0
 .../desktop/views/directives/user-preview.ts  |   0
 .../app/desktop/views/pages/drive.vue         |   0
 .../desktop/views/pages/home-customize.vue    |   0
 .../app/desktop/views/pages/home.vue          |   0
 .../app/desktop/views/pages/index.vue         |   0
 .../desktop/views/pages/messaging-room.vue    |   0
 .../app/desktop/views/pages/othello.vue       |   0
 .../app/desktop/views/pages/post.vue          |   0
 .../app/desktop/views/pages/search.vue        |   0
 .../app/desktop/views/pages/selectdrive.vue   |   0
 .../pages/user/user.followers-you-know.vue    |   0
 .../desktop/views/pages/user/user.friends.vue |   0
 .../desktop/views/pages/user/user.header.vue  |   0
 .../desktop/views/pages/user/user.home.vue    |   0
 .../desktop/views/pages/user/user.photos.vue  |   0
 .../desktop/views/pages/user/user.profile.vue |   0
 .../views/pages/user/user.timeline.vue        |   0
 .../app/desktop/views/pages/user/user.vue     |   0
 .../app/desktop/views/pages/welcome.vue       |   0
 .../app/desktop/views/widgets/activity.vue    |   0
 .../views/widgets/channel.channel.form.vue    |   0
 .../views/widgets/channel.channel.post.vue    |   0
 .../desktop/views/widgets/channel.channel.vue |   0
 .../app/desktop/views/widgets/channel.vue     |   0
 .../app/desktop/views/widgets/index.ts        |   0
 .../app/desktop/views/widgets/messaging.vue   |   0
 .../desktop/views/widgets/notifications.vue   |   0
 .../app/desktop/views/widgets/polls.vue       |   0
 .../app/desktop/views/widgets/post-form.vue   |   0
 .../app/desktop/views/widgets/profile.vue     |   0
 .../app/desktop/views/widgets/timemachine.vue |   0
 .../app/desktop/views/widgets/trends.vue      |   0
 .../app/desktop/views/widgets/users.vue       |   0
 src/{server/web => client}/app/dev/script.ts  |   0
 src/{server/web => client}/app/dev/style.styl |   0
 .../web => client}/app/dev/views/app.vue      |   0
 .../web => client}/app/dev/views/apps.vue     |   0
 .../web => client}/app/dev/views/index.vue    |   0
 .../web => client}/app/dev/views/new-app.vue  |   0
 .../web => client}/app/dev/views/ui.vue       |   0
 src/{server/web => client}/app/init.css       |   0
 src/{server/web => client}/app/init.ts        |   0
 .../app/mobile/api/choose-drive-file.ts       |   0
 .../app/mobile/api/choose-drive-folder.ts     |   0
 .../web => client}/app/mobile/api/dialog.ts   |   0
 .../web => client}/app/mobile/api/input.ts    |   0
 .../web => client}/app/mobile/api/notify.ts   |   0
 .../web => client}/app/mobile/api/post.ts     |   0
 .../web => client}/app/mobile/script.ts       |   0
 .../web => client}/app/mobile/style.styl      |   0
 .../app/mobile/views/components/activity.vue  |   0
 .../views/components/drive-file-chooser.vue   |   0
 .../views/components/drive-folder-chooser.vue |   0
 .../views/components/drive.file-detail.vue    |   0
 .../mobile/views/components/drive.file.vue    |   0
 .../mobile/views/components/drive.folder.vue  |   0
 .../app/mobile/views/components/drive.vue     |   0
 .../mobile/views/components/follow-button.vue |   0
 .../mobile/views/components/friends-maker.vue |   0
 .../app/mobile/views/components/index.ts      |   0
 .../mobile/views/components/media-image.vue   |   0
 .../mobile/views/components/media-video.vue   |   0
 .../views/components/notification-preview.vue |   0
 .../mobile/views/components/notification.vue  |   0
 .../mobile/views/components/notifications.vue |   0
 .../app/mobile/views/components/notify.vue    |   0
 .../app/mobile/views/components/post-card.vue |   0
 .../views/components/post-detail.sub.vue      |   0
 .../mobile/views/components/post-detail.vue   |   0
 .../app/mobile/views/components/post-form.vue |   0
 .../mobile/views/components/post-preview.vue  |   0
 .../app/mobile/views/components/post.sub.vue  |   0
 .../app/mobile/views/components/post.vue      |   0
 .../app/mobile/views/components/posts.vue     |   0
 .../views/components/sub-post-content.vue     |   0
 .../app/mobile/views/components/timeline.vue  |   0
 .../app/mobile/views/components/ui.header.vue |   0
 .../app/mobile/views/components/ui.nav.vue    |   0
 .../app/mobile/views/components/ui.vue        |   0
 .../app/mobile/views/components/user-card.vue |   0
 .../mobile/views/components/user-preview.vue  |   0
 .../mobile/views/components/user-timeline.vue |   0
 .../mobile/views/components/users-list.vue    |   0
 .../views/components/widget-container.vue     |   0
 .../app/mobile/views/directives/index.ts      |   0
 .../mobile/views/directives/user-preview.ts   |   0
 .../app/mobile/views/pages/drive.vue          |   0
 .../app/mobile/views/pages/followers.vue      |   0
 .../app/mobile/views/pages/following.vue      |   0
 .../app/mobile/views/pages/home.vue           |   0
 .../app/mobile/views/pages/index.vue          |   0
 .../app/mobile/views/pages/messaging-room.vue |   0
 .../app/mobile/views/pages/messaging.vue      |   0
 .../app/mobile/views/pages/notifications.vue  |   0
 .../app/mobile/views/pages/othello.vue        |   0
 .../app/mobile/views/pages/post.vue           |   0
 .../mobile/views/pages/profile-setting.vue    |   0
 .../app/mobile/views/pages/search.vue         |   0
 .../app/mobile/views/pages/selectdrive.vue    |   0
 .../app/mobile/views/pages/settings.vue       |   0
 .../app/mobile/views/pages/signup.vue         |   0
 .../app/mobile/views/pages/user.vue           |   0
 .../pages/user/home.followers-you-know.vue    |   0
 .../mobile/views/pages/user/home.friends.vue  |   0
 .../mobile/views/pages/user/home.photos.vue   |   0
 .../mobile/views/pages/user/home.posts.vue    |   0
 .../app/mobile/views/pages/user/home.vue      |   0
 .../app/mobile/views/pages/welcome.vue        |   0
 .../app/mobile/views/widgets/activity.vue     |   0
 .../app/mobile/views/widgets/index.ts         |   0
 .../app/mobile/views/widgets/profile.vue      |   0
 src/{server/web => client}/app/reset.styl     |   0
 src/{server/web => client}/app/safe.js        |   0
 .../web => client}/app/stats/style.styl       |   0
 .../web => client}/app/stats/tags/index.tag   |   0
 .../web => client}/app/stats/tags/index.ts    |   0
 .../web => client}/app/status/style.styl      |   0
 .../web => client}/app/status/tags/index.tag  |   0
 .../web => client}/app/status/tags/index.ts   |   0
 src/{server/web => client}/app/sw.js          |   0
 src/{server/web => client}/app/tsconfig.json  |   0
 src/{server/web => client}/app/v.d.ts         |   0
 src/{server/web => client}/assets/404.js      |   0
 .../web => client}/assets/code-highlight.css  |   0
 src/{server/web => client}/assets/error.jpg   |   0
 src/{server/web => client}/assets/favicon.ico |   0
 src/{server/web => client}/assets/label.svg   |   0
 .../web => client}/assets/manifest.json       |   0
 src/{server/web => client}/assets/message.mp3 |   0
 .../web => client}/assets/othello-put-me.mp3  |   0
 .../web => client}/assets/othello-put-you.mp3 |   0
 src/{server/web => client}/assets/post.mp3    |   0
 .../web => client}/assets/reactions/angry.png |   0
 .../assets/reactions/confused.png             |   0
 .../assets/reactions/congrats.png             |   0
 .../web => client}/assets/reactions/hmm.png   |   0
 .../web => client}/assets/reactions/laugh.png |   0
 .../web => client}/assets/reactions/like.png  |   0
 .../web => client}/assets/reactions/love.png  |   0
 .../assets/reactions/pudding.png              |   0
 .../assets/reactions/surprise.png             |   0
 .../web => client}/assets/recover.html        |   0
 src/{server/web => client}/assets/title.svg   |   0
 src/{server/web => client}/assets/unread.svg  |   0
 .../web => client}/assets/welcome-bg.svg      |   0
 .../web => client}/assets/welcome-fg.svg      |   0
 src/{server/web => client}/const.styl         |   2 +-
 src/{server/web => client}/docs/about.en.pug  |   0
 src/{server/web => client}/docs/about.ja.pug  |   0
 src/{server/web => client}/docs/api.ja.pug    |   0
 .../docs/api/endpoints/posts/create.yaml      |   0
 .../docs/api/endpoints/posts/timeline.yaml    |   0
 .../docs/api/endpoints/style.styl             |   0
 .../docs/api/endpoints/view.pug               |   0
 .../docs/api/entities/drive-file.yaml         |   0
 .../docs/api/entities/post.yaml               |   0
 .../docs/api/entities/style.styl              |   0
 .../docs/api/entities/user.yaml               |   0
 .../web => client}/docs/api/entities/view.pug |   0
 .../web => client}/docs/api/gulpfile.ts       |  24 +-
 .../web => client}/docs/api/mixins.pug        |   0
 .../web => client}/docs/api/style.styl        |   0
 src/{server/web => client}/docs/gulpfile.ts   |  16 +-
 src/{server/web => client}/docs/index.en.pug  |   0
 src/{server/web => client}/docs/index.ja.pug  |   0
 src/{server/web => client}/docs/layout.pug    |   0
 .../web => client}/docs/license.en.pug        |   0
 .../web => client}/docs/license.ja.pug        |   0
 src/{server/web => client}/docs/mute.ja.pug   |   0
 src/{server/web => client}/docs/search.ja.pug |   0
 src/{server/web => client}/docs/server.ts     |   0
 src/{server/web => client}/docs/style.styl    |   0
 src/{server/web => client}/docs/tou.ja.pug    |   0
 src/{server/web => client}/docs/ui.styl       |   0
 src/{server/web => client}/docs/vars.ts       |  16 +-
 src/{server/web => client}/element.scss       |   2 +-
 src/{server/web => client}/style.styl         |   0
 .../common/get-notification-summary.ts        |   0
 src/{server => }/common/get-post-summary.ts   |   0
 src/{server => }/common/get-reaction-emoji.ts |   0
 src/{server => }/common/othello/ai/back.ts    |   2 +-
 src/{server => }/common/othello/ai/front.ts   |   2 +-
 src/{server => }/common/othello/ai/index.ts   |   0
 src/{server => }/common/othello/core.ts       |   0
 src/{server => }/common/othello/maps.ts       |   0
 .../common/text/core/syntax-highlighter.ts    |   0
 .../api => }/common/text/elements/bold.ts     |   0
 .../api => }/common/text/elements/code.ts     |   0
 .../api => }/common/text/elements/emoji.ts    |   0
 .../api => }/common/text/elements/hashtag.ts  |   0
 .../common/text/elements/inline-code.ts       |   0
 .../api => }/common/text/elements/link.ts     |   0
 .../api => }/common/text/elements/mention.ts  |   2 +-
 .../api => }/common/text/elements/quote.ts    |   0
 .../api => }/common/text/elements/url.ts      |   0
 src/{server/api => }/common/text/index.ts     |   0
 src/{server => }/common/user/get-acct.ts      |   0
 src/{server => }/common/user/get-summary.ts   |   2 +-
 src/{server => }/common/user/parse-acct.ts    |   0
 src/{server/api => }/models/access-token.ts   |   2 +-
 src/{server/api => }/models/app.ts            |   4 +-
 src/{server/api => }/models/auth-session.ts   |   2 +-
 .../api => }/models/channel-watching.ts       |   2 +-
 src/{server/api => }/models/channel.ts        |   2 +-
 src/{server/api => }/models/drive-file.ts     |   4 +-
 src/{server/api => }/models/drive-folder.ts   |   2 +-
 src/{server/api => }/models/favorite.ts       |   2 +-
 src/{server/api => }/models/following.ts      |   2 +-
 .../api => }/models/messaging-history.ts      |   2 +-
 .../api => }/models/messaging-message.ts      |   2 +-
 src/{server/api => }/models/meta.ts           |   2 +-
 src/{server/api => }/models/mute.ts           |   2 +-
 src/{server/api => }/models/notification.ts   |   2 +-
 src/{server/api => }/models/othello-game.ts   |   2 +-
 .../api => }/models/othello-matching.ts       |   2 +-
 src/{server/api => }/models/poll-vote.ts      |   6 +-
 src/{server/api => }/models/post-reaction.ts  |   2 +-
 src/{server/api => }/models/post-watching.ts  |   2 +-
 src/{server/api => }/models/post.ts           |   2 +-
 src/{server/api => }/models/signin.ts         |   2 +-
 .../api => }/models/sw-subscription.ts        |   2 +-
 src/{server/api => }/models/user.ts           |   6 +-
 src/processor/report-github-failure.ts        |   2 +-
 src/server/api/authenticate.ts                |   6 +-
 src/server/api/bot/core.ts                    |  10 +-
 src/server/api/bot/interfaces/line.ts         |   8 +-
 src/server/api/common/drive/add-file.ts       |   8 +-
 .../api/common/drive/upload_from_url.ts       |   2 +-
 src/server/api/common/get-friends.ts          |   2 +-
 src/server/api/common/notify.ts               |   6 +-
 src/server/api/common/push-sw.ts              |   2 +-
 .../api/common/read-messaging-message.ts      |   4 +-
 src/server/api/common/read-notification.ts    |   2 +-
 src/server/api/common/watch-post.ts           |   2 +-
 src/server/api/endpoints/aggregation/posts.ts |   2 +-
 .../endpoints/aggregation/posts/reaction.ts   |   4 +-
 .../endpoints/aggregation/posts/reactions.ts  |   4 +-
 .../api/endpoints/aggregation/posts/reply.ts  |   2 +-
 .../api/endpoints/aggregation/posts/repost.ts |   2 +-
 src/server/api/endpoints/aggregation/users.ts |   2 +-
 .../endpoints/aggregation/users/activity.ts   |   4 +-
 .../endpoints/aggregation/users/followers.ts  |  15 +-
 .../endpoints/aggregation/users/following.ts  |  15 +-
 .../api/endpoints/aggregation/users/post.ts   |   4 +-
 .../endpoints/aggregation/users/reaction.ts   |   4 +-
 src/server/api/endpoints/app/create.ts        |   2 +-
 .../api/endpoints/app/name_id/available.ts    |   4 +-
 src/server/api/endpoints/app/show.ts          |   2 +-
 src/server/api/endpoints/auth/accept.ts       |   6 +-
 .../api/endpoints/auth/session/generate.ts    |   4 +-
 src/server/api/endpoints/auth/session/show.ts |   2 +-
 .../api/endpoints/auth/session/userkey.ts     |   8 +-
 src/server/api/endpoints/channels.ts          |   2 +-
 src/server/api/endpoints/channels/create.ts   |   6 +-
 src/server/api/endpoints/channels/posts.ts    |   4 +-
 src/server/api/endpoints/channels/show.ts     |   2 +-
 src/server/api/endpoints/channels/unwatch.ts  |   4 +-
 src/server/api/endpoints/channels/watch.ts    |   4 +-
 src/server/api/endpoints/drive.ts             |   2 +-
 src/server/api/endpoints/drive/files.ts       |   2 +-
 .../api/endpoints/drive/files/create.ts       |   2 +-
 src/server/api/endpoints/drive/files/find.ts  |   2 +-
 src/server/api/endpoints/drive/files/show.ts  |   2 +-
 .../api/endpoints/drive/files/update.ts       |   4 +-
 .../endpoints/drive/files/upload_from_url.ts  |   2 +-
 src/server/api/endpoints/drive/folders.ts     |   2 +-
 .../api/endpoints/drive/folders/create.ts     |   2 +-
 .../api/endpoints/drive/folders/find.ts       |   2 +-
 .../api/endpoints/drive/folders/show.ts       |   2 +-
 .../api/endpoints/drive/folders/update.ts     |   2 +-
 src/server/api/endpoints/drive/stream.ts      |   2 +-
 src/server/api/endpoints/following/create.ts  |   4 +-
 src/server/api/endpoints/following/delete.ts  |   4 +-
 src/server/api/endpoints/i.ts                 |   2 +-
 src/server/api/endpoints/i/2fa/done.ts        |   2 +-
 src/server/api/endpoints/i/2fa/register.ts    |   2 +-
 src/server/api/endpoints/i/2fa/unregister.ts  |   2 +-
 src/server/api/endpoints/i/appdata/get.ts     |  39 ---
 src/server/api/endpoints/i/appdata/set.ts     |  58 ----
 src/server/api/endpoints/i/authorized_apps.ts |   4 +-
 src/server/api/endpoints/i/change_password.ts |   2 +-
 src/server/api/endpoints/i/favorites.ts       |   4 +-
 src/server/api/endpoints/i/notifications.ts   |   6 +-
 src/server/api/endpoints/i/pin.ts             |   6 +-
 .../api/endpoints/i/regenerate_token.ts       |   2 +-
 src/server/api/endpoints/i/signin_history.ts  |   2 +-
 src/server/api/endpoints/i/update.ts          |   2 +-
 .../api/endpoints/i/update_client_setting.ts  |   2 +-
 src/server/api/endpoints/i/update_home.ts     |   2 +-
 .../api/endpoints/i/update_mobile_home.ts     |   2 +-
 src/server/api/endpoints/messaging/history.ts |   6 +-
 .../api/endpoints/messaging/messages.ts       |   6 +-
 .../endpoints/messaging/messages/create.ts    |  14 +-
 src/server/api/endpoints/messaging/unread.ts  |   4 +-
 src/server/api/endpoints/meta.ts              |   4 +-
 src/server/api/endpoints/mute/create.ts       |   4 +-
 src/server/api/endpoints/mute/delete.ts       |   4 +-
 src/server/api/endpoints/mute/list.ts         |   4 +-
 src/server/api/endpoints/my/apps.ts           |   2 +-
 .../notifications/get_unread_count.ts         |   4 +-
 .../notifications/mark_as_read_all.ts         |   2 +-
 src/server/api/endpoints/othello/games.ts     |   2 +-
 .../api/endpoints/othello/games/show.ts       |   4 +-
 .../api/endpoints/othello/invitations.ts      |   2 +-
 src/server/api/endpoints/othello/match.ts     |   8 +-
 .../api/endpoints/othello/match/cancel.ts     |   2 +-
 src/server/api/endpoints/posts.ts             |   2 +-
 src/server/api/endpoints/posts/categorize.ts  |  52 ---
 src/server/api/endpoints/posts/context.ts     |   2 +-
 src/server/api/endpoints/posts/create.ts      |  24 +-
 .../api/endpoints/posts/favorites/create.ts   |   4 +-
 .../api/endpoints/posts/favorites/delete.ts   |   4 +-
 src/server/api/endpoints/posts/mentions.ts    |   4 +-
 .../endpoints/posts/polls/recommendation.ts   |   4 +-
 src/server/api/endpoints/posts/polls/vote.ts  |   6 +-
 src/server/api/endpoints/posts/reactions.ts   |   4 +-
 .../api/endpoints/posts/reactions/create.ts   |   8 +-
 .../api/endpoints/posts/reactions/delete.ts   |   4 +-
 src/server/api/endpoints/posts/replies.ts     |   2 +-
 src/server/api/endpoints/posts/reposts.ts     |   2 +-
 src/server/api/endpoints/posts/search.ts      |   8 +-
 src/server/api/endpoints/posts/show.ts        |   2 +-
 src/server/api/endpoints/posts/timeline.ts    |   8 +-
 src/server/api/endpoints/posts/trend.ts       |   2 +-
 src/server/api/endpoints/stats.ts             |   4 +-
 src/server/api/endpoints/sw/register.ts       |   2 +-
 .../api/endpoints/username/available.ts       |   4 +-
 src/server/api/endpoints/users.ts             |   2 +-
 src/server/api/endpoints/users/followers.ts   |   6 +-
 src/server/api/endpoints/users/following.ts   |   6 +-
 .../users/get_frequently_replied_users.ts     |   4 +-
 src/server/api/endpoints/users/posts.ts       |   4 +-
 .../api/endpoints/users/recommendation.ts     |   2 +-
 src/server/api/endpoints/users/search.ts      |   2 +-
 .../api/endpoints/users/search_by_username.ts |   2 +-
 src/server/api/endpoints/users/show.ts        |   2 +-
 src/server/api/limitter.ts                    |   2 +-
 src/server/api/private/signin.ts              |   4 +-
 src/server/api/private/signup.ts              |   2 +-
 src/server/api/service/github.ts              |   4 +-
 src/server/api/service/twitter.ts             |   2 +-
 src/server/api/stream/channel.ts              |   4 +-
 src/server/api/stream/home.ts                 |   6 +-
 src/server/api/stream/messaging.ts            |   4 +-
 src/server/api/stream/othello-game.ts         |  10 +-
 src/server/api/stream/othello.ts              |   2 +-
 src/server/api/streaming.ts                   |   8 +-
 src/server/file/server.ts                     |   2 +-
 src/server/web/server.ts                      |  51 ++-
 src/server/web/{service => }/url-preview.ts   |   0
 src/tools/analysis/core.ts                    |  49 ---
 src/tools/analysis/extract-user-domains.ts    | 120 -------
 src/tools/analysis/extract-user-keywords.ts   | 154 ---------
 src/tools/analysis/mecab.js                   |  85 -----
 src/tools/analysis/naive-bayes.js             | 302 ------------------
 .../analysis/predict-all-post-category.ts     |  35 --
 src/tools/analysis/predict-user-interst.ts    |  45 ---
 tsconfig.json                                 |   2 +-
 webpack.config.ts                             |  24 +-
 552 files changed, 360 insertions(+), 1311 deletions(-)
 rename src/{server/web => client}/app/animation.styl (100%)
 rename src/{server/web => client}/app/app.styl (100%)
 rename src/{server/web => client}/app/app.vue (100%)
 rename src/{server/web => client}/app/auth/assets/logo.svg (100%)
 rename src/{server/web => client}/app/auth/script.ts (100%)
 rename src/{server/web => client}/app/auth/style.styl (100%)
 rename src/{server/web => client}/app/auth/views/form.vue (100%)
 rename src/{server/web => client}/app/auth/views/index.vue (100%)
 rename src/{server/web => client}/app/base.pug (76%)
 rename src/{server/web => client}/app/boot.js (100%)
 rename src/{server/web => client}/app/ch/script.ts (100%)
 rename src/{server/web => client}/app/ch/style.styl (100%)
 rename src/{server/web => client}/app/ch/tags/channel.tag (100%)
 rename src/{server/web => client}/app/ch/tags/header.tag (100%)
 rename src/{server/web => client}/app/ch/tags/index.tag (100%)
 rename src/{server/web => client}/app/ch/tags/index.ts (100%)
 rename src/{server/web => client}/app/common/define-widget.ts (100%)
 rename src/{server/web => client}/app/common/mios.ts (100%)
 rename src/{server/web => client}/app/common/scripts/check-for-update.ts (100%)
 rename src/{server/web => client}/app/common/scripts/compose-notification.ts (100%)
 rename src/{server/web => client}/app/common/scripts/contains.ts (100%)
 rename src/{server/web => client}/app/common/scripts/copy-to-clipboard.ts (100%)
 rename src/{server/web => client}/app/common/scripts/date-stringify.ts (100%)
 rename src/{server/web => client}/app/common/scripts/fuck-ad-block.ts (100%)
 rename src/{server/web => client}/app/common/scripts/gcd.ts (100%)
 rename src/{server/web => client}/app/common/scripts/get-kao.ts (100%)
 rename src/{server/web => client}/app/common/scripts/get-median.ts (100%)
 rename src/{server/web => client}/app/common/scripts/loading.ts (100%)
 rename src/{server/web => client}/app/common/scripts/parse-search-query.ts (100%)
 rename src/{server/web => client}/app/common/scripts/streaming/channel.ts (100%)
 rename src/{server/web => client}/app/common/scripts/streaming/drive.ts (100%)
 rename src/{server/web => client}/app/common/scripts/streaming/home.ts (100%)
 rename src/{server/web => client}/app/common/scripts/streaming/messaging-index.ts (100%)
 rename src/{server/web => client}/app/common/scripts/streaming/messaging.ts (100%)
 rename src/{server/web => client}/app/common/scripts/streaming/othello-game.ts (100%)
 rename src/{server/web => client}/app/common/scripts/streaming/othello.ts (100%)
 rename src/{server/web => client}/app/common/scripts/streaming/requests.ts (100%)
 rename src/{server/web => client}/app/common/scripts/streaming/server.ts (100%)
 rename src/{server/web => client}/app/common/scripts/streaming/stream-manager.ts (100%)
 rename src/{server/web => client}/app/common/scripts/streaming/stream.ts (100%)
 rename src/{server/web => client}/app/common/views/components/autocomplete.vue (100%)
 rename src/{server/web => client}/app/common/views/components/connect-failed.troubleshooter.vue (100%)
 rename src/{server/web => client}/app/common/views/components/connect-failed.vue (100%)
 rename src/{server/web => client}/app/common/views/components/ellipsis.vue (100%)
 rename src/{server/web => client}/app/common/views/components/file-type-icon.vue (100%)
 rename src/{server/web => client}/app/common/views/components/forkit.vue (100%)
 rename src/{server/web => client}/app/common/views/components/index.ts (100%)
 rename src/{server/web => client}/app/common/views/components/media-list.vue (100%)
 rename src/{server/web => client}/app/common/views/components/messaging-room.form.vue (100%)
 rename src/{server/web => client}/app/common/views/components/messaging-room.message.vue (100%)
 rename src/{server/web => client}/app/common/views/components/messaging-room.vue (100%)
 rename src/{server/web => client}/app/common/views/components/messaging.vue (100%)
 rename src/{server/web => client}/app/common/views/components/nav.vue (100%)
 rename src/{server/web => client}/app/common/views/components/othello.game.vue (100%)
 rename src/{server/web => client}/app/common/views/components/othello.gameroom.vue (100%)
 rename src/{server/web => client}/app/common/views/components/othello.room.vue (100%)
 rename src/{server/web => client}/app/common/views/components/othello.vue (100%)
 rename src/{server/web => client}/app/common/views/components/poll-editor.vue (100%)
 rename src/{server/web => client}/app/common/views/components/poll.vue (100%)
 rename src/{server/web => client}/app/common/views/components/post-html.ts (100%)
 rename src/{server/web => client}/app/common/views/components/post-menu.vue (100%)
 rename src/{server/web => client}/app/common/views/components/reaction-icon.vue (100%)
 rename src/{server/web => client}/app/common/views/components/reaction-picker.vue (100%)
 rename src/{server/web => client}/app/common/views/components/reactions-viewer.vue (100%)
 rename src/{server/web => client}/app/common/views/components/signin.vue (100%)
 rename src/{server/web => client}/app/common/views/components/signup.vue (100%)
 rename src/{server/web => client}/app/common/views/components/special-message.vue (100%)
 rename src/{server/web => client}/app/common/views/components/stream-indicator.vue (100%)
 rename src/{server/web => client}/app/common/views/components/switch.vue (100%)
 rename src/{server/web => client}/app/common/views/components/time.vue (100%)
 rename src/{server/web => client}/app/common/views/components/timer.vue (100%)
 rename src/{server/web => client}/app/common/views/components/twitter-setting.vue (100%)
 rename src/{server/web => client}/app/common/views/components/uploader.vue (100%)
 rename src/{server/web => client}/app/common/views/components/url-preview.vue (100%)
 rename src/{server/web => client}/app/common/views/components/url.vue (100%)
 rename src/{server/web => client}/app/common/views/components/welcome-timeline.vue (100%)
 rename src/{server/web => client}/app/common/views/directives/autocomplete.ts (100%)
 rename src/{server/web => client}/app/common/views/directives/index.ts (100%)
 rename src/{server/web => client}/app/common/views/filters/bytes.ts (100%)
 rename src/{server/web => client}/app/common/views/filters/index.ts (100%)
 rename src/{server/web => client}/app/common/views/filters/number.ts (100%)
 rename src/{server/web => client}/app/common/views/widgets/access-log.vue (100%)
 rename src/{server/web => client}/app/common/views/widgets/broadcast.vue (100%)
 rename src/{server/web => client}/app/common/views/widgets/calendar.vue (100%)
 rename src/{server/web => client}/app/common/views/widgets/donation.vue (100%)
 rename src/{server/web => client}/app/common/views/widgets/index.ts (100%)
 rename src/{server/web => client}/app/common/views/widgets/nav.vue (100%)
 rename src/{server/web => client}/app/common/views/widgets/photo-stream.vue (100%)
 rename src/{server/web => client}/app/common/views/widgets/rss.vue (100%)
 rename src/{server/web => client}/app/common/views/widgets/server.cpu-memory.vue (100%)
 rename src/{server/web => client}/app/common/views/widgets/server.cpu.vue (100%)
 rename src/{server/web => client}/app/common/views/widgets/server.disk.vue (100%)
 rename src/{server/web => client}/app/common/views/widgets/server.info.vue (100%)
 rename src/{server/web => client}/app/common/views/widgets/server.memory.vue (100%)
 rename src/{server/web => client}/app/common/views/widgets/server.pie.vue (100%)
 rename src/{server/web => client}/app/common/views/widgets/server.uptimes.vue (100%)
 rename src/{server/web => client}/app/common/views/widgets/server.vue (100%)
 rename src/{server/web => client}/app/common/views/widgets/slideshow.vue (100%)
 rename src/{server/web => client}/app/common/views/widgets/tips.vue (100%)
 rename src/{server/web => client}/app/common/views/widgets/version.vue (100%)
 rename src/{server/web => client}/app/config.ts (100%)
 rename src/{server/web => client}/app/desktop/api/choose-drive-file.ts (100%)
 rename src/{server/web => client}/app/desktop/api/choose-drive-folder.ts (100%)
 rename src/{server/web => client}/app/desktop/api/contextmenu.ts (100%)
 rename src/{server/web => client}/app/desktop/api/dialog.ts (100%)
 rename src/{server/web => client}/app/desktop/api/input.ts (100%)
 rename src/{server/web => client}/app/desktop/api/notify.ts (100%)
 rename src/{server/web => client}/app/desktop/api/post.ts (100%)
 rename src/{server/web => client}/app/desktop/api/update-avatar.ts (100%)
 rename src/{server/web => client}/app/desktop/api/update-banner.ts (100%)
 rename src/{server/web => client}/app/desktop/assets/grid.svg (100%)
 rename src/{server/web => client}/app/desktop/assets/header-logo-white.svg (100%)
 rename src/{server/web => client}/app/desktop/assets/header-logo.svg (100%)
 rename src/{server/web => client}/app/desktop/assets/index.jpg (100%)
 rename src/{server/web => client}/app/desktop/assets/remove.png (100%)
 rename src/{server/web => client}/app/desktop/script.ts (100%)
 rename src/{server/web => client}/app/desktop/style.styl (100%)
 rename src/{server/web => client}/app/desktop/ui.styl (100%)
 rename src/{server/web => client}/app/desktop/views/components/activity.calendar.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/activity.chart.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/activity.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/analog-clock.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/calendar.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/choose-file-from-drive-window.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/choose-folder-from-drive-window.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/context-menu.menu.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/context-menu.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/crop-window.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/dialog.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/drive-window.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/drive.file.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/drive.folder.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/drive.nav-folder.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/drive.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/ellipsis-icon.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/follow-button.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/followers-window.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/followers.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/following-window.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/following.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/friends-maker.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/game-window.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/home.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/index.ts (100%)
 rename src/{server/web => client}/app/desktop/views/components/input-dialog.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/media-image-dialog.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/media-image.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/media-video-dialog.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/media-video.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/mentions.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/messaging-room-window.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/messaging-window.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/notifications.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/post-detail.sub.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/post-detail.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/post-form-window.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/post-form.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/post-preview.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/posts.post.sub.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/posts.post.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/posts.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/progress-dialog.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/repost-form-window.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/repost-form.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/settings-window.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/settings.2fa.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/settings.api.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/settings.apps.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/settings.drive.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/settings.mute.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/settings.password.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/settings.profile.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/settings.signins.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/settings.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/sub-post-content.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/taskmanager.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/timeline.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/ui-notification.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/ui.header.account.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/ui.header.clock.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/ui.header.nav.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/ui.header.notifications.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/ui.header.post.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/ui.header.search.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/ui.header.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/ui.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/user-preview.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/users-list.item.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/users-list.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/widget-container.vue (100%)
 rename src/{server/web => client}/app/desktop/views/components/window.vue (100%)
 rename src/{server/web => client}/app/desktop/views/directives/index.ts (100%)
 rename src/{server/web => client}/app/desktop/views/directives/user-preview.ts (100%)
 rename src/{server/web => client}/app/desktop/views/pages/drive.vue (100%)
 rename src/{server/web => client}/app/desktop/views/pages/home-customize.vue (100%)
 rename src/{server/web => client}/app/desktop/views/pages/home.vue (100%)
 rename src/{server/web => client}/app/desktop/views/pages/index.vue (100%)
 rename src/{server/web => client}/app/desktop/views/pages/messaging-room.vue (100%)
 rename src/{server/web => client}/app/desktop/views/pages/othello.vue (100%)
 rename src/{server/web => client}/app/desktop/views/pages/post.vue (100%)
 rename src/{server/web => client}/app/desktop/views/pages/search.vue (100%)
 rename src/{server/web => client}/app/desktop/views/pages/selectdrive.vue (100%)
 rename src/{server/web => client}/app/desktop/views/pages/user/user.followers-you-know.vue (100%)
 rename src/{server/web => client}/app/desktop/views/pages/user/user.friends.vue (100%)
 rename src/{server/web => client}/app/desktop/views/pages/user/user.header.vue (100%)
 rename src/{server/web => client}/app/desktop/views/pages/user/user.home.vue (100%)
 rename src/{server/web => client}/app/desktop/views/pages/user/user.photos.vue (100%)
 rename src/{server/web => client}/app/desktop/views/pages/user/user.profile.vue (100%)
 rename src/{server/web => client}/app/desktop/views/pages/user/user.timeline.vue (100%)
 rename src/{server/web => client}/app/desktop/views/pages/user/user.vue (100%)
 rename src/{server/web => client}/app/desktop/views/pages/welcome.vue (100%)
 rename src/{server/web => client}/app/desktop/views/widgets/activity.vue (100%)
 rename src/{server/web => client}/app/desktop/views/widgets/channel.channel.form.vue (100%)
 rename src/{server/web => client}/app/desktop/views/widgets/channel.channel.post.vue (100%)
 rename src/{server/web => client}/app/desktop/views/widgets/channel.channel.vue (100%)
 rename src/{server/web => client}/app/desktop/views/widgets/channel.vue (100%)
 rename src/{server/web => client}/app/desktop/views/widgets/index.ts (100%)
 rename src/{server/web => client}/app/desktop/views/widgets/messaging.vue (100%)
 rename src/{server/web => client}/app/desktop/views/widgets/notifications.vue (100%)
 rename src/{server/web => client}/app/desktop/views/widgets/polls.vue (100%)
 rename src/{server/web => client}/app/desktop/views/widgets/post-form.vue (100%)
 rename src/{server/web => client}/app/desktop/views/widgets/profile.vue (100%)
 rename src/{server/web => client}/app/desktop/views/widgets/timemachine.vue (100%)
 rename src/{server/web => client}/app/desktop/views/widgets/trends.vue (100%)
 rename src/{server/web => client}/app/desktop/views/widgets/users.vue (100%)
 rename src/{server/web => client}/app/dev/script.ts (100%)
 rename src/{server/web => client}/app/dev/style.styl (100%)
 rename src/{server/web => client}/app/dev/views/app.vue (100%)
 rename src/{server/web => client}/app/dev/views/apps.vue (100%)
 rename src/{server/web => client}/app/dev/views/index.vue (100%)
 rename src/{server/web => client}/app/dev/views/new-app.vue (100%)
 rename src/{server/web => client}/app/dev/views/ui.vue (100%)
 rename src/{server/web => client}/app/init.css (100%)
 rename src/{server/web => client}/app/init.ts (100%)
 rename src/{server/web => client}/app/mobile/api/choose-drive-file.ts (100%)
 rename src/{server/web => client}/app/mobile/api/choose-drive-folder.ts (100%)
 rename src/{server/web => client}/app/mobile/api/dialog.ts (100%)
 rename src/{server/web => client}/app/mobile/api/input.ts (100%)
 rename src/{server/web => client}/app/mobile/api/notify.ts (100%)
 rename src/{server/web => client}/app/mobile/api/post.ts (100%)
 rename src/{server/web => client}/app/mobile/script.ts (100%)
 rename src/{server/web => client}/app/mobile/style.styl (100%)
 rename src/{server/web => client}/app/mobile/views/components/activity.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/drive-file-chooser.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/drive-folder-chooser.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/drive.file-detail.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/drive.file.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/drive.folder.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/drive.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/follow-button.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/friends-maker.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/index.ts (100%)
 rename src/{server/web => client}/app/mobile/views/components/media-image.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/media-video.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/notification-preview.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/notification.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/notifications.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/notify.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/post-card.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/post-detail.sub.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/post-detail.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/post-form.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/post-preview.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/post.sub.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/post.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/posts.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/sub-post-content.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/timeline.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/ui.header.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/ui.nav.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/ui.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/user-card.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/user-preview.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/user-timeline.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/users-list.vue (100%)
 rename src/{server/web => client}/app/mobile/views/components/widget-container.vue (100%)
 rename src/{server/web => client}/app/mobile/views/directives/index.ts (100%)
 rename src/{server/web => client}/app/mobile/views/directives/user-preview.ts (100%)
 rename src/{server/web => client}/app/mobile/views/pages/drive.vue (100%)
 rename src/{server/web => client}/app/mobile/views/pages/followers.vue (100%)
 rename src/{server/web => client}/app/mobile/views/pages/following.vue (100%)
 rename src/{server/web => client}/app/mobile/views/pages/home.vue (100%)
 rename src/{server/web => client}/app/mobile/views/pages/index.vue (100%)
 rename src/{server/web => client}/app/mobile/views/pages/messaging-room.vue (100%)
 rename src/{server/web => client}/app/mobile/views/pages/messaging.vue (100%)
 rename src/{server/web => client}/app/mobile/views/pages/notifications.vue (100%)
 rename src/{server/web => client}/app/mobile/views/pages/othello.vue (100%)
 rename src/{server/web => client}/app/mobile/views/pages/post.vue (100%)
 rename src/{server/web => client}/app/mobile/views/pages/profile-setting.vue (100%)
 rename src/{server/web => client}/app/mobile/views/pages/search.vue (100%)
 rename src/{server/web => client}/app/mobile/views/pages/selectdrive.vue (100%)
 rename src/{server/web => client}/app/mobile/views/pages/settings.vue (100%)
 rename src/{server/web => client}/app/mobile/views/pages/signup.vue (100%)
 rename src/{server/web => client}/app/mobile/views/pages/user.vue (100%)
 rename src/{server/web => client}/app/mobile/views/pages/user/home.followers-you-know.vue (100%)
 rename src/{server/web => client}/app/mobile/views/pages/user/home.friends.vue (100%)
 rename src/{server/web => client}/app/mobile/views/pages/user/home.photos.vue (100%)
 rename src/{server/web => client}/app/mobile/views/pages/user/home.posts.vue (100%)
 rename src/{server/web => client}/app/mobile/views/pages/user/home.vue (100%)
 rename src/{server/web => client}/app/mobile/views/pages/welcome.vue (100%)
 rename src/{server/web => client}/app/mobile/views/widgets/activity.vue (100%)
 rename src/{server/web => client}/app/mobile/views/widgets/index.ts (100%)
 rename src/{server/web => client}/app/mobile/views/widgets/profile.vue (100%)
 rename src/{server/web => client}/app/reset.styl (100%)
 rename src/{server/web => client}/app/safe.js (100%)
 rename src/{server/web => client}/app/stats/style.styl (100%)
 rename src/{server/web => client}/app/stats/tags/index.tag (100%)
 rename src/{server/web => client}/app/stats/tags/index.ts (100%)
 rename src/{server/web => client}/app/status/style.styl (100%)
 rename src/{server/web => client}/app/status/tags/index.tag (100%)
 rename src/{server/web => client}/app/status/tags/index.ts (100%)
 rename src/{server/web => client}/app/sw.js (100%)
 rename src/{server/web => client}/app/tsconfig.json (100%)
 rename src/{server/web => client}/app/v.d.ts (100%)
 rename src/{server/web => client}/assets/404.js (100%)
 rename src/{server/web => client}/assets/code-highlight.css (100%)
 rename src/{server/web => client}/assets/error.jpg (100%)
 rename src/{server/web => client}/assets/favicon.ico (100%)
 rename src/{server/web => client}/assets/label.svg (100%)
 rename src/{server/web => client}/assets/manifest.json (100%)
 rename src/{server/web => client}/assets/message.mp3 (100%)
 rename src/{server/web => client}/assets/othello-put-me.mp3 (100%)
 rename src/{server/web => client}/assets/othello-put-you.mp3 (100%)
 rename src/{server/web => client}/assets/post.mp3 (100%)
 rename src/{server/web => client}/assets/reactions/angry.png (100%)
 rename src/{server/web => client}/assets/reactions/confused.png (100%)
 rename src/{server/web => client}/assets/reactions/congrats.png (100%)
 rename src/{server/web => client}/assets/reactions/hmm.png (100%)
 rename src/{server/web => client}/assets/reactions/laugh.png (100%)
 rename src/{server/web => client}/assets/reactions/like.png (100%)
 rename src/{server/web => client}/assets/reactions/love.png (100%)
 rename src/{server/web => client}/assets/reactions/pudding.png (100%)
 rename src/{server/web => client}/assets/reactions/surprise.png (100%)
 rename src/{server/web => client}/assets/recover.html (100%)
 rename src/{server/web => client}/assets/title.svg (100%)
 rename src/{server/web => client}/assets/unread.svg (100%)
 rename src/{server/web => client}/assets/welcome-bg.svg (100%)
 rename src/{server/web => client}/assets/welcome-fg.svg (100%)
 rename src/{server/web => client}/const.styl (74%)
 rename src/{server/web => client}/docs/about.en.pug (100%)
 rename src/{server/web => client}/docs/about.ja.pug (100%)
 rename src/{server/web => client}/docs/api.ja.pug (100%)
 rename src/{server/web => client}/docs/api/endpoints/posts/create.yaml (100%)
 rename src/{server/web => client}/docs/api/endpoints/posts/timeline.yaml (100%)
 rename src/{server/web => client}/docs/api/endpoints/style.styl (100%)
 rename src/{server/web => client}/docs/api/endpoints/view.pug (100%)
 rename src/{server/web => client}/docs/api/entities/drive-file.yaml (100%)
 rename src/{server/web => client}/docs/api/entities/post.yaml (100%)
 rename src/{server/web => client}/docs/api/entities/style.styl (100%)
 rename src/{server/web => client}/docs/api/entities/user.yaml (100%)
 rename src/{server/web => client}/docs/api/entities/view.pug (100%)
 rename src/{server/web => client}/docs/api/gulpfile.ts (80%)
 rename src/{server/web => client}/docs/api/mixins.pug (100%)
 rename src/{server/web => client}/docs/api/style.styl (100%)
 rename src/{server/web => client}/docs/gulpfile.ts (74%)
 rename src/{server/web => client}/docs/index.en.pug (100%)
 rename src/{server/web => client}/docs/index.ja.pug (100%)
 rename src/{server/web => client}/docs/layout.pug (100%)
 rename src/{server/web => client}/docs/license.en.pug (100%)
 rename src/{server/web => client}/docs/license.ja.pug (100%)
 rename src/{server/web => client}/docs/mute.ja.pug (100%)
 rename src/{server/web => client}/docs/search.ja.pug (100%)
 rename src/{server/web => client}/docs/server.ts (100%)
 rename src/{server/web => client}/docs/style.styl (100%)
 rename src/{server/web => client}/docs/tou.ja.pug (100%)
 rename src/{server/web => client}/docs/ui.styl (100%)
 rename src/{server/web => client}/docs/vars.ts (76%)
 rename src/{server/web => client}/element.scss (91%)
 rename src/{server/web => client}/style.styl (100%)
 rename src/{server => }/common/get-notification-summary.ts (100%)
 rename src/{server => }/common/get-post-summary.ts (100%)
 rename src/{server => }/common/get-reaction-emoji.ts (100%)
 rename src/{server => }/common/othello/ai/back.ts (99%)
 rename src/{server => }/common/othello/ai/front.ts (99%)
 rename src/{server => }/common/othello/ai/index.ts (100%)
 rename src/{server => }/common/othello/core.ts (100%)
 rename src/{server => }/common/othello/maps.ts (100%)
 rename src/{server/api => }/common/text/core/syntax-highlighter.ts (100%)
 rename src/{server/api => }/common/text/elements/bold.ts (100%)
 rename src/{server/api => }/common/text/elements/code.ts (100%)
 rename src/{server/api => }/common/text/elements/emoji.ts (100%)
 rename src/{server/api => }/common/text/elements/hashtag.ts (100%)
 rename src/{server/api => }/common/text/elements/inline-code.ts (100%)
 rename src/{server/api => }/common/text/elements/link.ts (100%)
 rename src/{server/api => }/common/text/elements/mention.ts (82%)
 rename src/{server/api => }/common/text/elements/quote.ts (100%)
 rename src/{server/api => }/common/text/elements/url.ts (100%)
 rename src/{server/api => }/common/text/index.ts (100%)
 rename src/{server => }/common/user/get-acct.ts (100%)
 rename src/{server => }/common/user/get-summary.ts (90%)
 rename src/{server => }/common/user/parse-acct.ts (100%)
 rename src/{server/api => }/models/access-token.ts (90%)
 rename src/{server/api => }/models/app.ts (96%)
 rename src/{server/api => }/models/auth-session.ts (96%)
 rename src/{server/api => }/models/channel-watching.ts (88%)
 rename src/{server/api => }/models/channel.ts (97%)
 rename src/{server/api => }/models/drive-file.ts (96%)
 rename src/{server/api => }/models/drive-folder.ts (97%)
 rename src/{server/api => }/models/favorite.ts (85%)
 rename src/{server/api => }/models/following.ts (87%)
 rename src/{server/api => }/models/messaging-history.ts (88%)
 rename src/{server/api => }/models/messaging-message.ts (97%)
 rename src/{server/api => }/models/meta.ts (73%)
 rename src/{server/api => }/models/mute.ts (85%)
 rename src/{server/api => }/models/notification.ts (98%)
 rename src/{server/api => }/models/othello-game.ts (98%)
 rename src/{server/api => }/models/othello-matching.ts (96%)
 rename src/{server/api => }/models/poll-vote.ts (56%)
 rename src/{server/api => }/models/post-reaction.ts (96%)
 rename src/{server/api => }/models/post-watching.ts (86%)
 rename src/{server/api => }/models/post.ts (99%)
 rename src/{server/api => }/models/signin.ts (94%)
 rename src/{server/api => }/models/sw-subscription.ts (87%)
 rename src/{server/api => }/models/user.ts (98%)
 delete mode 100644 src/server/api/endpoints/i/appdata/get.ts
 delete mode 100644 src/server/api/endpoints/i/appdata/set.ts
 delete mode 100644 src/server/api/endpoints/posts/categorize.ts
 rename src/server/web/{service => }/url-preview.ts (100%)
 delete mode 100644 src/tools/analysis/core.ts
 delete mode 100644 src/tools/analysis/extract-user-domains.ts
 delete mode 100644 src/tools/analysis/extract-user-keywords.ts
 delete mode 100644 src/tools/analysis/mecab.js
 delete mode 100644 src/tools/analysis/naive-bayes.js
 delete mode 100644 src/tools/analysis/predict-all-post-category.ts
 delete mode 100644 src/tools/analysis/predict-user-interst.ts

diff --git a/gulpfile.ts b/gulpfile.ts
index 46727126c..a6e9e53df 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -39,10 +39,9 @@ if (isDebug) {
 
 const constants = require('./src/const.json');
 
-require('./src/server/web/docs/gulpfile.ts');
+require('./src/client/docs/gulpfile.ts');
 
 gulp.task('build', [
-	'build:js',
 	'build:ts',
 	'build:copy',
 	'build:client',
@@ -51,11 +50,6 @@ gulp.task('build', [
 
 gulp.task('rebuild', ['clean', 'build']);
 
-gulp.task('build:js', () =>
-	gulp.src(['./src/**/*.js', '!./src/server/web/**/*.js'])
-		.pipe(gulp.dest('./built/'))
-);
-
 gulp.task('build:ts', () => {
 	const tsProject = ts.createProject('./tsconfig.json');
 
@@ -71,7 +65,7 @@ gulp.task('build:copy', () =>
 	gulp.src([
 		'./build/Release/crypto_key.node',
 		'./src/**/assets/**/*',
-		'!./src/server/web/app/**/assets/**/*'
+		'!./src/client/app/**/assets/**/*'
 	]).pipe(gulp.dest('./built/'))
 );
 
@@ -114,29 +108,28 @@ gulp.task('default', ['build']);
 
 gulp.task('build:client', [
 	'build:ts',
-	'build:js',
 	'build:client:script',
 	'build:client:pug',
 	'copy:client'
 ]);
 
 gulp.task('build:client:script', () =>
-	gulp.src(['./src/server/web/app/boot.js', './src/server/web/app/safe.js'])
+	gulp.src(['./src/client/app/boot.js', './src/client/app/safe.js'])
 		.pipe(replace('VERSION', JSON.stringify(version)))
 		.pipe(replace('API', JSON.stringify(config.api_url)))
 		.pipe(replace('ENV', JSON.stringify(env)))
 		.pipe(isProduction ? uglify({
 			toplevel: true
 		} as any) : gutil.noop())
-		.pipe(gulp.dest('./built/server/web/assets/')) as any
+		.pipe(gulp.dest('./built/client/assets/')) as any
 );
 
 gulp.task('build:client:styles', () =>
-	gulp.src('./src/server/web/app/init.css')
+	gulp.src('./src/client/app/init.css')
 		.pipe(isProduction
 			? (cssnano as any)()
 			: gutil.noop())
-		.pipe(gulp.dest('./built/server/web/assets/'))
+		.pipe(gulp.dest('./built/client/assets/'))
 );
 
 gulp.task('copy:client', [
@@ -144,14 +137,14 @@ gulp.task('copy:client', [
 ], () =>
 		gulp.src([
 			'./assets/**/*',
-			'./src/server/web/assets/**/*',
-			'./src/server/web/app/*/assets/**/*'
+			'./src/client/assets/**/*',
+			'./src/client/app/*/assets/**/*'
 		])
 			.pipe(isProduction ? (imagemin as any)() : gutil.noop())
 			.pipe(rename(path => {
 				path.dirname = path.dirname.replace('assets', '.');
 			}))
-			.pipe(gulp.dest('./built/server/web/assets/'))
+			.pipe(gulp.dest('./built/client/assets/'))
 );
 
 gulp.task('build:client:pug', [
@@ -159,13 +152,13 @@ gulp.task('build:client:pug', [
 	'build:client:script',
 	'build:client:styles'
 ], () =>
-		gulp.src('./src/server/web/app/base.pug')
+		gulp.src('./src/client/app/base.pug')
 			.pipe(pug({
 				locals: {
 					themeColor: constants.themeColor,
 					facss: fa.dom.css(),
 					//hljscss: fs.readFileSync('./node_modules/highlight.js/styles/default.css', 'utf8')
-					hljscss: fs.readFileSync('./src/server/web/assets/code-highlight.css', 'utf8')
+					hljscss: fs.readFileSync('./src/client/assets/code-highlight.css', 'utf8')
 				}
 			}))
 			.pipe(htmlmin({
@@ -200,5 +193,5 @@ gulp.task('build:client:pug', [
 				// CSSも圧縮する
 				minifyCSS: true
 			}))
-			.pipe(gulp.dest('./built/server/web/app/'))
+			.pipe(gulp.dest('./built/client/app/'))
 );
diff --git a/src/server/web/app/animation.styl b/src/client/app/animation.styl
similarity index 100%
rename from src/server/web/app/animation.styl
rename to src/client/app/animation.styl
diff --git a/src/server/web/app/app.styl b/src/client/app/app.styl
similarity index 100%
rename from src/server/web/app/app.styl
rename to src/client/app/app.styl
diff --git a/src/server/web/app/app.vue b/src/client/app/app.vue
similarity index 100%
rename from src/server/web/app/app.vue
rename to src/client/app/app.vue
diff --git a/src/server/web/app/auth/assets/logo.svg b/src/client/app/auth/assets/logo.svg
similarity index 100%
rename from src/server/web/app/auth/assets/logo.svg
rename to src/client/app/auth/assets/logo.svg
diff --git a/src/server/web/app/auth/script.ts b/src/client/app/auth/script.ts
similarity index 100%
rename from src/server/web/app/auth/script.ts
rename to src/client/app/auth/script.ts
diff --git a/src/server/web/app/auth/style.styl b/src/client/app/auth/style.styl
similarity index 100%
rename from src/server/web/app/auth/style.styl
rename to src/client/app/auth/style.styl
diff --git a/src/server/web/app/auth/views/form.vue b/src/client/app/auth/views/form.vue
similarity index 100%
rename from src/server/web/app/auth/views/form.vue
rename to src/client/app/auth/views/form.vue
diff --git a/src/server/web/app/auth/views/index.vue b/src/client/app/auth/views/index.vue
similarity index 100%
rename from src/server/web/app/auth/views/index.vue
rename to src/client/app/auth/views/index.vue
diff --git a/src/server/web/app/base.pug b/src/client/app/base.pug
similarity index 76%
rename from src/server/web/app/base.pug
rename to src/client/app/base.pug
index 60eb1539e..32a95a6c9 100644
--- a/src/server/web/app/base.pug
+++ b/src/client/app/base.pug
@@ -14,12 +14,12 @@ html
 		title Misskey
 
 		style
-			include ./../../../../built/server/web/assets/init.css
+			include ./../../../built/client/assets/init.css
 		script
-			include ./../../../../built/server/web/assets/boot.js
+			include ./../../../built/client/assets/boot.js
 
 		script
-			include ./../../../../built/server/web/assets/safe.js
+			include ./../../../built/client/assets/safe.js
 
 		//- FontAwesome style
 		style #{facss}
diff --git a/src/server/web/app/boot.js b/src/client/app/boot.js
similarity index 100%
rename from src/server/web/app/boot.js
rename to src/client/app/boot.js
diff --git a/src/server/web/app/ch/script.ts b/src/client/app/ch/script.ts
similarity index 100%
rename from src/server/web/app/ch/script.ts
rename to src/client/app/ch/script.ts
diff --git a/src/server/web/app/ch/style.styl b/src/client/app/ch/style.styl
similarity index 100%
rename from src/server/web/app/ch/style.styl
rename to src/client/app/ch/style.styl
diff --git a/src/server/web/app/ch/tags/channel.tag b/src/client/app/ch/tags/channel.tag
similarity index 100%
rename from src/server/web/app/ch/tags/channel.tag
rename to src/client/app/ch/tags/channel.tag
diff --git a/src/server/web/app/ch/tags/header.tag b/src/client/app/ch/tags/header.tag
similarity index 100%
rename from src/server/web/app/ch/tags/header.tag
rename to src/client/app/ch/tags/header.tag
diff --git a/src/server/web/app/ch/tags/index.tag b/src/client/app/ch/tags/index.tag
similarity index 100%
rename from src/server/web/app/ch/tags/index.tag
rename to src/client/app/ch/tags/index.tag
diff --git a/src/server/web/app/ch/tags/index.ts b/src/client/app/ch/tags/index.ts
similarity index 100%
rename from src/server/web/app/ch/tags/index.ts
rename to src/client/app/ch/tags/index.ts
diff --git a/src/server/web/app/common/define-widget.ts b/src/client/app/common/define-widget.ts
similarity index 100%
rename from src/server/web/app/common/define-widget.ts
rename to src/client/app/common/define-widget.ts
diff --git a/src/server/web/app/common/mios.ts b/src/client/app/common/mios.ts
similarity index 100%
rename from src/server/web/app/common/mios.ts
rename to src/client/app/common/mios.ts
diff --git a/src/server/web/app/common/scripts/check-for-update.ts b/src/client/app/common/scripts/check-for-update.ts
similarity index 100%
rename from src/server/web/app/common/scripts/check-for-update.ts
rename to src/client/app/common/scripts/check-for-update.ts
diff --git a/src/server/web/app/common/scripts/compose-notification.ts b/src/client/app/common/scripts/compose-notification.ts
similarity index 100%
rename from src/server/web/app/common/scripts/compose-notification.ts
rename to src/client/app/common/scripts/compose-notification.ts
diff --git a/src/server/web/app/common/scripts/contains.ts b/src/client/app/common/scripts/contains.ts
similarity index 100%
rename from src/server/web/app/common/scripts/contains.ts
rename to src/client/app/common/scripts/contains.ts
diff --git a/src/server/web/app/common/scripts/copy-to-clipboard.ts b/src/client/app/common/scripts/copy-to-clipboard.ts
similarity index 100%
rename from src/server/web/app/common/scripts/copy-to-clipboard.ts
rename to src/client/app/common/scripts/copy-to-clipboard.ts
diff --git a/src/server/web/app/common/scripts/date-stringify.ts b/src/client/app/common/scripts/date-stringify.ts
similarity index 100%
rename from src/server/web/app/common/scripts/date-stringify.ts
rename to src/client/app/common/scripts/date-stringify.ts
diff --git a/src/server/web/app/common/scripts/fuck-ad-block.ts b/src/client/app/common/scripts/fuck-ad-block.ts
similarity index 100%
rename from src/server/web/app/common/scripts/fuck-ad-block.ts
rename to src/client/app/common/scripts/fuck-ad-block.ts
diff --git a/src/server/web/app/common/scripts/gcd.ts b/src/client/app/common/scripts/gcd.ts
similarity index 100%
rename from src/server/web/app/common/scripts/gcd.ts
rename to src/client/app/common/scripts/gcd.ts
diff --git a/src/server/web/app/common/scripts/get-kao.ts b/src/client/app/common/scripts/get-kao.ts
similarity index 100%
rename from src/server/web/app/common/scripts/get-kao.ts
rename to src/client/app/common/scripts/get-kao.ts
diff --git a/src/server/web/app/common/scripts/get-median.ts b/src/client/app/common/scripts/get-median.ts
similarity index 100%
rename from src/server/web/app/common/scripts/get-median.ts
rename to src/client/app/common/scripts/get-median.ts
diff --git a/src/server/web/app/common/scripts/loading.ts b/src/client/app/common/scripts/loading.ts
similarity index 100%
rename from src/server/web/app/common/scripts/loading.ts
rename to src/client/app/common/scripts/loading.ts
diff --git a/src/server/web/app/common/scripts/parse-search-query.ts b/src/client/app/common/scripts/parse-search-query.ts
similarity index 100%
rename from src/server/web/app/common/scripts/parse-search-query.ts
rename to src/client/app/common/scripts/parse-search-query.ts
diff --git a/src/server/web/app/common/scripts/streaming/channel.ts b/src/client/app/common/scripts/streaming/channel.ts
similarity index 100%
rename from src/server/web/app/common/scripts/streaming/channel.ts
rename to src/client/app/common/scripts/streaming/channel.ts
diff --git a/src/server/web/app/common/scripts/streaming/drive.ts b/src/client/app/common/scripts/streaming/drive.ts
similarity index 100%
rename from src/server/web/app/common/scripts/streaming/drive.ts
rename to src/client/app/common/scripts/streaming/drive.ts
diff --git a/src/server/web/app/common/scripts/streaming/home.ts b/src/client/app/common/scripts/streaming/home.ts
similarity index 100%
rename from src/server/web/app/common/scripts/streaming/home.ts
rename to src/client/app/common/scripts/streaming/home.ts
diff --git a/src/server/web/app/common/scripts/streaming/messaging-index.ts b/src/client/app/common/scripts/streaming/messaging-index.ts
similarity index 100%
rename from src/server/web/app/common/scripts/streaming/messaging-index.ts
rename to src/client/app/common/scripts/streaming/messaging-index.ts
diff --git a/src/server/web/app/common/scripts/streaming/messaging.ts b/src/client/app/common/scripts/streaming/messaging.ts
similarity index 100%
rename from src/server/web/app/common/scripts/streaming/messaging.ts
rename to src/client/app/common/scripts/streaming/messaging.ts
diff --git a/src/server/web/app/common/scripts/streaming/othello-game.ts b/src/client/app/common/scripts/streaming/othello-game.ts
similarity index 100%
rename from src/server/web/app/common/scripts/streaming/othello-game.ts
rename to src/client/app/common/scripts/streaming/othello-game.ts
diff --git a/src/server/web/app/common/scripts/streaming/othello.ts b/src/client/app/common/scripts/streaming/othello.ts
similarity index 100%
rename from src/server/web/app/common/scripts/streaming/othello.ts
rename to src/client/app/common/scripts/streaming/othello.ts
diff --git a/src/server/web/app/common/scripts/streaming/requests.ts b/src/client/app/common/scripts/streaming/requests.ts
similarity index 100%
rename from src/server/web/app/common/scripts/streaming/requests.ts
rename to src/client/app/common/scripts/streaming/requests.ts
diff --git a/src/server/web/app/common/scripts/streaming/server.ts b/src/client/app/common/scripts/streaming/server.ts
similarity index 100%
rename from src/server/web/app/common/scripts/streaming/server.ts
rename to src/client/app/common/scripts/streaming/server.ts
diff --git a/src/server/web/app/common/scripts/streaming/stream-manager.ts b/src/client/app/common/scripts/streaming/stream-manager.ts
similarity index 100%
rename from src/server/web/app/common/scripts/streaming/stream-manager.ts
rename to src/client/app/common/scripts/streaming/stream-manager.ts
diff --git a/src/server/web/app/common/scripts/streaming/stream.ts b/src/client/app/common/scripts/streaming/stream.ts
similarity index 100%
rename from src/server/web/app/common/scripts/streaming/stream.ts
rename to src/client/app/common/scripts/streaming/stream.ts
diff --git a/src/server/web/app/common/views/components/autocomplete.vue b/src/client/app/common/views/components/autocomplete.vue
similarity index 100%
rename from src/server/web/app/common/views/components/autocomplete.vue
rename to src/client/app/common/views/components/autocomplete.vue
diff --git a/src/server/web/app/common/views/components/connect-failed.troubleshooter.vue b/src/client/app/common/views/components/connect-failed.troubleshooter.vue
similarity index 100%
rename from src/server/web/app/common/views/components/connect-failed.troubleshooter.vue
rename to src/client/app/common/views/components/connect-failed.troubleshooter.vue
diff --git a/src/server/web/app/common/views/components/connect-failed.vue b/src/client/app/common/views/components/connect-failed.vue
similarity index 100%
rename from src/server/web/app/common/views/components/connect-failed.vue
rename to src/client/app/common/views/components/connect-failed.vue
diff --git a/src/server/web/app/common/views/components/ellipsis.vue b/src/client/app/common/views/components/ellipsis.vue
similarity index 100%
rename from src/server/web/app/common/views/components/ellipsis.vue
rename to src/client/app/common/views/components/ellipsis.vue
diff --git a/src/server/web/app/common/views/components/file-type-icon.vue b/src/client/app/common/views/components/file-type-icon.vue
similarity index 100%
rename from src/server/web/app/common/views/components/file-type-icon.vue
rename to src/client/app/common/views/components/file-type-icon.vue
diff --git a/src/server/web/app/common/views/components/forkit.vue b/src/client/app/common/views/components/forkit.vue
similarity index 100%
rename from src/server/web/app/common/views/components/forkit.vue
rename to src/client/app/common/views/components/forkit.vue
diff --git a/src/server/web/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts
similarity index 100%
rename from src/server/web/app/common/views/components/index.ts
rename to src/client/app/common/views/components/index.ts
diff --git a/src/server/web/app/common/views/components/media-list.vue b/src/client/app/common/views/components/media-list.vue
similarity index 100%
rename from src/server/web/app/common/views/components/media-list.vue
rename to src/client/app/common/views/components/media-list.vue
diff --git a/src/server/web/app/common/views/components/messaging-room.form.vue b/src/client/app/common/views/components/messaging-room.form.vue
similarity index 100%
rename from src/server/web/app/common/views/components/messaging-room.form.vue
rename to src/client/app/common/views/components/messaging-room.form.vue
diff --git a/src/server/web/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue
similarity index 100%
rename from src/server/web/app/common/views/components/messaging-room.message.vue
rename to src/client/app/common/views/components/messaging-room.message.vue
diff --git a/src/server/web/app/common/views/components/messaging-room.vue b/src/client/app/common/views/components/messaging-room.vue
similarity index 100%
rename from src/server/web/app/common/views/components/messaging-room.vue
rename to src/client/app/common/views/components/messaging-room.vue
diff --git a/src/server/web/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue
similarity index 100%
rename from src/server/web/app/common/views/components/messaging.vue
rename to src/client/app/common/views/components/messaging.vue
diff --git a/src/server/web/app/common/views/components/nav.vue b/src/client/app/common/views/components/nav.vue
similarity index 100%
rename from src/server/web/app/common/views/components/nav.vue
rename to src/client/app/common/views/components/nav.vue
diff --git a/src/server/web/app/common/views/components/othello.game.vue b/src/client/app/common/views/components/othello.game.vue
similarity index 100%
rename from src/server/web/app/common/views/components/othello.game.vue
rename to src/client/app/common/views/components/othello.game.vue
diff --git a/src/server/web/app/common/views/components/othello.gameroom.vue b/src/client/app/common/views/components/othello.gameroom.vue
similarity index 100%
rename from src/server/web/app/common/views/components/othello.gameroom.vue
rename to src/client/app/common/views/components/othello.gameroom.vue
diff --git a/src/server/web/app/common/views/components/othello.room.vue b/src/client/app/common/views/components/othello.room.vue
similarity index 100%
rename from src/server/web/app/common/views/components/othello.room.vue
rename to src/client/app/common/views/components/othello.room.vue
diff --git a/src/server/web/app/common/views/components/othello.vue b/src/client/app/common/views/components/othello.vue
similarity index 100%
rename from src/server/web/app/common/views/components/othello.vue
rename to src/client/app/common/views/components/othello.vue
diff --git a/src/server/web/app/common/views/components/poll-editor.vue b/src/client/app/common/views/components/poll-editor.vue
similarity index 100%
rename from src/server/web/app/common/views/components/poll-editor.vue
rename to src/client/app/common/views/components/poll-editor.vue
diff --git a/src/server/web/app/common/views/components/poll.vue b/src/client/app/common/views/components/poll.vue
similarity index 100%
rename from src/server/web/app/common/views/components/poll.vue
rename to src/client/app/common/views/components/poll.vue
diff --git a/src/server/web/app/common/views/components/post-html.ts b/src/client/app/common/views/components/post-html.ts
similarity index 100%
rename from src/server/web/app/common/views/components/post-html.ts
rename to src/client/app/common/views/components/post-html.ts
diff --git a/src/server/web/app/common/views/components/post-menu.vue b/src/client/app/common/views/components/post-menu.vue
similarity index 100%
rename from src/server/web/app/common/views/components/post-menu.vue
rename to src/client/app/common/views/components/post-menu.vue
diff --git a/src/server/web/app/common/views/components/reaction-icon.vue b/src/client/app/common/views/components/reaction-icon.vue
similarity index 100%
rename from src/server/web/app/common/views/components/reaction-icon.vue
rename to src/client/app/common/views/components/reaction-icon.vue
diff --git a/src/server/web/app/common/views/components/reaction-picker.vue b/src/client/app/common/views/components/reaction-picker.vue
similarity index 100%
rename from src/server/web/app/common/views/components/reaction-picker.vue
rename to src/client/app/common/views/components/reaction-picker.vue
diff --git a/src/server/web/app/common/views/components/reactions-viewer.vue b/src/client/app/common/views/components/reactions-viewer.vue
similarity index 100%
rename from src/server/web/app/common/views/components/reactions-viewer.vue
rename to src/client/app/common/views/components/reactions-viewer.vue
diff --git a/src/server/web/app/common/views/components/signin.vue b/src/client/app/common/views/components/signin.vue
similarity index 100%
rename from src/server/web/app/common/views/components/signin.vue
rename to src/client/app/common/views/components/signin.vue
diff --git a/src/server/web/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue
similarity index 100%
rename from src/server/web/app/common/views/components/signup.vue
rename to src/client/app/common/views/components/signup.vue
diff --git a/src/server/web/app/common/views/components/special-message.vue b/src/client/app/common/views/components/special-message.vue
similarity index 100%
rename from src/server/web/app/common/views/components/special-message.vue
rename to src/client/app/common/views/components/special-message.vue
diff --git a/src/server/web/app/common/views/components/stream-indicator.vue b/src/client/app/common/views/components/stream-indicator.vue
similarity index 100%
rename from src/server/web/app/common/views/components/stream-indicator.vue
rename to src/client/app/common/views/components/stream-indicator.vue
diff --git a/src/server/web/app/common/views/components/switch.vue b/src/client/app/common/views/components/switch.vue
similarity index 100%
rename from src/server/web/app/common/views/components/switch.vue
rename to src/client/app/common/views/components/switch.vue
diff --git a/src/server/web/app/common/views/components/time.vue b/src/client/app/common/views/components/time.vue
similarity index 100%
rename from src/server/web/app/common/views/components/time.vue
rename to src/client/app/common/views/components/time.vue
diff --git a/src/server/web/app/common/views/components/timer.vue b/src/client/app/common/views/components/timer.vue
similarity index 100%
rename from src/server/web/app/common/views/components/timer.vue
rename to src/client/app/common/views/components/timer.vue
diff --git a/src/server/web/app/common/views/components/twitter-setting.vue b/src/client/app/common/views/components/twitter-setting.vue
similarity index 100%
rename from src/server/web/app/common/views/components/twitter-setting.vue
rename to src/client/app/common/views/components/twitter-setting.vue
diff --git a/src/server/web/app/common/views/components/uploader.vue b/src/client/app/common/views/components/uploader.vue
similarity index 100%
rename from src/server/web/app/common/views/components/uploader.vue
rename to src/client/app/common/views/components/uploader.vue
diff --git a/src/server/web/app/common/views/components/url-preview.vue b/src/client/app/common/views/components/url-preview.vue
similarity index 100%
rename from src/server/web/app/common/views/components/url-preview.vue
rename to src/client/app/common/views/components/url-preview.vue
diff --git a/src/server/web/app/common/views/components/url.vue b/src/client/app/common/views/components/url.vue
similarity index 100%
rename from src/server/web/app/common/views/components/url.vue
rename to src/client/app/common/views/components/url.vue
diff --git a/src/server/web/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue
similarity index 100%
rename from src/server/web/app/common/views/components/welcome-timeline.vue
rename to src/client/app/common/views/components/welcome-timeline.vue
diff --git a/src/server/web/app/common/views/directives/autocomplete.ts b/src/client/app/common/views/directives/autocomplete.ts
similarity index 100%
rename from src/server/web/app/common/views/directives/autocomplete.ts
rename to src/client/app/common/views/directives/autocomplete.ts
diff --git a/src/server/web/app/common/views/directives/index.ts b/src/client/app/common/views/directives/index.ts
similarity index 100%
rename from src/server/web/app/common/views/directives/index.ts
rename to src/client/app/common/views/directives/index.ts
diff --git a/src/server/web/app/common/views/filters/bytes.ts b/src/client/app/common/views/filters/bytes.ts
similarity index 100%
rename from src/server/web/app/common/views/filters/bytes.ts
rename to src/client/app/common/views/filters/bytes.ts
diff --git a/src/server/web/app/common/views/filters/index.ts b/src/client/app/common/views/filters/index.ts
similarity index 100%
rename from src/server/web/app/common/views/filters/index.ts
rename to src/client/app/common/views/filters/index.ts
diff --git a/src/server/web/app/common/views/filters/number.ts b/src/client/app/common/views/filters/number.ts
similarity index 100%
rename from src/server/web/app/common/views/filters/number.ts
rename to src/client/app/common/views/filters/number.ts
diff --git a/src/server/web/app/common/views/widgets/access-log.vue b/src/client/app/common/views/widgets/access-log.vue
similarity index 100%
rename from src/server/web/app/common/views/widgets/access-log.vue
rename to src/client/app/common/views/widgets/access-log.vue
diff --git a/src/server/web/app/common/views/widgets/broadcast.vue b/src/client/app/common/views/widgets/broadcast.vue
similarity index 100%
rename from src/server/web/app/common/views/widgets/broadcast.vue
rename to src/client/app/common/views/widgets/broadcast.vue
diff --git a/src/server/web/app/common/views/widgets/calendar.vue b/src/client/app/common/views/widgets/calendar.vue
similarity index 100%
rename from src/server/web/app/common/views/widgets/calendar.vue
rename to src/client/app/common/views/widgets/calendar.vue
diff --git a/src/server/web/app/common/views/widgets/donation.vue b/src/client/app/common/views/widgets/donation.vue
similarity index 100%
rename from src/server/web/app/common/views/widgets/donation.vue
rename to src/client/app/common/views/widgets/donation.vue
diff --git a/src/server/web/app/common/views/widgets/index.ts b/src/client/app/common/views/widgets/index.ts
similarity index 100%
rename from src/server/web/app/common/views/widgets/index.ts
rename to src/client/app/common/views/widgets/index.ts
diff --git a/src/server/web/app/common/views/widgets/nav.vue b/src/client/app/common/views/widgets/nav.vue
similarity index 100%
rename from src/server/web/app/common/views/widgets/nav.vue
rename to src/client/app/common/views/widgets/nav.vue
diff --git a/src/server/web/app/common/views/widgets/photo-stream.vue b/src/client/app/common/views/widgets/photo-stream.vue
similarity index 100%
rename from src/server/web/app/common/views/widgets/photo-stream.vue
rename to src/client/app/common/views/widgets/photo-stream.vue
diff --git a/src/server/web/app/common/views/widgets/rss.vue b/src/client/app/common/views/widgets/rss.vue
similarity index 100%
rename from src/server/web/app/common/views/widgets/rss.vue
rename to src/client/app/common/views/widgets/rss.vue
diff --git a/src/server/web/app/common/views/widgets/server.cpu-memory.vue b/src/client/app/common/views/widgets/server.cpu-memory.vue
similarity index 100%
rename from src/server/web/app/common/views/widgets/server.cpu-memory.vue
rename to src/client/app/common/views/widgets/server.cpu-memory.vue
diff --git a/src/server/web/app/common/views/widgets/server.cpu.vue b/src/client/app/common/views/widgets/server.cpu.vue
similarity index 100%
rename from src/server/web/app/common/views/widgets/server.cpu.vue
rename to src/client/app/common/views/widgets/server.cpu.vue
diff --git a/src/server/web/app/common/views/widgets/server.disk.vue b/src/client/app/common/views/widgets/server.disk.vue
similarity index 100%
rename from src/server/web/app/common/views/widgets/server.disk.vue
rename to src/client/app/common/views/widgets/server.disk.vue
diff --git a/src/server/web/app/common/views/widgets/server.info.vue b/src/client/app/common/views/widgets/server.info.vue
similarity index 100%
rename from src/server/web/app/common/views/widgets/server.info.vue
rename to src/client/app/common/views/widgets/server.info.vue
diff --git a/src/server/web/app/common/views/widgets/server.memory.vue b/src/client/app/common/views/widgets/server.memory.vue
similarity index 100%
rename from src/server/web/app/common/views/widgets/server.memory.vue
rename to src/client/app/common/views/widgets/server.memory.vue
diff --git a/src/server/web/app/common/views/widgets/server.pie.vue b/src/client/app/common/views/widgets/server.pie.vue
similarity index 100%
rename from src/server/web/app/common/views/widgets/server.pie.vue
rename to src/client/app/common/views/widgets/server.pie.vue
diff --git a/src/server/web/app/common/views/widgets/server.uptimes.vue b/src/client/app/common/views/widgets/server.uptimes.vue
similarity index 100%
rename from src/server/web/app/common/views/widgets/server.uptimes.vue
rename to src/client/app/common/views/widgets/server.uptimes.vue
diff --git a/src/server/web/app/common/views/widgets/server.vue b/src/client/app/common/views/widgets/server.vue
similarity index 100%
rename from src/server/web/app/common/views/widgets/server.vue
rename to src/client/app/common/views/widgets/server.vue
diff --git a/src/server/web/app/common/views/widgets/slideshow.vue b/src/client/app/common/views/widgets/slideshow.vue
similarity index 100%
rename from src/server/web/app/common/views/widgets/slideshow.vue
rename to src/client/app/common/views/widgets/slideshow.vue
diff --git a/src/server/web/app/common/views/widgets/tips.vue b/src/client/app/common/views/widgets/tips.vue
similarity index 100%
rename from src/server/web/app/common/views/widgets/tips.vue
rename to src/client/app/common/views/widgets/tips.vue
diff --git a/src/server/web/app/common/views/widgets/version.vue b/src/client/app/common/views/widgets/version.vue
similarity index 100%
rename from src/server/web/app/common/views/widgets/version.vue
rename to src/client/app/common/views/widgets/version.vue
diff --git a/src/server/web/app/config.ts b/src/client/app/config.ts
similarity index 100%
rename from src/server/web/app/config.ts
rename to src/client/app/config.ts
diff --git a/src/server/web/app/desktop/api/choose-drive-file.ts b/src/client/app/desktop/api/choose-drive-file.ts
similarity index 100%
rename from src/server/web/app/desktop/api/choose-drive-file.ts
rename to src/client/app/desktop/api/choose-drive-file.ts
diff --git a/src/server/web/app/desktop/api/choose-drive-folder.ts b/src/client/app/desktop/api/choose-drive-folder.ts
similarity index 100%
rename from src/server/web/app/desktop/api/choose-drive-folder.ts
rename to src/client/app/desktop/api/choose-drive-folder.ts
diff --git a/src/server/web/app/desktop/api/contextmenu.ts b/src/client/app/desktop/api/contextmenu.ts
similarity index 100%
rename from src/server/web/app/desktop/api/contextmenu.ts
rename to src/client/app/desktop/api/contextmenu.ts
diff --git a/src/server/web/app/desktop/api/dialog.ts b/src/client/app/desktop/api/dialog.ts
similarity index 100%
rename from src/server/web/app/desktop/api/dialog.ts
rename to src/client/app/desktop/api/dialog.ts
diff --git a/src/server/web/app/desktop/api/input.ts b/src/client/app/desktop/api/input.ts
similarity index 100%
rename from src/server/web/app/desktop/api/input.ts
rename to src/client/app/desktop/api/input.ts
diff --git a/src/server/web/app/desktop/api/notify.ts b/src/client/app/desktop/api/notify.ts
similarity index 100%
rename from src/server/web/app/desktop/api/notify.ts
rename to src/client/app/desktop/api/notify.ts
diff --git a/src/server/web/app/desktop/api/post.ts b/src/client/app/desktop/api/post.ts
similarity index 100%
rename from src/server/web/app/desktop/api/post.ts
rename to src/client/app/desktop/api/post.ts
diff --git a/src/server/web/app/desktop/api/update-avatar.ts b/src/client/app/desktop/api/update-avatar.ts
similarity index 100%
rename from src/server/web/app/desktop/api/update-avatar.ts
rename to src/client/app/desktop/api/update-avatar.ts
diff --git a/src/server/web/app/desktop/api/update-banner.ts b/src/client/app/desktop/api/update-banner.ts
similarity index 100%
rename from src/server/web/app/desktop/api/update-banner.ts
rename to src/client/app/desktop/api/update-banner.ts
diff --git a/src/server/web/app/desktop/assets/grid.svg b/src/client/app/desktop/assets/grid.svg
similarity index 100%
rename from src/server/web/app/desktop/assets/grid.svg
rename to src/client/app/desktop/assets/grid.svg
diff --git a/src/server/web/app/desktop/assets/header-logo-white.svg b/src/client/app/desktop/assets/header-logo-white.svg
similarity index 100%
rename from src/server/web/app/desktop/assets/header-logo-white.svg
rename to src/client/app/desktop/assets/header-logo-white.svg
diff --git a/src/server/web/app/desktop/assets/header-logo.svg b/src/client/app/desktop/assets/header-logo.svg
similarity index 100%
rename from src/server/web/app/desktop/assets/header-logo.svg
rename to src/client/app/desktop/assets/header-logo.svg
diff --git a/src/server/web/app/desktop/assets/index.jpg b/src/client/app/desktop/assets/index.jpg
similarity index 100%
rename from src/server/web/app/desktop/assets/index.jpg
rename to src/client/app/desktop/assets/index.jpg
diff --git a/src/server/web/app/desktop/assets/remove.png b/src/client/app/desktop/assets/remove.png
similarity index 100%
rename from src/server/web/app/desktop/assets/remove.png
rename to src/client/app/desktop/assets/remove.png
diff --git a/src/server/web/app/desktop/script.ts b/src/client/app/desktop/script.ts
similarity index 100%
rename from src/server/web/app/desktop/script.ts
rename to src/client/app/desktop/script.ts
diff --git a/src/server/web/app/desktop/style.styl b/src/client/app/desktop/style.styl
similarity index 100%
rename from src/server/web/app/desktop/style.styl
rename to src/client/app/desktop/style.styl
diff --git a/src/server/web/app/desktop/ui.styl b/src/client/app/desktop/ui.styl
similarity index 100%
rename from src/server/web/app/desktop/ui.styl
rename to src/client/app/desktop/ui.styl
diff --git a/src/server/web/app/desktop/views/components/activity.calendar.vue b/src/client/app/desktop/views/components/activity.calendar.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/activity.calendar.vue
rename to src/client/app/desktop/views/components/activity.calendar.vue
diff --git a/src/server/web/app/desktop/views/components/activity.chart.vue b/src/client/app/desktop/views/components/activity.chart.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/activity.chart.vue
rename to src/client/app/desktop/views/components/activity.chart.vue
diff --git a/src/server/web/app/desktop/views/components/activity.vue b/src/client/app/desktop/views/components/activity.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/activity.vue
rename to src/client/app/desktop/views/components/activity.vue
diff --git a/src/server/web/app/desktop/views/components/analog-clock.vue b/src/client/app/desktop/views/components/analog-clock.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/analog-clock.vue
rename to src/client/app/desktop/views/components/analog-clock.vue
diff --git a/src/server/web/app/desktop/views/components/calendar.vue b/src/client/app/desktop/views/components/calendar.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/calendar.vue
rename to src/client/app/desktop/views/components/calendar.vue
diff --git a/src/server/web/app/desktop/views/components/choose-file-from-drive-window.vue b/src/client/app/desktop/views/components/choose-file-from-drive-window.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/choose-file-from-drive-window.vue
rename to src/client/app/desktop/views/components/choose-file-from-drive-window.vue
diff --git a/src/server/web/app/desktop/views/components/choose-folder-from-drive-window.vue b/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/choose-folder-from-drive-window.vue
rename to src/client/app/desktop/views/components/choose-folder-from-drive-window.vue
diff --git a/src/server/web/app/desktop/views/components/context-menu.menu.vue b/src/client/app/desktop/views/components/context-menu.menu.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/context-menu.menu.vue
rename to src/client/app/desktop/views/components/context-menu.menu.vue
diff --git a/src/server/web/app/desktop/views/components/context-menu.vue b/src/client/app/desktop/views/components/context-menu.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/context-menu.vue
rename to src/client/app/desktop/views/components/context-menu.vue
diff --git a/src/server/web/app/desktop/views/components/crop-window.vue b/src/client/app/desktop/views/components/crop-window.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/crop-window.vue
rename to src/client/app/desktop/views/components/crop-window.vue
diff --git a/src/server/web/app/desktop/views/components/dialog.vue b/src/client/app/desktop/views/components/dialog.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/dialog.vue
rename to src/client/app/desktop/views/components/dialog.vue
diff --git a/src/server/web/app/desktop/views/components/drive-window.vue b/src/client/app/desktop/views/components/drive-window.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/drive-window.vue
rename to src/client/app/desktop/views/components/drive-window.vue
diff --git a/src/server/web/app/desktop/views/components/drive.file.vue b/src/client/app/desktop/views/components/drive.file.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/drive.file.vue
rename to src/client/app/desktop/views/components/drive.file.vue
diff --git a/src/server/web/app/desktop/views/components/drive.folder.vue b/src/client/app/desktop/views/components/drive.folder.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/drive.folder.vue
rename to src/client/app/desktop/views/components/drive.folder.vue
diff --git a/src/server/web/app/desktop/views/components/drive.nav-folder.vue b/src/client/app/desktop/views/components/drive.nav-folder.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/drive.nav-folder.vue
rename to src/client/app/desktop/views/components/drive.nav-folder.vue
diff --git a/src/server/web/app/desktop/views/components/drive.vue b/src/client/app/desktop/views/components/drive.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/drive.vue
rename to src/client/app/desktop/views/components/drive.vue
diff --git a/src/server/web/app/desktop/views/components/ellipsis-icon.vue b/src/client/app/desktop/views/components/ellipsis-icon.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/ellipsis-icon.vue
rename to src/client/app/desktop/views/components/ellipsis-icon.vue
diff --git a/src/server/web/app/desktop/views/components/follow-button.vue b/src/client/app/desktop/views/components/follow-button.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/follow-button.vue
rename to src/client/app/desktop/views/components/follow-button.vue
diff --git a/src/server/web/app/desktop/views/components/followers-window.vue b/src/client/app/desktop/views/components/followers-window.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/followers-window.vue
rename to src/client/app/desktop/views/components/followers-window.vue
diff --git a/src/server/web/app/desktop/views/components/followers.vue b/src/client/app/desktop/views/components/followers.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/followers.vue
rename to src/client/app/desktop/views/components/followers.vue
diff --git a/src/server/web/app/desktop/views/components/following-window.vue b/src/client/app/desktop/views/components/following-window.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/following-window.vue
rename to src/client/app/desktop/views/components/following-window.vue
diff --git a/src/server/web/app/desktop/views/components/following.vue b/src/client/app/desktop/views/components/following.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/following.vue
rename to src/client/app/desktop/views/components/following.vue
diff --git a/src/server/web/app/desktop/views/components/friends-maker.vue b/src/client/app/desktop/views/components/friends-maker.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/friends-maker.vue
rename to src/client/app/desktop/views/components/friends-maker.vue
diff --git a/src/server/web/app/desktop/views/components/game-window.vue b/src/client/app/desktop/views/components/game-window.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/game-window.vue
rename to src/client/app/desktop/views/components/game-window.vue
diff --git a/src/server/web/app/desktop/views/components/home.vue b/src/client/app/desktop/views/components/home.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/home.vue
rename to src/client/app/desktop/views/components/home.vue
diff --git a/src/server/web/app/desktop/views/components/index.ts b/src/client/app/desktop/views/components/index.ts
similarity index 100%
rename from src/server/web/app/desktop/views/components/index.ts
rename to src/client/app/desktop/views/components/index.ts
diff --git a/src/server/web/app/desktop/views/components/input-dialog.vue b/src/client/app/desktop/views/components/input-dialog.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/input-dialog.vue
rename to src/client/app/desktop/views/components/input-dialog.vue
diff --git a/src/server/web/app/desktop/views/components/media-image-dialog.vue b/src/client/app/desktop/views/components/media-image-dialog.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/media-image-dialog.vue
rename to src/client/app/desktop/views/components/media-image-dialog.vue
diff --git a/src/server/web/app/desktop/views/components/media-image.vue b/src/client/app/desktop/views/components/media-image.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/media-image.vue
rename to src/client/app/desktop/views/components/media-image.vue
diff --git a/src/server/web/app/desktop/views/components/media-video-dialog.vue b/src/client/app/desktop/views/components/media-video-dialog.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/media-video-dialog.vue
rename to src/client/app/desktop/views/components/media-video-dialog.vue
diff --git a/src/server/web/app/desktop/views/components/media-video.vue b/src/client/app/desktop/views/components/media-video.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/media-video.vue
rename to src/client/app/desktop/views/components/media-video.vue
diff --git a/src/server/web/app/desktop/views/components/mentions.vue b/src/client/app/desktop/views/components/mentions.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/mentions.vue
rename to src/client/app/desktop/views/components/mentions.vue
diff --git a/src/server/web/app/desktop/views/components/messaging-room-window.vue b/src/client/app/desktop/views/components/messaging-room-window.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/messaging-room-window.vue
rename to src/client/app/desktop/views/components/messaging-room-window.vue
diff --git a/src/server/web/app/desktop/views/components/messaging-window.vue b/src/client/app/desktop/views/components/messaging-window.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/messaging-window.vue
rename to src/client/app/desktop/views/components/messaging-window.vue
diff --git a/src/server/web/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/notifications.vue
rename to src/client/app/desktop/views/components/notifications.vue
diff --git a/src/server/web/app/desktop/views/components/post-detail.sub.vue b/src/client/app/desktop/views/components/post-detail.sub.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/post-detail.sub.vue
rename to src/client/app/desktop/views/components/post-detail.sub.vue
diff --git a/src/server/web/app/desktop/views/components/post-detail.vue b/src/client/app/desktop/views/components/post-detail.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/post-detail.vue
rename to src/client/app/desktop/views/components/post-detail.vue
diff --git a/src/server/web/app/desktop/views/components/post-form-window.vue b/src/client/app/desktop/views/components/post-form-window.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/post-form-window.vue
rename to src/client/app/desktop/views/components/post-form-window.vue
diff --git a/src/server/web/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/post-form.vue
rename to src/client/app/desktop/views/components/post-form.vue
diff --git a/src/server/web/app/desktop/views/components/post-preview.vue b/src/client/app/desktop/views/components/post-preview.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/post-preview.vue
rename to src/client/app/desktop/views/components/post-preview.vue
diff --git a/src/server/web/app/desktop/views/components/posts.post.sub.vue b/src/client/app/desktop/views/components/posts.post.sub.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/posts.post.sub.vue
rename to src/client/app/desktop/views/components/posts.post.sub.vue
diff --git a/src/server/web/app/desktop/views/components/posts.post.vue b/src/client/app/desktop/views/components/posts.post.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/posts.post.vue
rename to src/client/app/desktop/views/components/posts.post.vue
diff --git a/src/server/web/app/desktop/views/components/posts.vue b/src/client/app/desktop/views/components/posts.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/posts.vue
rename to src/client/app/desktop/views/components/posts.vue
diff --git a/src/server/web/app/desktop/views/components/progress-dialog.vue b/src/client/app/desktop/views/components/progress-dialog.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/progress-dialog.vue
rename to src/client/app/desktop/views/components/progress-dialog.vue
diff --git a/src/server/web/app/desktop/views/components/repost-form-window.vue b/src/client/app/desktop/views/components/repost-form-window.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/repost-form-window.vue
rename to src/client/app/desktop/views/components/repost-form-window.vue
diff --git a/src/server/web/app/desktop/views/components/repost-form.vue b/src/client/app/desktop/views/components/repost-form.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/repost-form.vue
rename to src/client/app/desktop/views/components/repost-form.vue
diff --git a/src/server/web/app/desktop/views/components/settings-window.vue b/src/client/app/desktop/views/components/settings-window.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/settings-window.vue
rename to src/client/app/desktop/views/components/settings-window.vue
diff --git a/src/server/web/app/desktop/views/components/settings.2fa.vue b/src/client/app/desktop/views/components/settings.2fa.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/settings.2fa.vue
rename to src/client/app/desktop/views/components/settings.2fa.vue
diff --git a/src/server/web/app/desktop/views/components/settings.api.vue b/src/client/app/desktop/views/components/settings.api.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/settings.api.vue
rename to src/client/app/desktop/views/components/settings.api.vue
diff --git a/src/server/web/app/desktop/views/components/settings.apps.vue b/src/client/app/desktop/views/components/settings.apps.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/settings.apps.vue
rename to src/client/app/desktop/views/components/settings.apps.vue
diff --git a/src/server/web/app/desktop/views/components/settings.drive.vue b/src/client/app/desktop/views/components/settings.drive.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/settings.drive.vue
rename to src/client/app/desktop/views/components/settings.drive.vue
diff --git a/src/server/web/app/desktop/views/components/settings.mute.vue b/src/client/app/desktop/views/components/settings.mute.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/settings.mute.vue
rename to src/client/app/desktop/views/components/settings.mute.vue
diff --git a/src/server/web/app/desktop/views/components/settings.password.vue b/src/client/app/desktop/views/components/settings.password.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/settings.password.vue
rename to src/client/app/desktop/views/components/settings.password.vue
diff --git a/src/server/web/app/desktop/views/components/settings.profile.vue b/src/client/app/desktop/views/components/settings.profile.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/settings.profile.vue
rename to src/client/app/desktop/views/components/settings.profile.vue
diff --git a/src/server/web/app/desktop/views/components/settings.signins.vue b/src/client/app/desktop/views/components/settings.signins.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/settings.signins.vue
rename to src/client/app/desktop/views/components/settings.signins.vue
diff --git a/src/server/web/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/settings.vue
rename to src/client/app/desktop/views/components/settings.vue
diff --git a/src/server/web/app/desktop/views/components/sub-post-content.vue b/src/client/app/desktop/views/components/sub-post-content.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/sub-post-content.vue
rename to src/client/app/desktop/views/components/sub-post-content.vue
diff --git a/src/server/web/app/desktop/views/components/taskmanager.vue b/src/client/app/desktop/views/components/taskmanager.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/taskmanager.vue
rename to src/client/app/desktop/views/components/taskmanager.vue
diff --git a/src/server/web/app/desktop/views/components/timeline.vue b/src/client/app/desktop/views/components/timeline.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/timeline.vue
rename to src/client/app/desktop/views/components/timeline.vue
diff --git a/src/server/web/app/desktop/views/components/ui-notification.vue b/src/client/app/desktop/views/components/ui-notification.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/ui-notification.vue
rename to src/client/app/desktop/views/components/ui-notification.vue
diff --git a/src/server/web/app/desktop/views/components/ui.header.account.vue b/src/client/app/desktop/views/components/ui.header.account.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/ui.header.account.vue
rename to src/client/app/desktop/views/components/ui.header.account.vue
diff --git a/src/server/web/app/desktop/views/components/ui.header.clock.vue b/src/client/app/desktop/views/components/ui.header.clock.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/ui.header.clock.vue
rename to src/client/app/desktop/views/components/ui.header.clock.vue
diff --git a/src/server/web/app/desktop/views/components/ui.header.nav.vue b/src/client/app/desktop/views/components/ui.header.nav.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/ui.header.nav.vue
rename to src/client/app/desktop/views/components/ui.header.nav.vue
diff --git a/src/server/web/app/desktop/views/components/ui.header.notifications.vue b/src/client/app/desktop/views/components/ui.header.notifications.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/ui.header.notifications.vue
rename to src/client/app/desktop/views/components/ui.header.notifications.vue
diff --git a/src/server/web/app/desktop/views/components/ui.header.post.vue b/src/client/app/desktop/views/components/ui.header.post.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/ui.header.post.vue
rename to src/client/app/desktop/views/components/ui.header.post.vue
diff --git a/src/server/web/app/desktop/views/components/ui.header.search.vue b/src/client/app/desktop/views/components/ui.header.search.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/ui.header.search.vue
rename to src/client/app/desktop/views/components/ui.header.search.vue
diff --git a/src/server/web/app/desktop/views/components/ui.header.vue b/src/client/app/desktop/views/components/ui.header.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/ui.header.vue
rename to src/client/app/desktop/views/components/ui.header.vue
diff --git a/src/server/web/app/desktop/views/components/ui.vue b/src/client/app/desktop/views/components/ui.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/ui.vue
rename to src/client/app/desktop/views/components/ui.vue
diff --git a/src/server/web/app/desktop/views/components/user-preview.vue b/src/client/app/desktop/views/components/user-preview.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/user-preview.vue
rename to src/client/app/desktop/views/components/user-preview.vue
diff --git a/src/server/web/app/desktop/views/components/users-list.item.vue b/src/client/app/desktop/views/components/users-list.item.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/users-list.item.vue
rename to src/client/app/desktop/views/components/users-list.item.vue
diff --git a/src/server/web/app/desktop/views/components/users-list.vue b/src/client/app/desktop/views/components/users-list.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/users-list.vue
rename to src/client/app/desktop/views/components/users-list.vue
diff --git a/src/server/web/app/desktop/views/components/widget-container.vue b/src/client/app/desktop/views/components/widget-container.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/widget-container.vue
rename to src/client/app/desktop/views/components/widget-container.vue
diff --git a/src/server/web/app/desktop/views/components/window.vue b/src/client/app/desktop/views/components/window.vue
similarity index 100%
rename from src/server/web/app/desktop/views/components/window.vue
rename to src/client/app/desktop/views/components/window.vue
diff --git a/src/server/web/app/desktop/views/directives/index.ts b/src/client/app/desktop/views/directives/index.ts
similarity index 100%
rename from src/server/web/app/desktop/views/directives/index.ts
rename to src/client/app/desktop/views/directives/index.ts
diff --git a/src/server/web/app/desktop/views/directives/user-preview.ts b/src/client/app/desktop/views/directives/user-preview.ts
similarity index 100%
rename from src/server/web/app/desktop/views/directives/user-preview.ts
rename to src/client/app/desktop/views/directives/user-preview.ts
diff --git a/src/server/web/app/desktop/views/pages/drive.vue b/src/client/app/desktop/views/pages/drive.vue
similarity index 100%
rename from src/server/web/app/desktop/views/pages/drive.vue
rename to src/client/app/desktop/views/pages/drive.vue
diff --git a/src/server/web/app/desktop/views/pages/home-customize.vue b/src/client/app/desktop/views/pages/home-customize.vue
similarity index 100%
rename from src/server/web/app/desktop/views/pages/home-customize.vue
rename to src/client/app/desktop/views/pages/home-customize.vue
diff --git a/src/server/web/app/desktop/views/pages/home.vue b/src/client/app/desktop/views/pages/home.vue
similarity index 100%
rename from src/server/web/app/desktop/views/pages/home.vue
rename to src/client/app/desktop/views/pages/home.vue
diff --git a/src/server/web/app/desktop/views/pages/index.vue b/src/client/app/desktop/views/pages/index.vue
similarity index 100%
rename from src/server/web/app/desktop/views/pages/index.vue
rename to src/client/app/desktop/views/pages/index.vue
diff --git a/src/server/web/app/desktop/views/pages/messaging-room.vue b/src/client/app/desktop/views/pages/messaging-room.vue
similarity index 100%
rename from src/server/web/app/desktop/views/pages/messaging-room.vue
rename to src/client/app/desktop/views/pages/messaging-room.vue
diff --git a/src/server/web/app/desktop/views/pages/othello.vue b/src/client/app/desktop/views/pages/othello.vue
similarity index 100%
rename from src/server/web/app/desktop/views/pages/othello.vue
rename to src/client/app/desktop/views/pages/othello.vue
diff --git a/src/server/web/app/desktop/views/pages/post.vue b/src/client/app/desktop/views/pages/post.vue
similarity index 100%
rename from src/server/web/app/desktop/views/pages/post.vue
rename to src/client/app/desktop/views/pages/post.vue
diff --git a/src/server/web/app/desktop/views/pages/search.vue b/src/client/app/desktop/views/pages/search.vue
similarity index 100%
rename from src/server/web/app/desktop/views/pages/search.vue
rename to src/client/app/desktop/views/pages/search.vue
diff --git a/src/server/web/app/desktop/views/pages/selectdrive.vue b/src/client/app/desktop/views/pages/selectdrive.vue
similarity index 100%
rename from src/server/web/app/desktop/views/pages/selectdrive.vue
rename to src/client/app/desktop/views/pages/selectdrive.vue
diff --git a/src/server/web/app/desktop/views/pages/user/user.followers-you-know.vue b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
similarity index 100%
rename from src/server/web/app/desktop/views/pages/user/user.followers-you-know.vue
rename to src/client/app/desktop/views/pages/user/user.followers-you-know.vue
diff --git a/src/server/web/app/desktop/views/pages/user/user.friends.vue b/src/client/app/desktop/views/pages/user/user.friends.vue
similarity index 100%
rename from src/server/web/app/desktop/views/pages/user/user.friends.vue
rename to src/client/app/desktop/views/pages/user/user.friends.vue
diff --git a/src/server/web/app/desktop/views/pages/user/user.header.vue b/src/client/app/desktop/views/pages/user/user.header.vue
similarity index 100%
rename from src/server/web/app/desktop/views/pages/user/user.header.vue
rename to src/client/app/desktop/views/pages/user/user.header.vue
diff --git a/src/server/web/app/desktop/views/pages/user/user.home.vue b/src/client/app/desktop/views/pages/user/user.home.vue
similarity index 100%
rename from src/server/web/app/desktop/views/pages/user/user.home.vue
rename to src/client/app/desktop/views/pages/user/user.home.vue
diff --git a/src/server/web/app/desktop/views/pages/user/user.photos.vue b/src/client/app/desktop/views/pages/user/user.photos.vue
similarity index 100%
rename from src/server/web/app/desktop/views/pages/user/user.photos.vue
rename to src/client/app/desktop/views/pages/user/user.photos.vue
diff --git a/src/server/web/app/desktop/views/pages/user/user.profile.vue b/src/client/app/desktop/views/pages/user/user.profile.vue
similarity index 100%
rename from src/server/web/app/desktop/views/pages/user/user.profile.vue
rename to src/client/app/desktop/views/pages/user/user.profile.vue
diff --git a/src/server/web/app/desktop/views/pages/user/user.timeline.vue b/src/client/app/desktop/views/pages/user/user.timeline.vue
similarity index 100%
rename from src/server/web/app/desktop/views/pages/user/user.timeline.vue
rename to src/client/app/desktop/views/pages/user/user.timeline.vue
diff --git a/src/server/web/app/desktop/views/pages/user/user.vue b/src/client/app/desktop/views/pages/user/user.vue
similarity index 100%
rename from src/server/web/app/desktop/views/pages/user/user.vue
rename to src/client/app/desktop/views/pages/user/user.vue
diff --git a/src/server/web/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue
similarity index 100%
rename from src/server/web/app/desktop/views/pages/welcome.vue
rename to src/client/app/desktop/views/pages/welcome.vue
diff --git a/src/server/web/app/desktop/views/widgets/activity.vue b/src/client/app/desktop/views/widgets/activity.vue
similarity index 100%
rename from src/server/web/app/desktop/views/widgets/activity.vue
rename to src/client/app/desktop/views/widgets/activity.vue
diff --git a/src/server/web/app/desktop/views/widgets/channel.channel.form.vue b/src/client/app/desktop/views/widgets/channel.channel.form.vue
similarity index 100%
rename from src/server/web/app/desktop/views/widgets/channel.channel.form.vue
rename to src/client/app/desktop/views/widgets/channel.channel.form.vue
diff --git a/src/server/web/app/desktop/views/widgets/channel.channel.post.vue b/src/client/app/desktop/views/widgets/channel.channel.post.vue
similarity index 100%
rename from src/server/web/app/desktop/views/widgets/channel.channel.post.vue
rename to src/client/app/desktop/views/widgets/channel.channel.post.vue
diff --git a/src/server/web/app/desktop/views/widgets/channel.channel.vue b/src/client/app/desktop/views/widgets/channel.channel.vue
similarity index 100%
rename from src/server/web/app/desktop/views/widgets/channel.channel.vue
rename to src/client/app/desktop/views/widgets/channel.channel.vue
diff --git a/src/server/web/app/desktop/views/widgets/channel.vue b/src/client/app/desktop/views/widgets/channel.vue
similarity index 100%
rename from src/server/web/app/desktop/views/widgets/channel.vue
rename to src/client/app/desktop/views/widgets/channel.vue
diff --git a/src/server/web/app/desktop/views/widgets/index.ts b/src/client/app/desktop/views/widgets/index.ts
similarity index 100%
rename from src/server/web/app/desktop/views/widgets/index.ts
rename to src/client/app/desktop/views/widgets/index.ts
diff --git a/src/server/web/app/desktop/views/widgets/messaging.vue b/src/client/app/desktop/views/widgets/messaging.vue
similarity index 100%
rename from src/server/web/app/desktop/views/widgets/messaging.vue
rename to src/client/app/desktop/views/widgets/messaging.vue
diff --git a/src/server/web/app/desktop/views/widgets/notifications.vue b/src/client/app/desktop/views/widgets/notifications.vue
similarity index 100%
rename from src/server/web/app/desktop/views/widgets/notifications.vue
rename to src/client/app/desktop/views/widgets/notifications.vue
diff --git a/src/server/web/app/desktop/views/widgets/polls.vue b/src/client/app/desktop/views/widgets/polls.vue
similarity index 100%
rename from src/server/web/app/desktop/views/widgets/polls.vue
rename to src/client/app/desktop/views/widgets/polls.vue
diff --git a/src/server/web/app/desktop/views/widgets/post-form.vue b/src/client/app/desktop/views/widgets/post-form.vue
similarity index 100%
rename from src/server/web/app/desktop/views/widgets/post-form.vue
rename to src/client/app/desktop/views/widgets/post-form.vue
diff --git a/src/server/web/app/desktop/views/widgets/profile.vue b/src/client/app/desktop/views/widgets/profile.vue
similarity index 100%
rename from src/server/web/app/desktop/views/widgets/profile.vue
rename to src/client/app/desktop/views/widgets/profile.vue
diff --git a/src/server/web/app/desktop/views/widgets/timemachine.vue b/src/client/app/desktop/views/widgets/timemachine.vue
similarity index 100%
rename from src/server/web/app/desktop/views/widgets/timemachine.vue
rename to src/client/app/desktop/views/widgets/timemachine.vue
diff --git a/src/server/web/app/desktop/views/widgets/trends.vue b/src/client/app/desktop/views/widgets/trends.vue
similarity index 100%
rename from src/server/web/app/desktop/views/widgets/trends.vue
rename to src/client/app/desktop/views/widgets/trends.vue
diff --git a/src/server/web/app/desktop/views/widgets/users.vue b/src/client/app/desktop/views/widgets/users.vue
similarity index 100%
rename from src/server/web/app/desktop/views/widgets/users.vue
rename to src/client/app/desktop/views/widgets/users.vue
diff --git a/src/server/web/app/dev/script.ts b/src/client/app/dev/script.ts
similarity index 100%
rename from src/server/web/app/dev/script.ts
rename to src/client/app/dev/script.ts
diff --git a/src/server/web/app/dev/style.styl b/src/client/app/dev/style.styl
similarity index 100%
rename from src/server/web/app/dev/style.styl
rename to src/client/app/dev/style.styl
diff --git a/src/server/web/app/dev/views/app.vue b/src/client/app/dev/views/app.vue
similarity index 100%
rename from src/server/web/app/dev/views/app.vue
rename to src/client/app/dev/views/app.vue
diff --git a/src/server/web/app/dev/views/apps.vue b/src/client/app/dev/views/apps.vue
similarity index 100%
rename from src/server/web/app/dev/views/apps.vue
rename to src/client/app/dev/views/apps.vue
diff --git a/src/server/web/app/dev/views/index.vue b/src/client/app/dev/views/index.vue
similarity index 100%
rename from src/server/web/app/dev/views/index.vue
rename to src/client/app/dev/views/index.vue
diff --git a/src/server/web/app/dev/views/new-app.vue b/src/client/app/dev/views/new-app.vue
similarity index 100%
rename from src/server/web/app/dev/views/new-app.vue
rename to src/client/app/dev/views/new-app.vue
diff --git a/src/server/web/app/dev/views/ui.vue b/src/client/app/dev/views/ui.vue
similarity index 100%
rename from src/server/web/app/dev/views/ui.vue
rename to src/client/app/dev/views/ui.vue
diff --git a/src/server/web/app/init.css b/src/client/app/init.css
similarity index 100%
rename from src/server/web/app/init.css
rename to src/client/app/init.css
diff --git a/src/server/web/app/init.ts b/src/client/app/init.ts
similarity index 100%
rename from src/server/web/app/init.ts
rename to src/client/app/init.ts
diff --git a/src/server/web/app/mobile/api/choose-drive-file.ts b/src/client/app/mobile/api/choose-drive-file.ts
similarity index 100%
rename from src/server/web/app/mobile/api/choose-drive-file.ts
rename to src/client/app/mobile/api/choose-drive-file.ts
diff --git a/src/server/web/app/mobile/api/choose-drive-folder.ts b/src/client/app/mobile/api/choose-drive-folder.ts
similarity index 100%
rename from src/server/web/app/mobile/api/choose-drive-folder.ts
rename to src/client/app/mobile/api/choose-drive-folder.ts
diff --git a/src/server/web/app/mobile/api/dialog.ts b/src/client/app/mobile/api/dialog.ts
similarity index 100%
rename from src/server/web/app/mobile/api/dialog.ts
rename to src/client/app/mobile/api/dialog.ts
diff --git a/src/server/web/app/mobile/api/input.ts b/src/client/app/mobile/api/input.ts
similarity index 100%
rename from src/server/web/app/mobile/api/input.ts
rename to src/client/app/mobile/api/input.ts
diff --git a/src/server/web/app/mobile/api/notify.ts b/src/client/app/mobile/api/notify.ts
similarity index 100%
rename from src/server/web/app/mobile/api/notify.ts
rename to src/client/app/mobile/api/notify.ts
diff --git a/src/server/web/app/mobile/api/post.ts b/src/client/app/mobile/api/post.ts
similarity index 100%
rename from src/server/web/app/mobile/api/post.ts
rename to src/client/app/mobile/api/post.ts
diff --git a/src/server/web/app/mobile/script.ts b/src/client/app/mobile/script.ts
similarity index 100%
rename from src/server/web/app/mobile/script.ts
rename to src/client/app/mobile/script.ts
diff --git a/src/server/web/app/mobile/style.styl b/src/client/app/mobile/style.styl
similarity index 100%
rename from src/server/web/app/mobile/style.styl
rename to src/client/app/mobile/style.styl
diff --git a/src/server/web/app/mobile/views/components/activity.vue b/src/client/app/mobile/views/components/activity.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/activity.vue
rename to src/client/app/mobile/views/components/activity.vue
diff --git a/src/server/web/app/mobile/views/components/drive-file-chooser.vue b/src/client/app/mobile/views/components/drive-file-chooser.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/drive-file-chooser.vue
rename to src/client/app/mobile/views/components/drive-file-chooser.vue
diff --git a/src/server/web/app/mobile/views/components/drive-folder-chooser.vue b/src/client/app/mobile/views/components/drive-folder-chooser.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/drive-folder-chooser.vue
rename to src/client/app/mobile/views/components/drive-folder-chooser.vue
diff --git a/src/server/web/app/mobile/views/components/drive.file-detail.vue b/src/client/app/mobile/views/components/drive.file-detail.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/drive.file-detail.vue
rename to src/client/app/mobile/views/components/drive.file-detail.vue
diff --git a/src/server/web/app/mobile/views/components/drive.file.vue b/src/client/app/mobile/views/components/drive.file.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/drive.file.vue
rename to src/client/app/mobile/views/components/drive.file.vue
diff --git a/src/server/web/app/mobile/views/components/drive.folder.vue b/src/client/app/mobile/views/components/drive.folder.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/drive.folder.vue
rename to src/client/app/mobile/views/components/drive.folder.vue
diff --git a/src/server/web/app/mobile/views/components/drive.vue b/src/client/app/mobile/views/components/drive.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/drive.vue
rename to src/client/app/mobile/views/components/drive.vue
diff --git a/src/server/web/app/mobile/views/components/follow-button.vue b/src/client/app/mobile/views/components/follow-button.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/follow-button.vue
rename to src/client/app/mobile/views/components/follow-button.vue
diff --git a/src/server/web/app/mobile/views/components/friends-maker.vue b/src/client/app/mobile/views/components/friends-maker.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/friends-maker.vue
rename to src/client/app/mobile/views/components/friends-maker.vue
diff --git a/src/server/web/app/mobile/views/components/index.ts b/src/client/app/mobile/views/components/index.ts
similarity index 100%
rename from src/server/web/app/mobile/views/components/index.ts
rename to src/client/app/mobile/views/components/index.ts
diff --git a/src/server/web/app/mobile/views/components/media-image.vue b/src/client/app/mobile/views/components/media-image.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/media-image.vue
rename to src/client/app/mobile/views/components/media-image.vue
diff --git a/src/server/web/app/mobile/views/components/media-video.vue b/src/client/app/mobile/views/components/media-video.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/media-video.vue
rename to src/client/app/mobile/views/components/media-video.vue
diff --git a/src/server/web/app/mobile/views/components/notification-preview.vue b/src/client/app/mobile/views/components/notification-preview.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/notification-preview.vue
rename to src/client/app/mobile/views/components/notification-preview.vue
diff --git a/src/server/web/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/notification.vue
rename to src/client/app/mobile/views/components/notification.vue
diff --git a/src/server/web/app/mobile/views/components/notifications.vue b/src/client/app/mobile/views/components/notifications.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/notifications.vue
rename to src/client/app/mobile/views/components/notifications.vue
diff --git a/src/server/web/app/mobile/views/components/notify.vue b/src/client/app/mobile/views/components/notify.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/notify.vue
rename to src/client/app/mobile/views/components/notify.vue
diff --git a/src/server/web/app/mobile/views/components/post-card.vue b/src/client/app/mobile/views/components/post-card.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/post-card.vue
rename to src/client/app/mobile/views/components/post-card.vue
diff --git a/src/server/web/app/mobile/views/components/post-detail.sub.vue b/src/client/app/mobile/views/components/post-detail.sub.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/post-detail.sub.vue
rename to src/client/app/mobile/views/components/post-detail.sub.vue
diff --git a/src/server/web/app/mobile/views/components/post-detail.vue b/src/client/app/mobile/views/components/post-detail.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/post-detail.vue
rename to src/client/app/mobile/views/components/post-detail.vue
diff --git a/src/server/web/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/post-form.vue
rename to src/client/app/mobile/views/components/post-form.vue
diff --git a/src/server/web/app/mobile/views/components/post-preview.vue b/src/client/app/mobile/views/components/post-preview.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/post-preview.vue
rename to src/client/app/mobile/views/components/post-preview.vue
diff --git a/src/server/web/app/mobile/views/components/post.sub.vue b/src/client/app/mobile/views/components/post.sub.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/post.sub.vue
rename to src/client/app/mobile/views/components/post.sub.vue
diff --git a/src/server/web/app/mobile/views/components/post.vue b/src/client/app/mobile/views/components/post.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/post.vue
rename to src/client/app/mobile/views/components/post.vue
diff --git a/src/server/web/app/mobile/views/components/posts.vue b/src/client/app/mobile/views/components/posts.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/posts.vue
rename to src/client/app/mobile/views/components/posts.vue
diff --git a/src/server/web/app/mobile/views/components/sub-post-content.vue b/src/client/app/mobile/views/components/sub-post-content.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/sub-post-content.vue
rename to src/client/app/mobile/views/components/sub-post-content.vue
diff --git a/src/server/web/app/mobile/views/components/timeline.vue b/src/client/app/mobile/views/components/timeline.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/timeline.vue
rename to src/client/app/mobile/views/components/timeline.vue
diff --git a/src/server/web/app/mobile/views/components/ui.header.vue b/src/client/app/mobile/views/components/ui.header.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/ui.header.vue
rename to src/client/app/mobile/views/components/ui.header.vue
diff --git a/src/server/web/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/ui.nav.vue
rename to src/client/app/mobile/views/components/ui.nav.vue
diff --git a/src/server/web/app/mobile/views/components/ui.vue b/src/client/app/mobile/views/components/ui.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/ui.vue
rename to src/client/app/mobile/views/components/ui.vue
diff --git a/src/server/web/app/mobile/views/components/user-card.vue b/src/client/app/mobile/views/components/user-card.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/user-card.vue
rename to src/client/app/mobile/views/components/user-card.vue
diff --git a/src/server/web/app/mobile/views/components/user-preview.vue b/src/client/app/mobile/views/components/user-preview.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/user-preview.vue
rename to src/client/app/mobile/views/components/user-preview.vue
diff --git a/src/server/web/app/mobile/views/components/user-timeline.vue b/src/client/app/mobile/views/components/user-timeline.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/user-timeline.vue
rename to src/client/app/mobile/views/components/user-timeline.vue
diff --git a/src/server/web/app/mobile/views/components/users-list.vue b/src/client/app/mobile/views/components/users-list.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/users-list.vue
rename to src/client/app/mobile/views/components/users-list.vue
diff --git a/src/server/web/app/mobile/views/components/widget-container.vue b/src/client/app/mobile/views/components/widget-container.vue
similarity index 100%
rename from src/server/web/app/mobile/views/components/widget-container.vue
rename to src/client/app/mobile/views/components/widget-container.vue
diff --git a/src/server/web/app/mobile/views/directives/index.ts b/src/client/app/mobile/views/directives/index.ts
similarity index 100%
rename from src/server/web/app/mobile/views/directives/index.ts
rename to src/client/app/mobile/views/directives/index.ts
diff --git a/src/server/web/app/mobile/views/directives/user-preview.ts b/src/client/app/mobile/views/directives/user-preview.ts
similarity index 100%
rename from src/server/web/app/mobile/views/directives/user-preview.ts
rename to src/client/app/mobile/views/directives/user-preview.ts
diff --git a/src/server/web/app/mobile/views/pages/drive.vue b/src/client/app/mobile/views/pages/drive.vue
similarity index 100%
rename from src/server/web/app/mobile/views/pages/drive.vue
rename to src/client/app/mobile/views/pages/drive.vue
diff --git a/src/server/web/app/mobile/views/pages/followers.vue b/src/client/app/mobile/views/pages/followers.vue
similarity index 100%
rename from src/server/web/app/mobile/views/pages/followers.vue
rename to src/client/app/mobile/views/pages/followers.vue
diff --git a/src/server/web/app/mobile/views/pages/following.vue b/src/client/app/mobile/views/pages/following.vue
similarity index 100%
rename from src/server/web/app/mobile/views/pages/following.vue
rename to src/client/app/mobile/views/pages/following.vue
diff --git a/src/server/web/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue
similarity index 100%
rename from src/server/web/app/mobile/views/pages/home.vue
rename to src/client/app/mobile/views/pages/home.vue
diff --git a/src/server/web/app/mobile/views/pages/index.vue b/src/client/app/mobile/views/pages/index.vue
similarity index 100%
rename from src/server/web/app/mobile/views/pages/index.vue
rename to src/client/app/mobile/views/pages/index.vue
diff --git a/src/server/web/app/mobile/views/pages/messaging-room.vue b/src/client/app/mobile/views/pages/messaging-room.vue
similarity index 100%
rename from src/server/web/app/mobile/views/pages/messaging-room.vue
rename to src/client/app/mobile/views/pages/messaging-room.vue
diff --git a/src/server/web/app/mobile/views/pages/messaging.vue b/src/client/app/mobile/views/pages/messaging.vue
similarity index 100%
rename from src/server/web/app/mobile/views/pages/messaging.vue
rename to src/client/app/mobile/views/pages/messaging.vue
diff --git a/src/server/web/app/mobile/views/pages/notifications.vue b/src/client/app/mobile/views/pages/notifications.vue
similarity index 100%
rename from src/server/web/app/mobile/views/pages/notifications.vue
rename to src/client/app/mobile/views/pages/notifications.vue
diff --git a/src/server/web/app/mobile/views/pages/othello.vue b/src/client/app/mobile/views/pages/othello.vue
similarity index 100%
rename from src/server/web/app/mobile/views/pages/othello.vue
rename to src/client/app/mobile/views/pages/othello.vue
diff --git a/src/server/web/app/mobile/views/pages/post.vue b/src/client/app/mobile/views/pages/post.vue
similarity index 100%
rename from src/server/web/app/mobile/views/pages/post.vue
rename to src/client/app/mobile/views/pages/post.vue
diff --git a/src/server/web/app/mobile/views/pages/profile-setting.vue b/src/client/app/mobile/views/pages/profile-setting.vue
similarity index 100%
rename from src/server/web/app/mobile/views/pages/profile-setting.vue
rename to src/client/app/mobile/views/pages/profile-setting.vue
diff --git a/src/server/web/app/mobile/views/pages/search.vue b/src/client/app/mobile/views/pages/search.vue
similarity index 100%
rename from src/server/web/app/mobile/views/pages/search.vue
rename to src/client/app/mobile/views/pages/search.vue
diff --git a/src/server/web/app/mobile/views/pages/selectdrive.vue b/src/client/app/mobile/views/pages/selectdrive.vue
similarity index 100%
rename from src/server/web/app/mobile/views/pages/selectdrive.vue
rename to src/client/app/mobile/views/pages/selectdrive.vue
diff --git a/src/server/web/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue
similarity index 100%
rename from src/server/web/app/mobile/views/pages/settings.vue
rename to src/client/app/mobile/views/pages/settings.vue
diff --git a/src/server/web/app/mobile/views/pages/signup.vue b/src/client/app/mobile/views/pages/signup.vue
similarity index 100%
rename from src/server/web/app/mobile/views/pages/signup.vue
rename to src/client/app/mobile/views/pages/signup.vue
diff --git a/src/server/web/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue
similarity index 100%
rename from src/server/web/app/mobile/views/pages/user.vue
rename to src/client/app/mobile/views/pages/user.vue
diff --git a/src/server/web/app/mobile/views/pages/user/home.followers-you-know.vue b/src/client/app/mobile/views/pages/user/home.followers-you-know.vue
similarity index 100%
rename from src/server/web/app/mobile/views/pages/user/home.followers-you-know.vue
rename to src/client/app/mobile/views/pages/user/home.followers-you-know.vue
diff --git a/src/server/web/app/mobile/views/pages/user/home.friends.vue b/src/client/app/mobile/views/pages/user/home.friends.vue
similarity index 100%
rename from src/server/web/app/mobile/views/pages/user/home.friends.vue
rename to src/client/app/mobile/views/pages/user/home.friends.vue
diff --git a/src/server/web/app/mobile/views/pages/user/home.photos.vue b/src/client/app/mobile/views/pages/user/home.photos.vue
similarity index 100%
rename from src/server/web/app/mobile/views/pages/user/home.photos.vue
rename to src/client/app/mobile/views/pages/user/home.photos.vue
diff --git a/src/server/web/app/mobile/views/pages/user/home.posts.vue b/src/client/app/mobile/views/pages/user/home.posts.vue
similarity index 100%
rename from src/server/web/app/mobile/views/pages/user/home.posts.vue
rename to src/client/app/mobile/views/pages/user/home.posts.vue
diff --git a/src/server/web/app/mobile/views/pages/user/home.vue b/src/client/app/mobile/views/pages/user/home.vue
similarity index 100%
rename from src/server/web/app/mobile/views/pages/user/home.vue
rename to src/client/app/mobile/views/pages/user/home.vue
diff --git a/src/server/web/app/mobile/views/pages/welcome.vue b/src/client/app/mobile/views/pages/welcome.vue
similarity index 100%
rename from src/server/web/app/mobile/views/pages/welcome.vue
rename to src/client/app/mobile/views/pages/welcome.vue
diff --git a/src/server/web/app/mobile/views/widgets/activity.vue b/src/client/app/mobile/views/widgets/activity.vue
similarity index 100%
rename from src/server/web/app/mobile/views/widgets/activity.vue
rename to src/client/app/mobile/views/widgets/activity.vue
diff --git a/src/server/web/app/mobile/views/widgets/index.ts b/src/client/app/mobile/views/widgets/index.ts
similarity index 100%
rename from src/server/web/app/mobile/views/widgets/index.ts
rename to src/client/app/mobile/views/widgets/index.ts
diff --git a/src/server/web/app/mobile/views/widgets/profile.vue b/src/client/app/mobile/views/widgets/profile.vue
similarity index 100%
rename from src/server/web/app/mobile/views/widgets/profile.vue
rename to src/client/app/mobile/views/widgets/profile.vue
diff --git a/src/server/web/app/reset.styl b/src/client/app/reset.styl
similarity index 100%
rename from src/server/web/app/reset.styl
rename to src/client/app/reset.styl
diff --git a/src/server/web/app/safe.js b/src/client/app/safe.js
similarity index 100%
rename from src/server/web/app/safe.js
rename to src/client/app/safe.js
diff --git a/src/server/web/app/stats/style.styl b/src/client/app/stats/style.styl
similarity index 100%
rename from src/server/web/app/stats/style.styl
rename to src/client/app/stats/style.styl
diff --git a/src/server/web/app/stats/tags/index.tag b/src/client/app/stats/tags/index.tag
similarity index 100%
rename from src/server/web/app/stats/tags/index.tag
rename to src/client/app/stats/tags/index.tag
diff --git a/src/server/web/app/stats/tags/index.ts b/src/client/app/stats/tags/index.ts
similarity index 100%
rename from src/server/web/app/stats/tags/index.ts
rename to src/client/app/stats/tags/index.ts
diff --git a/src/server/web/app/status/style.styl b/src/client/app/status/style.styl
similarity index 100%
rename from src/server/web/app/status/style.styl
rename to src/client/app/status/style.styl
diff --git a/src/server/web/app/status/tags/index.tag b/src/client/app/status/tags/index.tag
similarity index 100%
rename from src/server/web/app/status/tags/index.tag
rename to src/client/app/status/tags/index.tag
diff --git a/src/server/web/app/status/tags/index.ts b/src/client/app/status/tags/index.ts
similarity index 100%
rename from src/server/web/app/status/tags/index.ts
rename to src/client/app/status/tags/index.ts
diff --git a/src/server/web/app/sw.js b/src/client/app/sw.js
similarity index 100%
rename from src/server/web/app/sw.js
rename to src/client/app/sw.js
diff --git a/src/server/web/app/tsconfig.json b/src/client/app/tsconfig.json
similarity index 100%
rename from src/server/web/app/tsconfig.json
rename to src/client/app/tsconfig.json
diff --git a/src/server/web/app/v.d.ts b/src/client/app/v.d.ts
similarity index 100%
rename from src/server/web/app/v.d.ts
rename to src/client/app/v.d.ts
diff --git a/src/server/web/assets/404.js b/src/client/assets/404.js
similarity index 100%
rename from src/server/web/assets/404.js
rename to src/client/assets/404.js
diff --git a/src/server/web/assets/code-highlight.css b/src/client/assets/code-highlight.css
similarity index 100%
rename from src/server/web/assets/code-highlight.css
rename to src/client/assets/code-highlight.css
diff --git a/src/server/web/assets/error.jpg b/src/client/assets/error.jpg
similarity index 100%
rename from src/server/web/assets/error.jpg
rename to src/client/assets/error.jpg
diff --git a/src/server/web/assets/favicon.ico b/src/client/assets/favicon.ico
similarity index 100%
rename from src/server/web/assets/favicon.ico
rename to src/client/assets/favicon.ico
diff --git a/src/server/web/assets/label.svg b/src/client/assets/label.svg
similarity index 100%
rename from src/server/web/assets/label.svg
rename to src/client/assets/label.svg
diff --git a/src/server/web/assets/manifest.json b/src/client/assets/manifest.json
similarity index 100%
rename from src/server/web/assets/manifest.json
rename to src/client/assets/manifest.json
diff --git a/src/server/web/assets/message.mp3 b/src/client/assets/message.mp3
similarity index 100%
rename from src/server/web/assets/message.mp3
rename to src/client/assets/message.mp3
diff --git a/src/server/web/assets/othello-put-me.mp3 b/src/client/assets/othello-put-me.mp3
similarity index 100%
rename from src/server/web/assets/othello-put-me.mp3
rename to src/client/assets/othello-put-me.mp3
diff --git a/src/server/web/assets/othello-put-you.mp3 b/src/client/assets/othello-put-you.mp3
similarity index 100%
rename from src/server/web/assets/othello-put-you.mp3
rename to src/client/assets/othello-put-you.mp3
diff --git a/src/server/web/assets/post.mp3 b/src/client/assets/post.mp3
similarity index 100%
rename from src/server/web/assets/post.mp3
rename to src/client/assets/post.mp3
diff --git a/src/server/web/assets/reactions/angry.png b/src/client/assets/reactions/angry.png
similarity index 100%
rename from src/server/web/assets/reactions/angry.png
rename to src/client/assets/reactions/angry.png
diff --git a/src/server/web/assets/reactions/confused.png b/src/client/assets/reactions/confused.png
similarity index 100%
rename from src/server/web/assets/reactions/confused.png
rename to src/client/assets/reactions/confused.png
diff --git a/src/server/web/assets/reactions/congrats.png b/src/client/assets/reactions/congrats.png
similarity index 100%
rename from src/server/web/assets/reactions/congrats.png
rename to src/client/assets/reactions/congrats.png
diff --git a/src/server/web/assets/reactions/hmm.png b/src/client/assets/reactions/hmm.png
similarity index 100%
rename from src/server/web/assets/reactions/hmm.png
rename to src/client/assets/reactions/hmm.png
diff --git a/src/server/web/assets/reactions/laugh.png b/src/client/assets/reactions/laugh.png
similarity index 100%
rename from src/server/web/assets/reactions/laugh.png
rename to src/client/assets/reactions/laugh.png
diff --git a/src/server/web/assets/reactions/like.png b/src/client/assets/reactions/like.png
similarity index 100%
rename from src/server/web/assets/reactions/like.png
rename to src/client/assets/reactions/like.png
diff --git a/src/server/web/assets/reactions/love.png b/src/client/assets/reactions/love.png
similarity index 100%
rename from src/server/web/assets/reactions/love.png
rename to src/client/assets/reactions/love.png
diff --git a/src/server/web/assets/reactions/pudding.png b/src/client/assets/reactions/pudding.png
similarity index 100%
rename from src/server/web/assets/reactions/pudding.png
rename to src/client/assets/reactions/pudding.png
diff --git a/src/server/web/assets/reactions/surprise.png b/src/client/assets/reactions/surprise.png
similarity index 100%
rename from src/server/web/assets/reactions/surprise.png
rename to src/client/assets/reactions/surprise.png
diff --git a/src/server/web/assets/recover.html b/src/client/assets/recover.html
similarity index 100%
rename from src/server/web/assets/recover.html
rename to src/client/assets/recover.html
diff --git a/src/server/web/assets/title.svg b/src/client/assets/title.svg
similarity index 100%
rename from src/server/web/assets/title.svg
rename to src/client/assets/title.svg
diff --git a/src/server/web/assets/unread.svg b/src/client/assets/unread.svg
similarity index 100%
rename from src/server/web/assets/unread.svg
rename to src/client/assets/unread.svg
diff --git a/src/server/web/assets/welcome-bg.svg b/src/client/assets/welcome-bg.svg
similarity index 100%
rename from src/server/web/assets/welcome-bg.svg
rename to src/client/assets/welcome-bg.svg
diff --git a/src/server/web/assets/welcome-fg.svg b/src/client/assets/welcome-fg.svg
similarity index 100%
rename from src/server/web/assets/welcome-fg.svg
rename to src/client/assets/welcome-fg.svg
diff --git a/src/server/web/const.styl b/src/client/const.styl
similarity index 74%
rename from src/server/web/const.styl
rename to src/client/const.styl
index f16e07782..b6560701d 100644
--- a/src/server/web/const.styl
+++ b/src/client/const.styl
@@ -1,4 +1,4 @@
-json('../../const.json')
+json('../const.json')
 
 $theme-color = themeColor
 $theme-color-foreground = themeColorForeground
diff --git a/src/server/web/docs/about.en.pug b/src/client/docs/about.en.pug
similarity index 100%
rename from src/server/web/docs/about.en.pug
rename to src/client/docs/about.en.pug
diff --git a/src/server/web/docs/about.ja.pug b/src/client/docs/about.ja.pug
similarity index 100%
rename from src/server/web/docs/about.ja.pug
rename to src/client/docs/about.ja.pug
diff --git a/src/server/web/docs/api.ja.pug b/src/client/docs/api.ja.pug
similarity index 100%
rename from src/server/web/docs/api.ja.pug
rename to src/client/docs/api.ja.pug
diff --git a/src/server/web/docs/api/endpoints/posts/create.yaml b/src/client/docs/api/endpoints/posts/create.yaml
similarity index 100%
rename from src/server/web/docs/api/endpoints/posts/create.yaml
rename to src/client/docs/api/endpoints/posts/create.yaml
diff --git a/src/server/web/docs/api/endpoints/posts/timeline.yaml b/src/client/docs/api/endpoints/posts/timeline.yaml
similarity index 100%
rename from src/server/web/docs/api/endpoints/posts/timeline.yaml
rename to src/client/docs/api/endpoints/posts/timeline.yaml
diff --git a/src/server/web/docs/api/endpoints/style.styl b/src/client/docs/api/endpoints/style.styl
similarity index 100%
rename from src/server/web/docs/api/endpoints/style.styl
rename to src/client/docs/api/endpoints/style.styl
diff --git a/src/server/web/docs/api/endpoints/view.pug b/src/client/docs/api/endpoints/view.pug
similarity index 100%
rename from src/server/web/docs/api/endpoints/view.pug
rename to src/client/docs/api/endpoints/view.pug
diff --git a/src/server/web/docs/api/entities/drive-file.yaml b/src/client/docs/api/entities/drive-file.yaml
similarity index 100%
rename from src/server/web/docs/api/entities/drive-file.yaml
rename to src/client/docs/api/entities/drive-file.yaml
diff --git a/src/server/web/docs/api/entities/post.yaml b/src/client/docs/api/entities/post.yaml
similarity index 100%
rename from src/server/web/docs/api/entities/post.yaml
rename to src/client/docs/api/entities/post.yaml
diff --git a/src/server/web/docs/api/entities/style.styl b/src/client/docs/api/entities/style.styl
similarity index 100%
rename from src/server/web/docs/api/entities/style.styl
rename to src/client/docs/api/entities/style.styl
diff --git a/src/server/web/docs/api/entities/user.yaml b/src/client/docs/api/entities/user.yaml
similarity index 100%
rename from src/server/web/docs/api/entities/user.yaml
rename to src/client/docs/api/entities/user.yaml
diff --git a/src/server/web/docs/api/entities/view.pug b/src/client/docs/api/entities/view.pug
similarity index 100%
rename from src/server/web/docs/api/entities/view.pug
rename to src/client/docs/api/entities/view.pug
diff --git a/src/server/web/docs/api/gulpfile.ts b/src/client/docs/api/gulpfile.ts
similarity index 80%
rename from src/server/web/docs/api/gulpfile.ts
rename to src/client/docs/api/gulpfile.ts
index 37935413d..16066b0d2 100644
--- a/src/server/web/docs/api/gulpfile.ts
+++ b/src/client/docs/api/gulpfile.ts
@@ -10,10 +10,10 @@ import * as pug from 'pug';
 import * as yaml from 'js-yaml';
 import * as mkdirp from 'mkdirp';
 
-import locales from '../../../../../locales';
-import I18nReplacer from '../../../../build/i18n';
-import fa from '../../../../build/fa';
-import config from './../../../../conf';
+import locales from '../../../../locales';
+import I18nReplacer from '../../../build/i18n';
+import fa from '../../../build/fa';
+import config from './../../../conf';
 
 import generateVars from '../vars';
 
@@ -94,7 +94,7 @@ gulp.task('doc:api', [
 
 gulp.task('doc:api:endpoints', async () => {
 	const commonVars = await generateVars();
-	glob('./src/server/web/docs/api/endpoints/**/*.yaml', (globErr, files) => {
+	glob('./src/client/docs/api/endpoints/**/*.yaml', (globErr, files) => {
 		if (globErr) {
 			console.error(globErr);
 			return;
@@ -115,10 +115,10 @@ gulp.task('doc:api:endpoints', async () => {
 				resDefs: ep.res ? extractDefs(ep.res) : null,
 			};
 			langs.forEach(lang => {
-				pug.renderFile('./src/server/web/docs/api/endpoints/view.pug', Object.assign({}, vars, {
+				pug.renderFile('./src/client/docs/api/endpoints/view.pug', Object.assign({}, vars, {
 					lang,
 					title: ep.endpoint,
-					src: `https://github.com/syuilo/misskey/tree/master/src/server/web/docs/api/endpoints/${ep.endpoint}.yaml`,
+					src: `https://github.com/syuilo/misskey/tree/master/src/client/docs/api/endpoints/${ep.endpoint}.yaml`,
 					kebab,
 					common: commonVars
 				}), (renderErr, html) => {
@@ -129,7 +129,7 @@ gulp.task('doc:api:endpoints', async () => {
 					const i18n = new I18nReplacer(lang);
 					html = html.replace(i18n.pattern, i18n.replacement);
 					html = fa(html);
-					const htmlPath = `./built/server/web/docs/${lang}/api/endpoints/${ep.endpoint}.html`;
+					const htmlPath = `./built/client/docs/${lang}/api/endpoints/${ep.endpoint}.html`;
 					mkdirp(path.dirname(htmlPath), (mkdirErr) => {
 						if (mkdirErr) {
 							console.error(mkdirErr);
@@ -145,7 +145,7 @@ gulp.task('doc:api:endpoints', async () => {
 
 gulp.task('doc:api:entities', async () => {
 	const commonVars = await generateVars();
-	glob('./src/server/web/docs/api/entities/**/*.yaml', (globErr, files) => {
+	glob('./src/client/docs/api/entities/**/*.yaml', (globErr, files) => {
 		if (globErr) {
 			console.error(globErr);
 			return;
@@ -159,10 +159,10 @@ gulp.task('doc:api:entities', async () => {
 				propDefs: extractDefs(entity.props),
 			};
 			langs.forEach(lang => {
-				pug.renderFile('./src/server/web/docs/api/entities/view.pug', Object.assign({}, vars, {
+				pug.renderFile('./src/client/docs/api/entities/view.pug', Object.assign({}, vars, {
 					lang,
 					title: entity.name,
-					src: `https://github.com/syuilo/misskey/tree/master/src/server/web/docs/api/entities/${kebab(entity.name)}.yaml`,
+					src: `https://github.com/syuilo/misskey/tree/master/src/client/docs/api/entities/${kebab(entity.name)}.yaml`,
 					kebab,
 					common: commonVars
 				}), (renderErr, html) => {
@@ -173,7 +173,7 @@ gulp.task('doc:api:entities', async () => {
 					const i18n = new I18nReplacer(lang);
 					html = html.replace(i18n.pattern, i18n.replacement);
 					html = fa(html);
-					const htmlPath = `./built/server/web/docs/${lang}/api/entities/${kebab(entity.name)}.html`;
+					const htmlPath = `./built/client/docs/${lang}/api/entities/${kebab(entity.name)}.html`;
 					mkdirp(path.dirname(htmlPath), (mkdirErr) => {
 						if (mkdirErr) {
 							console.error(mkdirErr);
diff --git a/src/server/web/docs/api/mixins.pug b/src/client/docs/api/mixins.pug
similarity index 100%
rename from src/server/web/docs/api/mixins.pug
rename to src/client/docs/api/mixins.pug
diff --git a/src/server/web/docs/api/style.styl b/src/client/docs/api/style.styl
similarity index 100%
rename from src/server/web/docs/api/style.styl
rename to src/client/docs/api/style.styl
diff --git a/src/server/web/docs/gulpfile.ts b/src/client/docs/gulpfile.ts
similarity index 74%
rename from src/server/web/docs/gulpfile.ts
rename to src/client/docs/gulpfile.ts
index 7b36cf667..56bf6188c 100644
--- a/src/server/web/docs/gulpfile.ts
+++ b/src/client/docs/gulpfile.ts
@@ -11,8 +11,8 @@ import * as mkdirp from 'mkdirp';
 import stylus = require('gulp-stylus');
 import cssnano = require('gulp-cssnano');
 
-import I18nReplacer from '../../../build/i18n';
-import fa from '../../../build/fa';
+import I18nReplacer from '../../build/i18n';
+import fa from '../../build/fa';
 import generateVars from './vars';
 
 require('./api/gulpfile.ts');
@@ -26,7 +26,7 @@ gulp.task('doc', [
 gulp.task('doc:docs', async () => {
 	const commonVars = await generateVars();
 
-	glob('./src/server/web/docs/**/*.*.pug', (globErr, files) => {
+	glob('./src/client/docs/**/*.*.pug', (globErr, files) => {
 		if (globErr) {
 			console.error(globErr);
 			return;
@@ -37,7 +37,7 @@ gulp.task('doc:docs', async () => {
 				common: commonVars,
 				lang: lang,
 				title: fs.readFileSync(file, 'utf-8').match(/^h1 (.+?)\r?\n/)[1],
-				src: `https://github.com/syuilo/misskey/tree/master/src/server/web/docs/${name}.${lang}.pug`,
+				src: `https://github.com/syuilo/misskey/tree/master/src/client/docs/${name}.${lang}.pug`,
 			};
 			pug.renderFile(file, vars, (renderErr, content) => {
 				if (renderErr) {
@@ -45,7 +45,7 @@ gulp.task('doc:docs', async () => {
 					return;
 				}
 
-				pug.renderFile('./src/server/web/docs/layout.pug', Object.assign({}, vars, {
+				pug.renderFile('./src/client/docs/layout.pug', Object.assign({}, vars, {
 					content
 				}), (renderErr2, html) => {
 					if (renderErr2) {
@@ -55,7 +55,7 @@ gulp.task('doc:docs', async () => {
 					const i18n = new I18nReplacer(lang);
 					html = html.replace(i18n.pattern, i18n.replacement);
 					html = fa(html);
-					const htmlPath = `./built/server/web/docs/${lang}/${name}.html`;
+					const htmlPath = `./built/client/docs/${lang}/${name}.html`;
 					mkdirp(path.dirname(htmlPath), (mkdirErr) => {
 						if (mkdirErr) {
 							console.error(mkdirErr);
@@ -70,8 +70,8 @@ gulp.task('doc:docs', async () => {
 });
 
 gulp.task('doc:styles', () =>
-	gulp.src('./src/server/web/docs/**/*.styl')
+	gulp.src('./src/client/docs/**/*.styl')
 		.pipe(stylus())
 		.pipe((cssnano as any)())
-		.pipe(gulp.dest('./built/server/web/docs/assets/'))
+		.pipe(gulp.dest('./built/client/docs/assets/'))
 );
diff --git a/src/server/web/docs/index.en.pug b/src/client/docs/index.en.pug
similarity index 100%
rename from src/server/web/docs/index.en.pug
rename to src/client/docs/index.en.pug
diff --git a/src/server/web/docs/index.ja.pug b/src/client/docs/index.ja.pug
similarity index 100%
rename from src/server/web/docs/index.ja.pug
rename to src/client/docs/index.ja.pug
diff --git a/src/server/web/docs/layout.pug b/src/client/docs/layout.pug
similarity index 100%
rename from src/server/web/docs/layout.pug
rename to src/client/docs/layout.pug
diff --git a/src/server/web/docs/license.en.pug b/src/client/docs/license.en.pug
similarity index 100%
rename from src/server/web/docs/license.en.pug
rename to src/client/docs/license.en.pug
diff --git a/src/server/web/docs/license.ja.pug b/src/client/docs/license.ja.pug
similarity index 100%
rename from src/server/web/docs/license.ja.pug
rename to src/client/docs/license.ja.pug
diff --git a/src/server/web/docs/mute.ja.pug b/src/client/docs/mute.ja.pug
similarity index 100%
rename from src/server/web/docs/mute.ja.pug
rename to src/client/docs/mute.ja.pug
diff --git a/src/server/web/docs/search.ja.pug b/src/client/docs/search.ja.pug
similarity index 100%
rename from src/server/web/docs/search.ja.pug
rename to src/client/docs/search.ja.pug
diff --git a/src/server/web/docs/server.ts b/src/client/docs/server.ts
similarity index 100%
rename from src/server/web/docs/server.ts
rename to src/client/docs/server.ts
diff --git a/src/server/web/docs/style.styl b/src/client/docs/style.styl
similarity index 100%
rename from src/server/web/docs/style.styl
rename to src/client/docs/style.styl
diff --git a/src/server/web/docs/tou.ja.pug b/src/client/docs/tou.ja.pug
similarity index 100%
rename from src/server/web/docs/tou.ja.pug
rename to src/client/docs/tou.ja.pug
diff --git a/src/server/web/docs/ui.styl b/src/client/docs/ui.styl
similarity index 100%
rename from src/server/web/docs/ui.styl
rename to src/client/docs/ui.styl
diff --git a/src/server/web/docs/vars.ts b/src/client/docs/vars.ts
similarity index 76%
rename from src/server/web/docs/vars.ts
rename to src/client/docs/vars.ts
index 5096a39c9..1a3b48bd7 100644
--- a/src/server/web/docs/vars.ts
+++ b/src/client/docs/vars.ts
@@ -5,27 +5,27 @@ import * as yaml from 'js-yaml';
 import * as licenseChecker from 'license-checker';
 import * as tmp from 'tmp';
 
-import { fa } from '../../../build/fa';
-import config from '../../../conf';
-import { licenseHtml } from '../../../build/license';
-const constants = require('../../../const.json');
+import { fa } from '../../build/fa';
+import config from '../../conf';
+import { licenseHtml } from '../../build/license';
+const constants = require('../../const.json');
 
 export default async function(): Promise<{ [key: string]: any }> {
 	const vars = {} as { [key: string]: any };
 
-	const endpoints = glob.sync('./src/server/web/docs/api/endpoints/**/*.yaml');
+	const endpoints = glob.sync('./src/client/docs/api/endpoints/**/*.yaml');
 	vars['endpoints'] = endpoints.map(ep => {
 		const _ep = yaml.safeLoad(fs.readFileSync(ep, 'utf-8'));
 		return _ep.endpoint;
 	});
 
-	const entities = glob.sync('./src/server/web/docs/api/entities/**/*.yaml');
+	const entities = glob.sync('./src/client/docs/api/entities/**/*.yaml');
 	vars['entities'] = entities.map(x => {
 		const _x = yaml.safeLoad(fs.readFileSync(x, 'utf-8'));
 		return _x.name;
 	});
 
-	const docs = glob.sync('./src/server/web/docs/**/*.*.pug');
+	const docs = glob.sync('./src/client/docs/**/*.*.pug');
 	vars['docs'] = {};
 	docs.forEach(x => {
 		const [, name, lang] = x.match(/docs\/(.+?)\.(.+?)\.pug$/);
@@ -53,7 +53,7 @@ export default async function(): Promise<{ [key: string]: any }> {
 		licenseText: ''
 	}), 'utf-8');
 	const dependencies = await util.promisify(licenseChecker.init).bind(licenseChecker)({
-		start: __dirname + '/../../../../',
+		start: __dirname + '/../../../',
 		customPath: tmpObj.name
 	});
 	tmpObj.removeCallback();
diff --git a/src/server/web/element.scss b/src/client/element.scss
similarity index 91%
rename from src/server/web/element.scss
rename to src/client/element.scss
index 7e6d0e709..917198e02 100644
--- a/src/server/web/element.scss
+++ b/src/client/element.scss
@@ -1,7 +1,7 @@
 /* Element variable definitons */
 /* SEE: http://element.eleme.io/#/en-US/component/custom-theme */
 
-@import '../../const.json';
+@import '../const.json';
 
 /* theme color */
 $--color-primary: $themeColor;
diff --git a/src/server/web/style.styl b/src/client/style.styl
similarity index 100%
rename from src/server/web/style.styl
rename to src/client/style.styl
diff --git a/src/server/common/get-notification-summary.ts b/src/common/get-notification-summary.ts
similarity index 100%
rename from src/server/common/get-notification-summary.ts
rename to src/common/get-notification-summary.ts
diff --git a/src/server/common/get-post-summary.ts b/src/common/get-post-summary.ts
similarity index 100%
rename from src/server/common/get-post-summary.ts
rename to src/common/get-post-summary.ts
diff --git a/src/server/common/get-reaction-emoji.ts b/src/common/get-reaction-emoji.ts
similarity index 100%
rename from src/server/common/get-reaction-emoji.ts
rename to src/common/get-reaction-emoji.ts
diff --git a/src/server/common/othello/ai/back.ts b/src/common/othello/ai/back.ts
similarity index 99%
rename from src/server/common/othello/ai/back.ts
rename to src/common/othello/ai/back.ts
index 629e57113..0950adaa9 100644
--- a/src/server/common/othello/ai/back.ts
+++ b/src/common/othello/ai/back.ts
@@ -8,7 +8,7 @@
 
 import * as request from 'request-promise-native';
 import Othello, { Color } from '../core';
-import conf from '../../../../conf';
+import conf from '../../../conf';
 
 let game;
 let form;
diff --git a/src/server/common/othello/ai/front.ts b/src/common/othello/ai/front.ts
similarity index 99%
rename from src/server/common/othello/ai/front.ts
rename to src/common/othello/ai/front.ts
index fb7a9be13..e5496132f 100644
--- a/src/server/common/othello/ai/front.ts
+++ b/src/common/othello/ai/front.ts
@@ -10,7 +10,7 @@ import * as childProcess from 'child_process';
 const WebSocket = require('ws');
 import * as ReconnectingWebSocket from 'reconnecting-websocket';
 import * as request from 'request-promise-native';
-import conf from '../../../../conf';
+import conf from '../../../conf';
 
 // 設定 ////////////////////////////////////////////////////////
 
diff --git a/src/server/common/othello/ai/index.ts b/src/common/othello/ai/index.ts
similarity index 100%
rename from src/server/common/othello/ai/index.ts
rename to src/common/othello/ai/index.ts
diff --git a/src/server/common/othello/core.ts b/src/common/othello/core.ts
similarity index 100%
rename from src/server/common/othello/core.ts
rename to src/common/othello/core.ts
diff --git a/src/server/common/othello/maps.ts b/src/common/othello/maps.ts
similarity index 100%
rename from src/server/common/othello/maps.ts
rename to src/common/othello/maps.ts
diff --git a/src/server/api/common/text/core/syntax-highlighter.ts b/src/common/text/core/syntax-highlighter.ts
similarity index 100%
rename from src/server/api/common/text/core/syntax-highlighter.ts
rename to src/common/text/core/syntax-highlighter.ts
diff --git a/src/server/api/common/text/elements/bold.ts b/src/common/text/elements/bold.ts
similarity index 100%
rename from src/server/api/common/text/elements/bold.ts
rename to src/common/text/elements/bold.ts
diff --git a/src/server/api/common/text/elements/code.ts b/src/common/text/elements/code.ts
similarity index 100%
rename from src/server/api/common/text/elements/code.ts
rename to src/common/text/elements/code.ts
diff --git a/src/server/api/common/text/elements/emoji.ts b/src/common/text/elements/emoji.ts
similarity index 100%
rename from src/server/api/common/text/elements/emoji.ts
rename to src/common/text/elements/emoji.ts
diff --git a/src/server/api/common/text/elements/hashtag.ts b/src/common/text/elements/hashtag.ts
similarity index 100%
rename from src/server/api/common/text/elements/hashtag.ts
rename to src/common/text/elements/hashtag.ts
diff --git a/src/server/api/common/text/elements/inline-code.ts b/src/common/text/elements/inline-code.ts
similarity index 100%
rename from src/server/api/common/text/elements/inline-code.ts
rename to src/common/text/elements/inline-code.ts
diff --git a/src/server/api/common/text/elements/link.ts b/src/common/text/elements/link.ts
similarity index 100%
rename from src/server/api/common/text/elements/link.ts
rename to src/common/text/elements/link.ts
diff --git a/src/server/api/common/text/elements/mention.ts b/src/common/text/elements/mention.ts
similarity index 82%
rename from src/server/api/common/text/elements/mention.ts
rename to src/common/text/elements/mention.ts
index 2025dfdaa..d05a76649 100644
--- a/src/server/api/common/text/elements/mention.ts
+++ b/src/common/text/elements/mention.ts
@@ -1,7 +1,7 @@
 /**
  * Mention
  */
-import parseAcct from '../../../../common/user/parse-acct';
+import parseAcct from '../../../common/user/parse-acct';
 
 module.exports = text => {
 	const match = text.match(/^(?:@[a-zA-Z0-9\-]+){1,2}/);
diff --git a/src/server/api/common/text/elements/quote.ts b/src/common/text/elements/quote.ts
similarity index 100%
rename from src/server/api/common/text/elements/quote.ts
rename to src/common/text/elements/quote.ts
diff --git a/src/server/api/common/text/elements/url.ts b/src/common/text/elements/url.ts
similarity index 100%
rename from src/server/api/common/text/elements/url.ts
rename to src/common/text/elements/url.ts
diff --git a/src/server/api/common/text/index.ts b/src/common/text/index.ts
similarity index 100%
rename from src/server/api/common/text/index.ts
rename to src/common/text/index.ts
diff --git a/src/server/common/user/get-acct.ts b/src/common/user/get-acct.ts
similarity index 100%
rename from src/server/common/user/get-acct.ts
rename to src/common/user/get-acct.ts
diff --git a/src/server/common/user/get-summary.ts b/src/common/user/get-summary.ts
similarity index 90%
rename from src/server/common/user/get-summary.ts
rename to src/common/user/get-summary.ts
index b314a5cef..47592c86b 100644
--- a/src/server/common/user/get-summary.ts
+++ b/src/common/user/get-summary.ts
@@ -1,4 +1,4 @@
-import { ILocalAccount, IUser } from '../../api/models/user';
+import { ILocalAccount, IUser } from '../../models/user';
 import getAcct from './get-acct';
 
 /**
diff --git a/src/server/common/user/parse-acct.ts b/src/common/user/parse-acct.ts
similarity index 100%
rename from src/server/common/user/parse-acct.ts
rename to src/common/user/parse-acct.ts
diff --git a/src/server/api/models/access-token.ts b/src/models/access-token.ts
similarity index 90%
rename from src/server/api/models/access-token.ts
rename to src/models/access-token.ts
index 59bb09426..4451ca140 100644
--- a/src/server/api/models/access-token.ts
+++ b/src/models/access-token.ts
@@ -1,5 +1,5 @@
 import * as mongo from 'mongodb';
-import db from '../../../db/mongodb';
+import db from '../db/mongodb';
 
 const AccessToken = db.get<IAccessTokens>('accessTokens');
 AccessToken.createIndex('token');
diff --git a/src/server/api/models/app.ts b/src/models/app.ts
similarity index 96%
rename from src/server/api/models/app.ts
rename to src/models/app.ts
index 3c17c50fd..3b80a1602 100644
--- a/src/server/api/models/app.ts
+++ b/src/models/app.ts
@@ -1,8 +1,8 @@
 import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
 import AccessToken from './access-token';
-import db from '../../../db/mongodb';
-import config from '../../../conf';
+import db from '../db/mongodb';
+import config from '../conf';
 
 const App = db.get<IApp>('apps');
 App.createIndex('nameId');
diff --git a/src/server/api/models/auth-session.ts b/src/models/auth-session.ts
similarity index 96%
rename from src/server/api/models/auth-session.ts
rename to src/models/auth-session.ts
index 2da40b1ea..6fe3468a7 100644
--- a/src/server/api/models/auth-session.ts
+++ b/src/models/auth-session.ts
@@ -1,6 +1,6 @@
 import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
-import db from '../../../db/mongodb';
+import db from '../db/mongodb';
 import { pack as packApp } from './app';
 
 const AuthSession = db.get<IAuthSession>('authSessions');
diff --git a/src/server/api/models/channel-watching.ts b/src/models/channel-watching.ts
similarity index 88%
rename from src/server/api/models/channel-watching.ts
rename to src/models/channel-watching.ts
index a26b7edb9..44ca06883 100644
--- a/src/server/api/models/channel-watching.ts
+++ b/src/models/channel-watching.ts
@@ -1,5 +1,5 @@
 import * as mongo from 'mongodb';
-import db from '../../../db/mongodb';
+import db from '../db/mongodb';
 
 const ChannelWatching = db.get<IChannelWatching>('channelWatching');
 export default ChannelWatching;
diff --git a/src/server/api/models/channel.ts b/src/models/channel.ts
similarity index 97%
rename from src/server/api/models/channel.ts
rename to src/models/channel.ts
index 9f94c5a8d..67386ac07 100644
--- a/src/server/api/models/channel.ts
+++ b/src/models/channel.ts
@@ -2,7 +2,7 @@ import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
 import { IUser } from './user';
 import Watching from './channel-watching';
-import db from '../../../db/mongodb';
+import db from '../db/mongodb';
 
 const Channel = db.get<IChannel>('channels');
 export default Channel;
diff --git a/src/server/api/models/drive-file.ts b/src/models/drive-file.ts
similarity index 96%
rename from src/server/api/models/drive-file.ts
rename to src/models/drive-file.ts
index 04c9c54bd..9e0df58c4 100644
--- a/src/server/api/models/drive-file.ts
+++ b/src/models/drive-file.ts
@@ -1,8 +1,8 @@
 import * as mongodb from 'mongodb';
 import deepcopy = require('deepcopy');
 import { pack as packFolder } from './drive-folder';
-import config from '../../../conf';
-import monkDb, { nativeDbConn } from '../../../db/mongodb';
+import config from '../conf';
+import monkDb, { nativeDbConn } from '../db/mongodb';
 
 const DriveFile = monkDb.get<IDriveFile>('driveFiles.files');
 
diff --git a/src/server/api/models/drive-folder.ts b/src/models/drive-folder.ts
similarity index 97%
rename from src/server/api/models/drive-folder.ts
rename to src/models/drive-folder.ts
index 4ecafaa15..ad27b151b 100644
--- a/src/server/api/models/drive-folder.ts
+++ b/src/models/drive-folder.ts
@@ -1,6 +1,6 @@
 import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
-import db from '../../../db/mongodb';
+import db from '../db/mongodb';
 import DriveFile from './drive-file';
 
 const DriveFolder = db.get<IDriveFolder>('drive_folders');
diff --git a/src/server/api/models/favorite.ts b/src/models/favorite.ts
similarity index 85%
rename from src/server/api/models/favorite.ts
rename to src/models/favorite.ts
index 5fb4db95a..2fa00e99c 100644
--- a/src/server/api/models/favorite.ts
+++ b/src/models/favorite.ts
@@ -1,5 +1,5 @@
 import * as mongo from 'mongodb';
-import db from '../../../db/mongodb';
+import db from '../db/mongodb';
 
 const Favorites = db.get<IFavorite>('favorites');
 export default Favorites;
diff --git a/src/server/api/models/following.ts b/src/models/following.ts
similarity index 87%
rename from src/server/api/models/following.ts
rename to src/models/following.ts
index 552e94604..3f8a9be50 100644
--- a/src/server/api/models/following.ts
+++ b/src/models/following.ts
@@ -1,5 +1,5 @@
 import * as mongo from 'mongodb';
-import db from '../../../db/mongodb';
+import db from '../db/mongodb';
 
 const Following = db.get<IFollowing>('following');
 export default Following;
diff --git a/src/server/api/models/messaging-history.ts b/src/models/messaging-history.ts
similarity index 88%
rename from src/server/api/models/messaging-history.ts
rename to src/models/messaging-history.ts
index 44a2adc31..6864e22d2 100644
--- a/src/server/api/models/messaging-history.ts
+++ b/src/models/messaging-history.ts
@@ -1,5 +1,5 @@
 import * as mongo from 'mongodb';
-import db from '../../../db/mongodb';
+import db from '../db/mongodb';
 
 const MessagingHistory = db.get<IMessagingHistory>('messagingHistories');
 export default MessagingHistory;
diff --git a/src/server/api/models/messaging-message.ts b/src/models/messaging-message.ts
similarity index 97%
rename from src/server/api/models/messaging-message.ts
rename to src/models/messaging-message.ts
index d3a418c9a..8bee657c3 100644
--- a/src/server/api/models/messaging-message.ts
+++ b/src/models/messaging-message.ts
@@ -2,7 +2,7 @@ import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
 import { pack as packUser } from './user';
 import { pack as packFile } from './drive-file';
-import db from '../../../db/mongodb';
+import db from '../db/mongodb';
 import parse from '../common/text';
 
 const MessagingMessage = db.get<IMessagingMessage>('messagingMessages');
diff --git a/src/server/api/models/meta.ts b/src/models/meta.ts
similarity index 73%
rename from src/server/api/models/meta.ts
rename to src/models/meta.ts
index cad7f5096..710bb2338 100644
--- a/src/server/api/models/meta.ts
+++ b/src/models/meta.ts
@@ -1,4 +1,4 @@
-import db from '../../../db/mongodb';
+import db from '../db/mongodb';
 
 const Meta = db.get<IMeta>('meta');
 export default Meta;
diff --git a/src/server/api/models/mute.ts b/src/models/mute.ts
similarity index 85%
rename from src/server/api/models/mute.ts
rename to src/models/mute.ts
index e5385ade3..879361596 100644
--- a/src/server/api/models/mute.ts
+++ b/src/models/mute.ts
@@ -1,5 +1,5 @@
 import * as mongo from 'mongodb';
-import db from '../../../db/mongodb';
+import db from '../db/mongodb';
 
 const Mute = db.get<IMute>('mute');
 export default Mute;
diff --git a/src/server/api/models/notification.ts b/src/models/notification.ts
similarity index 98%
rename from src/server/api/models/notification.ts
rename to src/models/notification.ts
index 237e2663f..078c8d511 100644
--- a/src/server/api/models/notification.ts
+++ b/src/models/notification.ts
@@ -1,6 +1,6 @@
 import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
-import db from '../../../db/mongodb';
+import db from '../db/mongodb';
 import { IUser, pack as packUser } from './user';
 import { pack as packPost } from './post';
 
diff --git a/src/server/api/models/othello-game.ts b/src/models/othello-game.ts
similarity index 98%
rename from src/server/api/models/othello-game.ts
rename to src/models/othello-game.ts
index ebe738815..297aee302 100644
--- a/src/server/api/models/othello-game.ts
+++ b/src/models/othello-game.ts
@@ -1,6 +1,6 @@
 import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
-import db from '../../../db/mongodb';
+import db from '../db/mongodb';
 import { IUser, pack as packUser } from './user';
 
 const OthelloGame = db.get<IOthelloGame>('othelloGames');
diff --git a/src/server/api/models/othello-matching.ts b/src/models/othello-matching.ts
similarity index 96%
rename from src/server/api/models/othello-matching.ts
rename to src/models/othello-matching.ts
index a294bd1ef..8082c258c 100644
--- a/src/server/api/models/othello-matching.ts
+++ b/src/models/othello-matching.ts
@@ -1,6 +1,6 @@
 import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
-import db from '../../../db/mongodb';
+import db from '../db/mongodb';
 import { IUser, pack as packUser } from './user';
 
 const Matching = db.get<IMatching>('othelloMatchings');
diff --git a/src/server/api/models/poll-vote.ts b/src/models/poll-vote.ts
similarity index 56%
rename from src/server/api/models/poll-vote.ts
rename to src/models/poll-vote.ts
index 1cad95e5d..cd18ffd5f 100644
--- a/src/server/api/models/poll-vote.ts
+++ b/src/models/poll-vote.ts
@@ -1,9 +1,5 @@
-<<<<<<< HEAD:src/server/api/models/poll-vote.ts
-import db from '../../../db/mongodb';
-=======
 import * as mongo from 'mongodb';
-import db from '../../db/mongodb';
->>>>>>> refs/remotes/origin/master:src/api/models/poll-vote.ts
+import db from '../db/mongodb';
 
 const PollVote = db.get<IPollVote>('pollVotes');
 export default PollVote;
diff --git a/src/server/api/models/post-reaction.ts b/src/models/post-reaction.ts
similarity index 96%
rename from src/server/api/models/post-reaction.ts
rename to src/models/post-reaction.ts
index f9a3f91c2..3fc33411f 100644
--- a/src/server/api/models/post-reaction.ts
+++ b/src/models/post-reaction.ts
@@ -1,6 +1,6 @@
 import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
-import db from '../../../db/mongodb';
+import db from '../db/mongodb';
 import Reaction from './post-reaction';
 import { pack as packUser } from './user';
 
diff --git a/src/server/api/models/post-watching.ts b/src/models/post-watching.ts
similarity index 86%
rename from src/server/api/models/post-watching.ts
rename to src/models/post-watching.ts
index abd632249..b4ddcaafa 100644
--- a/src/server/api/models/post-watching.ts
+++ b/src/models/post-watching.ts
@@ -1,5 +1,5 @@
 import * as mongo from 'mongodb';
-import db from '../../../db/mongodb';
+import db from '../db/mongodb';
 
 const PostWatching = db.get<IPostWatching>('postWatching');
 export default PostWatching;
diff --git a/src/server/api/models/post.ts b/src/models/post.ts
similarity index 99%
rename from src/server/api/models/post.ts
rename to src/models/post.ts
index 1bf4e0905..833e59932 100644
--- a/src/server/api/models/post.ts
+++ b/src/models/post.ts
@@ -1,7 +1,7 @@
 import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
 import rap from '@prezzemolo/rap';
-import db from '../../../db/mongodb';
+import db from '../db/mongodb';
 import { IUser, pack as packUser } from './user';
 import { pack as packApp } from './app';
 import { pack as packChannel } from './channel';
diff --git a/src/server/api/models/signin.ts b/src/models/signin.ts
similarity index 94%
rename from src/server/api/models/signin.ts
rename to src/models/signin.ts
index bec635947..7f56e1a28 100644
--- a/src/server/api/models/signin.ts
+++ b/src/models/signin.ts
@@ -1,6 +1,6 @@
 import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
-import db from '../../../db/mongodb';
+import db from '../db/mongodb';
 
 const Signin = db.get<ISignin>('signin');
 export default Signin;
diff --git a/src/server/api/models/sw-subscription.ts b/src/models/sw-subscription.ts
similarity index 87%
rename from src/server/api/models/sw-subscription.ts
rename to src/models/sw-subscription.ts
index d3bbd75a6..743d0d2dd 100644
--- a/src/server/api/models/sw-subscription.ts
+++ b/src/models/sw-subscription.ts
@@ -1,5 +1,5 @@
 import * as mongo from 'mongodb';
-import db from '../../../db/mongodb';
+import db from '../db/mongodb';
 
 const SwSubscription = db.get<ISwSubscription>('swSubscriptions');
 export default SwSubscription;
diff --git a/src/server/api/models/user.ts b/src/models/user.ts
similarity index 98%
rename from src/server/api/models/user.ts
rename to src/models/user.ts
index 419ad5397..4f2872800 100644
--- a/src/server/api/models/user.ts
+++ b/src/models/user.ts
@@ -1,12 +1,12 @@
 import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
 import rap from '@prezzemolo/rap';
-import db from '../../../db/mongodb';
+import db from '../db/mongodb';
 import { IPost, pack as packPost } from './post';
 import Following from './following';
 import Mute from './mute';
-import getFriends from '../common/get-friends';
-import config from '../../../conf';
+import getFriends from '../server/api/common/get-friends';
+import config from '../conf';
 
 const User = db.get<IUser>('users');
 
diff --git a/src/processor/report-github-failure.ts b/src/processor/report-github-failure.ts
index f42071142..610ffe276 100644
--- a/src/processor/report-github-failure.ts
+++ b/src/processor/report-github-failure.ts
@@ -1,5 +1,5 @@
 import * as request from 'request';
-import User from '../server/api/models/user';
+import User from '../models/user';
 const createPost = require('../server/api/endpoints/posts/create');
 
 export default ({ data }, done) => {
diff --git a/src/server/api/authenticate.ts b/src/server/api/authenticate.ts
index 7b3983a83..856674483 100644
--- a/src/server/api/authenticate.ts
+++ b/src/server/api/authenticate.ts
@@ -1,7 +1,7 @@
 import * as express from 'express';
-import App from './models/app';
-import { default as User, IUser } from './models/user';
-import AccessToken from './models/access-token';
+import App from '../../models/app';
+import { default as User, IUser } from '../../models/user';
+import AccessToken from '../../models/access-token';
 import isNativeToken from './common/is-native-token';
 
 export interface IAuthContext {
diff --git a/src/server/api/bot/core.ts b/src/server/api/bot/core.ts
index ec7c935f9..f84f1f5dc 100644
--- a/src/server/api/bot/core.ts
+++ b/src/server/api/bot/core.ts
@@ -1,12 +1,12 @@
 import * as EventEmitter from 'events';
 import * as bcrypt from 'bcryptjs';
 
-import User, { ILocalAccount, IUser, init as initUser } from '../models/user';
+import User, { ILocalAccount, IUser, init as initUser } from '../../../models/user';
 
-import getPostSummary from '../../common/get-post-summary';
-import getUserSummary from '../../common/user/get-summary';
-import parseAcct from '../../common/user/parse-acct';
-import getNotificationSummary from '../../common/get-notification-summary';
+import getPostSummary from '../../../common/get-post-summary';
+import getUserSummary from '../../../common/user/get-summary';
+import parseAcct from '../../../common/user/parse-acct';
+import getNotificationSummary from '../../../common/get-notification-summary';
 
 const hmm = [
 	'?',
diff --git a/src/server/api/bot/interfaces/line.ts b/src/server/api/bot/interfaces/line.ts
index 1340ac992..58fbb217b 100644
--- a/src/server/api/bot/interfaces/line.ts
+++ b/src/server/api/bot/interfaces/line.ts
@@ -2,14 +2,14 @@ import * as EventEmitter from 'events';
 import * as express from 'express';
 import * as request from 'request';
 import * as crypto from 'crypto';
-import User from '../../models/user';
+import User from '../../../../models/user';
 import config from '../../../../conf';
 import BotCore from '../core';
 import _redis from '../../../../db/redis';
 import prominence = require('prominence');
-import getAcct from '../../../common/user/get-acct';
-import parseAcct from '../../../common/user/parse-acct';
-import getPostSummary from '../../../common/get-post-summary';
+import getAcct from '../../../../common/user/get-acct';
+import parseAcct from '../../../../common/user/parse-acct';
+import getPostSummary from '../../../../common/get-post-summary';
 
 const redis = prominence(_redis);
 
diff --git a/src/server/api/common/drive/add-file.ts b/src/server/api/common/drive/add-file.ts
index 21ddd1aae..4551f5574 100644
--- a/src/server/api/common/drive/add-file.ts
+++ b/src/server/api/common/drive/add-file.ts
@@ -10,11 +10,11 @@ import * as debug from 'debug';
 import fileType = require('file-type');
 import prominence = require('prominence');
 
-import DriveFile, { getGridFSBucket } from '../../models/drive-file';
-import DriveFolder from '../../models/drive-folder';
-import { pack } from '../../models/drive-file';
+import DriveFile, { getGridFSBucket } from '../../../../models/drive-file';
+import DriveFolder from '../../../../models/drive-folder';
+import { pack } from '../../../../models/drive-file';
 import event, { publishDriveStream } from '../../event';
-import getAcct from '../../../common/user/get-acct';
+import getAcct from '../../../../common/user/get-acct';
 import config from '../../../../conf';
 
 const gm = _gm.subClass({
diff --git a/src/server/api/common/drive/upload_from_url.ts b/src/server/api/common/drive/upload_from_url.ts
index 5dd969593..b825e4c53 100644
--- a/src/server/api/common/drive/upload_from_url.ts
+++ b/src/server/api/common/drive/upload_from_url.ts
@@ -1,5 +1,5 @@
 import * as URL from 'url';
-import { IDriveFile, validateFileName } from '../../models/drive-file';
+import { IDriveFile, validateFileName } from '../../../../models/drive-file';
 import create from './add-file';
 import * as debug from 'debug';
 import * as tmp from 'tmp';
diff --git a/src/server/api/common/get-friends.ts b/src/server/api/common/get-friends.ts
index 7f548b3bb..e0942e029 100644
--- a/src/server/api/common/get-friends.ts
+++ b/src/server/api/common/get-friends.ts
@@ -1,5 +1,5 @@
 import * as mongodb from 'mongodb';
-import Following from '../models/following';
+import Following from '../../../models/following';
 
 export default async (me: mongodb.ObjectID, includeMe: boolean = true) => {
 	// Fetch relation to other users who the I follows
diff --git a/src/server/api/common/notify.ts b/src/server/api/common/notify.ts
index c4df17f88..f90506cf3 100644
--- a/src/server/api/common/notify.ts
+++ b/src/server/api/common/notify.ts
@@ -1,8 +1,8 @@
 import * as mongo from 'mongodb';
-import Notification from '../models/notification';
-import Mute from '../models/mute';
+import Notification from '../../../models/notification';
+import Mute from '../../../models/mute';
 import event from '../event';
-import { pack } from '../models/notification';
+import { pack } from '../../../models/notification';
 
 export default (
 	notifiee: mongo.ObjectID,
diff --git a/src/server/api/common/push-sw.ts b/src/server/api/common/push-sw.ts
index e5fbec10e..13227af8d 100644
--- a/src/server/api/common/push-sw.ts
+++ b/src/server/api/common/push-sw.ts
@@ -1,6 +1,6 @@
 const push = require('web-push');
 import * as mongo from 'mongodb';
-import Subscription from '../models/sw-subscription';
+import Subscription from '../../../models/sw-subscription';
 import config from '../../../conf';
 
 if (config.sw) {
diff --git a/src/server/api/common/read-messaging-message.ts b/src/server/api/common/read-messaging-message.ts
index 9047edec8..f728130bb 100644
--- a/src/server/api/common/read-messaging-message.ts
+++ b/src/server/api/common/read-messaging-message.ts
@@ -1,6 +1,6 @@
 import * as mongo from 'mongodb';
-import Message from '../models/messaging-message';
-import { IMessagingMessage as IMessage } from '../models/messaging-message';
+import Message from '../../../models/messaging-message';
+import { IMessagingMessage as IMessage } from '../../../models/messaging-message';
 import publishUserStream from '../event';
 import { publishMessagingStream } from '../event';
 import { publishMessagingIndexStream } from '../event';
diff --git a/src/server/api/common/read-notification.ts b/src/server/api/common/read-notification.ts
index 5bbf13632..27632c7ec 100644
--- a/src/server/api/common/read-notification.ts
+++ b/src/server/api/common/read-notification.ts
@@ -1,5 +1,5 @@
 import * as mongo from 'mongodb';
-import { default as Notification, INotification } from '../models/notification';
+import { default as Notification, INotification } from '../../../models/notification';
 import publishUserStream from '../event';
 
 /**
diff --git a/src/server/api/common/watch-post.ts b/src/server/api/common/watch-post.ts
index 61ea44443..83c9b94f3 100644
--- a/src/server/api/common/watch-post.ts
+++ b/src/server/api/common/watch-post.ts
@@ -1,5 +1,5 @@
 import * as mongodb from 'mongodb';
-import Watching from '../models/post-watching';
+import Watching from '../../../models/post-watching';
 
 export default async (me: mongodb.ObjectID, post: object) => {
 	// 自分の投稿はwatchできない
diff --git a/src/server/api/endpoints/aggregation/posts.ts b/src/server/api/endpoints/aggregation/posts.ts
index 67d261964..f4d401eda 100644
--- a/src/server/api/endpoints/aggregation/posts.ts
+++ b/src/server/api/endpoints/aggregation/posts.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post from '../../models/post';
+import Post from '../../../../models/post';
 
 /**
  * Aggregate posts
diff --git a/src/server/api/endpoints/aggregation/posts/reaction.ts b/src/server/api/endpoints/aggregation/posts/reaction.ts
index 9f9a4f37e..e62274533 100644
--- a/src/server/api/endpoints/aggregation/posts/reaction.ts
+++ b/src/server/api/endpoints/aggregation/posts/reaction.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post from '../../../models/post';
-import Reaction from '../../../models/post-reaction';
+import Post from '../../../../../models/post';
+import Reaction from '../../../../../models/post-reaction';
 
 /**
  * Aggregate reaction of a post
diff --git a/src/server/api/endpoints/aggregation/posts/reactions.ts b/src/server/api/endpoints/aggregation/posts/reactions.ts
index 2dc989281..5f23e296f 100644
--- a/src/server/api/endpoints/aggregation/posts/reactions.ts
+++ b/src/server/api/endpoints/aggregation/posts/reactions.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post from '../../../models/post';
-import Reaction from '../../../models/post-reaction';
+import Post from '../../../../../models/post';
+import Reaction from '../../../../../models/post-reaction';
 
 /**
  * Aggregate reactions of a post
diff --git a/src/server/api/endpoints/aggregation/posts/reply.ts b/src/server/api/endpoints/aggregation/posts/reply.ts
index 3b050582a..c76191e86 100644
--- a/src/server/api/endpoints/aggregation/posts/reply.ts
+++ b/src/server/api/endpoints/aggregation/posts/reply.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post from '../../../models/post';
+import Post from '../../../../../models/post';
 
 /**
  * Aggregate reply of a post
diff --git a/src/server/api/endpoints/aggregation/posts/repost.ts b/src/server/api/endpoints/aggregation/posts/repost.ts
index d9f3e36a0..a203605eb 100644
--- a/src/server/api/endpoints/aggregation/posts/repost.ts
+++ b/src/server/api/endpoints/aggregation/posts/repost.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post from '../../../models/post';
+import Post from '../../../../../models/post';
 
 /**
  * Aggregate repost of a post
diff --git a/src/server/api/endpoints/aggregation/users.ts b/src/server/api/endpoints/aggregation/users.ts
index a4e91a228..19776ed29 100644
--- a/src/server/api/endpoints/aggregation/users.ts
+++ b/src/server/api/endpoints/aggregation/users.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User from '../../models/user';
+import User from '../../../../models/user';
 
 /**
  * Aggregate users
diff --git a/src/server/api/endpoints/aggregation/users/activity.ts b/src/server/api/endpoints/aggregation/users/activity.ts
index d47761657..cef007229 100644
--- a/src/server/api/endpoints/aggregation/users/activity.ts
+++ b/src/server/api/endpoints/aggregation/users/activity.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User from '../../../models/user';
-import Post from '../../../models/post';
+import User from '../../../../../models/user';
+import Post from '../../../../../models/post';
 
 // TODO: likeやfollowも集計
 
diff --git a/src/server/api/endpoints/aggregation/users/followers.ts b/src/server/api/endpoints/aggregation/users/followers.ts
index 73a30281b..dda34ed7b 100644
--- a/src/server/api/endpoints/aggregation/users/followers.ts
+++ b/src/server/api/endpoints/aggregation/users/followers.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User from '../../../models/user';
-import Following from '../../../models/following';
+import User from '../../../../../models/user';
+import Following from '../../../../../models/following';
 
 /**
  * Aggregate followers of a user
@@ -39,11 +39,12 @@ module.exports = (params) => new Promise(async (res, rej) => {
 				{ deletedAt: { $gt: startTime } }
 			]
 		}, {
-			_id: false,
-			followerId: false,
-			followeeId: false
-		}, {
-			sort: { createdAt: -1 }
+			sort: { createdAt: -1 },
+			fields: {
+				_id: false,
+				followerId: false,
+				followeeId: false
+			}
 		});
 
 	const graph = [];
diff --git a/src/server/api/endpoints/aggregation/users/following.ts b/src/server/api/endpoints/aggregation/users/following.ts
index 16fc568d5..cd08d89e4 100644
--- a/src/server/api/endpoints/aggregation/users/following.ts
+++ b/src/server/api/endpoints/aggregation/users/following.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User from '../../../models/user';
-import Following from '../../../models/following';
+import User from '../../../../../models/user';
+import Following from '../../../../../models/following';
 
 /**
  * Aggregate following of a user
@@ -39,11 +39,12 @@ module.exports = (params) => new Promise(async (res, rej) => {
 				{ deletedAt: { $gt: startTime } }
 			]
 		}, {
-			_id: false,
-			followerId: false,
-			followeeId: false
-		}, {
-			sort: { createdAt: -1 }
+			sort: { createdAt: -1 },
+			fields: {
+				_id: false,
+				followerId: false,
+				followeeId: false
+			}
 		});
 
 	const graph = [];
diff --git a/src/server/api/endpoints/aggregation/users/post.ts b/src/server/api/endpoints/aggregation/users/post.ts
index c98874859..13617cf63 100644
--- a/src/server/api/endpoints/aggregation/users/post.ts
+++ b/src/server/api/endpoints/aggregation/users/post.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User from '../../../models/user';
-import Post from '../../../models/post';
+import User from '../../../../../models/user';
+import Post from '../../../../../models/post';
 
 /**
  * Aggregate post of a user
diff --git a/src/server/api/endpoints/aggregation/users/reaction.ts b/src/server/api/endpoints/aggregation/users/reaction.ts
index 60b33e9d1..0c42ba336 100644
--- a/src/server/api/endpoints/aggregation/users/reaction.ts
+++ b/src/server/api/endpoints/aggregation/users/reaction.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User from '../../../models/user';
-import Reaction from '../../../models/post-reaction';
+import User from '../../../../../models/user';
+import Reaction from '../../../../../models/post-reaction';
 
 /**
  * Aggregate reaction of a user
diff --git a/src/server/api/endpoints/app/create.ts b/src/server/api/endpoints/app/create.ts
index 713078463..f2033d33f 100644
--- a/src/server/api/endpoints/app/create.ts
+++ b/src/server/api/endpoints/app/create.ts
@@ -3,7 +3,7 @@
  */
 import rndstr from 'rndstr';
 import $ from 'cafy';
-import App, { isValidNameId, pack } from '../../models/app';
+import App, { isValidNameId, pack } from '../../../../models/app';
 
 /**
  * @swagger
diff --git a/src/server/api/endpoints/app/name_id/available.ts b/src/server/api/endpoints/app/name_id/available.ts
index 6d02b26d2..93b33cfa2 100644
--- a/src/server/api/endpoints/app/name_id/available.ts
+++ b/src/server/api/endpoints/app/name_id/available.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import App from '../../../models/app';
-import { isValidNameId } from '../../../models/app';
+import App from '../../../../../models/app';
+import { isValidNameId } from '../../../../../models/app';
 
 /**
  * @swagger
diff --git a/src/server/api/endpoints/app/show.ts b/src/server/api/endpoints/app/show.ts
index 34bb958ee..7c8d2881d 100644
--- a/src/server/api/endpoints/app/show.ts
+++ b/src/server/api/endpoints/app/show.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import App, { pack } from '../../models/app';
+import App, { pack } from '../../../../models/app';
 
 /**
  * @swagger
diff --git a/src/server/api/endpoints/auth/accept.ts b/src/server/api/endpoints/auth/accept.ts
index 5a1925144..aeabac2db 100644
--- a/src/server/api/endpoints/auth/accept.ts
+++ b/src/server/api/endpoints/auth/accept.ts
@@ -4,9 +4,9 @@
 import rndstr from 'rndstr';
 const crypto = require('crypto');
 import $ from 'cafy';
-import App from '../../models/app';
-import AuthSess from '../../models/auth-session';
-import AccessToken from '../../models/access-token';
+import App from '../../../../models/app';
+import AuthSess from '../../../../models/auth-session';
+import AccessToken from '../../../../models/access-token';
 
 /**
  * @swagger
diff --git a/src/server/api/endpoints/auth/session/generate.ts b/src/server/api/endpoints/auth/session/generate.ts
index 180ad83cc..ad03e538c 100644
--- a/src/server/api/endpoints/auth/session/generate.ts
+++ b/src/server/api/endpoints/auth/session/generate.ts
@@ -3,8 +3,8 @@
  */
 import * as uuid from 'uuid';
 import $ from 'cafy';
-import App from '../../../models/app';
-import AuthSess from '../../../models/auth-session';
+import App from '../../../../../models/app';
+import AuthSess from '../../../../../models/auth-session';
 import config from '../../../../../conf';
 
 /**
diff --git a/src/server/api/endpoints/auth/session/show.ts b/src/server/api/endpoints/auth/session/show.ts
index 869b714e7..f473d7397 100644
--- a/src/server/api/endpoints/auth/session/show.ts
+++ b/src/server/api/endpoints/auth/session/show.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import AuthSess, { pack } from '../../../models/auth-session';
+import AuthSess, { pack } from '../../../../../models/auth-session';
 
 /**
  * @swagger
diff --git a/src/server/api/endpoints/auth/session/userkey.ts b/src/server/api/endpoints/auth/session/userkey.ts
index 5d9983af6..7dbb5ea6e 100644
--- a/src/server/api/endpoints/auth/session/userkey.ts
+++ b/src/server/api/endpoints/auth/session/userkey.ts
@@ -2,10 +2,10 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import App from '../../../models/app';
-import AuthSess from '../../../models/auth-session';
-import AccessToken from '../../../models/access-token';
-import { pack } from '../../../models/user';
+import App from '../../../../../models/app';
+import AuthSess from '../../../../../models/auth-session';
+import AccessToken from '../../../../../models/access-token';
+import { pack } from '../../../../../models/user';
 
 /**
  * @swagger
diff --git a/src/server/api/endpoints/channels.ts b/src/server/api/endpoints/channels.ts
index a4acc0660..582e6ba43 100644
--- a/src/server/api/endpoints/channels.ts
+++ b/src/server/api/endpoints/channels.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Channel, { pack } from '../models/channel';
+import Channel, { pack } from '../../../models/channel';
 
 /**
  * Get all channels
diff --git a/src/server/api/endpoints/channels/create.ts b/src/server/api/endpoints/channels/create.ts
index 1dc453c4a..0f0f558c8 100644
--- a/src/server/api/endpoints/channels/create.ts
+++ b/src/server/api/endpoints/channels/create.ts
@@ -2,9 +2,9 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Channel from '../../models/channel';
-import Watching from '../../models/channel-watching';
-import { pack } from '../../models/channel';
+import Channel from '../../../../models/channel';
+import Watching from '../../../../models/channel-watching';
+import { pack } from '../../../../models/channel';
 
 /**
  * Create a channel
diff --git a/src/server/api/endpoints/channels/posts.ts b/src/server/api/endpoints/channels/posts.ts
index 348dbb108..e48f96da7 100644
--- a/src/server/api/endpoints/channels/posts.ts
+++ b/src/server/api/endpoints/channels/posts.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import { default as Channel, IChannel } from '../../models/channel';
-import Post, { pack } from '../../models/post';
+import { default as Channel, IChannel } from '../../../../models/channel';
+import Post, { pack } from '../../../../models/post';
 
 /**
  * Show a posts of a channel
diff --git a/src/server/api/endpoints/channels/show.ts b/src/server/api/endpoints/channels/show.ts
index 5874ed18a..3ce9ce474 100644
--- a/src/server/api/endpoints/channels/show.ts
+++ b/src/server/api/endpoints/channels/show.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Channel, { IChannel, pack } from '../../models/channel';
+import Channel, { IChannel, pack } from '../../../../models/channel';
 
 /**
  * Show a channel
diff --git a/src/server/api/endpoints/channels/unwatch.ts b/src/server/api/endpoints/channels/unwatch.ts
index 709313bc6..8220b90b6 100644
--- a/src/server/api/endpoints/channels/unwatch.ts
+++ b/src/server/api/endpoints/channels/unwatch.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Channel from '../../models/channel';
-import Watching from '../../models/channel-watching';
+import Channel from '../../../../models/channel';
+import Watching from '../../../../models/channel-watching';
 
 /**
  * Unwatch a channel
diff --git a/src/server/api/endpoints/channels/watch.ts b/src/server/api/endpoints/channels/watch.ts
index df9e70d5c..6906282a5 100644
--- a/src/server/api/endpoints/channels/watch.ts
+++ b/src/server/api/endpoints/channels/watch.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Channel from '../../models/channel';
-import Watching from '../../models/channel-watching';
+import Channel from '../../../../models/channel';
+import Watching from '../../../../models/channel-watching';
 
 /**
  * Watch a channel
diff --git a/src/server/api/endpoints/drive.ts b/src/server/api/endpoints/drive.ts
index eb2185391..d77ab2bbb 100644
--- a/src/server/api/endpoints/drive.ts
+++ b/src/server/api/endpoints/drive.ts
@@ -1,7 +1,7 @@
 /**
  * Module dependencies
  */
-import DriveFile from '../models/drive-file';
+import DriveFile from '../../../models/drive-file';
 
 /**
  * Get drive information
diff --git a/src/server/api/endpoints/drive/files.ts b/src/server/api/endpoints/drive/files.ts
index f982ef62e..63d69d145 100644
--- a/src/server/api/endpoints/drive/files.ts
+++ b/src/server/api/endpoints/drive/files.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import DriveFile, { pack } from '../../models/drive-file';
+import DriveFile, { pack } from '../../../../models/drive-file';
 
 /**
  * Get drive files
diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts
index 2cd89a8fa..53c8c7067 100644
--- a/src/server/api/endpoints/drive/files/create.ts
+++ b/src/server/api/endpoints/drive/files/create.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import { validateFileName, pack } from '../../../models/drive-file';
+import { validateFileName, pack } from '../../../../../models/drive-file';
 import create from '../../../common/drive/add-file';
 
 /**
diff --git a/src/server/api/endpoints/drive/files/find.ts b/src/server/api/endpoints/drive/files/find.ts
index 47ce89130..0ab6e5d3e 100644
--- a/src/server/api/endpoints/drive/files/find.ts
+++ b/src/server/api/endpoints/drive/files/find.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import DriveFile, { pack } from '../../../models/drive-file';
+import DriveFile, { pack } from '../../../../../models/drive-file';
 
 /**
  * Find a file(s)
diff --git a/src/server/api/endpoints/drive/files/show.ts b/src/server/api/endpoints/drive/files/show.ts
index 63920db7f..3398f2454 100644
--- a/src/server/api/endpoints/drive/files/show.ts
+++ b/src/server/api/endpoints/drive/files/show.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import DriveFile, { pack } from '../../../models/drive-file';
+import DriveFile, { pack } from '../../../../../models/drive-file';
 
 /**
  * Show a file
diff --git a/src/server/api/endpoints/drive/files/update.ts b/src/server/api/endpoints/drive/files/update.ts
index bfad45b0a..836b4cfcd 100644
--- a/src/server/api/endpoints/drive/files/update.ts
+++ b/src/server/api/endpoints/drive/files/update.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import DriveFolder from '../../../models/drive-folder';
-import DriveFile, { validateFileName, pack } from '../../../models/drive-file';
+import DriveFolder from '../../../../../models/drive-folder';
+import DriveFile, { validateFileName, pack } from '../../../../../models/drive-file';
 import { publishDriveStream } from '../../../event';
 
 /**
diff --git a/src/server/api/endpoints/drive/files/upload_from_url.ts b/src/server/api/endpoints/drive/files/upload_from_url.ts
index 1a4ce0bf0..7262f09bb 100644
--- a/src/server/api/endpoints/drive/files/upload_from_url.ts
+++ b/src/server/api/endpoints/drive/files/upload_from_url.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import { pack } from '../../../models/drive-file';
+import { pack } from '../../../../../models/drive-file';
 import uploadFromUrl from '../../../common/drive/upload_from_url';
 
 /**
diff --git a/src/server/api/endpoints/drive/folders.ts b/src/server/api/endpoints/drive/folders.ts
index c314837f7..489e47912 100644
--- a/src/server/api/endpoints/drive/folders.ts
+++ b/src/server/api/endpoints/drive/folders.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import DriveFolder, { pack } from '../../models/drive-folder';
+import DriveFolder, { pack } from '../../../../models/drive-folder';
 
 /**
  * Get drive folders
diff --git a/src/server/api/endpoints/drive/folders/create.ts b/src/server/api/endpoints/drive/folders/create.ts
index 564558606..24e035930 100644
--- a/src/server/api/endpoints/drive/folders/create.ts
+++ b/src/server/api/endpoints/drive/folders/create.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import DriveFolder, { isValidFolderName, pack } from '../../../models/drive-folder';
+import DriveFolder, { isValidFolderName, pack } from '../../../../../models/drive-folder';
 import { publishDriveStream } from '../../../event';
 
 /**
diff --git a/src/server/api/endpoints/drive/folders/find.ts b/src/server/api/endpoints/drive/folders/find.ts
index f46aaedd3..04dc38f87 100644
--- a/src/server/api/endpoints/drive/folders/find.ts
+++ b/src/server/api/endpoints/drive/folders/find.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import DriveFolder, { pack } from '../../../models/drive-folder';
+import DriveFolder, { pack } from '../../../../../models/drive-folder';
 
 /**
  * Find a folder(s)
diff --git a/src/server/api/endpoints/drive/folders/show.ts b/src/server/api/endpoints/drive/folders/show.ts
index a6d7e86df..b432f5a50 100644
--- a/src/server/api/endpoints/drive/folders/show.ts
+++ b/src/server/api/endpoints/drive/folders/show.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import DriveFolder, { pack } from '../../../models/drive-folder';
+import DriveFolder, { pack } from '../../../../../models/drive-folder';
 
 /**
  * Show a folder
diff --git a/src/server/api/endpoints/drive/folders/update.ts b/src/server/api/endpoints/drive/folders/update.ts
index fcfd24124..6c5a5c376 100644
--- a/src/server/api/endpoints/drive/folders/update.ts
+++ b/src/server/api/endpoints/drive/folders/update.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import DriveFolder, { isValidFolderName, pack } from '../../../models/drive-folder';
+import DriveFolder, { isValidFolderName, pack } from '../../../../../models/drive-folder';
 import { publishDriveStream } from '../../../event';
 
 /**
diff --git a/src/server/api/endpoints/drive/stream.ts b/src/server/api/endpoints/drive/stream.ts
index 71db38f3b..02313aa37 100644
--- a/src/server/api/endpoints/drive/stream.ts
+++ b/src/server/api/endpoints/drive/stream.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import DriveFile, { pack } from '../../models/drive-file';
+import DriveFile, { pack } from '../../../../models/drive-file';
 
 /**
  * Get drive stream
diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts
index 983d8040f..1e24388a7 100644
--- a/src/server/api/endpoints/following/create.ts
+++ b/src/server/api/endpoints/following/create.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User, { pack as packUser } from '../../models/user';
-import Following from '../../models/following';
+import User, { pack as packUser } from '../../../../models/user';
+import Following from '../../../../models/following';
 import notify from '../../common/notify';
 import event from '../../event';
 
diff --git a/src/server/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts
index 25eba8b26..7fc5f477f 100644
--- a/src/server/api/endpoints/following/delete.ts
+++ b/src/server/api/endpoints/following/delete.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User, { pack as packUser } from '../../models/user';
-import Following from '../../models/following';
+import User, { pack as packUser } from '../../../../models/user';
+import Following from '../../../../models/following';
 import event from '../../event';
 
 /**
diff --git a/src/server/api/endpoints/i.ts b/src/server/api/endpoints/i.ts
index f5e92b4de..44de71d16 100644
--- a/src/server/api/endpoints/i.ts
+++ b/src/server/api/endpoints/i.ts
@@ -1,7 +1,7 @@
 /**
  * Module dependencies
  */
-import User, { pack } from '../models/user';
+import User, { pack } from '../../../models/user';
 
 /**
  * Show myself
diff --git a/src/server/api/endpoints/i/2fa/done.ts b/src/server/api/endpoints/i/2fa/done.ts
index d61ebbe6f..0b2e32c13 100644
--- a/src/server/api/endpoints/i/2fa/done.ts
+++ b/src/server/api/endpoints/i/2fa/done.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import * as speakeasy from 'speakeasy';
-import User from '../../../models/user';
+import User from '../../../../../models/user';
 
 module.exports = async (params, user) => new Promise(async (res, rej) => {
 	// Get 'token' parameter
diff --git a/src/server/api/endpoints/i/2fa/register.ts b/src/server/api/endpoints/i/2fa/register.ts
index 0b49ad882..d2683fb61 100644
--- a/src/server/api/endpoints/i/2fa/register.ts
+++ b/src/server/api/endpoints/i/2fa/register.ts
@@ -5,7 +5,7 @@ import $ from 'cafy';
 import * as bcrypt from 'bcryptjs';
 import * as speakeasy from 'speakeasy';
 import * as QRCode from 'qrcode';
-import User from '../../../models/user';
+import User from '../../../../../models/user';
 import config from '../../../../../conf';
 
 module.exports = async (params, user) => new Promise(async (res, rej) => {
diff --git a/src/server/api/endpoints/i/2fa/unregister.ts b/src/server/api/endpoints/i/2fa/unregister.ts
index 0221ecb96..ff2a435fe 100644
--- a/src/server/api/endpoints/i/2fa/unregister.ts
+++ b/src/server/api/endpoints/i/2fa/unregister.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import * as bcrypt from 'bcryptjs';
-import User from '../../../models/user';
+import User from '../../../../../models/user';
 
 module.exports = async (params, user) => new Promise(async (res, rej) => {
 	// Get 'password' parameter
diff --git a/src/server/api/endpoints/i/appdata/get.ts b/src/server/api/endpoints/i/appdata/get.ts
deleted file mode 100644
index 0b34643f7..000000000
--- a/src/server/api/endpoints/i/appdata/get.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * Module dependencies
- */
-import $ from 'cafy';
-import Appdata from '../../../models/appdata';
-
-/**
- * Get app data
- *
- * @param {any} params
- * @param {any} user
- * @param {any} app
- * @param {Boolean} isSecure
- * @return {Promise<any>}
- */
-module.exports = (params, user, app) => new Promise(async (res, rej) => {
-	if (app == null) return rej('このAPIはサードパーティAppからのみ利用できます');
-
-	// Get 'key' parameter
-	const [key = null, keyError] = $(params.key).optional.nullable.string().match(/[a-z_]+/).$;
-	if (keyError) return rej('invalid key param');
-
-	const select = {};
-	if (key !== null) {
-		select[`data.${key}`] = true;
-	}
-	const appdata = await Appdata.findOne({
-		appId: app._id,
-		userId: user._id
-	}, {
-		fields: select
-	});
-
-	if (appdata) {
-		res(appdata.data);
-	} else {
-		res();
-	}
-});
diff --git a/src/server/api/endpoints/i/appdata/set.ts b/src/server/api/endpoints/i/appdata/set.ts
deleted file mode 100644
index 1e3232ce3..000000000
--- a/src/server/api/endpoints/i/appdata/set.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-/**
- * Module dependencies
- */
-import $ from 'cafy';
-import Appdata from '../../../models/appdata';
-
-/**
- * Set app data
- *
- * @param {any} params
- * @param {any} user
- * @param {any} app
- * @param {Boolean} isSecure
- * @return {Promise<any>}
- */
-module.exports = (params, user, app) => new Promise(async (res, rej) => {
-	if (app == null) return rej('このAPIはサードパーティAppからのみ利用できます');
-
-	// Get 'data' parameter
-	const [data, dataError] = $(params.data).optional.object()
-		.pipe(obj => {
-			const hasInvalidData = Object.entries(obj).some(([k, v]) =>
-				$(k).string().match(/^[a-z_]+$/).nok() && $(v).string().nok());
-			return !hasInvalidData;
-		}).$;
-	if (dataError) return rej('invalid data param');
-
-	// Get 'key' parameter
-	const [key, keyError] = $(params.key).optional.string().match(/[a-z_]+/).$;
-	if (keyError) return rej('invalid key param');
-
-	// Get 'value' parameter
-	const [value, valueError] = $(params.value).optional.string().$;
-	if (valueError) return rej('invalid value param');
-
-	const set = {};
-	if (data) {
-		Object.entries(data).forEach(([k, v]) => {
-			set[`data.${k}`] = v;
-		});
-	} else {
-		set[`data.${key}`] = value;
-	}
-
-	await Appdata.update({
-		appId: app._id,
-		userId: user._id
-	}, Object.assign({
-		appId: app._id,
-		userId: user._id
-	}, {
-			$set: set
-		}), {
-			upsert: true
-		});
-
-	res(204);
-});
diff --git a/src/server/api/endpoints/i/authorized_apps.ts b/src/server/api/endpoints/i/authorized_apps.ts
index 5a38d7c18..82fd2d251 100644
--- a/src/server/api/endpoints/i/authorized_apps.ts
+++ b/src/server/api/endpoints/i/authorized_apps.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import AccessToken from '../../models/access-token';
-import { pack } from '../../models/app';
+import AccessToken from '../../../../models/access-token';
+import { pack } from '../../../../models/app';
 
 /**
  * Get authorized apps of my account
diff --git a/src/server/api/endpoints/i/change_password.ts b/src/server/api/endpoints/i/change_password.ts
index e3b0127e7..a38b56a21 100644
--- a/src/server/api/endpoints/i/change_password.ts
+++ b/src/server/api/endpoints/i/change_password.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import * as bcrypt from 'bcryptjs';
-import User from '../../models/user';
+import User from '../../../../models/user';
 
 /**
  * Change password
diff --git a/src/server/api/endpoints/i/favorites.ts b/src/server/api/endpoints/i/favorites.ts
index 9f8becf21..0b594e318 100644
--- a/src/server/api/endpoints/i/favorites.ts
+++ b/src/server/api/endpoints/i/favorites.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Favorite from '../../models/favorite';
-import { pack } from '../../models/post';
+import Favorite from '../../../../models/favorite';
+import { pack } from '../../../../models/post';
 
 /**
  * Get followers of a user
diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts
index 7119bf6ea..5de087a9b 100644
--- a/src/server/api/endpoints/i/notifications.ts
+++ b/src/server/api/endpoints/i/notifications.ts
@@ -2,9 +2,9 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Notification from '../../models/notification';
-import Mute from '../../models/mute';
-import { pack } from '../../models/notification';
+import Notification from '../../../../models/notification';
+import Mute from '../../../../models/mute';
+import { pack } from '../../../../models/notification';
 import getFriends from '../../common/get-friends';
 import read from '../../common/read-notification';
 
diff --git a/src/server/api/endpoints/i/pin.ts b/src/server/api/endpoints/i/pin.ts
index 886a3edeb..2a5757977 100644
--- a/src/server/api/endpoints/i/pin.ts
+++ b/src/server/api/endpoints/i/pin.ts
@@ -2,9 +2,9 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User from '../../models/user';
-import Post from '../../models/post';
-import { pack } from '../../models/user';
+import User from '../../../../models/user';
+import Post from '../../../../models/post';
+import { pack } from '../../../../models/user';
 
 /**
  * Pin post
diff --git a/src/server/api/endpoints/i/regenerate_token.ts b/src/server/api/endpoints/i/regenerate_token.ts
index 9ac7b5507..c35778ac0 100644
--- a/src/server/api/endpoints/i/regenerate_token.ts
+++ b/src/server/api/endpoints/i/regenerate_token.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import * as bcrypt from 'bcryptjs';
-import User from '../../models/user';
+import User from '../../../../models/user';
 import event from '../../event';
 import generateUserToken from '../../common/generate-native-user-token';
 
diff --git a/src/server/api/endpoints/i/signin_history.ts b/src/server/api/endpoints/i/signin_history.ts
index a4ba22790..931b9e225 100644
--- a/src/server/api/endpoints/i/signin_history.ts
+++ b/src/server/api/endpoints/i/signin_history.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Signin, { pack } from '../../models/signin';
+import Signin, { pack } from '../../../../models/signin';
 
 /**
  * Get signin history of my account
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index 8147b1bba..8e198f3ad 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../models/user';
+import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../../../models/user';
 import event from '../../event';
 import config from '../../../../conf';
 
diff --git a/src/server/api/endpoints/i/update_client_setting.ts b/src/server/api/endpoints/i/update_client_setting.ts
index a0bef5e59..03867b401 100644
--- a/src/server/api/endpoints/i/update_client_setting.ts
+++ b/src/server/api/endpoints/i/update_client_setting.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User, { pack } from '../../models/user';
+import User, { pack } from '../../../../models/user';
 import event from '../../event';
 
 /**
diff --git a/src/server/api/endpoints/i/update_home.ts b/src/server/api/endpoints/i/update_home.ts
index 151c3e205..713cf9fcc 100644
--- a/src/server/api/endpoints/i/update_home.ts
+++ b/src/server/api/endpoints/i/update_home.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User from '../../models/user';
+import User from '../../../../models/user';
 import event from '../../event';
 
 module.exports = async (params, user) => new Promise(async (res, rej) => {
diff --git a/src/server/api/endpoints/i/update_mobile_home.ts b/src/server/api/endpoints/i/update_mobile_home.ts
index a8436b940..b06ca108a 100644
--- a/src/server/api/endpoints/i/update_mobile_home.ts
+++ b/src/server/api/endpoints/i/update_mobile_home.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User from '../../models/user';
+import User from '../../../../models/user';
 import event from '../../event';
 
 module.exports = async (params, user) => new Promise(async (res, rej) => {
diff --git a/src/server/api/endpoints/messaging/history.ts b/src/server/api/endpoints/messaging/history.ts
index 2bf3ed996..e42d34f21 100644
--- a/src/server/api/endpoints/messaging/history.ts
+++ b/src/server/api/endpoints/messaging/history.ts
@@ -2,9 +2,9 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import History from '../../models/messaging-history';
-import Mute from '../../models/mute';
-import { pack } from '../../models/messaging-message';
+import History from '../../../../models/messaging-history';
+import Mute from '../../../../models/mute';
+import { pack } from '../../../../models/messaging-message';
 
 /**
  * Show messaging history
diff --git a/src/server/api/endpoints/messaging/messages.ts b/src/server/api/endpoints/messaging/messages.ts
index dd80e41d0..092eab056 100644
--- a/src/server/api/endpoints/messaging/messages.ts
+++ b/src/server/api/endpoints/messaging/messages.ts
@@ -2,9 +2,9 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Message from '../../models/messaging-message';
-import User from '../../models/user';
-import { pack } from '../../models/messaging-message';
+import Message from '../../../../models/messaging-message';
+import User from '../../../../models/user';
+import { pack } from '../../../../models/messaging-message';
 import read from '../../common/read-messaging-message';
 
 /**
diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts
index 4edd72655..d8ffa9fde 100644
--- a/src/server/api/endpoints/messaging/messages/create.ts
+++ b/src/server/api/endpoints/messaging/messages/create.ts
@@ -2,13 +2,13 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Message from '../../../models/messaging-message';
-import { isValidText } from '../../../models/messaging-message';
-import History from '../../../models/messaging-history';
-import User from '../../../models/user';
-import Mute from '../../../models/mute';
-import DriveFile from '../../../models/drive-file';
-import { pack } from '../../../models/messaging-message';
+import Message from '../../../../../models/messaging-message';
+import { isValidText } from '../../../../../models/messaging-message';
+import History from '../../../../../models/messaging-history';
+import User from '../../../../../models/user';
+import Mute from '../../../../../models/mute';
+import DriveFile from '../../../../../models/drive-file';
+import { pack } from '../../../../../models/messaging-message';
 import publishUserStream from '../../../event';
 import { publishMessagingStream, publishMessagingIndexStream, pushSw } from '../../../event';
 import config from '../../../../../conf';
diff --git a/src/server/api/endpoints/messaging/unread.ts b/src/server/api/endpoints/messaging/unread.ts
index f7f4047b6..30d59dd8b 100644
--- a/src/server/api/endpoints/messaging/unread.ts
+++ b/src/server/api/endpoints/messaging/unread.ts
@@ -1,8 +1,8 @@
 /**
  * Module dependencies
  */
-import Message from '../../models/messaging-message';
-import Mute from '../../models/mute';
+import Message from '../../../../models/messaging-message';
+import Mute from '../../../../models/mute';
 
 /**
  * Get count of unread messages
diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts
index cb47ede57..4f0ae2a60 100644
--- a/src/server/api/endpoints/meta.ts
+++ b/src/server/api/endpoints/meta.ts
@@ -4,7 +4,7 @@
 import * as os from 'os';
 import version from '../../../version';
 import config from '../../../conf';
-import Meta from '../models/meta';
+import Meta from '../../../models/meta';
 
 /**
  * @swagger
@@ -40,7 +40,7 @@ import Meta from '../models/meta';
  * @return {Promise<any>}
  */
 module.exports = (params) => new Promise(async (res, rej) => {
-	const meta = (await Meta.findOne()) || {};
+	const meta: any = (await Meta.findOne()) || {};
 
 	res({
 		maintainer: config.maintainer,
diff --git a/src/server/api/endpoints/mute/create.ts b/src/server/api/endpoints/mute/create.ts
index e86023508..a7fa5f7b4 100644
--- a/src/server/api/endpoints/mute/create.ts
+++ b/src/server/api/endpoints/mute/create.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User from '../../models/user';
-import Mute from '../../models/mute';
+import User from '../../../../models/user';
+import Mute from '../../../../models/mute';
 
 /**
  * Mute a user
diff --git a/src/server/api/endpoints/mute/delete.ts b/src/server/api/endpoints/mute/delete.ts
index 7e361b479..687f01033 100644
--- a/src/server/api/endpoints/mute/delete.ts
+++ b/src/server/api/endpoints/mute/delete.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User from '../../models/user';
-import Mute from '../../models/mute';
+import User from '../../../../models/user';
+import Mute from '../../../../models/mute';
 
 /**
  * Unmute a user
diff --git a/src/server/api/endpoints/mute/list.ts b/src/server/api/endpoints/mute/list.ts
index 3401fba64..bd8040144 100644
--- a/src/server/api/endpoints/mute/list.ts
+++ b/src/server/api/endpoints/mute/list.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Mute from '../../models/mute';
-import { pack } from '../../models/user';
+import Mute from '../../../../models/mute';
+import { pack } from '../../../../models/user';
 import getFriends from '../../common/get-friends';
 
 /**
diff --git a/src/server/api/endpoints/my/apps.ts b/src/server/api/endpoints/my/apps.ts
index bc1290cac..2a3f8bcd7 100644
--- a/src/server/api/endpoints/my/apps.ts
+++ b/src/server/api/endpoints/my/apps.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import App, { pack } from '../../models/app';
+import App, { pack } from '../../../../models/app';
 
 /**
  * Get my apps
diff --git a/src/server/api/endpoints/notifications/get_unread_count.ts b/src/server/api/endpoints/notifications/get_unread_count.ts
index 8f9719fff..283ecd63b 100644
--- a/src/server/api/endpoints/notifications/get_unread_count.ts
+++ b/src/server/api/endpoints/notifications/get_unread_count.ts
@@ -1,8 +1,8 @@
 /**
  * Module dependencies
  */
-import Notification from '../../models/notification';
-import Mute from '../../models/mute';
+import Notification from '../../../../models/notification';
+import Mute from '../../../../models/mute';
 
 /**
  * Get count of unread notifications
diff --git a/src/server/api/endpoints/notifications/mark_as_read_all.ts b/src/server/api/endpoints/notifications/mark_as_read_all.ts
index 693de3d0e..3693ba87b 100644
--- a/src/server/api/endpoints/notifications/mark_as_read_all.ts
+++ b/src/server/api/endpoints/notifications/mark_as_read_all.ts
@@ -1,7 +1,7 @@
 /**
  * Module dependencies
  */
-import Notification from '../../models/notification';
+import Notification from '../../../../models/notification';
 import event from '../../event';
 
 /**
diff --git a/src/server/api/endpoints/othello/games.ts b/src/server/api/endpoints/othello/games.ts
index 37fa38418..d05c1c258 100644
--- a/src/server/api/endpoints/othello/games.ts
+++ b/src/server/api/endpoints/othello/games.ts
@@ -1,5 +1,5 @@
 import $ from 'cafy';
-import OthelloGame, { pack } from '../../models/othello-game';
+import OthelloGame, { pack } from '../../../../models/othello-game';
 
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Get 'my' parameter
diff --git a/src/server/api/endpoints/othello/games/show.ts b/src/server/api/endpoints/othello/games/show.ts
index f9084682f..0d3b53965 100644
--- a/src/server/api/endpoints/othello/games/show.ts
+++ b/src/server/api/endpoints/othello/games/show.ts
@@ -1,6 +1,6 @@
 import $ from 'cafy';
-import OthelloGame, { pack } from '../../../models/othello-game';
-import Othello from '../../../../common/othello/core';
+import OthelloGame, { pack } from '../../../../../models/othello-game';
+import Othello from '../../../../../common/othello/core';
 
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Get 'gameId' parameter
diff --git a/src/server/api/endpoints/othello/invitations.ts b/src/server/api/endpoints/othello/invitations.ts
index f6e0071a6..476153761 100644
--- a/src/server/api/endpoints/othello/invitations.ts
+++ b/src/server/api/endpoints/othello/invitations.ts
@@ -1,4 +1,4 @@
-import Matching, { pack as packMatching } from '../../models/othello-matching';
+import Matching, { pack as packMatching } from '../../../../models/othello-matching';
 
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Find session
diff --git a/src/server/api/endpoints/othello/match.ts b/src/server/api/endpoints/othello/match.ts
index f503c5834..03168095d 100644
--- a/src/server/api/endpoints/othello/match.ts
+++ b/src/server/api/endpoints/othello/match.ts
@@ -1,9 +1,9 @@
 import $ from 'cafy';
-import Matching, { pack as packMatching } from '../../models/othello-matching';
-import OthelloGame, { pack as packGame } from '../../models/othello-game';
-import User from '../../models/user';
+import Matching, { pack as packMatching } from '../../../../models/othello-matching';
+import OthelloGame, { pack as packGame } from '../../../../models/othello-game';
+import User from '../../../../models/user';
 import publishUserStream, { publishOthelloStream } from '../../event';
-import { eighteight } from '../../../common/othello/maps';
+import { eighteight } from '../../../../common/othello/maps';
 
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Get 'userId' parameter
diff --git a/src/server/api/endpoints/othello/match/cancel.ts b/src/server/api/endpoints/othello/match/cancel.ts
index ee0f82a61..562e69106 100644
--- a/src/server/api/endpoints/othello/match/cancel.ts
+++ b/src/server/api/endpoints/othello/match/cancel.ts
@@ -1,4 +1,4 @@
-import Matching from '../../../models/othello-matching';
+import Matching from '../../../../../models/othello-matching';
 
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	await Matching.remove({
diff --git a/src/server/api/endpoints/posts.ts b/src/server/api/endpoints/posts.ts
index bee1de02d..7af8cff67 100644
--- a/src/server/api/endpoints/posts.ts
+++ b/src/server/api/endpoints/posts.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post, { pack } from '../models/post';
+import Post, { pack } from '../../../models/post';
 
 /**
  * Lists all posts
diff --git a/src/server/api/endpoints/posts/categorize.ts b/src/server/api/endpoints/posts/categorize.ts
deleted file mode 100644
index 0436c8e69..000000000
--- a/src/server/api/endpoints/posts/categorize.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * Module dependencies
- */
-import $ from 'cafy';
-import Post from '../../models/post';
-
-/**
- * Categorize a post
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
- */
-module.exports = (params, user) => new Promise(async (res, rej) => {
-	if (!user.account.isPro) {
-		return rej('This endpoint is available only from a Pro account');
-	}
-
-	// Get 'postId' parameter
-	const [postId, postIdErr] = $(params.postId).id().$;
-	if (postIdErr) return rej('invalid postId param');
-
-	// Get categorizee
-	const post = await Post.findOne({
-		_id: postId
-	});
-
-	if (post === null) {
-		return rej('post not found');
-	}
-
-	if (post.is_category_verified) {
-		return rej('This post already has the verified category');
-	}
-
-	// Get 'category' parameter
-	const [category, categoryErr] = $(params.category).string().or([
-		'music', 'game', 'anime', 'it', 'gadgets', 'photography'
-	]).$;
-	if (categoryErr) return rej('invalid category param');
-
-	// Set category
-	Post.update({ _id: post._id }, {
-		$set: {
-			category: category,
-			is_category_verified: true
-		}
-	});
-
-	// Send response
-	res();
-});
diff --git a/src/server/api/endpoints/posts/context.ts b/src/server/api/endpoints/posts/context.ts
index 44a77d102..7abb045a4 100644
--- a/src/server/api/endpoints/posts/context.ts
+++ b/src/server/api/endpoints/posts/context.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post, { pack } from '../../models/post';
+import Post, { pack } from '../../../../models/post';
 
 /**
  * Show a context of a post
diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts
index 33042a51a..6b2957ae6 100644
--- a/src/server/api/endpoints/posts/create.ts
+++ b/src/server/api/endpoints/posts/create.ts
@@ -3,21 +3,21 @@
  */
 import $ from 'cafy';
 import deepEqual = require('deep-equal');
-import parse from '../../common/text';
-import { default as Post, IPost, isValidText } from '../../models/post';
-import { default as User, ILocalAccount, IUser } from '../../models/user';
-import { default as Channel, IChannel } from '../../models/channel';
-import Following from '../../models/following';
-import Mute from '../../models/mute';
-import DriveFile from '../../models/drive-file';
-import Watching from '../../models/post-watching';
-import ChannelWatching from '../../models/channel-watching';
-import { pack } from '../../models/post';
+import parse from '../../../../common/text';
+import { default as Post, IPost, isValidText } from '../../../../models/post';
+import { default as User, ILocalAccount, IUser } from '../../../../models/user';
+import { default as Channel, IChannel } from '../../../../models/channel';
+import Following from '../../../../models/following';
+import Mute from '../../../../models/mute';
+import DriveFile from '../../../../models/drive-file';
+import Watching from '../../../../models/post-watching';
+import ChannelWatching from '../../../../models/channel-watching';
+import { pack } from '../../../../models/post';
 import notify from '../../common/notify';
 import watch from '../../common/watch-post';
 import event, { pushSw, publishChannelStream } from '../../event';
-import getAcct from '../../../common/user/get-acct';
-import parseAcct from '../../../common/user/parse-acct';
+import getAcct from '../../../../common/user/get-acct';
+import parseAcct from '../../../../common/user/parse-acct';
 import config from '../../../../conf';
 
 /**
diff --git a/src/server/api/endpoints/posts/favorites/create.ts b/src/server/api/endpoints/posts/favorites/create.ts
index 6100e10b2..f537fb7dd 100644
--- a/src/server/api/endpoints/posts/favorites/create.ts
+++ b/src/server/api/endpoints/posts/favorites/create.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Favorite from '../../../models/favorite';
-import Post from '../../../models/post';
+import Favorite from '../../../../../models/favorite';
+import Post from '../../../../../models/post';
 
 /**
  * Favorite a post
diff --git a/src/server/api/endpoints/posts/favorites/delete.ts b/src/server/api/endpoints/posts/favorites/delete.ts
index db52036ec..28930337a 100644
--- a/src/server/api/endpoints/posts/favorites/delete.ts
+++ b/src/server/api/endpoints/posts/favorites/delete.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Favorite from '../../../models/favorite';
-import Post from '../../../models/post';
+import Favorite from '../../../../../models/favorite';
+import Post from '../../../../../models/post';
 
 /**
  * Unfavorite a post
diff --git a/src/server/api/endpoints/posts/mentions.ts b/src/server/api/endpoints/posts/mentions.ts
index 1b342e8de..d7302c062 100644
--- a/src/server/api/endpoints/posts/mentions.ts
+++ b/src/server/api/endpoints/posts/mentions.ts
@@ -2,9 +2,9 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post from '../../models/post';
+import Post from '../../../../models/post';
 import getFriends from '../../common/get-friends';
-import { pack } from '../../models/post';
+import { pack } from '../../../../models/post';
 
 /**
  * Get mentions of myself
diff --git a/src/server/api/endpoints/posts/polls/recommendation.ts b/src/server/api/endpoints/posts/polls/recommendation.ts
index 19ef0975f..d70674261 100644
--- a/src/server/api/endpoints/posts/polls/recommendation.ts
+++ b/src/server/api/endpoints/posts/polls/recommendation.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Vote from '../../../models/poll-vote';
-import Post, { pack } from '../../../models/post';
+import Vote from '../../../../../models/poll-vote';
+import Post, { pack } from '../../../../../models/post';
 
 /**
  * Get recommended polls
diff --git a/src/server/api/endpoints/posts/polls/vote.ts b/src/server/api/endpoints/posts/polls/vote.ts
index 734a3a3c4..b970c05e8 100644
--- a/src/server/api/endpoints/posts/polls/vote.ts
+++ b/src/server/api/endpoints/posts/polls/vote.ts
@@ -2,9 +2,9 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Vote from '../../../models/poll-vote';
-import Post from '../../../models/post';
-import Watching from '../../../models/post-watching';
+import Vote from '../../../../../models/poll-vote';
+import Post from '../../../../../models/post';
+import Watching from '../../../../../models/post-watching';
 import notify from '../../../common/notify';
 import watch from '../../../common/watch-post';
 import { publishPostStream } from '../../../event';
diff --git a/src/server/api/endpoints/posts/reactions.ts b/src/server/api/endpoints/posts/reactions.ts
index f753ba7c2..da733f533 100644
--- a/src/server/api/endpoints/posts/reactions.ts
+++ b/src/server/api/endpoints/posts/reactions.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post from '../../models/post';
-import Reaction, { pack } from '../../models/post-reaction';
+import Post from '../../../../models/post';
+import Reaction, { pack } from '../../../../models/post-reaction';
 
 /**
  * Show reactions of a post
diff --git a/src/server/api/endpoints/posts/reactions/create.ts b/src/server/api/endpoints/posts/reactions/create.ts
index a1e677980..5d2b5a7ed 100644
--- a/src/server/api/endpoints/posts/reactions/create.ts
+++ b/src/server/api/endpoints/posts/reactions/create.ts
@@ -2,10 +2,10 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Reaction from '../../../models/post-reaction';
-import Post, { pack as packPost } from '../../../models/post';
-import { pack as packUser } from '../../../models/user';
-import Watching from '../../../models/post-watching';
+import Reaction from '../../../../../models/post-reaction';
+import Post, { pack as packPost } from '../../../../../models/post';
+import { pack as packUser } from '../../../../../models/user';
+import Watching from '../../../../../models/post-watching';
 import notify from '../../../common/notify';
 import watch from '../../../common/watch-post';
 import { publishPostStream, pushSw } from '../../../event';
diff --git a/src/server/api/endpoints/posts/reactions/delete.ts b/src/server/api/endpoints/posts/reactions/delete.ts
index b09bcbb4b..11f5c7daf 100644
--- a/src/server/api/endpoints/posts/reactions/delete.ts
+++ b/src/server/api/endpoints/posts/reactions/delete.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Reaction from '../../../models/post-reaction';
-import Post from '../../../models/post';
+import Reaction from '../../../../../models/post-reaction';
+import Post from '../../../../../models/post';
 // import event from '../../../event';
 
 /**
diff --git a/src/server/api/endpoints/posts/replies.ts b/src/server/api/endpoints/posts/replies.ts
index db021505f..dd5a95c17 100644
--- a/src/server/api/endpoints/posts/replies.ts
+++ b/src/server/api/endpoints/posts/replies.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post, { pack } from '../../models/post';
+import Post, { pack } from '../../../../models/post';
 
 /**
  * Show a replies of a post
diff --git a/src/server/api/endpoints/posts/reposts.ts b/src/server/api/endpoints/posts/reposts.ts
index 51af41f52..ec6218ca3 100644
--- a/src/server/api/endpoints/posts/reposts.ts
+++ b/src/server/api/endpoints/posts/reposts.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post, { pack } from '../../models/post';
+import Post, { pack } from '../../../../models/post';
 
 /**
  * Show a reposts of a post
diff --git a/src/server/api/endpoints/posts/search.ts b/src/server/api/endpoints/posts/search.ts
index bb5c43892..21c4e77fd 100644
--- a/src/server/api/endpoints/posts/search.ts
+++ b/src/server/api/endpoints/posts/search.ts
@@ -3,11 +3,11 @@
  */
 import $ from 'cafy';
 const escapeRegexp = require('escape-regexp');
-import Post from '../../models/post';
-import User from '../../models/user';
-import Mute from '../../models/mute';
+import Post from '../../../../models/post';
+import User from '../../../../models/user';
+import Mute from '../../../../models/mute';
 import getFriends from '../../common/get-friends';
-import { pack } from '../../models/post';
+import { pack } from '../../../../models/post';
 
 /**
  * Search a post
diff --git a/src/server/api/endpoints/posts/show.ts b/src/server/api/endpoints/posts/show.ts
index bb4bcdb79..e1781b545 100644
--- a/src/server/api/endpoints/posts/show.ts
+++ b/src/server/api/endpoints/posts/show.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post, { pack } from '../../models/post';
+import Post, { pack } from '../../../../models/post';
 
 /**
  * Show a post
diff --git a/src/server/api/endpoints/posts/timeline.ts b/src/server/api/endpoints/posts/timeline.ts
index a3e915f16..b58d25fa8 100644
--- a/src/server/api/endpoints/posts/timeline.ts
+++ b/src/server/api/endpoints/posts/timeline.ts
@@ -3,11 +3,11 @@
  */
 import $ from 'cafy';
 import rap from '@prezzemolo/rap';
-import Post from '../../models/post';
-import Mute from '../../models/mute';
-import ChannelWatching from '../../models/channel-watching';
+import Post from '../../../../models/post';
+import Mute from '../../../../models/mute';
+import ChannelWatching from '../../../../models/channel-watching';
 import getFriends from '../../common/get-friends';
-import { pack } from '../../models/post';
+import { pack } from '../../../../models/post';
 
 /**
  * Get timeline of myself
diff --git a/src/server/api/endpoints/posts/trend.ts b/src/server/api/endpoints/posts/trend.ts
index bc0c47fbc..dbee16913 100644
--- a/src/server/api/endpoints/posts/trend.ts
+++ b/src/server/api/endpoints/posts/trend.ts
@@ -3,7 +3,7 @@
  */
 const ms = require('ms');
 import $ from 'cafy';
-import Post, { pack } from '../../models/post';
+import Post, { pack } from '../../../../models/post';
 
 /**
  * Get trend posts
diff --git a/src/server/api/endpoints/stats.ts b/src/server/api/endpoints/stats.ts
index 719792d40..0fb0c44b0 100644
--- a/src/server/api/endpoints/stats.ts
+++ b/src/server/api/endpoints/stats.ts
@@ -1,8 +1,8 @@
 /**
  * Module dependencies
  */
-import Post from '../models/post';
-import User from '../models/user';
+import Post from '../../../models/post';
+import User from '../../../models/user';
 
 /**
  * @swagger
diff --git a/src/server/api/endpoints/sw/register.ts b/src/server/api/endpoints/sw/register.ts
index 1542e1dbe..ef3428057 100644
--- a/src/server/api/endpoints/sw/register.ts
+++ b/src/server/api/endpoints/sw/register.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Subscription from '../../models/sw-subscription';
+import Subscription from '../../../../models/sw-subscription';
 
 /**
  * subscribe service worker
diff --git a/src/server/api/endpoints/username/available.ts b/src/server/api/endpoints/username/available.ts
index f23cdbd85..bd27c37de 100644
--- a/src/server/api/endpoints/username/available.ts
+++ b/src/server/api/endpoints/username/available.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User from '../../models/user';
-import { validateUsername } from '../../models/user';
+import User from '../../../../models/user';
+import { validateUsername } from '../../../../models/user';
 
 /**
  * Check available username
diff --git a/src/server/api/endpoints/users.ts b/src/server/api/endpoints/users.ts
index 393c3479c..e82d72748 100644
--- a/src/server/api/endpoints/users.ts
+++ b/src/server/api/endpoints/users.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User, { pack } from '../models/user';
+import User, { pack } from '../../../models/user';
 
 /**
  * Lists all users
diff --git a/src/server/api/endpoints/users/followers.ts b/src/server/api/endpoints/users/followers.ts
index fc09cfa2c..39b69a6aa 100644
--- a/src/server/api/endpoints/users/followers.ts
+++ b/src/server/api/endpoints/users/followers.ts
@@ -2,9 +2,9 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User from '../../models/user';
-import Following from '../../models/following';
-import { pack } from '../../models/user';
+import User from '../../../../models/user';
+import Following from '../../../../models/following';
+import { pack } from '../../../../models/user';
 import getFriends from '../../common/get-friends';
 
 /**
diff --git a/src/server/api/endpoints/users/following.ts b/src/server/api/endpoints/users/following.ts
index 3387dab36..aa6628dde 100644
--- a/src/server/api/endpoints/users/following.ts
+++ b/src/server/api/endpoints/users/following.ts
@@ -2,9 +2,9 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User from '../../models/user';
-import Following from '../../models/following';
-import { pack } from '../../models/user';
+import User from '../../../../models/user';
+import Following from '../../../../models/following';
+import { pack } from '../../../../models/user';
 import getFriends from '../../common/get-friends';
 
 /**
diff --git a/src/server/api/endpoints/users/get_frequently_replied_users.ts b/src/server/api/endpoints/users/get_frequently_replied_users.ts
index 991c5555b..3a116c8e2 100644
--- a/src/server/api/endpoints/users/get_frequently_replied_users.ts
+++ b/src/server/api/endpoints/users/get_frequently_replied_users.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post from '../../models/post';
-import User, { pack } from '../../models/user';
+import Post from '../../../../models/post';
+import User, { pack } from '../../../../models/user';
 
 module.exports = (params, me) => new Promise(async (res, rej) => {
 	// Get 'userId' parameter
diff --git a/src/server/api/endpoints/users/posts.ts b/src/server/api/endpoints/users/posts.ts
index 934690749..b6c533fb5 100644
--- a/src/server/api/endpoints/users/posts.ts
+++ b/src/server/api/endpoints/users/posts.ts
@@ -3,8 +3,8 @@
  */
 import $ from 'cafy';
 import getHostLower from '../../common/get-host-lower';
-import Post, { pack } from '../../models/post';
-import User from '../../models/user';
+import Post, { pack } from '../../../../models/post';
+import User from '../../../../models/user';
 
 /**
  * Get posts of a user
diff --git a/src/server/api/endpoints/users/recommendation.ts b/src/server/api/endpoints/users/recommendation.ts
index c5297cdc5..c81533969 100644
--- a/src/server/api/endpoints/users/recommendation.ts
+++ b/src/server/api/endpoints/users/recommendation.ts
@@ -3,7 +3,7 @@
  */
 const ms = require('ms');
 import $ from 'cafy';
-import User, { pack } from '../../models/user';
+import User, { pack } from '../../../../models/user';
 import getFriends from '../../common/get-friends';
 
 /**
diff --git a/src/server/api/endpoints/users/search.ts b/src/server/api/endpoints/users/search.ts
index b03ed2f2f..335043b02 100644
--- a/src/server/api/endpoints/users/search.ts
+++ b/src/server/api/endpoints/users/search.ts
@@ -3,7 +3,7 @@
  */
 import * as mongo from 'mongodb';
 import $ from 'cafy';
-import User, { pack } from '../../models/user';
+import User, { pack } from '../../../../models/user';
 import config from '../../../../conf';
 const escapeRegexp = require('escape-regexp');
 
diff --git a/src/server/api/endpoints/users/search_by_username.ts b/src/server/api/endpoints/users/search_by_username.ts
index 24e9c98e7..5f6ececff 100644
--- a/src/server/api/endpoints/users/search_by_username.ts
+++ b/src/server/api/endpoints/users/search_by_username.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User, { pack } from '../../models/user';
+import User, { pack } from '../../../../models/user';
 
 /**
  * Search a user by username
diff --git a/src/server/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts
index 16411dddc..0b7646f81 100644
--- a/src/server/api/endpoints/users/show.ts
+++ b/src/server/api/endpoints/users/show.ts
@@ -5,7 +5,7 @@ import $ from 'cafy';
 import { JSDOM } from 'jsdom';
 import { toUnicode, toASCII } from 'punycode';
 import uploadFromUrl from '../../common/drive/upload_from_url';
-import User, { pack, validateUsername, isValidName, isValidDescription } from '../../models/user';
+import User, { pack, validateUsername, isValidName, isValidDescription } from '../../../../models/user';
 const request = require('request-promise-native');
 const WebFinger = require('webfinger.js');
 
diff --git a/src/server/api/limitter.ts b/src/server/api/limitter.ts
index 33337fbb1..88ea6c367 100644
--- a/src/server/api/limitter.ts
+++ b/src/server/api/limitter.ts
@@ -3,7 +3,7 @@ import * as debug from 'debug';
 import limiterDB from '../../db/redis';
 import { Endpoint } from './endpoints';
 import { IAuthContext } from './authenticate';
-import getAcct from '../common/user/get-acct';
+import getAcct from '../../common/user/get-acct';
 
 const log = debug('misskey:limitter');
 
diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts
index 4b60f4c75..d78fa11b8 100644
--- a/src/server/api/private/signin.ts
+++ b/src/server/api/private/signin.ts
@@ -1,8 +1,8 @@
 import * as express from 'express';
 import * as bcrypt from 'bcryptjs';
 import * as speakeasy from 'speakeasy';
-import { default as User, ILocalAccount, IUser } from '../models/user';
-import Signin, { pack } from '../models/signin';
+import { default as User, ILocalAccount, IUser } from '../../../models/user';
+import Signin, { pack } from '../../../models/signin';
 import event from '../event';
 import signin from '../common/signin';
 import config from '../../../conf';
diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts
index cad9752c4..fd47b5303 100644
--- a/src/server/api/private/signup.ts
+++ b/src/server/api/private/signup.ts
@@ -3,7 +3,7 @@ import * as express from 'express';
 import * as bcrypt from 'bcryptjs';
 import { generate as generateKeypair } from '../../../crypto_key';
 import recaptcha = require('recaptcha-promise');
-import User, { IUser, validateUsername, validatePassword, pack } from '../models/user';
+import User, { IUser, validateUsername, validatePassword, pack } from '../../../models/user';
 import generateUserToken from '../common/generate-native-user-token';
 import config from '../../../conf';
 
diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts
index 98732e6b8..6eacdb747 100644
--- a/src/server/api/service/github.ts
+++ b/src/server/api/service/github.ts
@@ -1,7 +1,7 @@
 import * as EventEmitter from 'events';
 import * as express from 'express';
-const crypto = require('crypto');
-import User from '../models/user';
+//const crypto = require('crypto');
+import User from '../../../models/user';
 import config from '../../../conf';
 import queue from '../../../queue';
 
diff --git a/src/server/api/service/twitter.ts b/src/server/api/service/twitter.ts
index bdbedc864..d77341db2 100644
--- a/src/server/api/service/twitter.ts
+++ b/src/server/api/service/twitter.ts
@@ -5,7 +5,7 @@ import * as uuid from 'uuid';
 // const Twitter = require('twitter');
 import autwh from 'autwh';
 import redis from '../../../db/redis';
-import User, { pack } from '../models/user';
+import User, { pack } from '../../../models/user';
 import event from '../event';
 import config from '../../../conf';
 import signin from '../common/signin';
diff --git a/src/server/api/stream/channel.ts b/src/server/api/stream/channel.ts
index d67d77cbf..cb0427823 100644
--- a/src/server/api/stream/channel.ts
+++ b/src/server/api/stream/channel.ts
@@ -1,8 +1,10 @@
 import * as websocket from 'websocket';
 import * as redis from 'redis';
+import { ParsedUrlQuery } from 'querystring';
 
 export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient): void {
-	const channel = request.resourceURL.query.channel;
+	const q = request.resourceURL.query as ParsedUrlQuery;
+	const channel = q.channel;
 
 	// Subscribe channel stream
 	subscriber.subscribe(`misskey:channel-stream:${channel}`);
diff --git a/src/server/api/stream/home.ts b/src/server/api/stream/home.ts
index 291be0824..648bd7c3c 100644
--- a/src/server/api/stream/home.ts
+++ b/src/server/api/stream/home.ts
@@ -2,9 +2,9 @@ import * as websocket from 'websocket';
 import * as redis from 'redis';
 import * as debug from 'debug';
 
-import User from '../models/user';
-import Mute from '../models/mute';
-import { pack as packPost } from '../models/post';
+import User from '../../../models/user';
+import Mute from '../../../models/mute';
+import { pack as packPost } from '../../../models/post';
 import readNotification from '../common/read-notification';
 
 const log = debug('misskey');
diff --git a/src/server/api/stream/messaging.ts b/src/server/api/stream/messaging.ts
index a4a12426a..3e6c2cd50 100644
--- a/src/server/api/stream/messaging.ts
+++ b/src/server/api/stream/messaging.ts
@@ -1,9 +1,11 @@
 import * as websocket from 'websocket';
 import * as redis from 'redis';
 import read from '../common/read-messaging-message';
+import { ParsedUrlQuery } from 'querystring';
 
 export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void {
-	const otherparty = request.resourceURL.query.otherparty;
+	const q = request.resourceURL.query as ParsedUrlQuery;
+	const otherparty = q.otherparty as string;
 
 	// Subscribe messaging stream
 	subscriber.subscribe(`misskey:messaging-stream:${user._id}-${otherparty}`);
diff --git a/src/server/api/stream/othello-game.ts b/src/server/api/stream/othello-game.ts
index e48d93cdd..b6a251c4c 100644
--- a/src/server/api/stream/othello-game.ts
+++ b/src/server/api/stream/othello-game.ts
@@ -1,13 +1,15 @@
 import * as websocket from 'websocket';
 import * as redis from 'redis';
 import * as CRC32 from 'crc-32';
-import OthelloGame, { pack } from '../models/othello-game';
+import OthelloGame, { pack } from '../../../models/othello-game';
 import { publishOthelloGameStream } from '../event';
-import Othello from '../../common/othello/core';
-import * as maps from '../../common/othello/maps';
+import Othello from '../../../common/othello/core';
+import * as maps from '../../../common/othello/maps';
+import { ParsedUrlQuery } from 'querystring';
 
 export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user?: any): void {
-	const gameId = request.resourceURL.query.game;
+	const q = request.resourceURL.query as ParsedUrlQuery;
+	const gameId = q.game;
 
 	// Subscribe game stream
 	subscriber.subscribe(`misskey:othello-game-stream:${gameId}`);
diff --git a/src/server/api/stream/othello.ts b/src/server/api/stream/othello.ts
index 55c993ec8..4205afae7 100644
--- a/src/server/api/stream/othello.ts
+++ b/src/server/api/stream/othello.ts
@@ -1,7 +1,7 @@
 import * as mongo from 'mongodb';
 import * as websocket from 'websocket';
 import * as redis from 'redis';
-import Matching, { pack } from '../models/othello-matching';
+import Matching, { pack } from '../../../models/othello-matching';
 import publishUserStream from '../event';
 
 export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void {
diff --git a/src/server/api/streaming.ts b/src/server/api/streaming.ts
index 73f099bd8..c86c6a8b4 100644
--- a/src/server/api/streaming.ts
+++ b/src/server/api/streaming.ts
@@ -2,8 +2,8 @@ import * as http from 'http';
 import * as websocket from 'websocket';
 import * as redis from 'redis';
 import config from '../../conf';
-import { default as User, IUser } from './models/user';
-import AccessToken from './models/access-token';
+import { default as User, IUser } from '../../models/user';
+import AccessToken from '../../models/access-token';
 import isNativeToken from './common/is-native-token';
 
 import homeStream from './stream/home';
@@ -15,6 +15,7 @@ import othelloStream from './stream/othello';
 import serverStream from './stream/server';
 import requestsStream from './stream/requests';
 import channelStream from './stream/channel';
+import { ParsedUrlQuery } from 'querystring';
 
 module.exports = (server: http.Server) => {
 	/**
@@ -51,7 +52,8 @@ module.exports = (server: http.Server) => {
 			return;
 		}
 
-		const user = await authenticate(request.resourceURL.query.i);
+		const q = request.resourceURL.query as ParsedUrlQuery;
+		const user = await authenticate(q.i as string);
 
 		if (request.resourceURL.pathname === '/othello-game') {
 			othelloGameStream(request, connection, subscriber, user);
diff --git a/src/server/file/server.ts b/src/server/file/server.ts
index 3bda5b14f..062d260cb 100644
--- a/src/server/file/server.ts
+++ b/src/server/file/server.ts
@@ -10,7 +10,7 @@ import * as mongodb from 'mongodb';
 import * as _gm from 'gm';
 import * as stream from 'stream';
 
-import DriveFile, { getGridFSBucket } from '../api/models/drive-file';
+import DriveFile, { getGridFSBucket } from '../../models/drive-file';
 
 const gm = _gm.subClass({
 	imageMagick: true
diff --git a/src/server/web/server.ts b/src/server/web/server.ts
index b117f6ae8..2fc8f1b8a 100644
--- a/src/server/web/server.ts
+++ b/src/server/web/server.ts
@@ -1,5 +1,5 @@
 /**
- * Web Server
+ * Web Client Server
  */
 
 import * as path from 'path';
@@ -11,9 +11,9 @@ import * as bodyParser from 'body-parser';
 import * as favicon from 'serve-favicon';
 import * as compression from 'compression';
 
-/**
- * Init app
- */
+const client = `${__dirname}/../../client/`;
+
+// Create server
 const app = express();
 app.disable('x-powered-by');
 
@@ -25,51 +25,40 @@ app.use(bodyParser.json({
 }));
 app.use(compression());
 
-/**
- * Initialize requests
- */
 app.use((req, res, next) => {
 	res.header('X-Frame-Options', 'DENY');
 	next();
 });
 
-/**
- * Static assets
- */
-app.use(favicon(`${__dirname}/assets/favicon.ico`));
-app.get('/apple-touch-icon.png', (req, res) => res.sendFile(`${__dirname}/assets/apple-touch-icon.png`));
-app.use('/assets', express.static(`${__dirname}/assets`, {
+//#region static assets
+
+app.use(favicon(`${client}/assets/favicon.ico`));
+app.get('/apple-touch-icon.png', (req, res) => res.sendFile(`${client}/assets/apple-touch-icon.png`));
+app.use('/assets', express.static(`${client}/assets`, {
 	maxAge: ms('7 days')
 }));
-app.use('/assets/*.js', (req, res) => res.sendFile(`${__dirname}/assets/404.js`));
+app.use('/assets/*.js', (req, res) => res.sendFile(`${client}/assets/404.js`));
 app.use('/assets', (req, res) => {
 	res.sendStatus(404);
 });
 
-app.use('/recover', (req, res) => res.sendFile(`${__dirname}/assets/recover.html`));
+app.use('/recover', (req, res) => res.sendFile(`${client}/assets/recover.html`));
 
-/**
- * ServiceWroker
- */
+// ServiceWroker
 app.get(/^\/sw\.(.+?)\.js$/, (req, res) =>
-	res.sendFile(`${__dirname}/assets/sw.${req.params[0]}.js`));
+	res.sendFile(`${client}/assets/sw.${req.params[0]}.js`));
 
-/**
- * Manifest
- */
+// Manifest
 app.get('/manifest.json', (req, res) =>
-	res.sendFile(`${__dirname}/assets/manifest.json`));
+	res.sendFile(`${client}/assets/manifest.json`));
 
-/**
- * Common API
- */
-app.get(/\/api:url/, require('./service/url-preview'));
+//#endregion
 
-/**
- * Routing
- */
+app.get(/\/api:url/, require('./url-preview'));
+
+// Render base html for all requests
 app.get('*', (req, res) => {
-	res.sendFile(path.resolve(`${__dirname}/app/base.html`), {
+	res.sendFile(path.resolve(`${client}/app/base.html`), {
 		maxAge: ms('7 days')
 	});
 });
diff --git a/src/server/web/service/url-preview.ts b/src/server/web/url-preview.ts
similarity index 100%
rename from src/server/web/service/url-preview.ts
rename to src/server/web/url-preview.ts
diff --git a/src/tools/analysis/core.ts b/src/tools/analysis/core.ts
deleted file mode 100644
index 839fffd3c..000000000
--- a/src/tools/analysis/core.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-const bayes = require('./naive-bayes.js');
-
-const MeCab = require('./mecab');
-import Post from '../../server/api/models/post';
-
-/**
- * 投稿を学習したり与えられた投稿のカテゴリを予測します
- */
-export default class Categorizer {
-	private classifier: any;
-	private mecab: any;
-
-	constructor() {
-		this.mecab = new MeCab();
-
-		// BIND -----------------------------------
-		this.tokenizer = this.tokenizer.bind(this);
-	}
-
-	private tokenizer(text: string) {
-		const tokens = this.mecab.parseSync(text)
-			// 名詞だけに制限
-			.filter(token => token[1] === '名詞')
-			// 取り出し
-			.map(token => token[0]);
-
-		return tokens;
-	}
-
-	public async init() {
-		this.classifier = bayes({
-			tokenizer: this.tokenizer
-		});
-
-		// 訓練データ取得
-		const verifiedPosts = await Post.find({
-			is_category_verified: true
-		});
-
-		// 学習
-		verifiedPosts.forEach(post => {
-			this.classifier.learn(post.text, post.category);
-		});
-	}
-
-	public async predict(text) {
-		return this.classifier.categorize(text);
-	}
-}
diff --git a/src/tools/analysis/extract-user-domains.ts b/src/tools/analysis/extract-user-domains.ts
deleted file mode 100644
index 1aa456db8..000000000
--- a/src/tools/analysis/extract-user-domains.ts
+++ /dev/null
@@ -1,120 +0,0 @@
-import * as URL from 'url';
-
-import Post from '../../server/api/models/post';
-import User from '../../server/api/models/user';
-import parse from '../../server/api/common/text';
-
-process.on('unhandledRejection', console.dir);
-
-function tokenize(text: string) {
-	if (text == null) return [];
-
-	// パース
-	const ast = parse(text);
-
-	const domains = ast
-		// URLを抽出
-		.filter(t => t.type == 'url' || t.type == 'link')
-		.map(t => URL.parse(t.url).hostname);
-
-	return domains;
-}
-
-// Fetch all users
-User.find({}, {
-	fields: {
-		_id: true
-	}
-}).then(users => {
-	let i = -1;
-
-	const x = cb => {
-		if (++i == users.length) return cb();
-		extractDomainsOne(users[i]._id).then(() => x(cb), err => {
-			console.error(err);
-			setTimeout(() => {
-				i--;
-				x(cb);
-			}, 1000);
-		});
-	};
-
-	x(() => {
-		console.log('complete');
-	});
-});
-
-function extractDomainsOne(id) {
-	return new Promise(async (resolve, reject) => {
-		process.stdout.write(`extracting domains of ${id} ...`);
-
-		// Fetch recent posts
-		const recentPosts = await Post.find({
-			userId: id,
-			text: {
-				$exists: true
-			}
-		}, {
-			sort: {
-				_id: -1
-			},
-			limit: 10000,
-			fields: {
-				_id: false,
-				text: true
-			}
-		});
-
-		// 投稿が少なかったら中断
-		if (recentPosts.length < 100) {
-			process.stdout.write(' >>> -\n');
-			return resolve();
-		}
-
-		const domains = {};
-
-		// Extract domains from recent posts
-		recentPosts.forEach(post => {
-			const domainsOfPost = tokenize(post.text);
-
-			domainsOfPost.forEach(domain => {
-				if (domains[domain]) {
-					domains[domain]++;
-				} else {
-					domains[domain] = 1;
-				}
-			});
-		});
-
-		// Calc peak
-		let peak = 0;
-		Object.keys(domains).forEach(domain => {
-			if (domains[domain] > peak) peak = domains[domain];
-		});
-
-		// Sort domains by frequency
-		const domainsSorted = Object.keys(domains).sort((a, b) => domains[b] - domains[a]);
-
-		// Lookup top 10 domains
-		const topDomains = domainsSorted.slice(0, 10);
-
-		process.stdout.write(' >>> ' + topDomains.join(', ') + '\n');
-
-		// Make domains object (includes weights)
-		const domainsObj = topDomains.map(domain => ({
-			domain: domain,
-			weight: domains[domain] / peak
-		}));
-
-		// Save
-		User.update({ _id: id }, {
-			$set: {
-				domains: domainsObj
-			}
-		}).then(() => {
-			resolve();
-		}, err => {
-			reject(err);
-		});
-	});
-}
diff --git a/src/tools/analysis/extract-user-keywords.ts b/src/tools/analysis/extract-user-keywords.ts
deleted file mode 100644
index 9b0691b7d..000000000
--- a/src/tools/analysis/extract-user-keywords.ts
+++ /dev/null
@@ -1,154 +0,0 @@
-const moji = require('moji');
-
-const MeCab = require('./mecab');
-import Post from '../../server/api/models/post';
-import User from '../../server/api/models/user';
-import parse from '../../server/api/common/text';
-
-process.on('unhandledRejection', console.dir);
-
-const stopwords = [
-	'ー',
-
-	'の', 'に', 'は', 'を', 'た', 'が', 'で', 'て', 'と', 'し', 'れ', 'さ',
-  'ある', 'いる', 'も', 'する', 'から', 'な', 'こと', 'として', 'い', 'や', 'れる',
-  'など', 'なっ', 'ない', 'この', 'ため', 'その', 'あっ', 'よう', 'また', 'もの',
-  'という', 'あり', 'まで', 'られ', 'なる', 'へ', 'か', 'だ', 'これ', 'によって',
-  'により', 'おり', 'より', 'による', 'ず', 'なり', 'られる', 'において', 'ば', 'なかっ',
-  'なく', 'しかし', 'について', 'せ', 'だっ', 'その後', 'できる', 'それ', 'う', 'ので',
-  'なお', 'のみ', 'でき', 'き', 'つ', 'における', 'および', 'いう', 'さらに', 'でも',
-  'ら', 'たり', 'その他', 'に関する', 'たち', 'ます', 'ん', 'なら', 'に対して', '特に',
-  'せる', '及び', 'これら', 'とき', 'では', 'にて', 'ほか', 'ながら', 'うち', 'そして',
-  'とともに', 'ただし', 'かつて', 'それぞれ', 'または', 'お', 'ほど', 'ものの', 'に対する',
-	'ほとんど', 'と共に', 'といった', 'です', 'とも', 'ところ', 'ここ', '感じ', '気持ち',
-	'あと', '自分', 'すき', '()',
-
-	'about', 'after', 'all', 'also', 'am', 'an', 'and', 'another', 'any', 'are', 'as', 'at', 'be',
-  'because', 'been', 'before', 'being', 'between', 'both', 'but', 'by', 'came', 'can',
-  'come', 'could', 'did', 'do', 'each', 'for', 'from', 'get', 'got', 'has', 'had',
-  'he', 'have', 'her', 'here', 'him', 'himself', 'his', 'how', 'if', 'in', 'into',
-  'is', 'it', 'like', 'make', 'many', 'me', 'might', 'more', 'most', 'much', 'must',
-  'my', 'never', 'now', 'of', 'on', 'only', 'or', 'other', 'our', 'out', 'over',
-  'said', 'same', 'see', 'should', 'since', 'some', 'still', 'such', 'take', 'than',
-  'that', 'the', 'their', 'them', 'then', 'there', 'these', 'they', 'this', 'those',
-  'through', 'to', 'too', 'under', 'up', 'very', 'was', 'way', 'we', 'well', 'were',
-	'what', 'where', 'which', 'while', 'who', 'with', 'would', 'you', 'your', 'a', 'i'
-];
-
-const mecab = new MeCab();
-
-function tokenize(text: string) {
-	if (text == null) return [];
-
-	// パース
-	const ast = parse(text);
-
-	const plain = ast
-		// テキストのみ(URLなどを除外するという意)
-		.filter(t => t.type == 'text' || t.type == 'bold')
-		.map(t => t.content)
-		.join('');
-
-	const tokens = mecab.parseSync(plain)
-		// キーワードのみ
-		.filter(token => token[1] == '名詞' && (token[2] == '固有名詞' || token[2] == '一般'))
-		// 取り出し(&整形(全角を半角にしたり大文字を小文字で統一したり))
-		.map(token => moji(token[0]).convert('ZE', 'HE').convert('HK', 'ZK').toString().toLowerCase())
-		// ストップワードなど
-		.filter(word =>
-			stopwords.indexOf(word) === -1 &&
-			word.length > 1 &&
-			word.indexOf('!') === -1 &&
-			word.indexOf('!') === -1 &&
-			word.indexOf('?') === -1 &&
-			word.indexOf('?') === -1);
-
-	return tokens;
-}
-
-// Fetch all users
-User.find({}, {
-	fields: {
-		_id: true
-	}
-}).then(users => {
-	let i = -1;
-
-	const x = cb => {
-		if (++i == users.length) return cb();
-		extractKeywordsOne(users[i]._id).then(() => x(cb), err => {
-			console.error(err);
-			setTimeout(() => {
-				i--;
-				x(cb);
-			}, 1000);
-		});
-	};
-
-	x(() => {
-		console.log('complete');
-	});
-});
-
-function extractKeywordsOne(id) {
-	return new Promise(async (resolve, reject) => {
-		process.stdout.write(`extracting keywords of ${id} ...`);
-
-		// Fetch recent posts
-		const recentPosts = await Post.find({
-			userId: id,
-			text: {
-				$exists: true
-			}
-		}, {
-			sort: {
-				_id: -1
-			},
-			limit: 10000,
-			fields: {
-				_id: false,
-				text: true
-			}
-		});
-
-		// 投稿が少なかったら中断
-		if (recentPosts.length < 300) {
-			process.stdout.write(' >>> -\n');
-			return resolve();
-		}
-
-		const keywords = {};
-
-		// Extract keywords from recent posts
-		recentPosts.forEach(post => {
-			const keywordsOfPost = tokenize(post.text);
-
-			keywordsOfPost.forEach(keyword => {
-				if (keywords[keyword]) {
-					keywords[keyword]++;
-				} else {
-					keywords[keyword] = 1;
-				}
-			});
-		});
-
-		// Sort keywords by frequency
-		const keywordsSorted = Object.keys(keywords).sort((a, b) => keywords[b] - keywords[a]);
-
-		// Lookup top 10 keywords
-		const topKeywords = keywordsSorted.slice(0, 10);
-
-		process.stdout.write(' >>> ' + topKeywords.join(', ') + '\n');
-
-		// Save
-		User.update({ _id: id }, {
-			$set: {
-				keywords: topKeywords
-			}
-		}).then(() => {
-			resolve();
-		}, err => {
-			reject(err);
-		});
-	});
-}
diff --git a/src/tools/analysis/mecab.js b/src/tools/analysis/mecab.js
deleted file mode 100644
index 82f7d6d52..000000000
--- a/src/tools/analysis/mecab.js
+++ /dev/null
@@ -1,85 +0,0 @@
-// Original source code: https://github.com/hecomi/node-mecab-async
-// CUSTOMIZED BY SYUILO
-
-var exec     = require('child_process').exec;
-var execSync = require('child_process').execSync;
-var sq       = require('shell-quote');
-
-const config = require('../../conf').default;
-
-// for backward compatibility
-var MeCab = function() {};
-
-MeCab.prototype = {
-    command : config.analysis.mecab_command ? config.analysis.mecab_command : 'mecab',
-    _format: function(arrayResult) {
-        var result = [];
-        if (!arrayResult) { return result; }
-        // Reference: http://mecab.googlecode.com/svn/trunk/mecab/doc/index.html
-        // 表層形\t品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用形,活用型,原形,読み,発音
-        arrayResult.forEach(function(parsed) {
-            if (parsed.length <= 8) { return; }
-            result.push({
-                kanji         : parsed[0],
-                lexical       : parsed[1],
-                compound      : parsed[2],
-                compound2     : parsed[3],
-                compound3     : parsed[4],
-                conjugation   : parsed[5],
-                inflection    : parsed[6],
-                original      : parsed[7],
-                reading       : parsed[8],
-                pronunciation : parsed[9] || ''
-            });
-        });
-        return result;
-    },
-    _shellCommand : function(str) {
-        return sq.quote(['echo', str]) + ' | ' + this.command;
-    },
-    _parseMeCabResult : function(result) {
-        return result.split('\n').map(function(line) {
-            return line.replace('\t', ',').split(',');
-        });
-    },
-    parse : function(str, callback) {
-        process.nextTick(function() { // for bug
-            exec(MeCab._shellCommand(str), function(err, result) {
-                if (err) { return callback(err); }
-                callback(err, MeCab._parseMeCabResult(result).slice(0,-2));
-            });
-        });
-    },
-    parseSync : function(str) {
-        var result = execSync(MeCab._shellCommand(str));
-        return MeCab._parseMeCabResult(String(result)).slice(0, -2);
-    },
-    parseFormat : function(str, callback) {
-        MeCab.parse(str, function(err, result) {
-            if (err) { return callback(err); }
-            callback(err, MeCab._format(result));
-        });
-    },
-    parseSyncFormat : function(str) {
-        return MeCab._format(MeCab.parseSync(str));
-    },
-    _wakatsu : function(arr) {
-        return arr.map(function(data) { return data[0]; });
-    },
-    wakachi : function(str, callback) {
-        MeCab.parse(str, function(err, arr) {
-            if (err) { return callback(err); }
-            callback(null, MeCab._wakatsu(arr));
-        });
-    },
-    wakachiSync : function(str) {
-        var arr = MeCab.parseSync(str);
-        return MeCab._wakatsu(arr);
-    }
-};
-
-for (var x in MeCab.prototype) {
-    MeCab[x] = MeCab.prototype[x];
-}
-
-module.exports = MeCab;
diff --git a/src/tools/analysis/naive-bayes.js b/src/tools/analysis/naive-bayes.js
deleted file mode 100644
index 78f07153c..000000000
--- a/src/tools/analysis/naive-bayes.js
+++ /dev/null
@@ -1,302 +0,0 @@
-// Original source code: https://github.com/ttezel/bayes/blob/master/lib/naive_bayes.js (commit: 2c20d3066e4fc786400aaedcf3e42987e52abe3c)
-// CUSTOMIZED BY SYUILO
-
-/*
-		Expose our naive-bayes generator function
-*/
-module.exports = function (options) {
-	return new Naivebayes(options)
-}
-
-// keys we use to serialize a classifier's state
-var STATE_KEYS = module.exports.STATE_KEYS = [
-	'categories', 'docCount', 'totalDocuments', 'vocabulary', 'vocabularySize',
-	'wordCount', 'wordFrequencyCount', 'options'
-];
-
-/**
- * Initializes a NaiveBayes instance from a JSON state representation.
- * Use this with classifier.toJson().
- *
- * @param  {String} jsonStr   state representation obtained by classifier.toJson()
- * @return {NaiveBayes}       Classifier
- */
-module.exports.fromJson = function (jsonStr) {
-	var parsed;
-	try {
-		parsed = JSON.parse(jsonStr)
-	} catch (e) {
-		throw new Error('Naivebayes.fromJson expects a valid JSON string.')
-	}
-	// init a new classifier
-	var classifier = new Naivebayes(parsed.options)
-
-	// override the classifier's state
-	STATE_KEYS.forEach(function (k) {
-		if (!parsed[k]) {
-			throw new Error('Naivebayes.fromJson: JSON string is missing an expected property: `'+k+'`.')
-		}
-		classifier[k] = parsed[k]
-	})
-
-	return classifier
-}
-
-/**
- * Given an input string, tokenize it into an array of word tokens.
- * This is the default tokenization function used if user does not provide one in `options`.
- *
- * @param  {String} text
- * @return {Array}
- */
-var defaultTokenizer = function (text) {
-	//remove punctuation from text - remove anything that isn't a word char or a space
-	var rgxPunctuation = /[^(a-zA-ZA-Яa-я0-9_)+\s]/g
-
-	var sanitized = text.replace(rgxPunctuation, ' ')
-
-	return sanitized.split(/\s+/)
-}
-
-/**
- * Naive-Bayes Classifier
- *
- * This is a naive-bayes classifier that uses Laplace Smoothing.
- *
- * Takes an (optional) options object containing:
- *   - `tokenizer`  => custom tokenization function
- *
- */
-function Naivebayes (options) {
-	// set options object
-	this.options = {}
-	if (typeof options !== 'undefined') {
-		if (!options || typeof options !== 'object' || Array.isArray(options)) {
-			throw TypeError('NaiveBayes got invalid `options`: `' + options + '`. Pass in an object.')
-		}
-		this.options = options
-	}
-
-	this.tokenizer = this.options.tokenizer || defaultTokenizer
-
-	//initialize our vocabulary and its size
-	this.vocabulary = {}
-	this.vocabularySize = 0
-
-	//number of documents we have learned from
-	this.totalDocuments = 0
-
-	//document frequency table for each of our categories
-	//=> for each category, how often were documents mapped to it
-	this.docCount = {}
-
-	//for each category, how many words total were mapped to it
-	this.wordCount = {}
-
-	//word frequency table for each category
-	//=> for each category, how frequent was a given word mapped to it
-	this.wordFrequencyCount = {}
-
-	//hashmap of our category names
-	this.categories = {}
-}
-
-/**
- * Initialize each of our data structure entries for this new category
- *
- * @param  {String} categoryName
- */
-Naivebayes.prototype.initializeCategory = function (categoryName) {
-	if (!this.categories[categoryName]) {
-		this.docCount[categoryName] = 0
-		this.wordCount[categoryName] = 0
-		this.wordFrequencyCount[categoryName] = {}
-		this.categories[categoryName] = true
-	}
-	return this
-}
-
-/**
- * train our naive-bayes classifier by telling it what `category`
- * the `text` corresponds to.
- *
- * @param  {String} text
- * @param  {String} class
- */
-Naivebayes.prototype.learn = function (text, category) {
-	var self = this
-
-	//initialize category data structures if we've never seen this category
-	self.initializeCategory(category)
-
-	//update our count of how many documents mapped to this category
-	self.docCount[category]++
-
-	//update the total number of documents we have learned from
-	self.totalDocuments++
-
-	//normalize the text into a word array
-	var tokens = self.tokenizer(text)
-
-	//get a frequency count for each token in the text
-	var frequencyTable = self.frequencyTable(tokens)
-
-	/*
-			Update our vocabulary and our word frequency count for this category
-	*/
-
-	Object
-	.keys(frequencyTable)
-	.forEach(function (token) {
-		//add this word to our vocabulary if not already existing
-		if (!self.vocabulary[token]) {
-			self.vocabulary[token] = true
-			self.vocabularySize++
-		}
-
-		var frequencyInText = frequencyTable[token]
-
-		//update the frequency information for this word in this category
-		if (!self.wordFrequencyCount[category][token])
-			self.wordFrequencyCount[category][token] = frequencyInText
-		else
-			self.wordFrequencyCount[category][token] += frequencyInText
-
-		//update the count of all words we have seen mapped to this category
-		self.wordCount[category] += frequencyInText
-	})
-
-	return self
-}
-
-/**
- * Determine what category `text` belongs to.
- *
- * @param  {String} text
- * @return {String} category
- */
-Naivebayes.prototype.categorize = function (text) {
-	var self = this
-		, maxProbability = -Infinity
-		, chosenCategory = null
-
-	var tokens = self.tokenizer(text)
-	var frequencyTable = self.frequencyTable(tokens)
-
-	//iterate thru our categories to find the one with max probability for this text
-	Object
-	.keys(self.categories)
-	.forEach(function (category) {
-
-		//start by calculating the overall probability of this category
-		//=>  out of all documents we've ever looked at, how many were
-		//    mapped to this category
-		var categoryProbability = self.docCount[category] / self.totalDocuments
-
-		//take the log to avoid underflow
-		var logProbability = Math.log(categoryProbability)
-
-		//now determine P( w | c ) for each word `w` in the text
-		Object
-		.keys(frequencyTable)
-		.forEach(function (token) {
-			var frequencyInText = frequencyTable[token]
-			var tokenProbability = self.tokenProbability(token, category)
-
-			// console.log('token: %s category: `%s` tokenProbability: %d', token, category, tokenProbability)
-
-			//determine the log of the P( w | c ) for this word
-			logProbability += frequencyInText * Math.log(tokenProbability)
-		})
-
-		if (logProbability > maxProbability) {
-			maxProbability = logProbability
-			chosenCategory = category
-		}
-	})
-
-	return chosenCategory
-}
-
-/**
- * Calculate probability that a `token` belongs to a `category`
- *
- * @param  {String} token
- * @param  {String} category
- * @return {Number} probability
- */
-Naivebayes.prototype.tokenProbability = function (token, category) {
-	//how many times this word has occurred in documents mapped to this category
-	var wordFrequencyCount = this.wordFrequencyCount[category][token] || 0
-
-	//what is the count of all words that have ever been mapped to this category
-	var wordCount = this.wordCount[category]
-
-	//use laplace Add-1 Smoothing equation
-	return ( wordFrequencyCount + 1 ) / ( wordCount + this.vocabularySize )
-}
-
-/**
- * Build a frequency hashmap where
- * - the keys are the entries in `tokens`
- * - the values are the frequency of each entry in `tokens`
- *
- * @param  {Array} tokens  Normalized word array
- * @return {Object}
- */
-Naivebayes.prototype.frequencyTable = function (tokens) {
-	var frequencyTable = Object.create(null)
-
-	tokens.forEach(function (token) {
-		if (!frequencyTable[token])
-			frequencyTable[token] = 1
-		else
-			frequencyTable[token]++
-	})
-
-	return frequencyTable
-}
-
-/**
- * Dump the classifier's state as a JSON string.
- * @return {String} Representation of the classifier.
- */
-Naivebayes.prototype.toJson = function () {
-	var state = {}
-	var self = this
-	STATE_KEYS.forEach(function (k) {
-		state[k] = self[k]
-	})
-
-	var jsonStr = JSON.stringify(state)
-
-	return jsonStr
-}
-
-// (original method)
-Naivebayes.prototype.export = function () {
-	var state = {}
-	var self = this
-	STATE_KEYS.forEach(function (k) {
-		state[k] = self[k]
-	})
-
-	return state
-}
-
-module.exports.import = function (data) {
-	var parsed = data
-
-	// init a new classifier
-	var classifier = new Naivebayes()
-
-	// override the classifier's state
-	STATE_KEYS.forEach(function (k) {
-		if (!parsed[k]) {
-			throw new Error('Naivebayes.import: data is missing an expected property: `'+k+'`.')
-		}
-		classifier[k] = parsed[k]
-	})
-
-	return classifier
-}
diff --git a/src/tools/analysis/predict-all-post-category.ts b/src/tools/analysis/predict-all-post-category.ts
deleted file mode 100644
index 8564fd1b1..000000000
--- a/src/tools/analysis/predict-all-post-category.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import Post from '../../server/api/models/post';
-import Core from './core';
-
-const c = new Core();
-
-c.init().then(() => {
-	// 全ての(人間によって証明されていない)投稿を取得
-	Post.find({
-		text: {
-			$exists: true
-		},
-		is_category_verified: {
-			$ne: true
-		}
-	}, {
-		sort: {
-			_id: -1
-		},
-		fields: {
-			_id: true,
-			text: true
-		}
-	}).then(posts => {
-		posts.forEach(post => {
-			console.log(`predicting... ${post._id}`);
-			const category = c.predict(post.text);
-
-			Post.update({ _id: post._id }, {
-				$set: {
-					category: category
-				}
-			});
-		});
-	});
-});
diff --git a/src/tools/analysis/predict-user-interst.ts b/src/tools/analysis/predict-user-interst.ts
deleted file mode 100644
index a101f2010..000000000
--- a/src/tools/analysis/predict-user-interst.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import Post from '../../server/api/models/post';
-import User from '../../server/api/models/user';
-
-export async function predictOne(id) {
-	console.log(`predict interest of ${id} ...`);
-
-	// TODO: repostなども含める
-	const recentPosts = await Post.find({
-		userId: id,
-		category: {
-			$exists: true
-		}
-	}, {
-		sort: {
-			_id: -1
-		},
-		limit: 1000,
-		fields: {
-			_id: false,
-			category: true
-		}
-	});
-
-	const categories = {};
-
-	recentPosts.forEach(post => {
-		if (categories[post.category]) {
-			categories[post.category]++;
-		} else {
-			categories[post.category] = 1;
-		}
-	});
-}
-
-export async function predictAll() {
-	const allUsers = await User.find({}, {
-		fields: {
-			_id: true
-		}
-	});
-
-	allUsers.forEach(user => {
-		predictOne(user._id);
-	});
-}
diff --git a/tsconfig.json b/tsconfig.json
index 574c11bac..c407d554e 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -21,6 +21,6 @@
     "./src/**/*.ts"
   ],
   "exclude": [
-    "./src/server/web/app/**/*.ts"
+    "./src/client/app/**/*.ts"
   ]
 }
diff --git a/webpack.config.ts b/webpack.config.ts
index 53e3d2630..d486e100a 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -34,7 +34,7 @@ global['collapseSpacesReplacement'] = html => {
 };
 
 global['base64replacement'] = (_, key) => {
-	return fs.readFileSync(__dirname + '/src/server/web/' + key, 'base64');
+	return fs.readFileSync(__dirname + '/src/client/' + key, 'base64');
 };
 //#endregion
 
@@ -52,18 +52,18 @@ module.exports = entries.map(x => {
 
 	// Entries
 	const entry = {
-		desktop: './src/server/web/app/desktop/script.ts',
-		mobile: './src/server/web/app/mobile/script.ts',
-		//ch: './src/server/web/app/ch/script.ts',
-		//stats: './src/server/web/app/stats/script.ts',
-		//status: './src/server/web/app/status/script.ts',
-		dev: './src/server/web/app/dev/script.ts',
-		auth: './src/server/web/app/auth/script.ts',
-		sw: './src/server/web/app/sw.js'
+		desktop: './src/client/app/desktop/script.ts',
+		mobile: './src/client/app/mobile/script.ts',
+		//ch: './src/client/app/ch/script.ts',
+		//stats: './src/client/app/stats/script.ts',
+		//status: './src/client/app/status/script.ts',
+		dev: './src/client/app/dev/script.ts',
+		auth: './src/client/app/auth/script.ts',
+		sw: './src/client/app/sw.js'
 	};
 
 	const output = {
-		path: __dirname + '/built/server/web/assets',
+		path: __dirname + '/built/client/assets',
 		filename: `[name].${version}.${lang}.${isProduction ? 'min' : 'raw'}.js`
 	};
 
@@ -207,7 +207,7 @@ module.exports = entries.map(x => {
 					loader: 'ts-loader',
 					options: {
 						happyPackMode: true,
-						configFile: __dirname + '/../src/server/web/app/tsconfig.json',
+						configFile: __dirname + '/src/client/app/tsconfig.json',
 						appendTsSuffixTo: [/\.vue$/]
 					}
 				}, {
@@ -232,7 +232,7 @@ module.exports = entries.map(x => {
 				'.js', '.ts', '.json'
 			],
 			alias: {
-				'const.styl': __dirname + '/src/server/web/const.styl'
+				'const.styl': __dirname + '/src/client/const.styl'
 			}
 		},
 		resolveLoader: {

From 95a76dd21125bd4ab4c5bf87c119ced0596bb671 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 29 Mar 2018 20:34:39 +0900
Subject: [PATCH 0931/1250] :v:

---
 src/server/api/{server.ts => index.ts}  | 0
 src/server/file/{server.ts => index.ts} | 0
 src/server/index.ts                     | 6 +++---
 src/server/web/{server.ts => index.ts}  | 0
 4 files changed, 3 insertions(+), 3 deletions(-)
 rename src/server/api/{server.ts => index.ts} (100%)
 rename src/server/file/{server.ts => index.ts} (100%)
 rename src/server/web/{server.ts => index.ts} (100%)

diff --git a/src/server/api/server.ts b/src/server/api/index.ts
similarity index 100%
rename from src/server/api/server.ts
rename to src/server/api/index.ts
diff --git a/src/server/file/server.ts b/src/server/file/index.ts
similarity index 100%
rename from src/server/file/server.ts
rename to src/server/file/index.ts
diff --git a/src/server/index.ts b/src/server/index.ts
index 3908b8a52..fe22d9c9b 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -51,9 +51,9 @@ app.use((req, res, next) => {
 /**
  * Register modules
  */
-app.use('/api', require('./api/server'));
-app.use('/files', require('./file/server'));
-app.use(require('./web/server'));
+app.use('/api', require('./api'));
+app.use('/files', require('./file'));
+app.use(require('./web'));
 
 function createServer() {
 	if (config.https) {
diff --git a/src/server/web/server.ts b/src/server/web/index.ts
similarity index 100%
rename from src/server/web/server.ts
rename to src/server/web/index.ts

From b5420a669064b72649b2a8bdbc49820fc0fc78b4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 29 Mar 2018 20:37:24 +0900
Subject: [PATCH 0932/1250] Fix bug

---
 src/client/app/common/views/components/post-html.ts | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/client/app/common/views/components/post-html.ts b/src/client/app/common/views/components/post-html.ts
index 98da86617..39d783aac 100644
--- a/src/client/app/common/views/components/post-html.ts
+++ b/src/client/app/common/views/components/post-html.ts
@@ -90,7 +90,11 @@ export default Vue.component('mk-post-html', {
 					]);
 
 				case 'inline-code':
-					return createElement('code', token.html);
+					return createElement('code', {
+						domProps: {
+							innerHTML: token.html
+						}
+					});
 
 				case 'quote':
 					const text2 = token.quote.replace(/(\r\n|\n|\r)/g, '\n');

From f9e46cbdf368d5d3ffcbcbecf980316f356a8072 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 29 Mar 2018 20:50:45 +0900
Subject: [PATCH 0933/1250] oops

---
 src/{client/docs/server.ts => server/web/docs.ts} | 6 ++++--
 src/server/web/index.ts                           | 2 +-
 2 files changed, 5 insertions(+), 3 deletions(-)
 rename src/{client/docs/server.ts => server/web/docs.ts} (59%)

diff --git a/src/client/docs/server.ts b/src/server/web/docs.ts
similarity index 59%
rename from src/client/docs/server.ts
rename to src/server/web/docs.ts
index b2e50457e..e332d4fab 100644
--- a/src/client/docs/server.ts
+++ b/src/server/web/docs.ts
@@ -4,18 +4,20 @@
 
 import * as express from 'express';
 
+const docs = `${__dirname}/../../client/docs/`;
+
 /**
  * Init app
  */
 const app = express();
 app.disable('x-powered-by');
 
-app.use('/assets', express.static(`${__dirname}/assets`));
+app.use('/assets', express.static(`${docs}/assets`));
 
 /**
  * Routing
  */
 app.get(/^\/([a-z_\-\/]+?)$/, (req, res) =>
-	res.sendFile(`${__dirname}/${req.params[0]}.html`));
+	res.sendFile(`${docs}/${req.params[0]}.html`));
 
 module.exports = app;
diff --git a/src/server/web/index.ts b/src/server/web/index.ts
index 2fc8f1b8a..445f03de1 100644
--- a/src/server/web/index.ts
+++ b/src/server/web/index.ts
@@ -17,7 +17,7 @@ const client = `${__dirname}/../../client/`;
 const app = express();
 app.disable('x-powered-by');
 
-app.use('/docs', require('./docs/server'));
+app.use('/docs', require('./docs'));
 
 app.use(bodyParser.urlencoded({ extended: true }));
 app.use(bodyParser.json({

From 88e3edd2e16529028efac66f4ee26417e9d8cd2c Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 29 Mar 2018 21:25:11 +0900
Subject: [PATCH 0934/1250] Update README.md

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index ef12958c2..f7d67247a 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,7 @@ ultimately sophisticated new type of mini-blog based SNS.
 
 and more! You can touch with your own eyes at https://misskey.xyz/.
 
-:package: Setup and Installation
+:package: Setup
 ----------------------------------------------------------------
 If you want to run your own instance of Misskey,
 please see [Setup and installation guide](./docs/setup.en.md).

From 63830105b9ddef0a2990c11168c142a771921691 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 29 Mar 2018 21:36:27 +0900
Subject: [PATCH 0935/1250] Update show.ts

---
 src/server/api/endpoints/users/show.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/server/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts
index 0b7646f81..1e488dd35 100644
--- a/src/server/api/endpoints/users/show.ts
+++ b/src/server/api/endpoints/users/show.ts
@@ -155,7 +155,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 				postsCount,
 				likes_count,
 				liked_count: 0,
-				driveCapacity: 1073741824, // 1GB
+				driveCapacity: 1024 * 1024 * 8, // 8MiB
 				username: username,
 				usernameLower,
 				host: toUnicode(finger.subject.replace(/^.*?@/, '')),

From 5f8d803d5e5635bb141b2a907cffe718ccc84e8f Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 29 Mar 2018 21:44:39 +0900
Subject: [PATCH 0936/1250] Update show.ts

---
 src/server/api/endpoints/users/show.ts | 18 +++++++-----------
 1 file changed, 7 insertions(+), 11 deletions(-)

diff --git a/src/server/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts
index 1e488dd35..fb6996e2e 100644
--- a/src/server/api/endpoints/users/show.ts
+++ b/src/server/api/endpoints/users/show.ts
@@ -75,18 +75,17 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	// Lookup user
 	if (typeof host === 'string') {
 		const usernameLower = username.toLowerCase();
-		const hostLower_ascii = toASCII(host).toLowerCase();
-		const hostLower = toUnicode(hostLower_ascii);
+		const hostLowerAscii = toASCII(host).toLowerCase();
+		const hostLower = toUnicode(hostLowerAscii);
 
 		user = await findUser({ usernameLower, hostLower });
 
 		if (user === null) {
-			const acct_lower = `${usernameLower}@${hostLower_ascii}`;
+			const acctLower = `${usernameLower}@${hostLowerAscii}`;
 			let activityStreams;
 			let finger;
 			let followersCount;
 			let followingCount;
-			let likes_count;
 			let postsCount;
 
 			if (!validateUsername(username)) {
@@ -94,7 +93,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 			}
 
 			try {
-				finger = await webFingerAndVerify(acct_lower, acct_lower);
+				finger = await webFingerAndVerify(acctLower, acctLower);
 			} catch (exception) {
 				return rej('WebFinger lookup failed');
 			}
@@ -130,12 +129,11 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 			}
 
 			try {
-				[followersCount, followingCount, likes_count, postsCount] = await Promise.all([
+				[followersCount, followingCount, postsCount] = await Promise.all([
 					getCollectionCount(activityStreams.followers),
 					getCollectionCount(activityStreams.following),
-					getCollectionCount(activityStreams.liked),
 					getCollectionCount(activityStreams.outbox),
-					webFingerAndVerify(activityStreams.id, acct_lower),
+					webFingerAndVerify(activityStreams.id, acctLower),
 				]);
 			} catch (exception) {
 				return rej('failed to fetch assets');
@@ -153,10 +151,8 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 				followingCount,
 				name: activityStreams.name,
 				postsCount,
-				likes_count,
-				liked_count: 0,
 				driveCapacity: 1024 * 1024 * 8, // 8MiB
-				username: username,
+				username,
 				usernameLower,
 				host: toUnicode(finger.subject.replace(/^.*?@/, '')),
 				hostLower,

From 299b8c164b5b563ffafe02fabcb234a3dfbf043d Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 29 Mar 2018 21:45:43 +0900
Subject: [PATCH 0937/1250] Update show.ts

---
 src/server/api/endpoints/users/show.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/server/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts
index fb6996e2e..fd51d386b 100644
--- a/src/server/api/endpoints/users/show.ts
+++ b/src/server/api/endpoints/users/show.ts
@@ -66,7 +66,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Get 'host' parameter
 	const [host, hostErr] = $(params.host).optional.string().$;
-	if (hostErr) return rej('invalid username param');
+	if (hostErr) return rej('invalid host param');
 
 	if (userId === undefined && typeof username !== 'string') {
 		return rej('userId or pair of username and host is required');

From 118c712a2d1b421ac3f4c6151fca297eeee3717f Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 30 Mar 2018 00:00:19 +0900
Subject: [PATCH 0938/1250] Use id instead of username

Because username is mutable. id is immutable!
---
 src/models/user.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/models/user.ts b/src/models/user.ts
index 4f2872800..444a3e099 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -306,7 +306,7 @@ export const packForAp = (
 
 	if (!_user) return reject('invalid user arg.');
 
-	const userUrl = `${config.url}/@${_user.username}`;
+	const userUrl = `${config.url}/@@${_user._id}`;
 
 	resolve({
 		"@context": ["https://www.w3.org/ns/activitystreams", {

From d21887ef13a80b617df6346f6cbe6520bac95c17 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 30 Mar 2018 00:05:12 +0900
Subject: [PATCH 0939/1250] Update user.ts

---
 src/models/user.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/models/user.ts b/src/models/user.ts
index 444a3e099..658ae167f 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -319,6 +319,7 @@ export const packForAp = (
 		"liked": `${userUrl}/liked.json`,
 		"inbox": `${userUrl}/inbox.json`,
 		"outbox": `${userUrl}/outbox.json`,
+		"sharedInbox": `${config.url}/inbox`,
 		"preferredUsername": _user.username,
 		"name": _user.name,
 		"summary": _user.description,

From 23ab0a5e50b40f397f342752d1c14e382dace7b9 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 30 Mar 2018 00:13:20 +0900
Subject: [PATCH 0940/1250] Update user.ts

---
 src/models/user.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/models/user.ts b/src/models/user.ts
index 658ae167f..4fbfdec90 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -320,6 +320,7 @@ export const packForAp = (
 		"inbox": `${userUrl}/inbox.json`,
 		"outbox": `${userUrl}/outbox.json`,
 		"sharedInbox": `${config.url}/inbox`,
+		"url": `${config.url}/@${_user.username}`,
 		"preferredUsername": _user.username,
 		"name": _user.name,
 		"summary": _user.description,

From a9e4107416a56920dd4950be73b8e686f261cb70 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 30 Mar 2018 11:24:07 +0900
Subject: [PATCH 0941/1250] cw

---
 src/client/docs/api/endpoints/posts/create.yaml | 6 ++++++
 src/models/post.ts                              | 5 +++++
 src/server/api/endpoints/posts/create.ts        | 7 ++++++-
 3 files changed, 17 insertions(+), 1 deletion(-)

diff --git a/src/client/docs/api/endpoints/posts/create.yaml b/src/client/docs/api/endpoints/posts/create.yaml
index 11d9f40c5..d2d6e27fc 100644
--- a/src/client/docs/api/endpoints/posts/create.yaml
+++ b/src/client/docs/api/endpoints/posts/create.yaml
@@ -11,6 +11,12 @@ params:
     desc:
       ja: "投稿の本文"
       en: "The text of your post"
+  - name: "cw"
+    type: "string"
+    optional: true
+    desc:
+      ja: "コンテンツの警告。このパラメータを指定すると設定したテキストで投稿のコンテンツを隠す事が出来ます。"
+      en: "Content Warning"
   - name: "mediaIds"
     type: "id(DriveFile)[]"
     optional: true
diff --git a/src/models/post.ts b/src/models/post.ts
index 833e59932..9bc0c1d3b 100644
--- a/src/models/post.ts
+++ b/src/models/post.ts
@@ -18,6 +18,10 @@ export function isValidText(text: string): boolean {
 	return text.length <= 1000 && text.trim() != '';
 }
 
+export function isValidCw(text: string): boolean {
+	return text.length <= 100 && text.trim() != '';
+}
+
 export type IPost = {
 	_id: mongo.ObjectID;
 	channelId: mongo.ObjectID;
@@ -27,6 +31,7 @@ export type IPost = {
 	repostId: mongo.ObjectID;
 	poll: any; // todo
 	text: string;
+	cw: string;
 	userId: mongo.ObjectID;
 	appId: mongo.ObjectID;
 	viaMobile: boolean;
diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts
index 6b2957ae6..170b66719 100644
--- a/src/server/api/endpoints/posts/create.ts
+++ b/src/server/api/endpoints/posts/create.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import deepEqual = require('deep-equal');
 import parse from '../../../../common/text';
-import { default as Post, IPost, isValidText } from '../../../../models/post';
+import { default as Post, IPost, isValidText, isValidCw } from '../../../../models/post';
 import { default as User, ILocalAccount, IUser } from '../../../../models/user';
 import { default as Channel, IChannel } from '../../../../models/channel';
 import Following from '../../../../models/following';
@@ -33,6 +33,10 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 	const [text, textErr] = $(params.text).optional.string().pipe(isValidText).$;
 	if (textErr) return rej('invalid text');
 
+	// Get 'cw' parameter
+	const [cw, cwErr] = $(params.cw).optional.string().pipe(isValidCw).$;
+	if (cwErr) return rej('invalid cw');
+
 	// Get 'viaMobile' parameter
 	const [viaMobile = false, viaMobileErr] = $(params.viaMobile).optional.boolean().$;
 	if (viaMobileErr) return rej('invalid viaMobile');
@@ -255,6 +259,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		repostId: repost ? repost._id : undefined,
 		poll: poll,
 		text: text,
+		cw: cw,
 		tags: tags,
 		userId: user._id,
 		appId: app ? app._id : null,

From b7f63201669db4fbd031725c3ec96dfe12c6bf12 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Thu, 29 Mar 2018 21:47:17 +0900
Subject: [PATCH 0942/1250] Fix recommendation query

---
 src/server/api/endpoints/users/recommendation.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/server/api/endpoints/users/recommendation.ts b/src/server/api/endpoints/users/recommendation.ts
index c81533969..60483936f 100644
--- a/src/server/api/endpoints/users/recommendation.ts
+++ b/src/server/api/endpoints/users/recommendation.ts
@@ -36,7 +36,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 						$gte: new Date(Date.now() - ms('7days'))
 					}
 				}, {
-					host: { $not: null }
+					host: { $ne: null }
 				}
 			]
 		}, {

From b60d670dcdda698d9d54b0f78c97de69b89cc483 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Fri, 30 Mar 2018 13:00:05 +0900
Subject: [PATCH 0943/1250] Fix GitHub bot user query

---
 src/server/api/service/github.ts | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts
index 6eacdb747..a2359cfb6 100644
--- a/src/server/api/service/github.ts
+++ b/src/server/api/service/github.ts
@@ -9,7 +9,8 @@ module.exports = async (app: express.Application) => {
 	if (config.github_bot == null) return;
 
 	const bot = await User.findOne({
-		usernameLower: config.github_bot.username.toLowerCase()
+		usernameLower: config.github_bot.username.toLowerCase(),
+		host: null
 	});
 
 	if (bot == null) {

From e85b93d2b88c60256b8b858892fa7a50e43dadc0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 30 Mar 2018 15:09:20 +0900
Subject: [PATCH 0944/1250] Update dependencies :rocket:

---
 package.json | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/package.json b/package.json
index 336e04eae..d1f544f86 100644
--- a/package.json
+++ b/package.json
@@ -52,10 +52,10 @@
 		"@types/gulp-replace": "0.0.31",
 		"@types/gulp-uglify": "3.0.5",
 		"@types/gulp-util": "3.0.34",
-		"@types/inquirer": "0.0.38",
+		"@types/inquirer": "0.0.40",
 		"@types/is-root": "1.0.0",
 		"@types/is-url": "1.2.28",
-		"@types/js-yaml": "3.10.1",
+		"@types/js-yaml": "3.11.0",
 		"@types/license-checker": "15.0.0",
 		"@types/mkdirp": "0.5.2",
 		"@types/mocha": "5.0.0",
@@ -64,7 +64,7 @@
 		"@types/morgan": "1.7.35",
 		"@types/ms": "0.7.30",
 		"@types/multer": "1.3.6",
-		"@types/node": "9.6.0",
+		"@types/node": "9.6.1",
 		"@types/nopt": "3.0.29",
 		"@types/proxy-addr": "2.0.0",
 		"@types/pug": "2.0.4",
@@ -79,10 +79,10 @@
 		"@types/speakeasy": "2.0.2",
 		"@types/tmp": "0.0.33",
 		"@types/uuid": "3.4.3",
-		"@types/webpack": "4.1.2",
+		"@types/webpack": "4.1.3",
 		"@types/webpack-stream": "3.2.10",
 		"@types/websocket": "0.0.38",
-		"@types/ws": "4.0.1",
+		"@types/ws": "4.0.2",
 		"accesses": "2.5.0",
 		"animejs": "2.2.0",
 		"autosize": "4.0.1",
@@ -103,7 +103,7 @@
 		"deep-equal": "1.0.1",
 		"deepcopy": "0.6.3",
 		"diskusage": "0.2.4",
-		"elasticsearch": "14.2.1",
+		"elasticsearch": "14.2.2",
 		"element-ui": "2.3.2",
 		"emojilib": "2.2.12",
 		"escape-regexp": "0.0.1",
@@ -175,7 +175,7 @@
 		"s-age": "1.1.2",
 		"sass-loader": "6.0.7",
 		"seedrandom": "2.4.3",
-		"serve-favicon": "2.4.5",
+		"serve-favicon": "2.5.0",
 		"speakeasy": "2.0.0",
 		"style-loader": "0.20.3",
 		"stylus": "0.54.5",
@@ -206,7 +206,7 @@
 		"vuedraggable": "2.16.0",
 		"web-push": "3.3.0",
 		"webfinger.js": "2.6.6",
-		"webpack": "4.3.0",
+		"webpack": "4.4.1",
 		"webpack-cli": "2.0.13",
 		"webpack-replace-loader": "1.3.0",
 		"websocket": "1.0.25",

From b8dc02a29cbab8caddfdb2f6b1c60499100e1324 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 30 Mar 2018 18:48:51 +0900
Subject: [PATCH 0945/1250] =?UTF-8?q?=E8=BF=94=E4=BF=A1=E5=85=88=E3=83=97?=
 =?UTF-8?q?=E3=83=AC=E3=83=93=E3=83=A5=E3=83=BC=E3=81=AA=E3=81=A9=E3=81=A7?=
 =?UTF-8?q?=E3=81=AFURL=E3=83=97=E3=83=AC=E3=83=93=E3=83=A5=E3=83=BC?=
 =?UTF-8?q?=E3=82=92=E8=A1=A8=E7=A4=BA=E3=81=97=E3=81=AA=E3=81=84=E3=82=88?=
 =?UTF-8?q?=E3=81=86=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../desktop/views/components/sub-post-content.vue  | 14 +-------------
 1 file changed, 1 insertion(+), 13 deletions(-)

diff --git a/src/client/app/desktop/views/components/sub-post-content.vue b/src/client/app/desktop/views/components/sub-post-content.vue
index f13822331..a79e5e0a4 100644
--- a/src/client/app/desktop/views/components/sub-post-content.vue
+++ b/src/client/app/desktop/views/components/sub-post-content.vue
@@ -4,7 +4,6 @@
 		<a class="reply" v-if="post.replyId">%fa:reply%</a>
 		<mk-post-html :ast="post.ast" :i="os.i"/>
 		<a class="rp" v-if="post.repostId" :href="`/post:${post.repostId}`">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>
@@ -21,18 +20,7 @@
 import Vue from 'vue';
 
 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;
-			}
-		}
-	}
+	props: ['post']
 });
 </script>
 

From daa69b86c0c93d7b599037cc0175b640b8d1dc41 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 30 Mar 2018 21:31:51 +0900
Subject: [PATCH 0946/1250] oops

---
 src/client/app/common/define-widget.ts         |  2 +-
 src/client/app/mobile/views/pages/home.vue     | 18 +++++++++---------
 .../api/endpoints/i/update_mobile_home.ts      |  6 +++---
 tools/migration/nighthike/4.js                 |  3 ++-
 4 files changed, 15 insertions(+), 14 deletions(-)

diff --git a/src/client/app/common/define-widget.ts b/src/client/app/common/define-widget.ts
index 27db59b5e..9f8dcfc7e 100644
--- a/src/client/app/common/define-widget.ts
+++ b/src/client/app/common/define-widget.ts
@@ -56,7 +56,7 @@ export default function<T extends object>(data: {
 						id: this.id,
 						data: newProps
 					}).then(() => {
-						(this as any).os.i.account.clientSettings.mobile_home.find(w => w.id == this.id).data = newProps;
+						(this as any).os.i.account.clientSettings.mobileHome.find(w => w.id == this.id).data = newProps;
 					});
 				} else {
 					(this as any).api('i/update_home', {
diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue
index be9101aa7..f1f65f90c 100644
--- a/src/client/app/mobile/views/pages/home.vue
+++ b/src/client/app/mobile/views/pages/home.vue
@@ -82,8 +82,8 @@ export default Vue.extend({
 		};
 	},
 	created() {
-		if ((this as any).os.i.account.clientSettings.mobile_home == null) {
-			Vue.set((this as any).os.i.account.clientSettings, 'mobile_home', [{
+		if ((this as any).os.i.account.clientSettings.mobileHome == null) {
+			Vue.set((this as any).os.i.account.clientSettings, 'mobileHome', [{
 				name: 'calendar',
 				id: 'a', data: {}
 			}, {
@@ -105,14 +105,14 @@ export default Vue.extend({
 				name: 'version',
 				id: 'g', data: {}
 			}]);
-			this.widgets = (this as any).os.i.account.clientSettings.mobile_home;
+			this.widgets = (this as any).os.i.account.clientSettings.mobileHome;
 			this.saveHome();
 		} else {
-			this.widgets = (this as any).os.i.account.clientSettings.mobile_home;
+			this.widgets = (this as any).os.i.account.clientSettings.mobileHome;
 		}
 
 		this.$watch('os.i.account.clientSettings', i => {
-			this.widgets = (this as any).os.i.account.clientSettings.mobile_home;
+			this.widgets = (this as any).os.i.account.clientSettings.mobileHome;
 		}, {
 			deep: true
 		});
@@ -157,15 +157,15 @@ export default Vue.extend({
 		},
 		onHomeUpdated(data) {
 			if (data.home) {
-				(this as any).os.i.account.clientSettings.mobile_home = data.home;
+				(this as any).os.i.account.clientSettings.mobileHome = data.home;
 				this.widgets = data.home;
 			} else {
-				const w = (this as any).os.i.account.clientSettings.mobile_home.find(w => w.id == data.id);
+				const w = (this as any).os.i.account.clientSettings.mobileHome.find(w => w.id == data.id);
 				if (w != null) {
 					w.data = data.data;
 					this.$refs[w.id][0].preventSave = true;
 					this.$refs[w.id][0].props = w.data;
-					this.widgets = (this as any).os.i.account.clientSettings.mobile_home;
+					this.widgets = (this as any).os.i.account.clientSettings.mobileHome;
 				}
 			}
 		},
@@ -194,7 +194,7 @@ export default Vue.extend({
 			this.saveHome();
 		},
 		saveHome() {
-			(this as any).os.i.account.clientSettings.mobile_home = this.widgets;
+			(this as any).os.i.account.clientSettings.mobileHome = this.widgets;
 			(this as any).api('i/update_mobile_home', {
 				home: this.widgets
 			});
diff --git a/src/server/api/endpoints/i/update_mobile_home.ts b/src/server/api/endpoints/i/update_mobile_home.ts
index b06ca108a..6f28cebf9 100644
--- a/src/server/api/endpoints/i/update_mobile_home.ts
+++ b/src/server/api/endpoints/i/update_mobile_home.ts
@@ -25,7 +25,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	if (home) {
 		await User.update(user._id, {
 			$set: {
-				'account.clientSettings.mobile_home': home
+				'account.clientSettings.mobileHome': home
 			}
 		});
 
@@ -37,7 +37,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	} else {
 		if (id == null && data == null) return rej('you need to set id and data params if home param unset');
 
-		const _home = user.account.clientSettings.mobile_home || [];
+		const _home = user.account.clientSettings.mobileHome || [];
 		const widget = _home.find(w => w.id == id);
 
 		if (widget == null) return rej('widget not found');
@@ -46,7 +46,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 		await User.update(user._id, {
 			$set: {
-				'account.clientSettings.mobile_home': _home
+				'account.clientSettings.mobileHome': _home
 			}
 		});
 
diff --git a/tools/migration/nighthike/4.js b/tools/migration/nighthike/4.js
index 2e252b7f4..f308341f0 100644
--- a/tools/migration/nighthike/4.js
+++ b/tools/migration/nighthike/4.js
@@ -227,6 +227,7 @@ db.users.update({}, {
 		'account.twitter.access_token_secret': '',
 		'account.twitter.user_id': '',
 		'account.twitter.screen_name': '',
-		'account.line.user_id': ''
+		'account.line.user_id': '',
+		'account.client_settings.mobile_home': ''
 	}
 }, false, true);

From 58cd4aafd9702d3155536127881c4570a6382d46 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 30 Mar 2018 21:37:01 +0900
Subject: [PATCH 0947/1250] oops

---
 src/client/app/auth/views/form.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/client/app/auth/views/form.vue b/src/client/app/auth/views/form.vue
index 9d9e8cdb1..eb55b9035 100644
--- a/src/client/app/auth/views/form.vue
+++ b/src/client/app/auth/views/form.vue
@@ -2,7 +2,7 @@
 <div class="form">
 	<header>
 		<h1><i>{{ app.name }}</i>があなたのアカウントにアクセスすることを<b>許可</b>しますか?</h1>
-		<img :src="`${app.icon_url}?thumbnail&size=64`"/>
+		<img :src="`${app.iconUrl}?thumbnail&size=64`"/>
 	</header>
 	<div class="app">
 		<section>

From 813ac019c17e5eedccb2ed029509a55e326a964b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 30 Mar 2018 21:55:23 +0900
Subject: [PATCH 0948/1250] =?UTF-8?q?=E3=83=87=E3=83=95=E3=82=A9=E3=83=AB?=
 =?UTF-8?q?=E3=83=88=E3=81=A7=E3=83=89=E3=83=A9=E3=82=A4=E3=83=96=E5=AE=B9?=
 =?UTF-8?q?=E9=87=8F=E3=81=AF128MiB=E3=81=AB=E3=81=97=E3=81=9F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/server/api/private/signup.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts
index fd47b5303..45b978d0b 100644
--- a/src/server/api/private/signup.ts
+++ b/src/server/api/private/signup.ts
@@ -115,7 +115,7 @@ export default async (req: express.Request, res: express.Response) => {
 		followingCount: 0,
 		name: name,
 		postsCount: 0,
-		driveCapacity: 1073741824, // 1GB
+		driveCapacity: 1024 * 1024 * 128, // 128MiB
 		username: username,
 		usernameLower: username.toLowerCase(),
 		host: null,

From 14024f08041ddcfc2c172ddfc43f85d62c04ef2b Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Fri, 30 Mar 2018 16:51:08 +0000
Subject: [PATCH 0949/1250] fix(package): update @types/js-yaml to version
 3.11.1

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index d1f544f86..38e4dc629 100644
--- a/package.json
+++ b/package.json
@@ -55,7 +55,7 @@
 		"@types/inquirer": "0.0.40",
 		"@types/is-root": "1.0.0",
 		"@types/is-url": "1.2.28",
-		"@types/js-yaml": "3.11.0",
+		"@types/js-yaml": "3.11.1",
 		"@types/license-checker": "15.0.0",
 		"@types/mkdirp": "0.5.2",
 		"@types/mocha": "5.0.0",

From a18e5a90a800b7548050862adaa00e3d881703de Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Sat, 31 Mar 2018 19:45:56 +0900
Subject: [PATCH 0950/1250] Set empty array instead of null to mediaIds
 property of posts

---
 src/client/app/desktop/views/components/post-detail.sub.vue  | 2 +-
 src/client/app/desktop/views/components/post-detail.vue      | 2 +-
 src/client/app/desktop/views/components/posts.post.vue       | 2 +-
 src/client/app/desktop/views/components/sub-post-content.vue | 2 +-
 src/client/app/mobile/views/components/post-detail.vue       | 2 +-
 src/client/app/mobile/views/components/post.vue              | 2 +-
 src/client/app/mobile/views/components/sub-post-content.vue  | 2 +-
 src/client/docs/api/entities/post.yaml                       | 4 ++--
 src/server/api/endpoints/posts/create.ts                     | 2 +-
 tools/migration/nighthike/6.js                               | 1 +
 10 files changed, 11 insertions(+), 10 deletions(-)
 create mode 100644 tools/migration/nighthike/6.js

diff --git a/src/client/app/desktop/views/components/post-detail.sub.vue b/src/client/app/desktop/views/components/post-detail.sub.vue
index 35377e7c2..285b5dede 100644
--- a/src/client/app/desktop/views/components/post-detail.sub.vue
+++ b/src/client/app/desktop/views/components/post-detail.sub.vue
@@ -17,7 +17,7 @@
 		</header>
 		<div class="body">
 			<mk-post-html v-if="post.ast" :ast="post.ast" :i="os.i" :class="$style.text"/>
-			<div class="media" v-if="post.media">
+			<div class="media" v-if="post.media > 0">
 				<mk-media-list :media-list="post.media"/>
 			</div>
 		</div>
diff --git a/src/client/app/desktop/views/components/post-detail.vue b/src/client/app/desktop/views/components/post-detail.vue
index 5c7a7dfdb..1811e22ba 100644
--- a/src/client/app/desktop/views/components/post-detail.vue
+++ b/src/client/app/desktop/views/components/post-detail.vue
@@ -39,7 +39,7 @@
 		</header>
 		<div class="body">
 			<mk-post-html :class="$style.text" v-if="p.ast" :ast="p.ast" :i="os.i"/>
-			<div class="media" v-if="p.media">
+			<div class="media" v-if="p.media.length > 0">
 				<mk-media-list :media-list="p.media"/>
 			</div>
 			<mk-poll v-if="p.poll" :post="p"/>
diff --git a/src/client/app/desktop/views/components/posts.post.vue b/src/client/app/desktop/views/components/posts.post.vue
index 37c6e6304..aa1f1db41 100644
--- a/src/client/app/desktop/views/components/posts.post.vue
+++ b/src/client/app/desktop/views/components/posts.post.vue
@@ -41,7 +41,7 @@
 					<mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i" :class="$style.text"/>
 					<a class="rp" v-if="p.repost">RP:</a>
 				</div>
-				<div class="media" v-if="p.media">
+				<div class="media" v-if="p.media.length > 0">
 					<mk-media-list :media-list="p.media"/>
 				</div>
 				<mk-poll v-if="p.poll" :post="p" ref="pollViewer"/>
diff --git a/src/client/app/desktop/views/components/sub-post-content.vue b/src/client/app/desktop/views/components/sub-post-content.vue
index a79e5e0a4..1f5ce3898 100644
--- a/src/client/app/desktop/views/components/sub-post-content.vue
+++ b/src/client/app/desktop/views/components/sub-post-content.vue
@@ -5,7 +5,7 @@
 		<mk-post-html :ast="post.ast" :i="os.i"/>
 		<a class="rp" v-if="post.repostId" :href="`/post:${post.repostId}`">RP: ...</a>
 	</div>
-	<details v-if="post.media">
+	<details v-if="post.media.length > 0">
 		<summary>({{ post.media.length }}つのメディア)</summary>
 		<mk-media-list :media-list="post.media"/>
 	</details>
diff --git a/src/client/app/mobile/views/components/post-detail.vue b/src/client/app/mobile/views/components/post-detail.vue
index f0af1a61a..6411011b8 100644
--- a/src/client/app/mobile/views/components/post-detail.vue
+++ b/src/client/app/mobile/views/components/post-detail.vue
@@ -42,7 +42,7 @@
 			<div class="tags" v-if="p.tags && p.tags.length > 0">
 				<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
 			</div>
-			<div class="media" v-if="p.media">
+			<div class="media" v-if="p.media.length > 0">
 				<mk-media-list :media-list="p.media"/>
 			</div>
 			<mk-poll v-if="p.poll" :post="p"/>
diff --git a/src/client/app/mobile/views/components/post.vue b/src/client/app/mobile/views/components/post.vue
index a01eb7669..52fb09537 100644
--- a/src/client/app/mobile/views/components/post.vue
+++ b/src/client/app/mobile/views/components/post.vue
@@ -40,7 +40,7 @@
 					<mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i" :class="$style.text"/>
 					<a class="rp" v-if="p.repost != null">RP:</a>
 				</div>
-				<div class="media" v-if="p.media">
+				<div class="media" v-if="p.media.length > 0">
 					<mk-media-list :media-list="p.media"/>
 				</div>
 				<mk-poll v-if="p.poll" :post="p" ref="pollViewer"/>
diff --git a/src/client/app/mobile/views/components/sub-post-content.vue b/src/client/app/mobile/views/components/sub-post-content.vue
index b95883de7..5ff88089a 100644
--- a/src/client/app/mobile/views/components/sub-post-content.vue
+++ b/src/client/app/mobile/views/components/sub-post-content.vue
@@ -5,7 +5,7 @@
 		<mk-post-html v-if="post.ast" :ast="post.ast" :i="os.i"/>
 		<a class="rp" v-if="post.repostId">RP: ...</a>
 	</div>
-	<details v-if="post.media">
+	<details v-if="post.media.length > 0">
 		<summary>({{ post.media.length }}個のメディア)</summary>
 		<mk-media-list :media-list="post.media"/>
 	</details>
diff --git a/src/client/docs/api/entities/post.yaml b/src/client/docs/api/entities/post.yaml
index 74d7973e3..da79866ba 100644
--- a/src/client/docs/api/entities/post.yaml
+++ b/src/client/docs/api/entities/post.yaml
@@ -33,8 +33,8 @@ props:
     type: "id(DriveFile)[]"
     optional: true
     desc:
-      ja: "添付されているメディアのID"
-      en: "The IDs of the attached media"
+      ja: "添付されているメディアのID (なければレスポンスでは空配列)"
+      en: "The IDs of the attached media (empty array for response if no media is attached)"
   - name: "media"
     type: "entity(DriveFile)[]"
     optional: true
diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts
index 170b66719..aa7e93c28 100644
--- a/src/server/api/endpoints/posts/create.ts
+++ b/src/server/api/endpoints/posts/create.ts
@@ -254,7 +254,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		createdAt: new Date(),
 		channelId: channel ? channel._id : undefined,
 		index: channel ? channel.index + 1 : undefined,
-		mediaIds: files ? files.map(file => file._id) : undefined,
+		mediaIds: files ? files.map(file => file._id) : [],
 		replyId: reply ? reply._id : undefined,
 		repostId: repost ? repost._id : undefined,
 		poll: poll,
diff --git a/tools/migration/nighthike/6.js b/tools/migration/nighthike/6.js
new file mode 100644
index 000000000..ff78df4e0
--- /dev/null
+++ b/tools/migration/nighthike/6.js
@@ -0,0 +1 @@
+db.posts.update({ mediaIds: null }, { $set: { mediaIds: [] } }, false, true);

From 7696fd9fd2965d943705abefe10f78e270b6272b Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Sat, 31 Mar 2018 19:53:30 +0900
Subject: [PATCH 0951/1250] Store texts as HTML

---
 .../app/common/views/components/index.ts      |   2 +-
 .../components/messaging-room.message.vue     |  33 ++--
 .../app/common/views/components/post-html.ts  | 141 ------------------
 .../app/common/views/components/post-html.vue | 103 +++++++++++++
 .../app/common/views/components/url.vue       |  66 --------
 .../views/components/welcome-timeline.vue     |   2 +-
 .../views/components/post-detail.sub.vue      |   2 +-
 .../desktop/views/components/post-detail.vue  |  27 ++--
 .../desktop/views/components/posts.post.vue   |  31 ++--
 .../views/components/sub-post-content.vue     |   2 +-
 .../mobile/views/components/post-detail.vue   |  27 ++--
 .../app/mobile/views/components/post.vue      |  31 ++--
 .../views/components/sub-post-content.vue     |   2 +-
 src/client/docs/api/entities/post.yaml        |  10 +-
 src/common/text/html.ts                       |  83 +++++++++++
 .../{ => parse}/core/syntax-highlighter.ts    |   0
 src/common/text/{ => parse}/elements/bold.ts  |   0
 src/common/text/{ => parse}/elements/code.ts  |   0
 src/common/text/{ => parse}/elements/emoji.ts |   0
 .../text/{ => parse}/elements/hashtag.ts      |   0
 .../text/{ => parse}/elements/inline-code.ts  |   0
 src/common/text/{ => parse}/elements/link.ts  |   0
 .../text/{ => parse}/elements/mention.ts      |   2 +-
 src/common/text/{ => parse}/elements/quote.ts |   0
 src/common/text/{ => parse}/elements/url.ts   |   0
 src/common/text/{ => parse}/index.ts          |   0
 src/models/messaging-message.ts               |   7 +-
 src/models/post.ts                            |   7 +-
 .../endpoints/messaging/messages/create.ts    |   3 +
 src/server/api/endpoints/posts/create.ts      |   4 +-
 tools/migration/nighthike/7.js                |  16 ++
 31 files changed, 318 insertions(+), 283 deletions(-)
 delete mode 100644 src/client/app/common/views/components/post-html.ts
 create mode 100644 src/client/app/common/views/components/post-html.vue
 delete mode 100644 src/client/app/common/views/components/url.vue
 create mode 100644 src/common/text/html.ts
 rename src/common/text/{ => parse}/core/syntax-highlighter.ts (100%)
 rename src/common/text/{ => parse}/elements/bold.ts (100%)
 rename src/common/text/{ => parse}/elements/code.ts (100%)
 rename src/common/text/{ => parse}/elements/emoji.ts (100%)
 rename src/common/text/{ => parse}/elements/hashtag.ts (100%)
 rename src/common/text/{ => parse}/elements/inline-code.ts (100%)
 rename src/common/text/{ => parse}/elements/link.ts (100%)
 rename src/common/text/{ => parse}/elements/mention.ts (82%)
 rename src/common/text/{ => parse}/elements/quote.ts (100%)
 rename src/common/text/{ => parse}/elements/url.ts (100%)
 rename src/common/text/{ => parse}/index.ts (100%)
 create mode 100644 tools/migration/nighthike/7.js

diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts
index b58ba37ec..8c10bdee2 100644
--- a/src/client/app/common/views/components/index.ts
+++ b/src/client/app/common/views/components/index.ts
@@ -4,7 +4,7 @@ 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';
+import postHtml from './post-html.vue';
 import poll from './poll.vue';
 import pollEditor from './poll-editor.vue';
 import reactionIcon from './reaction-icon.vue';
diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue
index 94f87fd70..25ceab85a 100644
--- a/src/client/app/common/views/components/messaging-room.message.vue
+++ b/src/client/app/common/views/components/messaging-room.message.vue
@@ -4,13 +4,13 @@
 		<img class="avatar" :src="`${message.user.avatarUrl}?thumbnail&size=80`" alt=""/>
 	</router-link>
 	<div class="content">
-		<div class="balloon" :data-no-text="message.text == null">
+		<div class="balloon" :data-no-text="message.textHtml == null">
 			<p class="read" v-if="isMe && message.isRead">%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.isDeleted">
-				<mk-post-html class="text" v-if="message.ast" :ast="message.ast" :i="os.i"/>
+				<mk-post-html class="text" v-if="message.textHtml" ref="text" :html="message.textHtml" :i="os.i"/>
 				<div class="file" v-if="message.file">
 					<a :href="message.file.url" target="_blank" :title="message.file.name">
 						<img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/>
@@ -38,21 +38,32 @@ import getAcct from '../../../../../common/user/get-acct';
 
 export default Vue.extend({
 	props: ['message'],
+	data() {
+		return {
+			urls: []
+		};
+	},
 	computed: {
 		acct() {
 			return getAcct(this.message.user);
 		},
 		isMe(): boolean {
 			return this.message.userId == (this as any).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;
-			}
+		}
+	},
+	watch: {
+		message: {
+			handler(newMessage, oldMessage) {
+				if (!oldMessage || newMessage.textHtml !== oldMessage.textHtml) {
+					this.$nextTick(() => {
+						const elements = this.$refs.text.$el.getElementsByTagName('a');
+
+						this.urls = [].filter.call(elements, ({ origin }) => origin !== location.origin)
+							.map(({ href }) => href);
+					});
+				}
+			},
+			immediate: true
 		}
 	}
 });
diff --git a/src/client/app/common/views/components/post-html.ts b/src/client/app/common/views/components/post-html.ts
deleted file mode 100644
index 39d783aac..000000000
--- a/src/client/app/common/views/components/post-html.ts
+++ /dev/null
@@ -1,141 +0,0 @@
-import Vue from 'vue';
-import * as emojilib from 'emojilib';
-import getAcct from '../../../../../common/user/get-acct';
-import { url } from '../../../config';
-import MkUrl from './url.vue';
-
-const flatten = list => list.reduce(
-	(a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []
-);
-
-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 = flatten((this as any).ast.map(token => {
-			switch (token.type) {
-				case 'text':
-					const text = token.content.replace(/(\r\n|\n|\r)/g, '\n');
-
-					if ((this as any).shouldBreak) {
-						const x = text.split('\n')
-							.map(t => t == '' ? [createElement('br')] : [createElement('span', t), createElement('br')]);
-						x[x.length - 1].pop();
-						return x;
-					} else {
-						return createElement('span', text.replace(/\n/g, ' '));
-					}
-
-				case 'bold':
-					return createElement('strong', token.bold);
-
-				case 'url':
-					return createElement(MkUrl, {
-						props: {
-							url: token.content,
-							target: '_blank'
-						}
-					});
-
-				case 'link':
-					return createElement('a', {
-						attrs: {
-							class: 'link',
-							href: token.url,
-							target: '_blank',
-							title: token.url
-						}
-					}, token.title);
-
-				case 'mention':
-					return (createElement as any)('a', {
-						attrs: {
-							href: `${url}/@${getAcct(token)}`,
-							target: '_blank',
-							dataIsMe: (this as any).i && getAcct((this as any).i) == getAcct(token)
-						},
-						directives: [{
-							name: 'user-preview',
-							value: token.content
-						}]
-					}, token.content);
-
-				case 'hashtag':
-					return createElement('a', {
-						attrs: {
-							href: `${url}/search?q=${token.content}`,
-							target: '_blank'
-						}
-					}, token.content);
-
-				case 'code':
-					return createElement('pre', [
-						createElement('code', {
-							domProps: {
-								innerHTML: token.html
-							}
-						})
-					]);
-
-				case 'inline-code':
-					return createElement('code', {
-						domProps: {
-							innerHTML: token.html
-						}
-					});
-
-				case 'quote':
-					const text2 = token.quote.replace(/(\r\n|\n|\r)/g, '\n');
-
-					if ((this as any).shouldBreak) {
-						const x = text2.split('\n')
-							.map(t => [createElement('span', t), createElement('br')]);
-						x[x.length - 1].pop();
-						return createElement('div', {
-							attrs: {
-								class: 'quote'
-							}
-						}, x);
-					} else {
-						return createElement('span', {
-							attrs: {
-								class: 'quote'
-							}
-						}, text2.replace(/\n/g, ' '));
-					}
-
-				case 'emoji':
-					const emoji = emojilib.lib[token.emoji];
-					return createElement('span', emoji ? emoji.char : token.content);
-
-				default:
-					console.log('unknown ast type:', token.type);
-			}
-		}));
-
-		const _els = [];
-		els.forEach((el, i) => {
-			if (el.tag == 'br') {
-				if (els[i - 1].tag != 'div') {
-					_els.push(el);
-				}
-			} else {
-				_els.push(el);
-			}
-		});
-
-		return createElement('span', _els);
-	}
-});
diff --git a/src/client/app/common/views/components/post-html.vue b/src/client/app/common/views/components/post-html.vue
new file mode 100644
index 000000000..1c949052b
--- /dev/null
+++ b/src/client/app/common/views/components/post-html.vue
@@ -0,0 +1,103 @@
+<template><div class="mk-post-html" v-html="html"></div></template>
+
+<script lang="ts">
+import Vue from 'vue';
+import getAcct from '../../../../../common/user/get-acct';
+import { url } from '../../../config';
+
+function markUrl(a) {
+	while (a.firstChild) {
+		a.removeChild(a.firstChild);
+	}
+
+	const schema = document.createElement('span');
+	const delimiter = document.createTextNode('//');
+	const host = document.createElement('span');
+	const pathname = document.createElement('span');
+	const query = document.createElement('span');
+	const hash = document.createElement('span');
+
+	schema.className = 'schema';
+	schema.textContent = a.protocol;
+
+	host.className = 'host';
+	host.textContent = a.host;
+
+	pathname.className = 'pathname';
+	pathname.textContent = a.pathname;
+
+	query.className = 'query';
+	query.textContent = a.search;
+
+	hash.className = 'hash';
+	hash.textContent = a.hash;
+
+	a.appendChild(schema);
+	a.appendChild(delimiter);
+	a.appendChild(host);
+	a.appendChild(pathname);
+	a.appendChild(query);
+	a.appendChild(hash);
+}
+
+function markMe(me, a) {
+	a.setAttribute("data-is-me", me && `${url}/@${getAcct(me)}` == a.href);
+}
+
+function markTarget(a) {
+	a.setAttribute("target", "_blank");
+}
+
+export default Vue.component('mk-post-html', {
+	props: {
+		html: {
+			type: String,
+			required: true
+		},
+		i: {
+			type: Object,
+			default: null
+		}
+	},
+	watch {
+		html: {
+			handler() {
+				this.$nextTick(() => [].forEach.call(this.$el.getElementsByTagName('a'), a => {
+					if (a.href === a.textContent) {
+						markUrl(a);
+					} else {
+						markMe((this as any).i, a);
+					}
+
+					markTarget(a);
+				}));
+			},
+			immediate: true,
+		}
+	}
+});
+</script>
+
+<style lang="stylus">
+.mk-post-html
+	a
+		word-break break-all
+
+		> .schema
+			opacity 0.5
+
+		> .host
+			font-weight bold
+
+		> .pathname
+			opacity 0.8
+
+		> .query
+			opacity 0.5
+
+		> .hash
+			font-style italic
+
+	p
+		margin 0
+</style>
diff --git a/src/client/app/common/views/components/url.vue b/src/client/app/common/views/components/url.vue
deleted file mode 100644
index 14d4fc82f..000000000
--- a/src/client/app/common/views/components/url.vue
+++ /dev/null
@@ -1,66 +0,0 @@
-<template>
-<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="ts">
-import Vue from 'vue';
-
-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>
-.mk-url
-	word-break break-all
-
-	> [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>
diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue
index 8f6199732..f379029f9 100644
--- a/src/client/app/common/views/components/welcome-timeline.vue
+++ b/src/client/app/common/views/components/welcome-timeline.vue
@@ -15,7 +15,7 @@
 				</div>
 			</header>
 			<div class="text">
-				<mk-post-html :ast="post.ast"/>
+				<mk-post-html :html="post.textHtml"/>
 			</div>
 		</div>
 	</div>
diff --git a/src/client/app/desktop/views/components/post-detail.sub.vue b/src/client/app/desktop/views/components/post-detail.sub.vue
index 285b5dede..b6148d9b2 100644
--- a/src/client/app/desktop/views/components/post-detail.sub.vue
+++ b/src/client/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="os.i" :class="$style.text"/>
+			<mk-post-html v-if="post.textHtml" :html="post.textHtml" :i="os.i" :class="$style.text"/>
 			<div class="media" v-if="post.media > 0">
 				<mk-media-list :media-list="post.media"/>
 			</div>
diff --git a/src/client/app/desktop/views/components/post-detail.vue b/src/client/app/desktop/views/components/post-detail.vue
index 1811e22ba..e75ebe34b 100644
--- a/src/client/app/desktop/views/components/post-detail.vue
+++ b/src/client/app/desktop/views/components/post-detail.vue
@@ -38,7 +38,7 @@
 			</router-link>
 		</header>
 		<div class="body">
-			<mk-post-html :class="$style.text" v-if="p.ast" :ast="p.ast" :i="os.i"/>
+			<mk-post-html :class="$style.text" v-if="p.text" ref="text" :text="p.text" :i="os.i"/>
 			<div class="media" v-if="p.media.length > 0">
 				<mk-media-list :media-list="p.media"/>
 			</div>
@@ -109,6 +109,7 @@ export default Vue.extend({
 			context: [],
 			contextFetching: false,
 			replies: [],
+			urls: []
 		};
 	},
 	computed: {
@@ -130,15 +131,6 @@ export default Vue.extend({
 		},
 		title(): string {
 			return dateStringify(this.p.createdAt);
-		},
-		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() {
@@ -170,6 +162,21 @@ export default Vue.extend({
 			}
 		}
 	},
+	watch: {
+		post: {
+			handler(newPost, oldPost) {
+				if (!oldPost || newPost.text !== oldPost.text) {
+					this.$nextTick(() => {
+						const elements = this.$refs.text.$el.getElementsByTagName('a');
+
+						this.urls = [].filter.call(elements, ({ origin }) => origin !== location.origin)
+							.map(({ href }) => href);
+					});
+				}
+			},
+			immediate: true
+		}
+	},
 	methods: {
 		fetchContext() {
 			this.contextFetching = true;
diff --git a/src/client/app/desktop/views/components/posts.post.vue b/src/client/app/desktop/views/components/posts.post.vue
index aa1f1db41..f3566c81b 100644
--- a/src/client/app/desktop/views/components/posts.post.vue
+++ b/src/client/app/desktop/views/components/posts.post.vue
@@ -38,7 +38,7 @@
 				</p>
 				<div class="text">
 					<a class="reply" v-if="p.reply">%fa:reply%</a>
-					<mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i" :class="$style.text"/>
+					<mk-post-html v-if="p.textHtml" ref="text" :html="p.textHtml" :i="os.i" :class="$style.text"/>
 					<a class="rp" v-if="p.repost">RP:</a>
 				</div>
 				<div class="media" v-if="p.media.length > 0">
@@ -112,7 +112,8 @@ export default Vue.extend({
 		return {
 			isDetailOpened: false,
 			connection: null,
-			connectionId: null
+			connectionId: null,
+			urls: []
 		};
 	},
 	computed: {
@@ -140,15 +141,6 @@ export default Vue.extend({
 		},
 		url(): string {
 			return `/@${this.acct}/${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() {
@@ -190,6 +182,21 @@ export default Vue.extend({
 			(this as any).os.stream.dispose(this.connectionId);
 		}
 	},
+	watch: {
+		post: {
+			handler(newPost, oldPost) {
+				if (!oldPost || newPost.textHtml !== oldPost.textHtml) {
+					this.$nextTick(() => {
+						const elements = this.$refs.text.$el.getElementsByTagName('a');
+
+						this.urls = [].filter.call(elements, ({ origin }) => origin !== location.origin)
+							.map(({ href }) => href);
+					});
+				}
+			},
+			immediate: true
+		}
+	},
 	methods: {
 		capture(withHandler = false) {
 			if ((this as any).os.isSignedIn) {
@@ -450,7 +457,7 @@ export default Vue.extend({
 					font-size 1.1em
 					color #717171
 
-					>>> .quote
+					>>> blockquote
 						margin 8px
 						padding 6px 12px
 						color #aaa
diff --git a/src/client/app/desktop/views/components/sub-post-content.vue b/src/client/app/desktop/views/components/sub-post-content.vue
index 1f5ce3898..58c81e755 100644
--- a/src/client/app/desktop/views/components/sub-post-content.vue
+++ b/src/client/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.replyId">%fa:reply%</a>
-		<mk-post-html :ast="post.ast" :i="os.i"/>
+		<mk-post-html ref="text" :html="post.textHtml" :i="os.i"/>
 		<a class="rp" v-if="post.repostId" :href="`/post:${post.repostId}`">RP: ...</a>
 	</div>
 	<details v-if="post.media.length > 0">
diff --git a/src/client/app/mobile/views/components/post-detail.vue b/src/client/app/mobile/views/components/post-detail.vue
index 6411011b8..77a73426f 100644
--- a/src/client/app/mobile/views/components/post-detail.vue
+++ b/src/client/app/mobile/views/components/post-detail.vue
@@ -38,7 +38,7 @@
 			</div>
 		</header>
 		<div class="body">
-			<mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i" :class="$style.text"/>
+			<mk-post-html v-if="p.text" :ast="p.text" :i="os.i" :class="$style.text"/>
 			<div class="tags" v-if="p.tags && p.tags.length > 0">
 				<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
 			</div>
@@ -103,6 +103,7 @@ export default Vue.extend({
 			context: [],
 			contextFetching: false,
 			replies: [],
+			urls: []
 		};
 	},
 	computed: {
@@ -127,15 +128,6 @@ export default Vue.extend({
 					.map(key => this.p.reactionCounts[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() {
@@ -167,6 +159,21 @@ export default Vue.extend({
 			}
 		}
 	},
+	watch: {
+		post: {
+			handler(newPost, oldPost) {
+				if (!oldPost || newPost.text !== oldPost.text) {
+					this.$nextTick(() => {
+						const elements = this.$refs.text.$el.getElementsByTagName('a');
+
+						this.urls = [].filter.call(elements, ({ origin }) => origin !== location.origin)
+							.map(({ href }) => href);
+					});
+				}
+			},
+			immediate: true
+		}
+	},
 	methods: {
 		fetchContext() {
 			this.contextFetching = true;
diff --git a/src/client/app/mobile/views/components/post.vue b/src/client/app/mobile/views/components/post.vue
index 52fb09537..96ec9632f 100644
--- a/src/client/app/mobile/views/components/post.vue
+++ b/src/client/app/mobile/views/components/post.vue
@@ -37,7 +37,7 @@
 					<a class="reply" v-if="p.reply">
 						%fa:reply%
 					</a>
-					<mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i" :class="$style.text"/>
+					<mk-post-html v-if="p.text" ref="text" :text="p.text" :i="os.i" :class="$style.text"/>
 					<a class="rp" v-if="p.repost != null">RP:</a>
 				</div>
 				<div class="media" v-if="p.media.length > 0">
@@ -90,7 +90,8 @@ export default Vue.extend({
 	data() {
 		return {
 			connection: null,
-			connectionId: null
+			connectionId: null,
+			urls: []
 		};
 	},
 	computed: {
@@ -118,15 +119,6 @@ export default Vue.extend({
 		},
 		url(): string {
 			return `/@${this.pAcct}/${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() {
@@ -168,6 +160,21 @@ export default Vue.extend({
 			(this as any).os.stream.dispose(this.connectionId);
 		}
 	},
+	watch: {
+		post: {
+			handler(newPost, oldPost) {
+				if (!oldPost || newPost.text !== oldPost.text) {
+					this.$nextTick(() => {
+						const elements = this.$refs.text.$el.getElementsByTagName('a');
+
+						this.urls = [].filter.call(elements, ({ origin }) => origin !== location.origin)
+							.map(({ href }) => href);
+					});
+				}
+			},
+			immediate: true
+		}
+	},
 	methods: {
 		capture(withHandler = false) {
 			if ((this as any).os.isSignedIn) {
@@ -389,7 +396,7 @@ export default Vue.extend({
 					font-size 1.1em
 					color #717171
 
-					>>> .quote
+					>>> blockquote
 						margin 8px
 						padding 6px 12px
 						color #aaa
diff --git a/src/client/app/mobile/views/components/sub-post-content.vue b/src/client/app/mobile/views/components/sub-post-content.vue
index 5ff88089a..955bb406b 100644
--- a/src/client/app/mobile/views/components/sub-post-content.vue
+++ b/src/client/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.replyId">%fa:reply%</a>
-		<mk-post-html v-if="post.ast" :ast="post.ast" :i="os.i"/>
+		<mk-post-html v-if="post.text" :ast="post.text" :i="os.i"/>
 		<a class="rp" v-if="post.repostId">RP: ...</a>
 	</div>
 	<details v-if="post.media.length > 0">
diff --git a/src/client/docs/api/entities/post.yaml b/src/client/docs/api/entities/post.yaml
index da79866ba..707770012 100644
--- a/src/client/docs/api/entities/post.yaml
+++ b/src/client/docs/api/entities/post.yaml
@@ -27,8 +27,14 @@ props:
     type: "string"
     optional: true
     desc:
-      ja: "投稿の本文"
-      en: "The text of this post"
+      ja: "投稿の本文 (ローカルの場合Markdown風のフォーマット)"
+      en: "The text of this post (in Markdown like format if local)"
+  - name: "textHtml"
+    type: "string"
+    optional: true
+    desc:
+      ja: "投稿の本文 (HTML) (投稿時は無視)"
+      en: "The text of this post (in HTML. Ignored when posting.)"
   - name: "mediaIds"
     type: "id(DriveFile)[]"
     optional: true
diff --git a/src/common/text/html.ts b/src/common/text/html.ts
new file mode 100644
index 000000000..797f3b3f3
--- /dev/null
+++ b/src/common/text/html.ts
@@ -0,0 +1,83 @@
+import { lib as emojilib } from 'emojilib';
+import { JSDOM } from 'jsdom';
+
+const handlers = {
+	bold({ document }, { bold }) {
+		const b = document.createElement('b');
+		b.textContent = bold;
+		document.body.appendChild(b);
+	},
+
+	code({ document }, { code }) {
+		const pre = document.createElement('pre');
+		const inner = document.createElement('code');
+		inner.innerHTML = code;
+		pre.appendChild(inner);
+		document.body.appendChild(pre);
+	},
+
+	emoji({ document }, { content, emoji }) {
+		const found = emojilib[emoji];
+		const node = document.createTextNode(found ? found.char : content);
+		document.body.appendChild(node);
+	},
+
+	hashtag({ document }, { hashtag }) {
+		const a = document.createElement('a');
+		a.href = '/search?q=#' + hashtag;
+		a.textContent = hashtag;
+	},
+
+	'inline-code'({ document }, { code }) {
+		const element = document.createElement('code');
+		element.textContent = code;
+		document.body.appendChild(element);
+	},
+
+	link({ document }, { url, title }) {
+		const a = document.createElement('a');
+		a.href = url;
+		a.textContent = title;
+		document.body.appendChild(a);
+	},
+
+	mention({ document }, { content }) {
+		const a = document.createElement('a');
+		a.href = '/' + content;
+		a.textContent = content;
+		document.body.appendChild(a);
+	},
+
+	quote({ document }, { quote }) {
+		const blockquote = document.createElement('blockquote');
+		blockquote.textContent = quote;
+		document.body.appendChild(blockquote);
+	},
+
+	text({ document }, { content }) {
+		for (const text of content.split('\n')) {
+			const node = document.createTextNode(text);
+			document.body.appendChild(node);
+
+			const br = document.createElement('br');
+			document.body.appendChild(br);
+		}
+	},
+
+	url({ document }, { url }) {
+		const a = document.createElement('a');
+		a.href = url;
+		a.textContent = url;
+		document.body.appendChild(a);
+	}
+};
+
+export default tokens => {
+	const { window } = new JSDOM('');
+
+	for (const token of tokens) {
+		handlers[token.type](window, token);
+	}
+
+	return `<p>${window.document.body.innerHTML}</p>`;
+};
diff --git a/src/common/text/core/syntax-highlighter.ts b/src/common/text/parse/core/syntax-highlighter.ts
similarity index 100%
rename from src/common/text/core/syntax-highlighter.ts
rename to src/common/text/parse/core/syntax-highlighter.ts
diff --git a/src/common/text/elements/bold.ts b/src/common/text/parse/elements/bold.ts
similarity index 100%
rename from src/common/text/elements/bold.ts
rename to src/common/text/parse/elements/bold.ts
diff --git a/src/common/text/elements/code.ts b/src/common/text/parse/elements/code.ts
similarity index 100%
rename from src/common/text/elements/code.ts
rename to src/common/text/parse/elements/code.ts
diff --git a/src/common/text/elements/emoji.ts b/src/common/text/parse/elements/emoji.ts
similarity index 100%
rename from src/common/text/elements/emoji.ts
rename to src/common/text/parse/elements/emoji.ts
diff --git a/src/common/text/elements/hashtag.ts b/src/common/text/parse/elements/hashtag.ts
similarity index 100%
rename from src/common/text/elements/hashtag.ts
rename to src/common/text/parse/elements/hashtag.ts
diff --git a/src/common/text/elements/inline-code.ts b/src/common/text/parse/elements/inline-code.ts
similarity index 100%
rename from src/common/text/elements/inline-code.ts
rename to src/common/text/parse/elements/inline-code.ts
diff --git a/src/common/text/elements/link.ts b/src/common/text/parse/elements/link.ts
similarity index 100%
rename from src/common/text/elements/link.ts
rename to src/common/text/parse/elements/link.ts
diff --git a/src/common/text/elements/mention.ts b/src/common/text/parse/elements/mention.ts
similarity index 82%
rename from src/common/text/elements/mention.ts
rename to src/common/text/parse/elements/mention.ts
index d05a76649..2025dfdaa 100644
--- a/src/common/text/elements/mention.ts
+++ b/src/common/text/parse/elements/mention.ts
@@ -1,7 +1,7 @@
 /**
  * Mention
  */
-import parseAcct from '../../../common/user/parse-acct';
+import parseAcct from '../../../../common/user/parse-acct';
 
 module.exports = text => {
 	const match = text.match(/^(?:@[a-zA-Z0-9\-]+){1,2}/);
diff --git a/src/common/text/elements/quote.ts b/src/common/text/parse/elements/quote.ts
similarity index 100%
rename from src/common/text/elements/quote.ts
rename to src/common/text/parse/elements/quote.ts
diff --git a/src/common/text/elements/url.ts b/src/common/text/parse/elements/url.ts
similarity index 100%
rename from src/common/text/elements/url.ts
rename to src/common/text/parse/elements/url.ts
diff --git a/src/common/text/index.ts b/src/common/text/parse/index.ts
similarity index 100%
rename from src/common/text/index.ts
rename to src/common/text/parse/index.ts
diff --git a/src/models/messaging-message.ts b/src/models/messaging-message.ts
index 8bee657c3..974ee54ab 100644
--- a/src/models/messaging-message.ts
+++ b/src/models/messaging-message.ts
@@ -3,7 +3,6 @@ import deepcopy = require('deepcopy');
 import { pack as packUser } from './user';
 import { pack as packFile } from './drive-file';
 import db from '../db/mongodb';
-import parse from '../common/text';
 
 const MessagingMessage = db.get<IMessagingMessage>('messagingMessages');
 export default MessagingMessage;
@@ -12,6 +11,7 @@ export interface IMessagingMessage {
 	_id: mongo.ObjectID;
 	createdAt: Date;
 	text: string;
+	textHtml: string;
 	userId: mongo.ObjectID;
 	recipientId: mongo.ObjectID;
 	isRead: boolean;
@@ -60,11 +60,6 @@ export const pack = (
 	_message.id = _message._id;
 	delete _message._id;
 
-	// Parse text
-	if (_message.text) {
-		_message.ast = parse(_message.text);
-	}
-
 	// Populate user
 	_message.user = await packUser(_message.userId, me);
 
diff --git a/src/models/post.ts b/src/models/post.ts
index 9bc0c1d3b..6c853e4f8 100644
--- a/src/models/post.ts
+++ b/src/models/post.ts
@@ -8,7 +8,6 @@ import { pack as packChannel } from './channel';
 import Vote from './poll-vote';
 import Reaction from './post-reaction';
 import { pack as packFile } from './drive-file';
-import parse from '../common/text';
 
 const Post = db.get<IPost>('posts');
 
@@ -31,6 +30,7 @@ export type IPost = {
 	repostId: mongo.ObjectID;
 	poll: any; // todo
 	text: string;
+	textHtml: string;
 	cw: string;
 	userId: mongo.ObjectID;
 	appId: mongo.ObjectID;
@@ -103,11 +103,6 @@ export const pack = async (
 	delete _post.mentions;
 	if (_post.geo) delete _post.geo.type;
 
-	// Parse text
-	if (_post.text) {
-		_post.ast = parse(_post.text);
-	}
-
 	// Populate user
 	_post.user = packUser(_post.userId, meId);
 
diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts
index d8ffa9fde..3d3b204da 100644
--- a/src/server/api/endpoints/messaging/messages/create.ts
+++ b/src/server/api/endpoints/messaging/messages/create.ts
@@ -11,6 +11,8 @@ import DriveFile from '../../../../../models/drive-file';
 import { pack } from '../../../../../models/messaging-message';
 import publishUserStream from '../../../event';
 import { publishMessagingStream, publishMessagingIndexStream, pushSw } from '../../../event';
+import html from '../../../../../common/text/html';
+import parse from '../../../../../common/text/parse';
 import config from '../../../../../conf';
 
 /**
@@ -74,6 +76,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		fileId: file ? file._id : undefined,
 		recipientId: recipient._id,
 		text: text ? text : undefined,
+		textHtml: text ? html(parse(text)) : undefined,
 		userId: user._id,
 		isRead: false
 	});
diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts
index aa7e93c28..5342f7772 100644
--- a/src/server/api/endpoints/posts/create.ts
+++ b/src/server/api/endpoints/posts/create.ts
@@ -3,7 +3,8 @@
  */
 import $ from 'cafy';
 import deepEqual = require('deep-equal');
-import parse from '../../../../common/text';
+import html from '../../../../common/text/html';
+import parse from '../../../../common/text/parse';
 import { default as Post, IPost, isValidText, isValidCw } from '../../../../models/post';
 import { default as User, ILocalAccount, IUser } from '../../../../models/user';
 import { default as Channel, IChannel } from '../../../../models/channel';
@@ -259,6 +260,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		repostId: repost ? repost._id : undefined,
 		poll: poll,
 		text: text,
+		textHtml: tokens === null ? null : html(tokens),
 		cw: cw,
 		tags: tags,
 		userId: user._id,
diff --git a/tools/migration/nighthike/7.js b/tools/migration/nighthike/7.js
new file mode 100644
index 000000000..c5055da8b
--- /dev/null
+++ b/tools/migration/nighthike/7.js
@@ -0,0 +1,16 @@
+// for Node.js interpretation
+
+const Message = require('../../../built/models/messaging-message').default;
+const Post = require('../../../built/models/post').default;
+const html = require('../../../built/common/text/html').default;
+const parse = require('../../../built/common/text/parse').default;
+
+Promise.all([Message, Post].map(async model => {
+	const documents = await model.find();
+
+	return Promise.all(documents.map(({ _id, text }) => model.update(_id, {
+		$set: {
+			textHtml: html(parse(text))
+		}
+	})));
+})).catch(console.error).then(process.exit);

From d84e0265e5250342a82299c7d117c0545c9beb16 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Sat, 31 Mar 2018 19:55:00 +0900
Subject: [PATCH 0952/1250] Implement remote status retrieval

---
 package.json                                  |   1 +
 src/{server/api => }/common/drive/add-file.ts |  12 +-
 .../api => }/common/drive/upload_from_url.ts  |   2 +-
 src/{server/api => common}/event.ts           |   4 +-
 src/{server/api => }/common/push-sw.ts        |   4 +-
 src/common/remote/activitypub/act/create.ts   |   9 +
 src/common/remote/activitypub/act/index.ts    |  22 +++
 src/common/remote/activitypub/create.ts       |  86 ++++++++++
 .../remote/activitypub/resolve-person.ts      | 104 +++++++++++
 src/common/remote/activitypub/resolver.ts     |  97 +++++++++++
 src/common/remote/activitypub/type.ts         |   3 +
 src/common/remote/resolve-user.ts             |  26 +++
 src/common/remote/webfinger.ts                |  25 +++
 src/models/remote-user-object.ts              |  15 ++
 src/models/user.ts                            |   3 +
 src/processor/http/index.ts                   |   9 +
 src/processor/http/perform-activitypub.ts     |   6 +
 .../{ => http}/report-github-failure.ts       |   4 +-
 src/processor/index.ts                        |  13 +-
 src/server/api/common/notify.ts               |   2 +-
 .../api/common/read-messaging-message.ts      |   6 +-
 src/server/api/common/read-notification.ts    |   2 +-
 .../api/endpoints/drive/files/create.ts       |   2 +-
 .../api/endpoints/drive/files/update.ts       |   2 +-
 .../endpoints/drive/files/upload_from_url.ts  |   2 +-
 .../api/endpoints/drive/folders/create.ts     |   2 +-
 .../api/endpoints/drive/folders/update.ts     |   2 +-
 src/server/api/endpoints/following/create.ts  |   2 +-
 src/server/api/endpoints/following/delete.ts  |   2 +-
 .../api/endpoints/i/regenerate_token.ts       |   2 +-
 src/server/api/endpoints/i/update.ts          |   2 +-
 .../api/endpoints/i/update_client_setting.ts  |   2 +-
 src/server/api/endpoints/i/update_home.ts     |   2 +-
 .../api/endpoints/i/update_mobile_home.ts     |   2 +-
 .../endpoints/messaging/messages/create.ts    |   4 +-
 .../notifications/mark_as_read_all.ts         |   2 +-
 src/server/api/endpoints/othello/match.ts     |   2 +-
 src/server/api/endpoints/posts/create.ts      |   2 +-
 src/server/api/endpoints/posts/polls/vote.ts  |   2 +-
 .../api/endpoints/posts/reactions/create.ts   |   2 +-
 src/server/api/endpoints/users/show.ts        | 162 +-----------------
 src/server/api/private/signin.ts              |   2 +-
 src/server/api/service/github.ts              |   3 +-
 src/server/api/service/twitter.ts             |   2 +-
 src/server/api/stream/othello-game.ts         |   2 +-
 src/server/api/stream/othello.ts              |   2 +-
 46 files changed, 468 insertions(+), 198 deletions(-)
 rename src/{server/api => }/common/drive/add-file.ts (95%)
 rename src/{server/api => }/common/drive/upload_from_url.ts (92%)
 rename src/{server/api => common}/event.ts (97%)
 rename src/{server/api => }/common/push-sw.ts (92%)
 create mode 100644 src/common/remote/activitypub/act/create.ts
 create mode 100644 src/common/remote/activitypub/act/index.ts
 create mode 100644 src/common/remote/activitypub/create.ts
 create mode 100644 src/common/remote/activitypub/resolve-person.ts
 create mode 100644 src/common/remote/activitypub/resolver.ts
 create mode 100644 src/common/remote/activitypub/type.ts
 create mode 100644 src/common/remote/resolve-user.ts
 create mode 100644 src/common/remote/webfinger.ts
 create mode 100644 src/models/remote-user-object.ts
 create mode 100644 src/processor/http/index.ts
 create mode 100644 src/processor/http/perform-activitypub.ts
 rename src/processor/{ => http}/report-github-failure.ts (87%)

diff --git a/package.json b/package.json
index d1f544f86..4275c1c1c 100644
--- a/package.json
+++ b/package.json
@@ -103,6 +103,7 @@
 		"deep-equal": "1.0.1",
 		"deepcopy": "0.6.3",
 		"diskusage": "0.2.4",
+		"dompurify": "^1.0.3",
 		"elasticsearch": "14.2.2",
 		"element-ui": "2.3.2",
 		"emojilib": "2.2.12",
diff --git a/src/server/api/common/drive/add-file.ts b/src/common/drive/add-file.ts
similarity index 95%
rename from src/server/api/common/drive/add-file.ts
rename to src/common/drive/add-file.ts
index 4551f5574..52a7713dd 100644
--- a/src/server/api/common/drive/add-file.ts
+++ b/src/common/drive/add-file.ts
@@ -10,12 +10,12 @@ import * as debug from 'debug';
 import fileType = require('file-type');
 import prominence = require('prominence');
 
-import DriveFile, { getGridFSBucket } from '../../../../models/drive-file';
-import DriveFolder from '../../../../models/drive-folder';
-import { pack } from '../../../../models/drive-file';
-import event, { publishDriveStream } from '../../event';
-import getAcct from '../../../../common/user/get-acct';
-import config from '../../../../conf';
+import DriveFile, { getGridFSBucket } from '../../models/drive-file';
+import DriveFolder from '../../models/drive-folder';
+import { pack } from '../../models/drive-file';
+import event, { publishDriveStream } from '../event';
+import getAcct from '../user/get-acct';
+import config from '../../conf';
 
 const gm = _gm.subClass({
 	imageMagick: true
diff --git a/src/server/api/common/drive/upload_from_url.ts b/src/common/drive/upload_from_url.ts
similarity index 92%
rename from src/server/api/common/drive/upload_from_url.ts
rename to src/common/drive/upload_from_url.ts
index b825e4c53..5dd969593 100644
--- a/src/server/api/common/drive/upload_from_url.ts
+++ b/src/common/drive/upload_from_url.ts
@@ -1,5 +1,5 @@
 import * as URL from 'url';
-import { IDriveFile, validateFileName } from '../../../../models/drive-file';
+import { IDriveFile, validateFileName } from '../../models/drive-file';
 import create from './add-file';
 import * as debug from 'debug';
 import * as tmp from 'tmp';
diff --git a/src/server/api/event.ts b/src/common/event.ts
similarity index 97%
rename from src/server/api/event.ts
rename to src/common/event.ts
index 98bf16113..53520f11c 100644
--- a/src/server/api/event.ts
+++ b/src/common/event.ts
@@ -1,7 +1,7 @@
 import * as mongo from 'mongodb';
 import * as redis from 'redis';
-import swPush from './common/push-sw';
-import config from '../../conf';
+import swPush from './push-sw';
+import config from '../conf';
 
 type ID = string | mongo.ObjectID;
 
diff --git a/src/server/api/common/push-sw.ts b/src/common/push-sw.ts
similarity index 92%
rename from src/server/api/common/push-sw.ts
rename to src/common/push-sw.ts
index 13227af8d..44c328e83 100644
--- a/src/server/api/common/push-sw.ts
+++ b/src/common/push-sw.ts
@@ -1,7 +1,7 @@
 const push = require('web-push');
 import * as mongo from 'mongodb';
-import Subscription from '../../../models/sw-subscription';
-import config from '../../../conf';
+import Subscription from '../models/sw-subscription';
+import config from '../conf';
 
 if (config.sw) {
 	// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録
diff --git a/src/common/remote/activitypub/act/create.ts b/src/common/remote/activitypub/act/create.ts
new file mode 100644
index 000000000..6c62f7ab9
--- /dev/null
+++ b/src/common/remote/activitypub/act/create.ts
@@ -0,0 +1,9 @@
+import create from '../create';
+
+export default (resolver, actor, activity) => {
+	if ('actor' in activity && actor.account.uri !== activity.actor) {
+		throw new Error;
+	}
+
+	return create(resolver, actor, activity.object);
+};
diff --git a/src/common/remote/activitypub/act/index.ts b/src/common/remote/activitypub/act/index.ts
new file mode 100644
index 000000000..0f4084a61
--- /dev/null
+++ b/src/common/remote/activitypub/act/index.ts
@@ -0,0 +1,22 @@
+import create from './create';
+import createObject from '../create';
+import Resolver from '../resolver';
+
+export default (actor, value) => {
+	return (new Resolver).resolve(value).then(resolved => Promise.all(resolved.map(async asyncResult => {
+		const { resolver, object } = await asyncResult;
+		const created = await (await createObject(resolver, actor, [object]))[0];
+
+		if (created !== null) {
+			return created;
+		}
+
+		switch (object.type) {
+		case 'Create':
+			return create(resolver, actor, object);
+
+		default:
+			return null;
+		}
+	})));
+}
diff --git a/src/common/remote/activitypub/create.ts b/src/common/remote/activitypub/create.ts
new file mode 100644
index 000000000..4aaaeb306
--- /dev/null
+++ b/src/common/remote/activitypub/create.ts
@@ -0,0 +1,86 @@
+import { JSDOM } from 'jsdom';
+import config from '../../../conf';
+import Post from '../../../models/post';
+import RemoteUserObject, { IRemoteUserObject } from '../../../models/remote-user-object';
+import uploadFromUrl from '../../drive/upload_from_url';
+const createDOMPurify = require('dompurify');
+
+function createRemoteUserObject($ref, $id, { id }) {
+	const object = { $ref, $id };
+
+	if (!id) {
+		return { object };
+	}
+
+	return RemoteUserObject.insert({ uri: id, object });
+}
+
+async function createImage(actor, object) {
+	if ('attributedTo' in object && actor.account.uri !== object.attributedTo) {
+		throw new Error;
+	}
+
+	const { _id } = await uploadFromUrl(object.url, actor);
+	return createRemoteUserObject('driveFiles.files', _id, object);
+}
+
+async function createNote(resolver, actor, object) {
+	if ('attributedTo' in object && actor.account.uri !== object.attributedTo) {
+		throw new Error;
+	}
+
+	const mediaIds = 'attachment' in object &&
+		(await Promise.all(await create(resolver, actor, object.attachment)))
+			.filter(media => media !== null && media.object.$ref === 'driveFiles.files')
+			.map(({ object }) => object.$id);
+
+	const { window } = new JSDOM(object.content);
+
+	const { _id } = await Post.insert({
+		channelId: undefined,
+		index: undefined,
+		createdAt: new Date(object.published),
+		mediaIds,
+		replyId: undefined,
+		repostId: undefined,
+		poll: undefined,
+		text: window.document.body.textContent,
+		textHtml: object.content && createDOMPurify(window).sanitize(object.content),
+		userId: actor._id,
+		appId: null,
+		viaMobile: false,
+		geo: undefined
+	});
+
+	// Register to search database
+	if (object.content && config.elasticsearch.enable) {
+		const es = require('../../db/elasticsearch');
+
+		es.index({
+			index: 'misskey',
+			type: 'post',
+			id: _id.toString(),
+			body: {
+				text: window.document.body.textContent
+			}
+		});
+	}
+
+	return createRemoteUserObject('posts', _id, object);
+}
+
+export default async function create(parentResolver, actor, value): Promise<Promise<IRemoteUserObject>[]> {
+	const results = await parentResolver.resolveRemoteUserObjects(value);
+
+	return results.map(asyncResult => asyncResult.then(({ resolver, object }) => {
+		switch (object.type) {
+		case 'Image':
+			return createImage(actor, object);
+
+		case 'Note':
+			return createNote(resolver, actor, object);
+		}
+
+		return null;
+	}));
+};
diff --git a/src/common/remote/activitypub/resolve-person.ts b/src/common/remote/activitypub/resolve-person.ts
new file mode 100644
index 000000000..c7c131b0e
--- /dev/null
+++ b/src/common/remote/activitypub/resolve-person.ts
@@ -0,0 +1,104 @@
+import { JSDOM } from 'jsdom';
+import { toUnicode } from 'punycode';
+import User, { validateUsername, isValidName, isValidDescription } from '../../../models/user';
+import queue from '../../../queue';
+import webFinger from '../webfinger';
+import create from './create';
+import Resolver from './resolver';
+
+async function isCollection(collection) {
+	return ['Collection', 'OrderedCollection'].includes(collection.type);
+}
+
+export default async (value, usernameLower, hostLower, acctLower) => {
+	if (!validateUsername(usernameLower)) {
+		throw new Error;
+	}
+
+	const { resolver, object } = await (new Resolver).resolveOne(value);
+
+	if (
+		object === null ||
+		object.type !== 'Person' ||
+		typeof object.preferredUsername !== 'string' ||
+		object.preferredUsername.toLowerCase() !== usernameLower ||
+		!isValidName(object.name) ||
+		!isValidDescription(object.summary)
+	) {
+		throw new Error;
+	}
+
+	const [followers, following, outbox, finger] = await Promise.all([
+		resolver.resolveOne(object.followers).then(
+			resolved => isCollection(resolved.object) ? resolved.object : null,
+			() => null
+		),
+		resolver.resolveOne(object.following).then(
+			resolved => isCollection(resolved.object) ? resolved.object : null,
+			() => null
+		),
+		resolver.resolveOne(object.outbox).then(
+			resolved => isCollection(resolved.object) ? resolved.object : null,
+			() => null
+		),
+		webFinger(object.id, acctLower),
+	]);
+
+	const summaryDOM = JSDOM.fragment(object.summary);
+
+	// Create user
+	const user = await User.insert({
+		avatarId: null,
+		bannerId: null,
+		createdAt: Date.parse(object.published),
+		description: summaryDOM.textContent,
+		followersCount: followers.totalItem,
+		followingCount: following.totalItem,
+		name: object.name,
+		postsCount: outbox.totalItem,
+		driveCapacity: 1024 * 1024 * 8, // 8MiB
+		username: object.preferredUsername,
+		usernameLower,
+		host: toUnicode(finger.subject.replace(/^.*?@/, '')),
+		hostLower,
+		account: {
+			uri: object.id,
+		},
+	});
+
+	queue.create('http', {
+		type: 'performActivityPub',
+		actor: user._id,
+		outbox
+	}).save();
+
+	const [avatarId, bannerId] = await Promise.all([
+		object.icon,
+		object.image
+	].map(async value => {
+		if (value === undefined) {
+			return null;
+		}
+
+		try {
+			const created = await create(resolver, user, value);
+
+			await Promise.all(created.map(asyncCreated => asyncCreated.then(created => {
+				if (created !== null && created.object.$ref === 'driveFiles.files') {
+					throw created.object.$id;
+				}
+			}, () => {})));
+
+			return null;
+		} catch (id) {
+			return id;
+		}
+	}));
+
+	User.update({ _id: user._id }, { $set: { avatarId, bannerId } });
+
+	user.avatarId = avatarId;
+	user.bannerId = bannerId;
+
+	return user;
+};
diff --git a/src/common/remote/activitypub/resolver.ts b/src/common/remote/activitypub/resolver.ts
new file mode 100644
index 000000000..50ac1b0b1
--- /dev/null
+++ b/src/common/remote/activitypub/resolver.ts
@@ -0,0 +1,97 @@
+import RemoteUserObject from '../../../models/remote-user-object';
+import { IObject } from './type';
+const request = require('request-promise-native');
+
+type IResult = {
+  resolver: Resolver;
+  object: IObject;
+};
+
+async function resolveUnrequestedOne(this: Resolver, value) {
+	if (typeof value !== 'string') {
+		return { resolver: this, object: value };
+	}
+
+	const resolver = new Resolver(this.requesting);
+
+	resolver.requesting.add(value);
+
+	const object = await request({
+		url: value,
+		headers: {
+			Accept: 'application/activity+json, application/ld+json'
+		},
+		json: true
+	});
+
+	if (object === null || (
+		Array.isArray(object['@context']) ?
+			!object['@context'].includes('https://www.w3.org/ns/activitystreams') :
+			object['@context'] !== 'https://www.w3.org/ns/activitystreams'
+	)) {
+		throw new Error;
+	}
+
+	return { resolver, object };
+}
+
+async function resolveCollection(this: Resolver, value) {
+	if (Array.isArray(value)) {
+		return value;
+	}
+
+	const resolved = typeof value === 'string' ?
+		await resolveUnrequestedOne.call(this, value) :
+		value;
+
+	switch (resolved.type) {
+	case 'Collection':
+		return resolved.items;
+
+	case 'OrderedCollection':
+		return resolved.orderedItems;
+
+	default:
+		return [resolved];
+	}
+}
+
+export default class Resolver {
+	requesting: Set<string>;
+
+	constructor(iterable?: Iterable<string>) {
+		this.requesting = new Set(iterable);
+	}
+
+	async resolve(value): Promise<Promise<IResult>[]> {
+		const collection = await resolveCollection.call(this, value);
+
+		return collection
+			.filter(element => !this.requesting.has(element))
+			.map(resolveUnrequestedOne.bind(this));
+	}
+
+	resolveOne(value) {
+		if (this.requesting.has(value)) {
+			throw new Error;
+		}
+
+		return resolveUnrequestedOne.call(this, value);
+	}
+
+	async resolveRemoteUserObjects(value) {
+		const collection = await resolveCollection.call(this, value);
+
+		return collection.filter(element => !this.requesting.has(element)).map(element => {
+			if (typeof element === 'string') {
+				const object = RemoteUserObject.findOne({ uri: element });
+
+				if (object !== null) {
+					return object;
+				}
+			}
+
+			return resolveUnrequestedOne.call(this, element);
+		});
+	}
+}
diff --git a/src/common/remote/activitypub/type.ts b/src/common/remote/activitypub/type.ts
new file mode 100644
index 000000000..5c4750e14
--- /dev/null
+++ b/src/common/remote/activitypub/type.ts
@@ -0,0 +1,3 @@
+export type IObject = {
+	type: string;
+}
diff --git a/src/common/remote/resolve-user.ts b/src/common/remote/resolve-user.ts
new file mode 100644
index 000000000..13d155830
--- /dev/null
+++ b/src/common/remote/resolve-user.ts
@@ -0,0 +1,26 @@
+import { toUnicode, toASCII } from 'punycode';
+import User from '../../models/user';
+import resolvePerson from './activitypub/resolve-person';
+import webFinger from './webfinger';
+
+export default async (username, host, option) => {
+	const usernameLower = username.toLowerCase();
+	const hostLowerAscii = toASCII(host).toLowerCase();
+	const hostLower = toUnicode(hostLowerAscii);
+
+	let user = await User.findOne({ usernameLower, hostLower }, option);
+
+	if (user === null) {
+		const acctLower = `${usernameLower}@${hostLowerAscii}`;
+
+		const finger = await webFinger(acctLower, acctLower);
+		const self = finger.links.find(link => link.rel && link.rel.toLowerCase() === 'self');
+		if (!self) {
+			throw new Error;
+		}
+
+		user = await resolvePerson(self.href, usernameLower, hostLower, acctLower);
+	}
+
+	return user;
+};
diff --git a/src/common/remote/webfinger.ts b/src/common/remote/webfinger.ts
new file mode 100644
index 000000000..23f0aaa55
--- /dev/null
+++ b/src/common/remote/webfinger.ts
@@ -0,0 +1,25 @@
+const WebFinger = require('webfinger.js');
+
+const webFinger = new WebFinger({});
+
+type ILink = {
+  href: string;
+  rel: string;
+}
+
+type IWebFinger = {
+  links: Array<ILink>;
+  subject: string;
+}
+
+export default (query, verifier): Promise<IWebFinger> => new Promise((res, rej) => webFinger.lookup(query, (error, result) => {
+	if (error) {
+		return rej(error);
+	}
+
+	if (result.object.subject.toLowerCase().replace(/^acct:/, '') !== verifier) {
+		return rej('WebFinger verfification failed');
+	}
+
+	res(result.object);
+}));
diff --git a/src/models/remote-user-object.ts b/src/models/remote-user-object.ts
new file mode 100644
index 000000000..fb5b337c9
--- /dev/null
+++ b/src/models/remote-user-object.ts
@@ -0,0 +1,15 @@
+import * as mongodb from 'mongodb';
+import db from '../db/mongodb';
+
+const RemoteUserObject = db.get<IRemoteUserObject>('remoteUserObjects');
+
+export default RemoteUserObject;
+
+export type IRemoteUserObject = {
+	_id: mongodb.ObjectID;
+	uri: string;
+	object: {
+		$ref: string;
+		$id: mongodb.ObjectID;
+	}
+};
diff --git a/src/models/user.ts b/src/models/user.ts
index 4fbfdec90..4728682d6 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -97,6 +97,9 @@ export type IUser = {
 	account: ILocalAccount | IRemoteAccount;
 };
 
+export type ILocalUser = IUser & { account: ILocalAccount };
+export type IRemoteUser = IUser & { account: IRemoteAccount };
+
 export function init(user): IUser {
 	user._id = new mongo.ObjectID(user._id);
 	user.avatarId = new mongo.ObjectID(user.avatarId);
diff --git a/src/processor/http/index.ts b/src/processor/http/index.ts
new file mode 100644
index 000000000..da942ad2a
--- /dev/null
+++ b/src/processor/http/index.ts
@@ -0,0 +1,9 @@
+import performActivityPub from './perform-activitypub';
+import reportGitHubFailure from './report-github-failure';
+
+const handlers = {
+  performActivityPub,
+  reportGitHubFailure,
+};
+
+export default (job, done) => handlers[job.data.type](job).then(() => done(), done);
diff --git a/src/processor/http/perform-activitypub.ts b/src/processor/http/perform-activitypub.ts
new file mode 100644
index 000000000..5b1a02173
--- /dev/null
+++ b/src/processor/http/perform-activitypub.ts
@@ -0,0 +1,6 @@
+import User from '../../models/user';
+import act from '../../common/remote/activitypub/act';
+
+export default ({ data }, done) => User.findOne({ _id: data.actor })
+	.then(actor => act(actor, data.outbox))
+	.then(() => done(), done);
diff --git a/src/processor/report-github-failure.ts b/src/processor/http/report-github-failure.ts
similarity index 87%
rename from src/processor/report-github-failure.ts
rename to src/processor/http/report-github-failure.ts
index 610ffe276..53924a0fb 100644
--- a/src/processor/report-github-failure.ts
+++ b/src/processor/http/report-github-failure.ts
@@ -1,6 +1,6 @@
 import * as request from 'request';
-import User from '../models/user';
-const createPost = require('../server/api/endpoints/posts/create');
+import User from '../../models/user';
+const createPost = require('../../server/api/endpoints/posts/create');
 
 export default ({ data }, done) => {
 	const asyncBot = User.findOne({ _id: data.userId });
diff --git a/src/processor/index.ts b/src/processor/index.ts
index f06cf24e8..cd271d372 100644
--- a/src/processor/index.ts
+++ b/src/processor/index.ts
@@ -1,4 +1,13 @@
 import queue from '../queue';
-import reportGitHubFailure from './report-github-failure';
+import http from './http';
 
-export default () => queue.process('gitHubFailureReport', reportGitHubFailure);
+/*
+	256 is the default concurrency limit of Mozilla Firefox and Google
+	Chromium.
+
+	a8af215e691f3a2205a3758d2d96e9d328e100ff - chromium/src.git - Git at Google
+	https://chromium.googlesource.com/chromium/src.git/+/a8af215e691f3a2205a3758d2d96e9d328e100ff
+	Network.http.max-connections - MozillaZine Knowledge Base
+	http://kb.mozillazine.org/Network.http.max-connections
+*/
+export default () => queue.process('http', 256, http);
diff --git a/src/server/api/common/notify.ts b/src/server/api/common/notify.ts
index f90506cf3..69bf8480b 100644
--- a/src/server/api/common/notify.ts
+++ b/src/server/api/common/notify.ts
@@ -1,7 +1,7 @@
 import * as mongo from 'mongodb';
 import Notification from '../../../models/notification';
 import Mute from '../../../models/mute';
-import event from '../event';
+import event from '../../../common/event';
 import { pack } from '../../../models/notification';
 
 export default (
diff --git a/src/server/api/common/read-messaging-message.ts b/src/server/api/common/read-messaging-message.ts
index f728130bb..127ea1865 100644
--- a/src/server/api/common/read-messaging-message.ts
+++ b/src/server/api/common/read-messaging-message.ts
@@ -1,9 +1,9 @@
 import * as mongo from 'mongodb';
 import Message from '../../../models/messaging-message';
 import { IMessagingMessage as IMessage } from '../../../models/messaging-message';
-import publishUserStream from '../event';
-import { publishMessagingStream } from '../event';
-import { publishMessagingIndexStream } from '../event';
+import publishUserStream from '../../../common/event';
+import { publishMessagingStream } from '../../../common/event';
+import { publishMessagingIndexStream } from '../../../common/event';
 
 /**
  * Mark as read message(s)
diff --git a/src/server/api/common/read-notification.ts b/src/server/api/common/read-notification.ts
index 27632c7ec..9b2012182 100644
--- a/src/server/api/common/read-notification.ts
+++ b/src/server/api/common/read-notification.ts
@@ -1,6 +1,6 @@
 import * as mongo from 'mongodb';
 import { default as Notification, INotification } from '../../../models/notification';
-import publishUserStream from '../event';
+import publishUserStream from '../../../common/event';
 
 /**
  * Mark as read notification(s)
diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts
index 53c8c7067..cf4e35cd1 100644
--- a/src/server/api/endpoints/drive/files/create.ts
+++ b/src/server/api/endpoints/drive/files/create.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import { validateFileName, pack } from '../../../../../models/drive-file';
-import create from '../../../common/drive/add-file';
+import create from '../../../../../common/drive/add-file';
 
 /**
  * Create a file
diff --git a/src/server/api/endpoints/drive/files/update.ts b/src/server/api/endpoints/drive/files/update.ts
index 836b4cfcd..5d0b915f9 100644
--- a/src/server/api/endpoints/drive/files/update.ts
+++ b/src/server/api/endpoints/drive/files/update.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import DriveFolder from '../../../../../models/drive-folder';
 import DriveFile, { validateFileName, pack } from '../../../../../models/drive-file';
-import { publishDriveStream } from '../../../event';
+import { publishDriveStream } from '../../../../../common/event';
 
 /**
  * Update a file
diff --git a/src/server/api/endpoints/drive/files/upload_from_url.ts b/src/server/api/endpoints/drive/files/upload_from_url.ts
index 7262f09bb..01d875055 100644
--- a/src/server/api/endpoints/drive/files/upload_from_url.ts
+++ b/src/server/api/endpoints/drive/files/upload_from_url.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import { pack } from '../../../../../models/drive-file';
-import uploadFromUrl from '../../../common/drive/upload_from_url';
+import uploadFromUrl from '../../../../../common/drive/upload_from_url';
 
 /**
  * Create a file from a URL
diff --git a/src/server/api/endpoints/drive/folders/create.ts b/src/server/api/endpoints/drive/folders/create.ts
index 24e035930..bd3b0a0b1 100644
--- a/src/server/api/endpoints/drive/folders/create.ts
+++ b/src/server/api/endpoints/drive/folders/create.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import DriveFolder, { isValidFolderName, pack } from '../../../../../models/drive-folder';
-import { publishDriveStream } from '../../../event';
+import { publishDriveStream } from '../../../../../common/event';
 
 /**
  * Create drive folder
diff --git a/src/server/api/endpoints/drive/folders/update.ts b/src/server/api/endpoints/drive/folders/update.ts
index 6c5a5c376..5ac81e5b5 100644
--- a/src/server/api/endpoints/drive/folders/update.ts
+++ b/src/server/api/endpoints/drive/folders/update.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import DriveFolder, { isValidFolderName, pack } from '../../../../../models/drive-folder';
-import { publishDriveStream } from '../../../event';
+import { publishDriveStream } from '../../../../../common/event';
 
 /**
  * Update a folder
diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts
index 1e24388a7..a689250e3 100644
--- a/src/server/api/endpoints/following/create.ts
+++ b/src/server/api/endpoints/following/create.ts
@@ -5,7 +5,7 @@ import $ from 'cafy';
 import User, { pack as packUser } from '../../../../models/user';
 import Following from '../../../../models/following';
 import notify from '../../common/notify';
-import event from '../../event';
+import event from '../../../../common/event';
 
 /**
  * Follow a user
diff --git a/src/server/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts
index 7fc5f477f..ecca27d57 100644
--- a/src/server/api/endpoints/following/delete.ts
+++ b/src/server/api/endpoints/following/delete.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import User, { pack as packUser } from '../../../../models/user';
 import Following from '../../../../models/following';
-import event from '../../event';
+import event from '../../../../common/event';
 
 /**
  * Unfollow a user
diff --git a/src/server/api/endpoints/i/regenerate_token.ts b/src/server/api/endpoints/i/regenerate_token.ts
index c35778ac0..0af622fd9 100644
--- a/src/server/api/endpoints/i/regenerate_token.ts
+++ b/src/server/api/endpoints/i/regenerate_token.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import * as bcrypt from 'bcryptjs';
 import User from '../../../../models/user';
-import event from '../../event';
+import event from '../../../../common/event';
 import generateUserToken from '../../common/generate-native-user-token';
 
 /**
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index 8e198f3ad..b465e763e 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../../../models/user';
-import event from '../../event';
+import event from '../../../../common/event';
 import config from '../../../../conf';
 
 /**
diff --git a/src/server/api/endpoints/i/update_client_setting.ts b/src/server/api/endpoints/i/update_client_setting.ts
index 03867b401..79789e664 100644
--- a/src/server/api/endpoints/i/update_client_setting.ts
+++ b/src/server/api/endpoints/i/update_client_setting.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import User, { pack } from '../../../../models/user';
-import event from '../../event';
+import event from '../../../../common/event';
 
 /**
  * Update myself
diff --git a/src/server/api/endpoints/i/update_home.ts b/src/server/api/endpoints/i/update_home.ts
index 713cf9fcc..437f51d6f 100644
--- a/src/server/api/endpoints/i/update_home.ts
+++ b/src/server/api/endpoints/i/update_home.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import User from '../../../../models/user';
-import event from '../../event';
+import event from '../../../../common/event';
 
 module.exports = async (params, user) => new Promise(async (res, rej) => {
 	// Get 'home' parameter
diff --git a/src/server/api/endpoints/i/update_mobile_home.ts b/src/server/api/endpoints/i/update_mobile_home.ts
index 6f28cebf9..783ca09d1 100644
--- a/src/server/api/endpoints/i/update_mobile_home.ts
+++ b/src/server/api/endpoints/i/update_mobile_home.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import User from '../../../../models/user';
-import event from '../../event';
+import event from '../../../../common/event';
 
 module.exports = async (params, user) => new Promise(async (res, rej) => {
 	// Get 'home' parameter
diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts
index 3d3b204da..b2b6c971d 100644
--- a/src/server/api/endpoints/messaging/messages/create.ts
+++ b/src/server/api/endpoints/messaging/messages/create.ts
@@ -9,8 +9,8 @@ import User from '../../../../../models/user';
 import Mute from '../../../../../models/mute';
 import DriveFile from '../../../../../models/drive-file';
 import { pack } from '../../../../../models/messaging-message';
-import publishUserStream from '../../../event';
-import { publishMessagingStream, publishMessagingIndexStream, pushSw } from '../../../event';
+import publishUserStream from '../../../../../common/event';
+import { publishMessagingStream, publishMessagingIndexStream, pushSw } from '../../../../../common/event';
 import html from '../../../../../common/text/html';
 import parse from '../../../../../common/text/parse';
 import config from '../../../../../conf';
diff --git a/src/server/api/endpoints/notifications/mark_as_read_all.ts b/src/server/api/endpoints/notifications/mark_as_read_all.ts
index 3693ba87b..f9bc6ebf7 100644
--- a/src/server/api/endpoints/notifications/mark_as_read_all.ts
+++ b/src/server/api/endpoints/notifications/mark_as_read_all.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import Notification from '../../../../models/notification';
-import event from '../../event';
+import event from '../../../../common/event';
 
 /**
  * Mark as read all notifications
diff --git a/src/server/api/endpoints/othello/match.ts b/src/server/api/endpoints/othello/match.ts
index 03168095d..992b93d41 100644
--- a/src/server/api/endpoints/othello/match.ts
+++ b/src/server/api/endpoints/othello/match.ts
@@ -2,7 +2,7 @@ import $ from 'cafy';
 import Matching, { pack as packMatching } from '../../../../models/othello-matching';
 import OthelloGame, { pack as packGame } from '../../../../models/othello-game';
 import User from '../../../../models/user';
-import publishUserStream, { publishOthelloStream } from '../../event';
+import publishUserStream, { publishOthelloStream } from '../../../../common/event';
 import { eighteight } from '../../../../common/othello/maps';
 
 module.exports = (params, user) => new Promise(async (res, rej) => {
diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts
index 5342f7772..42901ebcb 100644
--- a/src/server/api/endpoints/posts/create.ts
+++ b/src/server/api/endpoints/posts/create.ts
@@ -16,7 +16,7 @@ import ChannelWatching from '../../../../models/channel-watching';
 import { pack } from '../../../../models/post';
 import notify from '../../common/notify';
 import watch from '../../common/watch-post';
-import event, { pushSw, publishChannelStream } from '../../event';
+import event, { pushSw, publishChannelStream } from '../../../../common/event';
 import getAcct from '../../../../common/user/get-acct';
 import parseAcct from '../../../../common/user/parse-acct';
 import config from '../../../../conf';
diff --git a/src/server/api/endpoints/posts/polls/vote.ts b/src/server/api/endpoints/posts/polls/vote.ts
index b970c05e8..98df074e5 100644
--- a/src/server/api/endpoints/posts/polls/vote.ts
+++ b/src/server/api/endpoints/posts/polls/vote.ts
@@ -7,7 +7,7 @@ import Post from '../../../../../models/post';
 import Watching from '../../../../../models/post-watching';
 import notify from '../../../common/notify';
 import watch from '../../../common/watch-post';
-import { publishPostStream } from '../../../event';
+import { publishPostStream } from '../../../../../common/event';
 
 /**
  * Vote poll of a post
diff --git a/src/server/api/endpoints/posts/reactions/create.ts b/src/server/api/endpoints/posts/reactions/create.ts
index 5d2b5a7ed..8db76d643 100644
--- a/src/server/api/endpoints/posts/reactions/create.ts
+++ b/src/server/api/endpoints/posts/reactions/create.ts
@@ -8,7 +8,7 @@ import { pack as packUser } from '../../../../../models/user';
 import Watching from '../../../../../models/post-watching';
 import notify from '../../../common/notify';
 import watch from '../../../common/watch-post';
-import { publishPostStream, pushSw } from '../../../event';
+import { publishPostStream, pushSw } from '../../../../../common/event';
 
 /**
  * React to a post
diff --git a/src/server/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts
index fd51d386b..9cd8716fe 100644
--- a/src/server/api/endpoints/users/show.ts
+++ b/src/server/api/endpoints/users/show.ts
@@ -2,49 +2,10 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import { JSDOM } from 'jsdom';
-import { toUnicode, toASCII } from 'punycode';
-import uploadFromUrl from '../../common/drive/upload_from_url';
-import User, { pack, validateUsername, isValidName, isValidDescription } from '../../../../models/user';
-const request = require('request-promise-native');
-const WebFinger = require('webfinger.js');
+import User, { pack } from '../../../../models/user';
+import resolveRemoteUser from '../../../../common/remote/resolve-user';
 
-const webFinger = new WebFinger({});
-
-async function getCollectionCount(url) {
-	if (!url) {
-		return null;
-	}
-
-	try {
-		const collection = await request({ url, json: true });
-		return collection ? collection.totalItems : null;
-	} catch (exception) {
-		return null;
-	}
-}
-
-function findUser(q) {
-	return User.findOne(q, {
-		fields: {
-			data: false
-		}
-	});
-}
-
-function webFingerAndVerify(query, verifier) {
-	return new Promise((res, rej) => webFinger.lookup(query, (error, result) => {
-		if (error) {
-			return rej(error);
-		}
-
-		if (result.object.subject.toLowerCase().replace(/^acct:/, '') !== verifier) {
-			return rej('WebFinger verfification failed');
-		}
-
-		res(result.object);
-	}));
-}
+const cursorOption = { fields: { data: false } };
 
 /**
  * Show a user
@@ -74,124 +35,17 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Lookup user
 	if (typeof host === 'string') {
-		const usernameLower = username.toLowerCase();
-		const hostLowerAscii = toASCII(host).toLowerCase();
-		const hostLower = toUnicode(hostLowerAscii);
-
-		user = await findUser({ usernameLower, hostLower });
-
-		if (user === null) {
-			const acctLower = `${usernameLower}@${hostLowerAscii}`;
-			let activityStreams;
-			let finger;
-			let followersCount;
-			let followingCount;
-			let postsCount;
-
-			if (!validateUsername(username)) {
-				return rej('username validation failed');
-			}
-
-			try {
-				finger = await webFingerAndVerify(acctLower, acctLower);
-			} catch (exception) {
-				return rej('WebFinger lookup failed');
-			}
-
-			const self = finger.links.find(link => link.rel && link.rel.toLowerCase() === 'self');
-			if (!self) {
-				return rej('WebFinger has no reference to self representation');
-			}
-
-			try {
-				activityStreams = await request({
-					url: self.href,
-					headers: {
-						Accept: 'application/activity+json, application/ld+json'
-					},
-					json: true
-				});
-			} catch (exception) {
-				return rej('failed to retrieve ActivityStreams representation');
-			}
-
-			if (!(activityStreams &&
-				(Array.isArray(activityStreams['@context']) ?
-					activityStreams['@context'].includes('https://www.w3.org/ns/activitystreams') :
-					activityStreams['@context'] === 'https://www.w3.org/ns/activitystreams') &&
-				activityStreams.type === 'Person' &&
-				typeof activityStreams.preferredUsername === 'string' &&
-				activityStreams.preferredUsername.toLowerCase() === usernameLower &&
-				isValidName(activityStreams.name) &&
-				isValidDescription(activityStreams.summary)
-			)) {
-				return rej('failed ActivityStreams validation');
-			}
-
-			try {
-				[followersCount, followingCount, postsCount] = await Promise.all([
-					getCollectionCount(activityStreams.followers),
-					getCollectionCount(activityStreams.following),
-					getCollectionCount(activityStreams.outbox),
-					webFingerAndVerify(activityStreams.id, acctLower),
-				]);
-			} catch (exception) {
-				return rej('failed to fetch assets');
-			}
-
-			const summaryDOM = JSDOM.fragment(activityStreams.summary);
-
-			// Create user
-			user = await User.insert({
-				avatarId: null,
-				bannerId: null,
-				createdAt: new Date(),
-				description: summaryDOM.textContent,
-				followersCount,
-				followingCount,
-				name: activityStreams.name,
-				postsCount,
-				driveCapacity: 1024 * 1024 * 8, // 8MiB
-				username,
-				usernameLower,
-				host: toUnicode(finger.subject.replace(/^.*?@/, '')),
-				hostLower,
-				account: {
-					uri: activityStreams.id,
-				},
-			});
-
-			const [icon, image] = await Promise.all([
-				activityStreams.icon,
-				activityStreams.image,
-			].map(async image => {
-				if (!image || image.type !== 'Image') {
-					return { _id: null };
-				}
-
-				try {
-					return await uploadFromUrl(image.url, user);
-				} catch (exception) {
-					return { _id: null };
-				}
-			}));
-
-			User.update({ _id: user._id }, {
-				$set: {
-					avatarId: icon._id,
-					bannerId: image._id,
-				},
-			});
-
-			user.avatarId = icon._id;
-			user.bannerId = icon._id;
+		try {
+			user = await resolveRemoteUser(username, host, cursorOption);
+		} catch (exception) {
+			return rej('failed to resolve remote user');
 		}
 	} else {
 		const q = userId !== undefined
 			? { _id: userId }
 			: { usernameLower: username.toLowerCase(), host: null };
 
-		user = await findUser(q);
+		user = await User.findOne(q, cursorOption);
 
 		if (user === null) {
 			return rej('user not found');
diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts
index d78fa11b8..4b7064491 100644
--- a/src/server/api/private/signin.ts
+++ b/src/server/api/private/signin.ts
@@ -3,7 +3,7 @@ import * as bcrypt from 'bcryptjs';
 import * as speakeasy from 'speakeasy';
 import { default as User, ILocalAccount, IUser } from '../../../models/user';
 import Signin, { pack } from '../../../models/signin';
-import event from '../event';
+import event from '../../../common/event';
 import signin from '../common/signin';
 import config from '../../../conf';
 
diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts
index a2359cfb6..b4068c729 100644
--- a/src/server/api/service/github.ts
+++ b/src/server/api/service/github.ts
@@ -42,7 +42,8 @@ module.exports = async (app: express.Application) => {
 				const commit = event.commit;
 				const parent = commit.parents[0];
 
-				queue.create('gitHubFailureReport', {
+				queue.create('http', {
+					type: 'gitHubFailureReport',
 					userId: bot._id,
 					parentUrl: parent.url,
 					htmlUrl: commit.html_url,
diff --git a/src/server/api/service/twitter.ts b/src/server/api/service/twitter.ts
index d77341db2..73822b0bd 100644
--- a/src/server/api/service/twitter.ts
+++ b/src/server/api/service/twitter.ts
@@ -6,7 +6,7 @@ import * as uuid from 'uuid';
 import autwh from 'autwh';
 import redis from '../../../db/redis';
 import User, { pack } from '../../../models/user';
-import event from '../event';
+import event from '../../../common/event';
 import config from '../../../conf';
 import signin from '../common/signin';
 
diff --git a/src/server/api/stream/othello-game.ts b/src/server/api/stream/othello-game.ts
index b6a251c4c..b11915f8f 100644
--- a/src/server/api/stream/othello-game.ts
+++ b/src/server/api/stream/othello-game.ts
@@ -2,7 +2,7 @@ import * as websocket from 'websocket';
 import * as redis from 'redis';
 import * as CRC32 from 'crc-32';
 import OthelloGame, { pack } from '../../../models/othello-game';
-import { publishOthelloGameStream } from '../event';
+import { publishOthelloGameStream } from '../../../common/event';
 import Othello from '../../../common/othello/core';
 import * as maps from '../../../common/othello/maps';
 import { ParsedUrlQuery } from 'querystring';
diff --git a/src/server/api/stream/othello.ts b/src/server/api/stream/othello.ts
index 4205afae7..1cf9a1494 100644
--- a/src/server/api/stream/othello.ts
+++ b/src/server/api/stream/othello.ts
@@ -2,7 +2,7 @@ import * as mongo from 'mongodb';
 import * as websocket from 'websocket';
 import * as redis from 'redis';
 import Matching, { pack } from '../../../models/othello-matching';
-import publishUserStream from '../event';
+import publishUserStream from '../../../common/event';
 
 export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void {
 	// Subscribe othello stream

From f18330a89fd0a613536402ab44869c7ef505f04d Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sat, 31 Mar 2018 12:11:56 +0000
Subject: [PATCH 0953/1250] fix(package): update html-minifier to version
 3.5.13

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 4275c1c1c..2b8c1bca5 100644
--- a/package.json
+++ b/package.json
@@ -133,7 +133,7 @@
 		"gulp-util": "3.0.8",
 		"hard-source-webpack-plugin": "0.6.4",
 		"highlight.js": "9.12.0",
-		"html-minifier": "3.5.12",
+		"html-minifier": "3.5.13",
 		"inquirer": "5.2.0",
 		"is-root": "2.0.0",
 		"is-url": "1.2.4",

From 08fefc0184e83ad6af919a05f3e0e4ad8074e116 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 31 Mar 2018 21:41:08 +0900
Subject: [PATCH 0954/1250] Use Vue rendering function

and some refactors
---
 .../app/common/views/components/index.ts      |   2 +-
 .../components/messaging-room.message.vue     |  41 ++---
 .../app/common/views/components/post-html.ts  | 157 ++++++++++++++++++
 .../app/common/views/components/post-html.vue | 103 ------------
 .../app/common/views/components/url.vue       |  57 +++++++
 .../views/components/welcome-timeline.vue     |   2 +-
 .../views/components/post-detail.sub.vue      |   2 +-
 .../desktop/views/components/post-detail.vue  |  42 +++--
 .../desktop/views/components/posts.post.vue   |  41 ++---
 .../views/components/sub-post-content.vue     |   2 +-
 .../mobile/views/components/post-detail.vue   |  37 +++--
 .../app/mobile/views/components/post.vue      |  43 ++---
 .../views/components/sub-post-content.vue     |   2 +-
 src/common/text/parse/index.ts                |   2 +-
 14 files changed, 322 insertions(+), 211 deletions(-)
 create mode 100644 src/client/app/common/views/components/post-html.ts
 delete mode 100644 src/client/app/common/views/components/post-html.vue
 create mode 100644 src/client/app/common/views/components/url.vue

diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts
index 8c10bdee2..b58ba37ec 100644
--- a/src/client/app/common/views/components/index.ts
+++ b/src/client/app/common/views/components/index.ts
@@ -4,7 +4,7 @@ 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';
+import postHtml from './post-html';
 import poll from './poll.vue';
 import pollEditor from './poll-editor.vue';
 import reactionIcon from './reaction-icon.vue';
diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue
index 25ceab85a..91af26bff 100644
--- a/src/client/app/common/views/components/messaging-room.message.vue
+++ b/src/client/app/common/views/components/messaging-room.message.vue
@@ -4,13 +4,13 @@
 		<img class="avatar" :src="`${message.user.avatarUrl}?thumbnail&size=80`" alt=""/>
 	</router-link>
 	<div class="content">
-		<div class="balloon" :data-no-text="message.textHtml == null">
+		<div class="balloon" :data-no-text="message.text == null">
 			<p class="read" v-if="isMe && message.isRead">%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.isDeleted">
-				<mk-post-html class="text" v-if="message.textHtml" ref="text" :html="message.textHtml" :i="os.i"/>
+				<mk-post-html class="text" v-if="message.text" ref="text" :text="message.text" :i="os.i"/>
 				<div class="file" v-if="message.file">
 					<a :href="message.file.url" target="_blank" :title="message.file.name">
 						<img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/>
@@ -35,35 +35,30 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../common/user/get-acct';
+import parse from '../../../../../common/text/parse';
 
 export default Vue.extend({
-	props: ['message'],
-	data() {
-		return {
-			urls: []
-		};
+	props: {
+		message: {
+			required: true
+		}
 	},
 	computed: {
-		acct() {
+		acct(): string {
 			return getAcct(this.message.user);
 		},
 		isMe(): boolean {
 			return this.message.userId == (this as any).os.i.id;
-		}
-	},
-	watch: {
-		message: {
-			handler(newMessage, oldMessage) {
-				if (!oldMessage || newMessage.textHtml !== oldMessage.textHtml) {
-					this.$nextTick(() => {
-						const elements = this.$refs.text.$el.getElementsByTagName('a');
-
-						this.urls = [].filter.call(elements, ({ origin }) => origin !== location.origin)
-							.map(({ href }) => href);
-					});
-				}
-			},
-			immediate: true
+		},
+		urls(): string[] {
+			if (this.message.text) {
+				const ast = parse(this.message.text);
+				return ast
+					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
+					.map(t => t.url);
+			} else {
+				return null;
+			}
 		}
 	}
 });
diff --git a/src/client/app/common/views/components/post-html.ts b/src/client/app/common/views/components/post-html.ts
new file mode 100644
index 000000000..c5c3b7275
--- /dev/null
+++ b/src/client/app/common/views/components/post-html.ts
@@ -0,0 +1,157 @@
+import Vue from 'vue';
+import * as emojilib from 'emojilib';
+import parse from '../../../../../common/text/parse';
+import getAcct from '../../../../../common/user/get-acct';
+import { url } from '../../../config';
+import MkUrl from './url.vue';
+
+const flatten = list => list.reduce(
+	(a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []
+);
+
+export default Vue.component('mk-post-html', {
+	props: {
+		text: {
+			type: String,
+			required: true
+		},
+		ast: {
+			type: [],
+			required: false
+		},
+		shouldBreak: {
+			type: Boolean,
+			default: true
+		},
+		i: {
+			type: Object,
+			default: null
+		}
+	},
+
+	render(createElement) {
+		let ast;
+
+		if (this.ast == null) {
+			// Parse text to ast
+			ast = parse(this.text);
+		} else {
+			ast = this.ast;
+		}
+
+		// Parse ast to DOM
+		const els = flatten(ast.map(token => {
+			switch (token.type) {
+				case 'text':
+					const text = token.content.replace(/(\r\n|\n|\r)/g, '\n');
+
+					if (this.shouldBreak) {
+						const x = text.split('\n')
+							.map(t => t == '' ? [createElement('br')] : [createElement('span', t), createElement('br')]);
+						x[x.length - 1].pop();
+						return x;
+					} else {
+						return createElement('span', text.replace(/\n/g, ' '));
+					}
+
+				case 'bold':
+					return createElement('strong', token.bold);
+
+				case 'url':
+					return createElement(MkUrl, {
+						props: {
+							url: token.content,
+							target: '_blank'
+						}
+					});
+
+				case 'link':
+					return createElement('a', {
+						attrs: {
+							class: 'link',
+							href: token.url,
+							target: '_blank',
+							title: token.url
+						}
+					}, token.title);
+
+				case 'mention':
+					return (createElement as any)('a', {
+						attrs: {
+							href: `${url}/@${getAcct(token)}`,
+							target: '_blank',
+							dataIsMe: (this as any).i && getAcct((this as any).i) == getAcct(token)
+						},
+						directives: [{
+							name: 'user-preview',
+							value: token.content
+						}]
+					}, token.content);
+
+				case 'hashtag':
+					return createElement('a', {
+						attrs: {
+							href: `${url}/search?q=${token.content}`,
+							target: '_blank'
+						}
+					}, token.content);
+
+				case 'code':
+					return createElement('pre', [
+						createElement('code', {
+							domProps: {
+								innerHTML: token.html
+							}
+						})
+					]);
+
+				case 'inline-code':
+					return createElement('code', {
+						domProps: {
+							innerHTML: token.html
+						}
+					});
+
+				case 'quote':
+					const text2 = token.quote.replace(/(\r\n|\n|\r)/g, '\n');
+
+					if (this.shouldBreak) {
+						const x = text2.split('\n')
+							.map(t => [createElement('span', t), createElement('br')]);
+						x[x.length - 1].pop();
+						return createElement('div', {
+							attrs: {
+								class: 'quote'
+							}
+						}, x);
+					} else {
+						return createElement('span', {
+							attrs: {
+								class: 'quote'
+							}
+						}, text2.replace(/\n/g, ' '));
+					}
+
+				case 'emoji':
+					const emoji = emojilib.lib[token.emoji];
+					return createElement('span', emoji ? emoji.char : token.content);
+
+				default:
+					console.log('unknown ast type:', token.type);
+			}
+		}));
+
+		const _els = [];
+		els.forEach((el, i) => {
+			if (el.tag == 'br') {
+				if (els[i - 1].tag != 'div') {
+					_els.push(el);
+				}
+			} else {
+				_els.push(el);
+			}
+		});
+
+		return createElement('span', _els);
+	}
+});
diff --git a/src/client/app/common/views/components/post-html.vue b/src/client/app/common/views/components/post-html.vue
deleted file mode 100644
index 1c949052b..000000000
--- a/src/client/app/common/views/components/post-html.vue
+++ /dev/null
@@ -1,103 +0,0 @@
-<template><div class="mk-post-html" v-html="html"></div></template>
-
-<script lang="ts">
-import Vue from 'vue';
-import getAcct from '../../../../../common/user/get-acct';
-import { url } from '../../../config';
-
-function markUrl(a) {
-	while (a.firstChild) {
-		a.removeChild(a.firstChild);
-	}
-
-	const schema = document.createElement('span');
-	const delimiter = document.createTextNode('//');
-	const host = document.createElement('span');
-	const pathname = document.createElement('span');
-	const query = document.createElement('span');
-	const hash = document.createElement('span');
-
-	schema.className = 'schema';
-	schema.textContent = a.protocol;
-
-	host.className = 'host';
-	host.textContent = a.host;
-
-	pathname.className = 'pathname';
-	pathname.textContent = a.pathname;
-
-	query.className = 'query';
-	query.textContent = a.search;
-
-	hash.className = 'hash';
-	hash.textContent = a.hash;
-
-	a.appendChild(schema);
-	a.appendChild(delimiter);
-	a.appendChild(host);
-	a.appendChild(pathname);
-	a.appendChild(query);
-	a.appendChild(hash);
-}
-
-function markMe(me, a) {
-	a.setAttribute("data-is-me", me && `${url}/@${getAcct(me)}` == a.href);
-}
-
-function markTarget(a) {
-	a.setAttribute("target", "_blank");
-}
-
-export default Vue.component('mk-post-html', {
-	props: {
-		html: {
-			type: String,
-			required: true
-		},
-		i: {
-			type: Object,
-			default: null
-		}
-	},
-	watch {
-		html: {
-			handler() {
-				this.$nextTick(() => [].forEach.call(this.$el.getElementsByTagName('a'), a => {
-					if (a.href === a.textContent) {
-						markUrl(a);
-					} else {
-						markMe((this as any).i, a);
-					}
-
-					markTarget(a);
-				}));
-			},
-			immediate: true,
-		}
-	}
-});
-</script>
-
-<style lang="stylus">
-.mk-post-html
-	a
-		word-break break-all
-
-		> .schema
-			opacity 0.5
-
-		> .host
-			font-weight bold
-
-		> .pathname
-			opacity 0.8
-
-		> .query
-			opacity 0.5
-
-		> .hash
-			font-style italic
-
-	p
-		margin 0
-</style>
diff --git a/src/client/app/common/views/components/url.vue b/src/client/app/common/views/components/url.vue
new file mode 100644
index 000000000..e6ffe4466
--- /dev/null
+++ b/src/client/app/common/views/components/url.vue
@@ -0,0 +1,57 @@
+<template>
+<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="ts">
+import Vue from 'vue';
+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>
+.mk-url
+	word-break break-all
+	> [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>
diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue
index f379029f9..09b090bdc 100644
--- a/src/client/app/common/views/components/welcome-timeline.vue
+++ b/src/client/app/common/views/components/welcome-timeline.vue
@@ -15,7 +15,7 @@
 				</div>
 			</header>
 			<div class="text">
-				<mk-post-html :html="post.textHtml"/>
+				<mk-post-html :text="post.text"/>
 			</div>
 		</div>
 	</div>
diff --git a/src/client/app/desktop/views/components/post-detail.sub.vue b/src/client/app/desktop/views/components/post-detail.sub.vue
index b6148d9b2..1d5649cf9 100644
--- a/src/client/app/desktop/views/components/post-detail.sub.vue
+++ b/src/client/app/desktop/views/components/post-detail.sub.vue
@@ -16,7 +16,7 @@
 			</div>
 		</header>
 		<div class="body">
-			<mk-post-html v-if="post.textHtml" :html="post.textHtml" :i="os.i" :class="$style.text"/>
+			<mk-post-html v-if="post.text" :text="post.text" :i="os.i" :class="$style.text"/>
 			<div class="media" v-if="post.media > 0">
 				<mk-media-list :media-list="post.media"/>
 			</div>
diff --git a/src/client/app/desktop/views/components/post-detail.vue b/src/client/app/desktop/views/components/post-detail.vue
index e75ebe34b..70bfdbba3 100644
--- a/src/client/app/desktop/views/components/post-detail.vue
+++ b/src/client/app/desktop/views/components/post-detail.vue
@@ -38,7 +38,7 @@
 			</router-link>
 		</header>
 		<div class="body">
-			<mk-post-html :class="$style.text" v-if="p.text" ref="text" :text="p.text" :i="os.i"/>
+			<mk-post-html :class="$style.text" v-if="p.text" :text="p.text" :i="os.i"/>
 			<div class="media" v-if="p.media.length > 0">
 				<mk-media-list :media-list="p.media"/>
 			</div>
@@ -79,6 +79,7 @@
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
 import getAcct from '../../../../../common/user/get-acct';
+import parse from '../../../../../common/text/parse';
 
 import MkPostFormWindow from './post-form-window.vue';
 import MkRepostFormWindow from './repost-form-window.vue';
@@ -90,6 +91,7 @@ export default Vue.extend({
 	components: {
 		XSub
 	},
+
 	props: {
 		post: {
 			type: Object,
@@ -99,19 +101,15 @@ export default Vue.extend({
 			default: false
 		}
 	},
-	computed: {
-		acct() {
-			return getAcct(this.post.user);
-		}
-	},
+
 	data() {
 		return {
 			context: [],
 			contextFetching: false,
-			replies: [],
-			urls: []
+			replies: []
 		};
 	},
+
 	computed: {
 		isRepost(): boolean {
 			return (this.post.repost &&
@@ -131,8 +129,22 @@ export default Vue.extend({
 		},
 		title(): string {
 			return dateStringify(this.p.createdAt);
+		},
+		acct(): string {
+			return getAcct(this.p.user);
+		},
+		urls(): string[] {
+			if (this.p.text) {
+				const ast = parse(this.p.text);
+				return ast
+					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
+					.map(t => t.url);
+			} else {
+				return null;
+			}
 		}
 	},
+
 	mounted() {
 		// Get replies
 		if (!this.compact) {
@@ -162,21 +174,7 @@ export default Vue.extend({
 			}
 		}
 	},
-	watch: {
-		post: {
-			handler(newPost, oldPost) {
-				if (!oldPost || newPost.text !== oldPost.text) {
-					this.$nextTick(() => {
-						const elements = this.$refs.text.$el.getElementsByTagName('a');
 
-						this.urls = [].filter.call(elements, ({ origin }) => origin !== location.origin)
-							.map(({ href }) => href);
-					});
-				}
-			},
-			immediate: true
-		}
-	},
 	methods: {
 		fetchContext() {
 			this.contextFetching = true;
diff --git a/src/client/app/desktop/views/components/posts.post.vue b/src/client/app/desktop/views/components/posts.post.vue
index f3566c81b..c31e28d67 100644
--- a/src/client/app/desktop/views/components/posts.post.vue
+++ b/src/client/app/desktop/views/components/posts.post.vue
@@ -38,7 +38,7 @@
 				</p>
 				<div class="text">
 					<a class="reply" v-if="p.reply">%fa:reply%</a>
-					<mk-post-html v-if="p.textHtml" ref="text" :html="p.textHtml" :i="os.i" :class="$style.text"/>
+					<mk-post-html v-if="p.textHtml" :text="p.text" :i="os.i" :class="$style.text"/>
 					<a class="rp" v-if="p.repost">RP:</a>
 				</div>
 				<div class="media" v-if="p.media.length > 0">
@@ -86,6 +86,8 @@
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
 import getAcct from '../../../../../common/user/get-acct';
+import parse from '../../../../../common/text/parse';
+
 import MkPostFormWindow from './post-form-window.vue';
 import MkRepostFormWindow from './repost-form-window.vue';
 import MkPostMenu from '../../../common/views/components/post-menu.vue';
@@ -107,17 +109,19 @@ export default Vue.extend({
 	components: {
 		XSub
 	},
+
 	props: ['post'],
+
 	data() {
 		return {
 			isDetailOpened: false,
 			connection: null,
-			connectionId: null,
-			urls: []
+			connectionId: null
 		};
 	},
+
 	computed: {
-		acct() {
+		acct(): string {
 			return getAcct(this.p.user);
 		},
 		isRepost(): boolean {
@@ -141,14 +145,26 @@ export default Vue.extend({
 		},
 		url(): string {
 			return `/@${this.acct}/${this.p.id}`;
+		},
+		urls(): string[] {
+			if (this.p.text) {
+				const ast = parse(this.p.text);
+				return ast
+					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
+					.map(t => t.url);
+			} else {
+				return null;
+			}
 		}
 	},
+
 	created() {
 		if ((this as any).os.isSignedIn) {
 			this.connection = (this as any).os.stream.getConnection();
 			this.connectionId = (this as any).os.stream.use();
 		}
 	},
+
 	mounted() {
 		this.capture(true);
 
@@ -174,6 +190,7 @@ export default Vue.extend({
 			}
 		}
 	},
+
 	beforeDestroy() {
 		this.decapture(true);
 
@@ -182,21 +199,7 @@ export default Vue.extend({
 			(this as any).os.stream.dispose(this.connectionId);
 		}
 	},
-	watch: {
-		post: {
-			handler(newPost, oldPost) {
-				if (!oldPost || newPost.textHtml !== oldPost.textHtml) {
-					this.$nextTick(() => {
-						const elements = this.$refs.text.$el.getElementsByTagName('a');
 
-						this.urls = [].filter.call(elements, ({ origin }) => origin !== location.origin)
-							.map(({ href }) => href);
-					});
-				}
-			},
-			immediate: true
-		}
-	},
 	methods: {
 		capture(withHandler = false) {
 			if ((this as any).os.isSignedIn) {
@@ -457,7 +460,7 @@ export default Vue.extend({
 					font-size 1.1em
 					color #717171
 
-					>>> blockquote
+					>>> .quote
 						margin 8px
 						padding 6px 12px
 						color #aaa
diff --git a/src/client/app/desktop/views/components/sub-post-content.vue b/src/client/app/desktop/views/components/sub-post-content.vue
index 58c81e755..17899af28 100644
--- a/src/client/app/desktop/views/components/sub-post-content.vue
+++ b/src/client/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.replyId">%fa:reply%</a>
-		<mk-post-html ref="text" :html="post.textHtml" :i="os.i"/>
+		<mk-post-html :text="post.text" :i="os.i"/>
 		<a class="rp" v-if="post.repostId" :href="`/post:${post.repostId}`">RP: ...</a>
 	</div>
 	<details v-if="post.media.length > 0">
diff --git a/src/client/app/mobile/views/components/post-detail.vue b/src/client/app/mobile/views/components/post-detail.vue
index 77a73426f..0a4e36fc6 100644
--- a/src/client/app/mobile/views/components/post-detail.vue
+++ b/src/client/app/mobile/views/components/post-detail.vue
@@ -81,6 +81,8 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../common/user/get-acct';
+import parse from '../../../../../common/text/parse';
+
 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';
@@ -89,6 +91,7 @@ export default Vue.extend({
 	components: {
 		XSub
 	},
+
 	props: {
 		post: {
 			type: Object,
@@ -98,19 +101,20 @@ export default Vue.extend({
 			default: false
 		}
 	},
+
 	data() {
 		return {
 			context: [],
 			contextFetching: false,
-			replies: [],
-			urls: []
+			replies: []
 		};
 	},
+
 	computed: {
-		acct() {
+		acct(): string {
 			return getAcct(this.post.user);
 		},
-		pAcct() {
+		pAcct(): string {
 			return getAcct(this.p.user);
 		},
 		isRepost(): boolean {
@@ -128,8 +132,19 @@ export default Vue.extend({
 					.map(key => this.p.reactionCounts[key])
 					.reduce((a, b) => a + b)
 				: 0;
+		},
+		urls(): string[] {
+			if (this.p.text) {
+				const ast = parse(this.p.text);
+				return ast
+					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
+					.map(t => t.url);
+			} else {
+				return null;
+			}
 		}
 	},
+
 	mounted() {
 		// Get replies
 		if (!this.compact) {
@@ -159,21 +174,7 @@ export default Vue.extend({
 			}
 		}
 	},
-	watch: {
-		post: {
-			handler(newPost, oldPost) {
-				if (!oldPost || newPost.text !== oldPost.text) {
-					this.$nextTick(() => {
-						const elements = this.$refs.text.$el.getElementsByTagName('a');
 
-						this.urls = [].filter.call(elements, ({ origin }) => origin !== location.origin)
-							.map(({ href }) => href);
-					});
-				}
-			},
-			immediate: true
-		}
-	},
 	methods: {
 		fetchContext() {
 			this.contextFetching = true;
diff --git a/src/client/app/mobile/views/components/post.vue b/src/client/app/mobile/views/components/post.vue
index 96ec9632f..f4f845b49 100644
--- a/src/client/app/mobile/views/components/post.vue
+++ b/src/client/app/mobile/views/components/post.vue
@@ -37,7 +37,7 @@
 					<a class="reply" v-if="p.reply">
 						%fa:reply%
 					</a>
-					<mk-post-html v-if="p.text" ref="text" :text="p.text" :i="os.i" :class="$style.text"/>
+					<mk-post-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/>
 					<a class="rp" v-if="p.repost != null">RP:</a>
 				</div>
 				<div class="media" v-if="p.media.length > 0">
@@ -78,6 +78,8 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../common/user/get-acct';
+import parse from '../../../../../common/text/parse';
+
 import MkPostMenu from '../../../common/views/components/post-menu.vue';
 import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
 import XSub from './post.sub.vue';
@@ -86,19 +88,21 @@ export default Vue.extend({
 	components: {
 		XSub
 	},
+
 	props: ['post'],
+
 	data() {
 		return {
 			connection: null,
-			connectionId: null,
-			urls: []
+			connectionId: null
 		};
 	},
+
 	computed: {
-		acct() {
+		acct(): string {
 			return getAcct(this.post.user);
 		},
-		pAcct() {
+		pAcct(): string {
 			return getAcct(this.p.user);
 		},
 		isRepost(): boolean {
@@ -119,14 +123,26 @@ export default Vue.extend({
 		},
 		url(): string {
 			return `/@${this.pAcct}/${this.p.id}`;
+		},
+		urls(): string[] {
+			if (this.p.text) {
+				const ast = parse(this.p.text);
+				return ast
+					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
+					.map(t => t.url);
+			} else {
+				return null;
+			}
 		}
 	},
+
 	created() {
 		if ((this as any).os.isSignedIn) {
 			this.connection = (this as any).os.stream.getConnection();
 			this.connectionId = (this as any).os.stream.use();
 		}
 	},
+
 	mounted() {
 		this.capture(true);
 
@@ -152,6 +168,7 @@ export default Vue.extend({
 			}
 		}
 	},
+
 	beforeDestroy() {
 		this.decapture(true);
 
@@ -160,21 +177,7 @@ export default Vue.extend({
 			(this as any).os.stream.dispose(this.connectionId);
 		}
 	},
-	watch: {
-		post: {
-			handler(newPost, oldPost) {
-				if (!oldPost || newPost.text !== oldPost.text) {
-					this.$nextTick(() => {
-						const elements = this.$refs.text.$el.getElementsByTagName('a');
 
-						this.urls = [].filter.call(elements, ({ origin }) => origin !== location.origin)
-							.map(({ href }) => href);
-					});
-				}
-			},
-			immediate: true
-		}
-	},
 	methods: {
 		capture(withHandler = false) {
 			if ((this as any).os.isSignedIn) {
@@ -396,7 +399,7 @@ export default Vue.extend({
 					font-size 1.1em
 					color #717171
 
-					>>> blockquote
+					>>> .quote
 						margin 8px
 						padding 6px 12px
 						color #aaa
diff --git a/src/client/app/mobile/views/components/sub-post-content.vue b/src/client/app/mobile/views/components/sub-post-content.vue
index 955bb406b..97dd987dd 100644
--- a/src/client/app/mobile/views/components/sub-post-content.vue
+++ b/src/client/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.replyId">%fa:reply%</a>
-		<mk-post-html v-if="post.text" :ast="post.text" :i="os.i"/>
+		<mk-post-html v-if="post.text" :text="post.text" :i="os.i"/>
 		<a class="rp" v-if="post.repostId">RP: ...</a>
 	</div>
 	<details v-if="post.media.length > 0">
diff --git a/src/common/text/parse/index.ts b/src/common/text/parse/index.ts
index 1e2398dc3..b958da81b 100644
--- a/src/common/text/parse/index.ts
+++ b/src/common/text/parse/index.ts
@@ -14,7 +14,7 @@ const elements = [
 	require('./elements/emoji')
 ];
 
-export default (source: string) => {
+export default (source: string): any[] => {
 
 	if (source == '') {
 		return null;

From 6a6eb96daa28d32c40a22587548cb6f102d504e2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 31 Mar 2018 21:46:58 +0900
Subject: [PATCH 0955/1250] Fix bug

---
 .../app/desktop/views/components/post-detail.vue      | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/src/client/app/desktop/views/components/post-detail.vue b/src/client/app/desktop/views/components/post-detail.vue
index 70bfdbba3..d6481e13d 100644
--- a/src/client/app/desktop/views/components/post-detail.vue
+++ b/src/client/app/desktop/views/components/post-detail.vue
@@ -27,13 +27,13 @@
 		</p>
 	</div>
 	<article>
-		<router-link class="avatar-anchor" :to="`/@${acct}`">
+		<router-link class="avatar-anchor" :to="`/@${pAcct}`">
 			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
 		</router-link>
 		<header>
-			<router-link class="name" :to="`/@${acct}`" v-user-preview="p.user.id">{{ p.user.name }}</router-link>
-			<span class="username">@{{ acct }}</span>
-			<router-link class="time" :to="`/@${acct}/${p.id}`">
+			<router-link class="name" :to="`/@${pAcct}`" v-user-preview="p.user.id">{{ p.user.name }}</router-link>
+			<span class="username">@{{ pAcct }}</span>
+			<router-link class="time" :to="`/@${pAcct}/${p.id}`">
 				<mk-time :time="p.createdAt"/>
 			</router-link>
 		</header>
@@ -131,6 +131,9 @@ export default Vue.extend({
 			return dateStringify(this.p.createdAt);
 		},
 		acct(): string {
+			return getAcct(this.post.user);
+		},
+		pAcct(): string {
 			return getAcct(this.p.user);
 		},
 		urls(): string[] {

From 102d8467ace1e36382014254d18ae5480c14def5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 31 Mar 2018 21:49:26 +0900
Subject: [PATCH 0956/1250] Update migration script

---
 tools/migration/nighthike/6.js | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/tools/migration/nighthike/6.js b/tools/migration/nighthike/6.js
index ff78df4e0..8b97e9f7b 100644
--- a/tools/migration/nighthike/6.js
+++ b/tools/migration/nighthike/6.js
@@ -1 +1,13 @@
-db.posts.update({ mediaIds: null }, { $set: { mediaIds: [] } }, false, true);
+db.posts.update({
+	$or: [{
+		mediaIds: null
+	}, {
+		mediaIds: {
+			$exist: false
+		}
+	}]
+}, {
+	$set: {
+		mediaIds: []
+	}
+}, false, true);

From c20d3c7ecd262f2642dc396adf91c3a951669826 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 31 Mar 2018 21:54:29 +0900
Subject: [PATCH 0957/1250] Update migration script

---
 tools/migration/nighthike/7.js | 44 ++++++++++++++++++++++++++--------
 tools/migration/nighthike/8.js | 40 +++++++++++++++++++++++++++++++
 2 files changed, 74 insertions(+), 10 deletions(-)
 create mode 100644 tools/migration/nighthike/8.js

diff --git a/tools/migration/nighthike/7.js b/tools/migration/nighthike/7.js
index c5055da8b..c8efb8952 100644
--- a/tools/migration/nighthike/7.js
+++ b/tools/migration/nighthike/7.js
@@ -1,16 +1,40 @@
-// for Node.js interpretation
+// for Node.js interpret
 
-const Message = require('../../../built/models/messaging-message').default;
-const Post = require('../../../built/models/post').default;
+const { default: Post } = require('../../../built/api/models/post');
+const { default: zip } = require('@prezzemolo/zip')
 const html = require('../../../built/common/text/html').default;
 const parse = require('../../../built/common/text/parse').default;
 
-Promise.all([Message, Post].map(async model => {
-	const documents = await model.find();
-
-	return Promise.all(documents.map(({ _id, text }) => model.update(_id, {
+const migrate = async (post) => {
+	const result = await Post.update(post._id, {
 		$set: {
-			textHtml: html(parse(text))
+			textHtml: post.text ? html(parse(post.text)) : null
 		}
-	})));
-})).catch(console.error).then(process.exit);
+	});
+	return result.ok === 1;
+}
+
+async function main() {
+	const count = await Post.count({});
+
+	const dop = Number.parseInt(process.argv[2]) || 5
+	const idop = ((count - (count % dop)) / dop) + 1
+
+	return zip(
+		1,
+		async (time) => {
+			console.log(`${time} / ${idop}`)
+			const doc = await Post.find({}, {
+				limit: dop, skip: time * dop
+			})
+			return Promise.all(doc.map(migrate))
+		},
+		idop
+	).then(a => {
+		const rv = []
+		a.forEach(e => rv.push(...e))
+		return rv
+	})
+}
+
+main().then(console.dir).catch(console.error)
diff --git a/tools/migration/nighthike/8.js b/tools/migration/nighthike/8.js
new file mode 100644
index 000000000..5e0cf9507
--- /dev/null
+++ b/tools/migration/nighthike/8.js
@@ -0,0 +1,40 @@
+// for Node.js interpret
+
+const { default: Message } = require('../../../built/api/models/message');
+const { default: zip } = require('@prezzemolo/zip')
+const html = require('../../../built/common/text/html').default;
+const parse = require('../../../built/common/text/parse').default;
+
+const migrate = async (message) => {
+	const result = await Message.update(message._id, {
+		$set: {
+			textHtml: message.text ? html(parse(message.text)) : null
+		}
+	});
+	return result.ok === 1;
+}
+
+async function main() {
+	const count = await Message.count({});
+
+	const dop = Number.parseInt(process.argv[2]) || 5
+	const idop = ((count - (count % dop)) / dop) + 1
+
+	return zip(
+		1,
+		async (time) => {
+			console.log(`${time} / ${idop}`)
+			const doc = await Message.find({}, {
+				limit: dop, skip: time * dop
+			})
+			return Promise.all(doc.map(migrate))
+		},
+		idop
+	).then(a => {
+		const rv = []
+		a.forEach(e => rv.push(...e))
+		return rv
+	})
+}
+
+main().then(console.dir).catch(console.error)

From 86c52c3adcae32c625407f9800f28d0bfbca40cd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 31 Mar 2018 22:02:30 +0900
Subject: [PATCH 0958/1250] Fix tests

---
 test/api.js | 338 ++++++++++++++++++++++++++--------------------------
 1 file changed, 169 insertions(+), 169 deletions(-)

diff --git a/test/api.js b/test/api.js
index c2c08dd95..5b3b8e38d 100644
--- a/test/api.js
+++ b/test/api.js
@@ -46,12 +46,12 @@ describe('API', () => {
 	beforeEach(() => Promise.all([
 		db.get('users').drop(),
 		db.get('posts').drop(),
-		db.get('drive_files.files').drop(),
-		db.get('drive_files.chunks').drop(),
-		db.get('drive_folders').drop(),
+		db.get('driveFiles.files').drop(),
+		db.get('driveFiles.chunks').drop(),
+		db.get('driveFolders').drop(),
 		db.get('apps').drop(),
-		db.get('access_tokens').drop(),
-		db.get('auth_sessions').drop()
+		db.get('accessTokens').drop(),
+		db.get('authSessions').drop()
 	]));
 
 	it('greet server', done => {
@@ -195,7 +195,7 @@ describe('API', () => {
 		it('ユーザーが取得できる', async(async () => {
 			const me = await insertSakurako();
 			const res = await request('/users/show', {
-				user_id: me._id.toString()
+				userId: me._id.toString()
 			}, me);
 			res.should.have.status(200);
 			res.body.should.be.a('object');
@@ -204,14 +204,14 @@ describe('API', () => {
 
 		it('ユーザーが存在しなかったら怒る', async(async () => {
 			const res = await request('/users/show', {
-				user_id: '000000000000000000000000'
+				userId: '000000000000000000000000'
 			});
 			res.should.have.status(400);
 		}));
 
 		it('間違ったIDで怒られる', async(async () => {
 			const res = await request('/users/show', {
-				user_id: 'kyoppie'
+				userId: 'kyoppie'
 			});
 			res.should.have.status(400);
 		}));
@@ -226,32 +226,32 @@ describe('API', () => {
 			const res = await request('/posts/create', post, me);
 			res.should.have.status(200);
 			res.body.should.be.a('object');
-			res.body.should.have.property('created_post');
-			res.body.created_post.should.have.property('text').eql(post.text);
+			res.body.should.have.property('createdPost');
+			res.body.createdPost.should.have.property('text').eql(post.text);
 		}));
 
 		it('ファイルを添付できる', async(async () => {
 			const me = await insertSakurako();
 			const file = await insertDriveFile({
-				user_id: me._id
+				userId: me._id
 			});
 			const res = await request('/posts/create', {
-				media_ids: [file._id.toString()]
+				mediaIds: [file._id.toString()]
 			}, me);
 			res.should.have.status(200);
 			res.body.should.be.a('object');
-			res.body.should.have.property('created_post');
-			res.body.created_post.should.have.property('media_ids').eql([file._id.toString()]);
+			res.body.should.have.property('createdPost');
+			res.body.createdPost.should.have.property('mediaIds').eql([file._id.toString()]);
 		}));
 
 		it('他人のファイルは添付できない', async(async () => {
 			const me = await insertSakurako();
 			const hima = await insertHimawari();
 			const file = await insertDriveFile({
-				user_id: hima._id
+				userId: hima._id
 			});
 			const res = await request('/posts/create', {
-				media_ids: [file._id.toString()]
+				mediaIds: [file._id.toString()]
 			}, me);
 			res.should.have.status(400);
 		}));
@@ -259,7 +259,7 @@ describe('API', () => {
 		it('存在しないファイルは添付できない', async(async () => {
 			const me = await insertSakurako();
 			const res = await request('/posts/create', {
-				media_ids: ['000000000000000000000000']
+				mediaIds: ['000000000000000000000000']
 			}, me);
 			res.should.have.status(400);
 		}));
@@ -267,7 +267,7 @@ describe('API', () => {
 		it('不正なファイルIDで怒られる', async(async () => {
 			const me = await insertSakurako();
 			const res = await request('/posts/create', {
-				media_ids: ['kyoppie']
+				mediaIds: ['kyoppie']
 			}, me);
 			res.should.have.status(400);
 		}));
@@ -275,65 +275,65 @@ describe('API', () => {
 		it('返信できる', async(async () => {
 			const hima = await insertHimawari();
 			const himaPost = await db.get('posts').insert({
-				user_id: hima._id,
+				userId: hima._id,
 				text: 'ひま'
 			});
 
 			const me = await insertSakurako();
 			const post = {
 				text: 'さく',
-				reply_id: himaPost._id.toString()
+				replyId: himaPost._id.toString()
 			};
 			const res = await request('/posts/create', post, me);
 			res.should.have.status(200);
 			res.body.should.be.a('object');
-			res.body.should.have.property('created_post');
-			res.body.created_post.should.have.property('text').eql(post.text);
-			res.body.created_post.should.have.property('reply_id').eql(post.reply_id);
-			res.body.created_post.should.have.property('reply');
-			res.body.created_post.reply.should.have.property('text').eql(himaPost.text);
+			res.body.should.have.property('createdPost');
+			res.body.createdPost.should.have.property('text').eql(post.text);
+			res.body.createdPost.should.have.property('replyId').eql(post.replyId);
+			res.body.createdPost.should.have.property('reply');
+			res.body.createdPost.reply.should.have.property('text').eql(himaPost.text);
 		}));
 
 		it('repostできる', async(async () => {
 			const hima = await insertHimawari();
 			const himaPost = await db.get('posts').insert({
-				user_id: hima._id,
+				userId: hima._id,
 				text: 'こらっさくらこ!'
 			});
 
 			const me = await insertSakurako();
 			const post = {
-				repost_id: himaPost._id.toString()
+				repostId: himaPost._id.toString()
 			};
 			const res = await request('/posts/create', post, me);
 			res.should.have.status(200);
 			res.body.should.be.a('object');
-			res.body.should.have.property('created_post');
-			res.body.created_post.should.have.property('repost_id').eql(post.repost_id);
-			res.body.created_post.should.have.property('repost');
-			res.body.created_post.repost.should.have.property('text').eql(himaPost.text);
+			res.body.should.have.property('createdPost');
+			res.body.createdPost.should.have.property('repostId').eql(post.repostId);
+			res.body.createdPost.should.have.property('repost');
+			res.body.createdPost.repost.should.have.property('text').eql(himaPost.text);
 		}));
 
 		it('引用repostできる', async(async () => {
 			const hima = await insertHimawari();
 			const himaPost = await db.get('posts').insert({
-				user_id: hima._id,
+				userId: hima._id,
 				text: 'こらっさくらこ!'
 			});
 
 			const me = await insertSakurako();
 			const post = {
 				text: 'さく',
-				repost_id: himaPost._id.toString()
+				repostId: himaPost._id.toString()
 			};
 			const res = await request('/posts/create', post, me);
 			res.should.have.status(200);
 			res.body.should.be.a('object');
-			res.body.should.have.property('created_post');
-			res.body.created_post.should.have.property('text').eql(post.text);
-			res.body.created_post.should.have.property('repost_id').eql(post.repost_id);
-			res.body.created_post.should.have.property('repost');
-			res.body.created_post.repost.should.have.property('text').eql(himaPost.text);
+			res.body.should.have.property('createdPost');
+			res.body.createdPost.should.have.property('text').eql(post.text);
+			res.body.createdPost.should.have.property('repostId').eql(post.repostId);
+			res.body.createdPost.should.have.property('repost');
+			res.body.createdPost.repost.should.have.property('text').eql(himaPost.text);
 		}));
 
 		it('文字数ぎりぎりで怒られない', async(async () => {
@@ -358,7 +358,7 @@ describe('API', () => {
 			const me = await insertSakurako();
 			const post = {
 				text: 'さく',
-				reply_id: '000000000000000000000000'
+				replyId: '000000000000000000000000'
 			};
 			const res = await request('/posts/create', post, me);
 			res.should.have.status(400);
@@ -367,7 +367,7 @@ describe('API', () => {
 		it('存在しないrepost対象で怒られる', async(async () => {
 			const me = await insertSakurako();
 			const post = {
-				repost_id: '000000000000000000000000'
+				repostId: '000000000000000000000000'
 			};
 			const res = await request('/posts/create', post, me);
 			res.should.have.status(400);
@@ -377,7 +377,7 @@ describe('API', () => {
 			const me = await insertSakurako();
 			const post = {
 				text: 'さく',
-				reply_id: 'kyoppie'
+				replyId: 'kyoppie'
 			};
 			const res = await request('/posts/create', post, me);
 			res.should.have.status(400);
@@ -386,7 +386,7 @@ describe('API', () => {
 		it('不正なrepost対象IDで怒られる', async(async () => {
 			const me = await insertSakurako();
 			const post = {
-				repost_id: 'kyoppie'
+				repostId: 'kyoppie'
 			};
 			const res = await request('/posts/create', post, me);
 			res.should.have.status(400);
@@ -402,8 +402,8 @@ describe('API', () => {
 			}, me);
 			res.should.have.status(200);
 			res.body.should.be.a('object');
-			res.body.should.have.property('created_post');
-			res.body.created_post.should.have.property('poll');
+			res.body.should.have.property('createdPost');
+			res.body.createdPost.should.have.property('poll');
 		}));
 
 		it('投票の選択肢が無くて怒られる', async(async () => {
@@ -439,11 +439,11 @@ describe('API', () => {
 		it('投稿が取得できる', async(async () => {
 			const me = await insertSakurako();
 			const myPost = await db.get('posts').insert({
-				user_id: me._id,
+				userId: me._id,
 				text: 'お腹ペコい'
 			});
 			const res = await request('/posts/show', {
-				post_id: myPost._id.toString()
+				postId: myPost._id.toString()
 			}, me);
 			res.should.have.status(200);
 			res.body.should.be.a('object');
@@ -452,14 +452,14 @@ describe('API', () => {
 
 		it('投稿が存在しなかったら怒る', async(async () => {
 			const res = await request('/posts/show', {
-				post_id: '000000000000000000000000'
+				postId: '000000000000000000000000'
 			});
 			res.should.have.status(400);
 		}));
 
 		it('間違ったIDで怒られる', async(async () => {
 			const res = await request('/posts/show', {
-				post_id: 'kyoppie'
+				postId: 'kyoppie'
 			});
 			res.should.have.status(400);
 		}));
@@ -469,13 +469,13 @@ describe('API', () => {
 		it('リアクションできる', async(async () => {
 			const hima = await insertHimawari();
 			const himaPost = await db.get('posts').insert({
-				user_id: hima._id,
+				userId: hima._id,
 				text: 'ひま'
 			});
 
 			const me = await insertSakurako();
 			const res = await request('/posts/reactions/create', {
-				post_id: himaPost._id.toString(),
+				postId: himaPost._id.toString(),
 				reaction: 'like'
 			}, me);
 			res.should.have.status(204);
@@ -484,12 +484,12 @@ describe('API', () => {
 		it('自分の投稿にはリアクションできない', async(async () => {
 			const me = await insertSakurako();
 			const myPost = await db.get('posts').insert({
-				user_id: me._id,
+				userId: me._id,
 				text: 'お腹ペコい'
 			});
 
 			const res = await request('/posts/reactions/create', {
-				post_id: myPost._id.toString(),
+				postId: myPost._id.toString(),
 				reaction: 'like'
 			}, me);
 			res.should.have.status(400);
@@ -498,19 +498,19 @@ describe('API', () => {
 		it('二重にリアクションできない', async(async () => {
 			const hima = await insertHimawari();
 			const himaPost = await db.get('posts').insert({
-				user_id: hima._id,
+				userId: hima._id,
 				text: 'ひま'
 			});
 
 			const me = await insertSakurako();
-			await db.get('post_reactions').insert({
-				user_id: me._id,
-				post_id: himaPost._id,
+			await db.get('postReactions').insert({
+				userId: me._id,
+				postId: himaPost._id,
 				reaction: 'like'
 			});
 
 			const res = await request('/posts/reactions/create', {
-				post_id: himaPost._id.toString(),
+				postId: himaPost._id.toString(),
 				reaction: 'like'
 			}, me);
 			res.should.have.status(400);
@@ -519,7 +519,7 @@ describe('API', () => {
 		it('存在しない投稿にはリアクションできない', async(async () => {
 			const me = await insertSakurako();
 			const res = await request('/posts/reactions/create', {
-				post_id: '000000000000000000000000',
+				postId: '000000000000000000000000',
 				reaction: 'like'
 			}, me);
 			res.should.have.status(400);
@@ -534,7 +534,7 @@ describe('API', () => {
 		it('間違ったIDで怒られる', async(async () => {
 			const me = await insertSakurako();
 			const res = await request('/posts/reactions/create', {
-				post_id: 'kyoppie',
+				postId: 'kyoppie',
 				reaction: 'like'
 			}, me);
 			res.should.have.status(400);
@@ -545,19 +545,19 @@ describe('API', () => {
 		it('リアクションをキャンセルできる', async(async () => {
 			const hima = await insertHimawari();
 			const himaPost = await db.get('posts').insert({
-				user_id: hima._id,
+				userId: hima._id,
 				text: 'ひま'
 			});
 
 			const me = await insertSakurako();
-			await db.get('post_reactions').insert({
-				user_id: me._id,
-				post_id: himaPost._id,
+			await db.get('postReactions').insert({
+				userId: me._id,
+				postId: himaPost._id,
 				reaction: 'like'
 			});
 
 			const res = await request('/posts/reactions/delete', {
-				post_id: himaPost._id.toString()
+				postId: himaPost._id.toString()
 			}, me);
 			res.should.have.status(204);
 		}));
@@ -565,13 +565,13 @@ describe('API', () => {
 		it('リアクションしていない投稿はリアクションをキャンセルできない', async(async () => {
 			const hima = await insertHimawari();
 			const himaPost = await db.get('posts').insert({
-				user_id: hima._id,
+				userId: hima._id,
 				text: 'ひま'
 			});
 
 			const me = await insertSakurako();
 			const res = await request('/posts/reactions/delete', {
-				post_id: himaPost._id.toString()
+				postId: himaPost._id.toString()
 			}, me);
 			res.should.have.status(400);
 		}));
@@ -579,7 +579,7 @@ describe('API', () => {
 		it('存在しない投稿はリアクションをキャンセルできない', async(async () => {
 			const me = await insertSakurako();
 			const res = await request('/posts/reactions/delete', {
-				post_id: '000000000000000000000000'
+				postId: '000000000000000000000000'
 			}, me);
 			res.should.have.status(400);
 		}));
@@ -593,7 +593,7 @@ describe('API', () => {
 		it('間違ったIDで怒られる', async(async () => {
 			const me = await insertSakurako();
 			const res = await request('/posts/reactions/delete', {
-				post_id: 'kyoppie'
+				postId: 'kyoppie'
 			}, me);
 			res.should.have.status(400);
 		}));
@@ -604,7 +604,7 @@ describe('API', () => {
 			const hima = await insertHimawari();
 			const me = await insertSakurako();
 			const res = await request('/following/create', {
-				user_id: hima._id.toString()
+				userId: hima._id.toString()
 			}, me);
 			res.should.have.status(204);
 		}));
@@ -613,12 +613,12 @@ describe('API', () => {
 			const hima = await insertHimawari();
 			const me = await insertSakurako();
 			await db.get('following').insert({
-				followee_id: hima._id,
-				follower_id: me._id,
-				deleted_at: new Date()
+				followeeId: hima._id,
+				followerId: me._id,
+				deletedAt: new Date()
 			});
 			const res = await request('/following/create', {
-				user_id: hima._id.toString()
+				userId: hima._id.toString()
 			}, me);
 			res.should.have.status(204);
 		}));
@@ -627,11 +627,11 @@ describe('API', () => {
 			const hima = await insertHimawari();
 			const me = await insertSakurako();
 			await db.get('following').insert({
-				followee_id: hima._id,
-				follower_id: me._id
+				followeeId: hima._id,
+				followerId: me._id
 			});
 			const res = await request('/following/create', {
-				user_id: hima._id.toString()
+				userId: hima._id.toString()
 			}, me);
 			res.should.have.status(400);
 		}));
@@ -639,7 +639,7 @@ describe('API', () => {
 		it('存在しないユーザーはフォローできない', async(async () => {
 			const me = await insertSakurako();
 			const res = await request('/following/create', {
-				user_id: '000000000000000000000000'
+				userId: '000000000000000000000000'
 			}, me);
 			res.should.have.status(400);
 		}));
@@ -647,7 +647,7 @@ describe('API', () => {
 		it('自分自身はフォローできない', async(async () => {
 			const me = await insertSakurako();
 			const res = await request('/following/create', {
-				user_id: me._id.toString()
+				userId: me._id.toString()
 			}, me);
 			res.should.have.status(400);
 		}));
@@ -661,7 +661,7 @@ describe('API', () => {
 		it('間違ったIDで怒られる', async(async () => {
 			const me = await insertSakurako();
 			const res = await request('/following/create', {
-				user_id: 'kyoppie'
+				userId: 'kyoppie'
 			}, me);
 			res.should.have.status(400);
 		}));
@@ -672,11 +672,11 @@ describe('API', () => {
 			const hima = await insertHimawari();
 			const me = await insertSakurako();
 			await db.get('following').insert({
-				followee_id: hima._id,
-				follower_id: me._id
+				followeeId: hima._id,
+				followerId: me._id
 			});
 			const res = await request('/following/delete', {
-				user_id: hima._id.toString()
+				userId: hima._id.toString()
 			}, me);
 			res.should.have.status(204);
 		}));
@@ -685,16 +685,16 @@ describe('API', () => {
 			const hima = await insertHimawari();
 			const me = await insertSakurako();
 			await db.get('following').insert({
-				followee_id: hima._id,
-				follower_id: me._id,
-				deleted_at: new Date()
+				followeeId: hima._id,
+				followerId: me._id,
+				deletedAt: new Date()
 			});
 			await db.get('following').insert({
-				followee_id: hima._id,
-				follower_id: me._id
+				followeeId: hima._id,
+				followerId: me._id
 			});
 			const res = await request('/following/delete', {
-				user_id: hima._id.toString()
+				userId: hima._id.toString()
 			}, me);
 			res.should.have.status(204);
 		}));
@@ -703,7 +703,7 @@ describe('API', () => {
 			const hima = await insertHimawari();
 			const me = await insertSakurako();
 			const res = await request('/following/delete', {
-				user_id: hima._id.toString()
+				userId: hima._id.toString()
 			}, me);
 			res.should.have.status(400);
 		}));
@@ -711,7 +711,7 @@ describe('API', () => {
 		it('存在しないユーザーはフォロー解除できない', async(async () => {
 			const me = await insertSakurako();
 			const res = await request('/following/delete', {
-				user_id: '000000000000000000000000'
+				userId: '000000000000000000000000'
 			}, me);
 			res.should.have.status(400);
 		}));
@@ -719,7 +719,7 @@ describe('API', () => {
 		it('自分自身はフォロー解除できない', async(async () => {
 			const me = await insertSakurako();
 			const res = await request('/following/delete', {
-				user_id: me._id.toString()
+				userId: me._id.toString()
 			}, me);
 			res.should.have.status(400);
 		}));
@@ -733,7 +733,7 @@ describe('API', () => {
 		it('間違ったIDで怒られる', async(async () => {
 			const me = await insertSakurako();
 			const res = await request('/following/delete', {
-				user_id: 'kyoppie'
+				userId: 'kyoppie'
 			}, me);
 			res.should.have.status(400);
 		}));
@@ -743,15 +743,15 @@ describe('API', () => {
 		it('ドライブ情報を取得できる', async(async () => {
 			const me = await insertSakurako();
 			await insertDriveFile({
-				user_id: me._id,
+				userId: me._id,
 				datasize: 256
 			});
 			await insertDriveFile({
-				user_id: me._id,
+				userId: me._id,
 				datasize: 512
 			});
 			await insertDriveFile({
-				user_id: me._id,
+				userId: me._id,
 				datasize: 1024
 			});
 			const res = await request('/drive', {}, me);
@@ -784,11 +784,11 @@ describe('API', () => {
 		it('名前を更新できる', async(async () => {
 			const me = await insertSakurako();
 			const file = await insertDriveFile({
-				user_id: me._id
+				userId: me._id
 			});
 			const newName = 'いちごパスタ.png';
 			const res = await request('/drive/files/update', {
-				file_id: file._id.toString(),
+				fileId: file._id.toString(),
 				name: newName
 			}, me);
 			res.should.have.status(200);
@@ -800,10 +800,10 @@ describe('API', () => {
 			const me = await insertSakurako();
 			const hima = await insertHimawari();
 			const file = await insertDriveFile({
-				user_id: hima._id
+				userId: hima._id
 			});
 			const res = await request('/drive/files/update', {
-				file_id: file._id.toString(),
+				fileId: file._id.toString(),
 				name: 'いちごパスタ.png'
 			}, me);
 			res.should.have.status(400);
@@ -812,47 +812,47 @@ describe('API', () => {
 		it('親フォルダを更新できる', async(async () => {
 			const me = await insertSakurako();
 			const file = await insertDriveFile({
-				user_id: me._id
+				userId: me._id
 			});
 			const folder = await insertDriveFolder({
-				user_id: me._id
+				userId: me._id
 			});
 			const res = await request('/drive/files/update', {
-				file_id: file._id.toString(),
-				folder_id: folder._id.toString()
+				fileId: file._id.toString(),
+				folderId: folder._id.toString()
 			}, me);
 			res.should.have.status(200);
 			res.body.should.be.a('object');
-			res.body.should.have.property('folder_id').eql(folder._id.toString());
+			res.body.should.have.property('folderId').eql(folder._id.toString());
 		}));
 
 		it('親フォルダを無しにできる', async(async () => {
 			const me = await insertSakurako();
 			const file = await insertDriveFile({
-				user_id: me._id,
-				folder_id: '000000000000000000000000'
+				userId: me._id,
+				folderId: '000000000000000000000000'
 			});
 			const res = await request('/drive/files/update', {
-				file_id: file._id.toString(),
-				folder_id: null
+				fileId: file._id.toString(),
+				folderId: null
 			}, me);
 			res.should.have.status(200);
 			res.body.should.be.a('object');
-			res.body.should.have.property('folder_id').eql(null);
+			res.body.should.have.property('folderId').eql(null);
 		}));
 
 		it('他人のフォルダには入れられない', async(async () => {
 			const me = await insertSakurako();
 			const hima = await insertHimawari();
 			const file = await insertDriveFile({
-				user_id: me._id
+				userId: me._id
 			});
 			const folder = await insertDriveFolder({
-				user_id: hima._id
+				userId: hima._id
 			});
 			const res = await request('/drive/files/update', {
-				file_id: file._id.toString(),
-				folder_id: folder._id.toString()
+				fileId: file._id.toString(),
+				folderId: folder._id.toString()
 			}, me);
 			res.should.have.status(400);
 		}));
@@ -860,11 +860,11 @@ describe('API', () => {
 		it('存在しないフォルダで怒られる', async(async () => {
 			const me = await insertSakurako();
 			const file = await insertDriveFile({
-				user_id: me._id
+				userId: me._id
 			});
 			const res = await request('/drive/files/update', {
-				file_id: file._id.toString(),
-				folder_id: '000000000000000000000000'
+				fileId: file._id.toString(),
+				folderId: '000000000000000000000000'
 			}, me);
 			res.should.have.status(400);
 		}));
@@ -872,11 +872,11 @@ describe('API', () => {
 		it('不正なフォルダIDで怒られる', async(async () => {
 			const me = await insertSakurako();
 			const file = await insertDriveFile({
-				user_id: me._id
+				userId: me._id
 			});
 			const res = await request('/drive/files/update', {
-				file_id: file._id.toString(),
-				folder_id: 'kyoppie'
+				fileId: file._id.toString(),
+				folderId: 'kyoppie'
 			}, me);
 			res.should.have.status(400);
 		}));
@@ -884,7 +884,7 @@ describe('API', () => {
 		it('ファイルが存在しなかったら怒る', async(async () => {
 			const me = await insertSakurako();
 			const res = await request('/drive/files/update', {
-				file_id: '000000000000000000000000',
+				fileId: '000000000000000000000000',
 				name: 'いちごパスタ.png'
 			}, me);
 			res.should.have.status(400);
@@ -893,7 +893,7 @@ describe('API', () => {
 		it('間違ったIDで怒られる', async(async () => {
 			const me = await insertSakurako();
 			const res = await request('/drive/files/update', {
-				file_id: 'kyoppie',
+				fileId: 'kyoppie',
 				name: 'いちごパスタ.png'
 			}, me);
 			res.should.have.status(400);
@@ -916,10 +916,10 @@ describe('API', () => {
 		it('名前を更新できる', async(async () => {
 			const me = await insertSakurako();
 			const folder = await insertDriveFolder({
-				user_id: me._id
+				userId: me._id
 			});
 			const res = await request('/drive/folders/update', {
-				folder_id: folder._id.toString(),
+				folderId: folder._id.toString(),
 				name: 'new name'
 			}, me);
 			res.should.have.status(200);
@@ -931,10 +931,10 @@ describe('API', () => {
 			const me = await insertSakurako();
 			const hima = await insertHimawari();
 			const folder = await insertDriveFolder({
-				user_id: hima._id
+				userId: hima._id
 			});
 			const res = await request('/drive/folders/update', {
-				folder_id: folder._id.toString(),
+				folderId: folder._id.toString(),
 				name: 'new name'
 			}, me);
 			res.should.have.status(400);
@@ -943,47 +943,47 @@ describe('API', () => {
 		it('親フォルダを更新できる', async(async () => {
 			const me = await insertSakurako();
 			const folder = await insertDriveFolder({
-				user_id: me._id
+				userId: me._id
 			});
 			const parentFolder = await insertDriveFolder({
-				user_id: me._id
+				userId: me._id
 			});
 			const res = await request('/drive/folders/update', {
-				folder_id: folder._id.toString(),
-				parent_id: parentFolder._id.toString()
+				folderId: folder._id.toString(),
+				parentId: parentFolder._id.toString()
 			}, me);
 			res.should.have.status(200);
 			res.body.should.be.a('object');
-			res.body.should.have.property('parent_id').eql(parentFolder._id.toString());
+			res.body.should.have.property('parentId').eql(parentFolder._id.toString());
 		}));
 
 		it('親フォルダを無しに更新できる', async(async () => {
 			const me = await insertSakurako();
 			const folder = await insertDriveFolder({
-				user_id: me._id,
-				parent_id: '000000000000000000000000'
+				userId: me._id,
+				parentId: '000000000000000000000000'
 			});
 			const res = await request('/drive/folders/update', {
-				folder_id: folder._id.toString(),
-				parent_id: null
+				folderId: folder._id.toString(),
+				parentId: null
 			}, me);
 			res.should.have.status(200);
 			res.body.should.be.a('object');
-			res.body.should.have.property('parent_id').eql(null);
+			res.body.should.have.property('parentId').eql(null);
 		}));
 
 		it('他人のフォルダを親フォルダに設定できない', async(async () => {
 			const me = await insertSakurako();
 			const hima = await insertHimawari();
 			const folder = await insertDriveFolder({
-				user_id: me._id
+				userId: me._id
 			});
 			const parentFolder = await insertDriveFolder({
-				user_id: hima._id
+				userId: hima._id
 			});
 			const res = await request('/drive/folders/update', {
-				folder_id: folder._id.toString(),
-				parent_id: parentFolder._id.toString()
+				folderId: folder._id.toString(),
+				parentId: parentFolder._id.toString()
 			}, me);
 			res.should.have.status(400);
 		}));
@@ -992,11 +992,11 @@ describe('API', () => {
 			const me = await insertSakurako();
 			const folder = await insertDriveFolder();
 			const parentFolder = await insertDriveFolder({
-				parent_id: folder._id
+				parentId: folder._id
 			});
 			const res = await request('/drive/folders/update', {
-				folder_id: folder._id.toString(),
-				parent_id: parentFolder._id.toString()
+				folderId: folder._id.toString(),
+				parentId: parentFolder._id.toString()
 			}, me);
 			res.should.have.status(400);
 		}));
@@ -1005,14 +1005,14 @@ describe('API', () => {
 			const me = await insertSakurako();
 			const folderA = await insertDriveFolder();
 			const folderB = await insertDriveFolder({
-				parent_id: folderA._id
+				parentId: folderA._id
 			});
 			const folderC = await insertDriveFolder({
-				parent_id: folderB._id
+				parentId: folderB._id
 			});
 			const res = await request('/drive/folders/update', {
-				folder_id: folderA._id.toString(),
-				parent_id: folderC._id.toString()
+				folderId: folderA._id.toString(),
+				parentId: folderC._id.toString()
 			}, me);
 			res.should.have.status(400);
 		}));
@@ -1021,8 +1021,8 @@ describe('API', () => {
 			const me = await insertSakurako();
 			const folder = await insertDriveFolder();
 			const res = await request('/drive/folders/update', {
-				folder_id: folder._id.toString(),
-				parent_id: '000000000000000000000000'
+				folderId: folder._id.toString(),
+				parentId: '000000000000000000000000'
 			}, me);
 			res.should.have.status(400);
 		}));
@@ -1031,8 +1031,8 @@ describe('API', () => {
 			const me = await insertSakurako();
 			const folder = await insertDriveFolder();
 			const res = await request('/drive/folders/update', {
-				folder_id: folder._id.toString(),
-				parent_id: 'kyoppie'
+				folderId: folder._id.toString(),
+				parentId: 'kyoppie'
 			}, me);
 			res.should.have.status(400);
 		}));
@@ -1040,7 +1040,7 @@ describe('API', () => {
 		it('存在しないフォルダを更新できない', async(async () => {
 			const me = await insertSakurako();
 			const res = await request('/drive/folders/update', {
-				folder_id: '000000000000000000000000'
+				folderId: '000000000000000000000000'
 			}, me);
 			res.should.have.status(400);
 		}));
@@ -1048,7 +1048,7 @@ describe('API', () => {
 		it('不正なフォルダIDで怒られる', async(async () => {
 			const me = await insertSakurako();
 			const res = await request('/drive/folders/update', {
-				folder_id: 'kyoppie'
+				folderId: 'kyoppie'
 			}, me);
 			res.should.have.status(400);
 		}));
@@ -1059,7 +1059,7 @@ describe('API', () => {
 			const me = await insertSakurako();
 			const hima = await insertHimawari();
 			const res = await request('/messaging/messages/create', {
-				user_id: hima._id.toString(),
+				userId: hima._id.toString(),
 				text: 'Hey hey ひまわり'
 			}, me);
 			res.should.have.status(200);
@@ -1070,7 +1070,7 @@ describe('API', () => {
 		it('自分自身にはメッセージを送信できない', async(async () => {
 			const me = await insertSakurako();
 			const res = await request('/messaging/messages/create', {
-				user_id: me._id.toString(),
+				userId: me._id.toString(),
 				text: 'Yo'
 			}, me);
 			res.should.have.status(400);
@@ -1079,7 +1079,7 @@ describe('API', () => {
 		it('存在しないユーザーにはメッセージを送信できない', async(async () => {
 			const me = await insertSakurako();
 			const res = await request('/messaging/messages/create', {
-				user_id: '000000000000000000000000',
+				userId: '000000000000000000000000',
 				text: 'Yo'
 			}, me);
 			res.should.have.status(400);
@@ -1088,7 +1088,7 @@ describe('API', () => {
 		it('不正なユーザーIDで怒られる', async(async () => {
 			const me = await insertSakurako();
 			const res = await request('/messaging/messages/create', {
-				user_id: 'kyoppie',
+				userId: 'kyoppie',
 				text: 'Yo'
 			}, me);
 			res.should.have.status(400);
@@ -1098,7 +1098,7 @@ describe('API', () => {
 			const me = await insertSakurako();
 			const hima = await insertHimawari();
 			const res = await request('/messaging/messages/create', {
-				user_id: hima._id.toString()
+				userId: hima._id.toString()
 			}, me);
 			res.should.have.status(400);
 		}));
@@ -1107,7 +1107,7 @@ describe('API', () => {
 			const me = await insertSakurako();
 			const hima = await insertHimawari();
 			const res = await request('/messaging/messages/create', {
-				user_id: hima._id.toString(),
+				userId: hima._id.toString(),
 				text: '!'.repeat(1001)
 			}, me);
 			res.should.have.status(400);
@@ -1118,7 +1118,7 @@ describe('API', () => {
 		it('認証セッションを作成できる', async(async () => {
 			const app = await insertApp();
 			const res = await request('/auth/session/generate', {
-				app_secret: app.secret
+				appSecret: app.secret
 			});
 			res.should.have.status(200);
 			res.body.should.be.a('object');
@@ -1126,14 +1126,14 @@ describe('API', () => {
 			res.body.should.have.property('url');
 		}));
 
-		it('app_secret 無しで怒られる', async(async () => {
+		it('appSecret 無しで怒られる', async(async () => {
 			const res = await request('/auth/session/generate', {});
 			res.should.have.status(400);
 		}));
 
-		it('誤った app secret で怒られる', async(async () => {
+		it('誤った appSecret で怒られる', async(async () => {
 			const res = await request('/auth/session/generate', {
-				app_secret: 'kyoppie'
+				appSecret: 'kyoppie'
 			});
 			res.should.have.status(400);
 		}));
@@ -1159,14 +1159,14 @@ function deepAssign(destination, ...sources) {
 function insertSakurako(opts) {
 	return db.get('users').insert(deepAssign({
 		username: 'sakurako',
-		username_lower: 'sakurako',
+		usernameLower: 'sakurako',
 		account: {
 			keypair: '-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAtdTG9rlFWjNqhgbg2V6X5XF1WpQXZS3KNXykEWl2UAiMyfVV\nBvf3zQP0dDEdNtcqdPJgis03bpiHCzQusc/YLyHYB0m+TJXsxJatb8cqUogOFeE4\ngQ4Dc5kAT6gLh/d4yz03EIg9bizX07EiGWnZqWxb+21ypqsPxST64sAtG9f5O/G4\nXe2m3cSbfAAvEUP1Ig1LUNyJB4jhM60w1cQic/qO8++sk/+GoX9g71X+i4NArGv+\n1c11acDIIPGAAQpFeYVeGaKakNDNp8RtJJp8R8FLwJXZ4/gATBnScCiHUSrGfRly\nYyR0w/BNlQ6/NijAdB9pR5csPvyIPkx1gauZewIDAQABAoIBAQCwWf/mhuY2h6uG\n9eDZsZ7Mj2/sO7k9Dl4R5iMSKCDxmnlB3slqitExa+aJUqEs8R5icjkkJcjfYNuJ\nCEFJf3YCsGZfGyyQBtCuEh2ATcBEb2SJ3/f3YuoCEaB1oVwdsOzc4TAovpol4yQo\nUqHp1/mdElVb01jhQQN4h1c02IJnfzvfU1C8szBni+Etfd+MxqGfv006DY3KOEb3\nlCrCS3GmooJW2Fjj7q1kCcaEQbMB1/aQHLXd1qe3KJOzXh3Voxsp/jEH0hvp2TII\nfY9UK+b7mA+xlvXwKuTkHVaZm0ylg0nbembS8MF4GfFMujinSexvLrVKaQhdMFoF\nvBLxHYHRAoGBANfNVYJYeCDPFNLmak5Xg33Rfvc2II8UmrZOVdhOWs8ZK0pis9e+\nPo2MKtTzrzipXI2QXv5w7kO+LJWNDva+xRlW8Wlj9Dde9QdQ7Y8+dk7SJgf24DzM\n023elgX5DvTeLODjStk6SMPRL0FmGovUqAAA8ZeHtJzkIr1HROWnQiwnAoGBANez\nhFwKnVoQu0RpBz/i4W0RKIxOwltN2zmlN8KjJPhSy00A7nBUfKLRbcwiSHE98Yi/\nUrXwMwR5QeD2ngngRppddJnpiRfjNjnsaqeqNtpO8AxB3XjpCC5zmHUMFHKvPpDj\n1zU/F44li0YjKcMBebZy9PbfAjrIgJfxhPo/oXiNAoGAfx6gaTjOAp2ZaaZ7Jozc\nkyft/5et1DrR6+P3I4T8bxQncRj1UXfqhxzzOiAVrm3tbCKIIp/JarRCtRGzp9u2\nZPfXGzra6CcSdW3Rkli7/jBCYNynOIl7XjQI8ZnFmq6phwu80ntH07mMeZy4tHff\nQqlLpvQ0i1rDr/Wkexdsnm8CgYBgxha9ILoF/Xm3MJPjEsxmnYsen/tM8XpIu5pv\nxbhBfQvfKWrQlOcyOVnUexEbVVo3KvdVz0VkXW60GpE/BxNGEGXO49rxD6x1gl87\nh/+CJGZIaYiOxaY5CP2+jcPizEL6yG32Yq8TxD5fIkmLRu8vbxX+aIFclDY1dVNe\n3wt3xQKBgGEL0EjwRch+P2V+YHAhbETPrEqJjHRWT95pIdF9XtC8fasSOVH81cLX\nXXsX1FTvOJNwG9Nk8rQjYJXGTb2O/2unaazlYUwxKwVpwuGzz/vhH/roHZBAkIVT\njvpykpn9QMezEdpzj5BEv01QzSYBPzIh5myrpoJIoSW7py7zFG3h\n-----END RSA PRIVATE KEY-----\n',
 			token: '!00000000000000000000000000000000',
 			password: '$2a$08$FnHXg3tP.M/kINWgQSXNqeoBsiVrkj.ecXX8mW9rfBzMRkibYfjYy', // HimawariDaisuki06160907
 			profile: {},
 			settings: {},
-			client_settings: {}
+			clientSettings: {}
 		}
 	}, opts));
 }
@@ -1174,20 +1174,20 @@ function insertSakurako(opts) {
 function insertHimawari(opts) {
 	return db.get('users').insert(deepAssign({
 		username: 'himawari',
-		username_lower: 'himawari',
+		usernameLower: 'himawari',
 		account: {
 			keypair: '-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAtdTG9rlFWjNqhgbg2V6X5XF1WpQXZS3KNXykEWl2UAiMyfVV\nBvf3zQP0dDEdNtcqdPJgis03bpiHCzQusc/YLyHYB0m+TJXsxJatb8cqUogOFeE4\ngQ4Dc5kAT6gLh/d4yz03EIg9bizX07EiGWnZqWxb+21ypqsPxST64sAtG9f5O/G4\nXe2m3cSbfAAvEUP1Ig1LUNyJB4jhM60w1cQic/qO8++sk/+GoX9g71X+i4NArGv+\n1c11acDIIPGAAQpFeYVeGaKakNDNp8RtJJp8R8FLwJXZ4/gATBnScCiHUSrGfRly\nYyR0w/BNlQ6/NijAdB9pR5csPvyIPkx1gauZewIDAQABAoIBAQCwWf/mhuY2h6uG\n9eDZsZ7Mj2/sO7k9Dl4R5iMSKCDxmnlB3slqitExa+aJUqEs8R5icjkkJcjfYNuJ\nCEFJf3YCsGZfGyyQBtCuEh2ATcBEb2SJ3/f3YuoCEaB1oVwdsOzc4TAovpol4yQo\nUqHp1/mdElVb01jhQQN4h1c02IJnfzvfU1C8szBni+Etfd+MxqGfv006DY3KOEb3\nlCrCS3GmooJW2Fjj7q1kCcaEQbMB1/aQHLXd1qe3KJOzXh3Voxsp/jEH0hvp2TII\nfY9UK+b7mA+xlvXwKuTkHVaZm0ylg0nbembS8MF4GfFMujinSexvLrVKaQhdMFoF\nvBLxHYHRAoGBANfNVYJYeCDPFNLmak5Xg33Rfvc2II8UmrZOVdhOWs8ZK0pis9e+\nPo2MKtTzrzipXI2QXv5w7kO+LJWNDva+xRlW8Wlj9Dde9QdQ7Y8+dk7SJgf24DzM\n023elgX5DvTeLODjStk6SMPRL0FmGovUqAAA8ZeHtJzkIr1HROWnQiwnAoGBANez\nhFwKnVoQu0RpBz/i4W0RKIxOwltN2zmlN8KjJPhSy00A7nBUfKLRbcwiSHE98Yi/\nUrXwMwR5QeD2ngngRppddJnpiRfjNjnsaqeqNtpO8AxB3XjpCC5zmHUMFHKvPpDj\n1zU/F44li0YjKcMBebZy9PbfAjrIgJfxhPo/oXiNAoGAfx6gaTjOAp2ZaaZ7Jozc\nkyft/5et1DrR6+P3I4T8bxQncRj1UXfqhxzzOiAVrm3tbCKIIp/JarRCtRGzp9u2\nZPfXGzra6CcSdW3Rkli7/jBCYNynOIl7XjQI8ZnFmq6phwu80ntH07mMeZy4tHff\nQqlLpvQ0i1rDr/Wkexdsnm8CgYBgxha9ILoF/Xm3MJPjEsxmnYsen/tM8XpIu5pv\nxbhBfQvfKWrQlOcyOVnUexEbVVo3KvdVz0VkXW60GpE/BxNGEGXO49rxD6x1gl87\nh/+CJGZIaYiOxaY5CP2+jcPizEL6yG32Yq8TxD5fIkmLRu8vbxX+aIFclDY1dVNe\n3wt3xQKBgGEL0EjwRch+P2V+YHAhbETPrEqJjHRWT95pIdF9XtC8fasSOVH81cLX\nXXsX1FTvOJNwG9Nk8rQjYJXGTb2O/2unaazlYUwxKwVpwuGzz/vhH/roHZBAkIVT\njvpykpn9QMezEdpzj5BEv01QzSYBPzIh5myrpoJIoSW7py7zFG3h\n-----END RSA PRIVATE KEY-----\n',
 			token: '!00000000000000000000000000000001',
 			password: '$2a$08$OPESxR2RE/ZijjGanNKk6ezSqGFitqsbZqTjWUZPLhORMKxHCbc4O', // ilovesakurako
 			profile: {},
 			settings: {},
-			client_settings: {}
+			clientSettings: {}
 		}
 	}, opts));
 }
 
 function insertDriveFile(opts) {
-	return db.get('drive_files.files').insert({
+	return db.get('driveFiles.files').insert({
 		length: opts.datasize,
 		filename: 'strawberry-pasta.png',
 		metadata: opts
@@ -1195,9 +1195,9 @@ function insertDriveFile(opts) {
 }
 
 function insertDriveFolder(opts) {
-	return db.get('drive_folders').insert(deepAssign({
+	return db.get('driveFolders').insert(deepAssign({
 		name: 'my folder',
-		parent_id: null
+		parentId: null
 	}, opts));
 }
 

From 565b6dfd68935783848aecd3096a336400e8c0d7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 31 Mar 2018 22:22:11 +0900
Subject: [PATCH 0959/1250] Fix

---
 test/api.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/api.js b/test/api.js
index 5b3b8e38d..962730836 100644
--- a/test/api.js
+++ b/test/api.js
@@ -17,7 +17,7 @@ const should = _chai.should();
 
 _chai.use(chaiHttp);
 
-const server = require('../built/server/api/server');
+const server = require('../built/server/api');
 const db = require('../built/db/mongodb').default;
 
 const async = fn => (done) => {

From 04bca07b950c2799594d37f414567cdd3fa73fcc Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 1 Apr 2018 11:05:56 +0900
Subject: [PATCH 0960/1250] Fix

---
 test/text.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/test/text.js b/test/text.js
index 4f739cc1b..514a91f83 100644
--- a/test/text.js
+++ b/test/text.js
@@ -4,8 +4,8 @@
 
 const assert = require('assert');
 
-const analyze = require('../built/server/api/common/text').default;
-const syntaxhighlighter = require('../built/server/api/common/text/core/syntax-highlighter').default;
+const analyze = require('../built/common/text').default;
+const syntaxhighlighter = require('../built/common/text/core/syntax-highlighter').default;
 
 describe('Text', () => {
 	it('can be analyzed', () => {

From 07a9eda9dadf337a15da8d41bf34b5ee08f2d4b9 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Sun, 1 Apr 2018 12:24:29 +0900
Subject: [PATCH 0961/1250] Implement Activity Streams representation of user

---
 src/crypto_key.d.ts       |  1 +
 src/models/user.ts        | 55 -----------------------------------
 src/server/activitypub.ts | 60 +++++++++++++++++++++++++++++++++++++++
 src/server/index.ts       |  2 ++
 4 files changed, 63 insertions(+), 55 deletions(-)
 create mode 100644 src/server/activitypub.ts

diff --git a/src/crypto_key.d.ts b/src/crypto_key.d.ts
index 28ac2f968..48efef298 100644
--- a/src/crypto_key.d.ts
+++ b/src/crypto_key.d.ts
@@ -1 +1,2 @@
+export function extractPublic(keypair: String): String;
 export function generate(): String;
diff --git a/src/models/user.ts b/src/models/user.ts
index 4fbfdec90..d228766e3 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -275,61 +275,6 @@ export const pack = (
 	resolve(_user);
 });
 
-/**
- * Pack a user for ActivityPub
- *
- * @param user target
- * @return Packed user
- */
-export const packForAp = (
-	user: string | mongo.ObjectID | IUser
-) => new Promise<any>(async (resolve, reject) => {
-
-	let _user: any;
-
-	const fields = {
-		// something
-	};
-
-	// Populate the user if 'user' is ID
-	if (mongo.ObjectID.prototype.isPrototypeOf(user)) {
-		_user = await User.findOne({
-			_id: user
-		}, { fields });
-	} else if (typeof user === 'string') {
-		_user = await User.findOne({
-			_id: new mongo.ObjectID(user)
-		}, { fields });
-	} else {
-		_user = deepcopy(user);
-	}
-
-	if (!_user) return reject('invalid user arg.');
-
-	const userUrl = `${config.url}/@@${_user._id}`;
-
-	resolve({
-		"@context": ["https://www.w3.org/ns/activitystreams", {
-			"@language": "ja"
-		}],
-		"type": "Person",
-		"id": userUrl,
-		"following": `${userUrl}/following.json`,
-		"followers": `${userUrl}/followers.json`,
-		"liked": `${userUrl}/liked.json`,
-		"inbox": `${userUrl}/inbox.json`,
-		"outbox": `${userUrl}/outbox.json`,
-		"sharedInbox": `${config.url}/inbox`,
-		"url": `${config.url}/@${_user.username}`,
-		"preferredUsername": _user.username,
-		"name": _user.name,
-		"summary": _user.description,
-		"icon": [
-			`${config.drive_url}/${_user.avatarId}`
-		]
-	});
-});
-
 /*
 function img(url) {
 	return {
diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts
new file mode 100644
index 000000000..6cc31de7b
--- /dev/null
+++ b/src/server/activitypub.ts
@@ -0,0 +1,60 @@
+import config from '../conf';
+import { extractPublic } from '../crypto_key';
+import parseAcct from '../common/user/parse-acct';
+import User, { ILocalAccount } from '../models/user';
+const express = require('express');
+
+const app = express();
+
+app.get('/@:user', async (req, res, next) => {
+	const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']);
+	if (!['application/activity+json', 'application/ld+json'].includes(accepted)) {
+		return next();
+	}
+
+	const { username, host } = parseAcct(req.params.user);
+	if (host !== null) {
+		return res.send(422);
+	}
+
+	const user = await User.findOne({
+		usernameLower: username.toLowerCase(),
+		host: null
+	});
+	if (user === null) {
+		return res.send(404);
+	}
+
+	const id = `${config.url}/@${user.username}`;
+
+	if (username !== user.username) {
+		return res.redirect(id);
+	}
+
+	res.json({
+		'@context': [
+			'https://www.w3.org/ns/activitystreams',
+			'https://w3id.org/security/v1'
+		],
+		type: 'Person',
+		id,
+		preferredUsername: user.username,
+		name: user.name,
+		summary: user.description,
+		icon: user.avatarId && {
+			type: 'Image',
+			url: `${config.drive_url}/${user.avatarId}`
+		},
+		image: user.bannerId && {
+			type: 'Image',
+			url: `${config.drive_url}/${user.bannerId}`
+		},
+		publicKey: {
+			type: 'Key',
+			owner: id,
+			publicKeyPem: extractPublic((user.account as ILocalAccount).keypair)
+		}
+	});
+});
+
+export default app;
diff --git a/src/server/index.ts b/src/server/index.ts
index fe22d9c9b..92d46d46a 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -9,6 +9,7 @@ import * as express from 'express';
 import * as morgan from 'morgan';
 import Accesses from 'accesses';
 
+import activityPub from './activitypub';
 import log from './log-request';
 import config from '../conf';
 
@@ -53,6 +54,7 @@ app.use((req, res, next) => {
  */
 app.use('/api', require('./api'));
 app.use('/files', require('./file'));
+app.use(activityPub);
 app.use(require('./web'));
 
 function createServer() {

From 4786a15034bb8c3fd6b2952a225f98c3b238515e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 1 Apr 2018 12:33:26 +0900
Subject: [PATCH 0962/1250] Use sendStatus instead of send

---
 src/server/activitypub.ts | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts
index 6cc31de7b..abcee43d3 100644
--- a/src/server/activitypub.ts
+++ b/src/server/activitypub.ts
@@ -1,8 +1,9 @@
+import * as express from 'express';
+
 import config from '../conf';
 import { extractPublic } from '../crypto_key';
 import parseAcct from '../common/user/parse-acct';
 import User, { ILocalAccount } from '../models/user';
-const express = require('express');
 
 const app = express();
 
@@ -14,7 +15,7 @@ app.get('/@:user', async (req, res, next) => {
 
 	const { username, host } = parseAcct(req.params.user);
 	if (host !== null) {
-		return res.send(422);
+		return res.sendStatus(422);
 	}
 
 	const user = await User.findOne({
@@ -22,7 +23,7 @@ app.get('/@:user', async (req, res, next) => {
 		host: null
 	});
 	if (user === null) {
-		return res.send(404);
+		return res.sendStatus(404);
 	}
 
 	const id = `${config.url}/@${user.username}`;

From 807a88fb9dad5eabded0088e46df032602525ac5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 1 Apr 2018 12:37:53 +0900
Subject: [PATCH 0963/1250] :v:

---
 src/client/docs/api/gulpfile.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/client/docs/api/gulpfile.ts b/src/client/docs/api/gulpfile.ts
index 16066b0d2..4b962fe0c 100644
--- a/src/client/docs/api/gulpfile.ts
+++ b/src/client/docs/api/gulpfile.ts
@@ -101,7 +101,7 @@ gulp.task('doc:api:endpoints', async () => {
 		}
 		//console.log(files);
 		files.forEach(file => {
-			const ep = yaml.safeLoad(fs.readFileSync(file, 'utf-8'));
+			const ep: any = yaml.safeLoad(fs.readFileSync(file, 'utf-8'));
 			const vars = {
 				endpoint: ep.endpoint,
 				url: {

From 2617c07f9a68605cce4084e1b3851d55a84492e9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 1 Apr 2018 12:41:19 +0900
Subject: [PATCH 0964/1250] Fix test

---
 test/text.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/text.js b/test/text.js
index 514a91f83..1c034d633 100644
--- a/test/text.js
+++ b/test/text.js
@@ -4,7 +4,7 @@
 
 const assert = require('assert');
 
-const analyze = require('../built/common/text').default;
+const analyze = require('../built/common/text/parse').default;
 const syntaxhighlighter = require('../built/common/text/core/syntax-highlighter').default;
 
 describe('Text', () => {

From 713480d923d326e273207ec667b27b63429bae67 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 1 Apr 2018 12:43:59 +0900
Subject: [PATCH 0965/1250] Disable needless header

---
 src/server/activitypub.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts
index abcee43d3..a48a8e643 100644
--- a/src/server/activitypub.ts
+++ b/src/server/activitypub.ts
@@ -6,6 +6,7 @@ import parseAcct from '../common/user/parse-acct';
 import User, { ILocalAccount } from '../models/user';
 
 const app = express();
+app.disable('x-powered-by');
 
 app.get('/@:user', async (req, res, next) => {
 	const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']);

From 1efde507929fa80a65b1d5bb1893b661eb62333f Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Sun, 1 Apr 2018 14:10:22 +0900
Subject: [PATCH 0966/1250] Mark host parameter of /api/users/show nullable

---
 src/server/api/endpoints/users/show.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/server/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts
index fd51d386b..3095d55f1 100644
--- a/src/server/api/endpoints/users/show.ts
+++ b/src/server/api/endpoints/users/show.ts
@@ -65,7 +65,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	if (usernameErr) return rej('invalid username param');
 
 	// Get 'host' parameter
-	const [host, hostErr] = $(params.host).optional.string().$;
+	const [host, hostErr] = $(params.host).nullable.optional.string().$;
 	if (hostErr) return rej('invalid host param');
 
 	if (userId === undefined && typeof username !== 'string') {

From 388a9c2b18ef71b1530927d5e310d1daeb2351ec Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Sun, 1 Apr 2018 14:12:07 +0900
Subject: [PATCH 0967/1250] Implement WebFinger

---
 src/server/index.ts     |  2 ++
 src/server/webfinger.ts | 47 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 49 insertions(+)
 create mode 100644 src/server/webfinger.ts

diff --git a/src/server/index.ts b/src/server/index.ts
index 92d46d46a..187479011 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -10,6 +10,7 @@ import * as morgan from 'morgan';
 import Accesses from 'accesses';
 
 import activityPub from './activitypub';
+import webFinger from './webfinger';
 import log from './log-request';
 import config from '../conf';
 
@@ -55,6 +56,7 @@ app.use((req, res, next) => {
 app.use('/api', require('./api'));
 app.use('/files', require('./file'));
 app.use(activityPub);
+app.use(webFinger);
 app.use(require('./web'));
 
 function createServer() {
diff --git a/src/server/webfinger.ts b/src/server/webfinger.ts
new file mode 100644
index 000000000..864bb4af5
--- /dev/null
+++ b/src/server/webfinger.ts
@@ -0,0 +1,47 @@
+import config from '../conf';
+import parseAcct from '../common/user/parse-acct';
+import User from '../models/user';
+const express = require('express');
+
+const app = express();
+
+app.get('/.well-known/webfinger', async (req, res) => {
+	if (typeof req.query.resource !== 'string') {
+		return res.sendStatus(400);
+	}
+
+	const resourceLower = req.query.resource.toLowerCase();
+	const webPrefix = config.url.toLowerCase() + '/@';
+	let acctLower;
+
+	if (resourceLower.startsWith(webPrefix)) {
+		acctLower = resourceLower.slice(webPrefix.length);
+	} else if (resourceLower.startsWith('acct:')) {
+		acctLower = resourceLower.slice('acct:'.length);
+	} else {
+		acctLower = resourceLower;
+	}
+
+	const parsedAcctLower = parseAcct(acctLower);
+	if (![null, config.host.toLowerCase()].includes(parsedAcctLower.host)) {
+		return res.sendStatus(422);
+	}
+
+	const user = await User.findOne({ usernameLower: parsedAcctLower.username, host: null });
+	if (user === null) {
+		return res.sendStatus(404);
+	}
+
+	return res.json({
+		subject: `acct:${user.username}@${config.host}`,
+		links: [
+			{
+				rel: 'self',
+				type: 'application/activity+json',
+				href: `${config.url}/@${user.username}`
+			}
+		]
+	});
+});
+
+export default app;

From 2ba079342f1d4fd2ef5f012446910d025312a144 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 1 Apr 2018 15:58:32 +0900
Subject: [PATCH 0968/1250] [wip] dark mode

---
 src/client/app/desktop/views/components/ui.header.vue | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)

diff --git a/src/client/app/desktop/views/components/ui.header.vue b/src/client/app/desktop/views/components/ui.header.vue
index 7e337d2ae..448d04d26 100644
--- a/src/client/app/desktop/views/components/ui.header.vue
+++ b/src/client/app/desktop/views/components/ui.header.vue
@@ -95,7 +95,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.header
+root(isDark)
 	position -webkit-sticky
 	position sticky
 	top 0
@@ -112,7 +112,7 @@ export default Vue.extend({
 			z-index 1000
 			width 100%
 			height 48px
-			background #f7f7f7
+			background isDark ? #313543 : #f7f7f7
 
 		> .main
 			z-index 1001
@@ -169,4 +169,10 @@ export default Vue.extend({
 						> .mk-ui-header-search
 							display none
 
+.header[data-is-darkmode]
+	root(true)
+
+.header
+	root(false)
+
 </style>

From b8ff725aff77cae2812d5e00917a5eb81b01d053 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Sun, 1 Apr 2018 15:58:49 +0900
Subject: [PATCH 0969/1250] Implement inbox

---
 package.json                                  |  1 +
 .../remote/activitypub/resolve-person.ts      |  4 ++
 src/models/user.ts                            |  4 ++
 src/server/activitypub/inbox.ts               | 42 +++++++++++++++++++
 src/server/activitypub/index.ts               | 12 ++++++
 .../{activitypub.ts => activitypub/user.ts}   | 12 +++---
 6 files changed, 69 insertions(+), 6 deletions(-)
 create mode 100644 src/server/activitypub/inbox.ts
 create mode 100644 src/server/activitypub/index.ts
 rename src/server/{activitypub.ts => activitypub/user.ts} (79%)

diff --git a/package.json b/package.json
index 4275c1c1c..14c89927e 100644
--- a/package.json
+++ b/package.json
@@ -134,6 +134,7 @@
 		"hard-source-webpack-plugin": "0.6.4",
 		"highlight.js": "9.12.0",
 		"html-minifier": "3.5.12",
+		"http-signature": "^1.2.0",
 		"inquirer": "5.2.0",
 		"is-root": "2.0.0",
 		"is-url": "1.2.4",
diff --git a/src/common/remote/activitypub/resolve-person.ts b/src/common/remote/activitypub/resolve-person.ts
index c7c131b0e..999a37eea 100644
--- a/src/common/remote/activitypub/resolve-person.ts
+++ b/src/common/remote/activitypub/resolve-person.ts
@@ -62,6 +62,10 @@ export default async (value, usernameLower, hostLower, acctLower) => {
 		host: toUnicode(finger.subject.replace(/^.*?@/, '')),
 		hostLower,
 		account: {
+			publicKey: {
+				id: object.publicKey.id,
+				publicKeyPem: object.publicKey.publicKeyPem
+			},
 			uri: object.id,
 		},
 	});
diff --git a/src/models/user.ts b/src/models/user.ts
index 02e6a570b..9588c4515 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -71,6 +71,10 @@ export type ILocalAccount = {
 
 export type IRemoteAccount = {
 	uri: string;
+	publicKey: {
+		id: string;
+		publicKeyPem: string;
+	};
 };
 
 export type IUser = {
diff --git a/src/server/activitypub/inbox.ts b/src/server/activitypub/inbox.ts
new file mode 100644
index 000000000..0d4af7c49
--- /dev/null
+++ b/src/server/activitypub/inbox.ts
@@ -0,0 +1,42 @@
+import * as bodyParser from 'body-parser';
+import * as express from 'express';
+import { parseRequest, verifySignature } from 'http-signature';
+import User, { IRemoteAccount } from '../../models/user';
+import queue from '../../queue';
+
+const app = express();
+app.disable('x-powered-by');
+app.use(bodyParser.json());
+
+app.get('/@:user/inbox', async (req, res) => {
+	let parsed;
+
+	try {
+		parsed = parseRequest(req);
+	} catch (exception) {
+		return res.sendStatus(401);
+	}
+
+	const user = await User.findOne({
+		host: { $ne: null },
+		account: { publicKey: { id: parsed.keyId } }
+	});
+
+	if (user === null) {
+		return res.sendStatus(401);
+	}
+
+	if (!verifySignature(parsed, (user.account as IRemoteAccount).publicKey.publicKeyPem)) {
+		return res.sendStatus(401);
+	}
+
+	queue.create('http', {
+		type: 'performActivityPub',
+		actor: user._id,
+		outbox: req.body,
+	}).save();
+
+	return res.sendStatus(200);
+});
+
+export default app;
diff --git a/src/server/activitypub/index.ts b/src/server/activitypub/index.ts
new file mode 100644
index 000000000..07ff407a7
--- /dev/null
+++ b/src/server/activitypub/index.ts
@@ -0,0 +1,12 @@
+import * as express from 'express';
+
+import user from './user';
+import inbox from './inbox';
+
+const app = express();
+app.disable('x-powered-by');
+
+app.use(user);
+app.use(inbox);
+
+export default app;
diff --git a/src/server/activitypub.ts b/src/server/activitypub/user.ts
similarity index 79%
rename from src/server/activitypub.ts
rename to src/server/activitypub/user.ts
index a48a8e643..488de93a9 100644
--- a/src/server/activitypub.ts
+++ b/src/server/activitypub/user.ts
@@ -1,16 +1,15 @@
 import * as express from 'express';
-
-import config from '../conf';
-import { extractPublic } from '../crypto_key';
-import parseAcct from '../common/user/parse-acct';
-import User, { ILocalAccount } from '../models/user';
+import config from '../../conf';
+import { extractPublic } from '../../crypto_key';
+import parseAcct from '../../common/user/parse-acct';
+import User, { ILocalAccount } from '../../models/user';
 
 const app = express();
 app.disable('x-powered-by');
 
 app.get('/@:user', async (req, res, next) => {
 	const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']);
-	if (!['application/activity+json', 'application/ld+json'].includes(accepted)) {
+	if (!(['application/activity+json', 'application/ld+json'] as Array<any>).includes(accepted)) {
 		return next();
 	}
 
@@ -40,6 +39,7 @@ app.get('/@:user', async (req, res, next) => {
 		],
 		type: 'Person',
 		id,
+		inbox: `${id}/inbox`,
 		preferredUsername: user.username,
 		name: user.name,
 		summary: user.description,

From 13949270d288ff7d80c08e51974350cb35986b75 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 1 Apr 2018 16:04:23 +0900
Subject: [PATCH 0970/1250] Use dot notation

---
 src/server/activitypub/inbox.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/server/activitypub/inbox.ts b/src/server/activitypub/inbox.ts
index 0d4af7c49..f76e750d2 100644
--- a/src/server/activitypub/inbox.ts
+++ b/src/server/activitypub/inbox.ts
@@ -19,7 +19,7 @@ app.get('/@:user/inbox', async (req, res) => {
 
 	const user = await User.findOne({
 		host: { $ne: null },
-		account: { publicKey: { id: parsed.keyId } }
+		'account.publicKey.id': parsed.keyId
 	});
 
 	if (user === null) {

From 7fe9c9e73fa00edb51a9c4907ee929f18977e42d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 1 Apr 2018 16:46:33 +0900
Subject: [PATCH 0971/1250] Fix

---
 src/server/activitypub/inbox.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/server/activitypub/inbox.ts b/src/server/activitypub/inbox.ts
index f76e750d2..b4761d997 100644
--- a/src/server/activitypub/inbox.ts
+++ b/src/server/activitypub/inbox.ts
@@ -8,7 +8,7 @@ const app = express();
 app.disable('x-powered-by');
 app.use(bodyParser.json());
 
-app.get('/@:user/inbox', async (req, res) => {
+app.post('/@:user/inbox', async (req, res) => {
 	let parsed;
 
 	try {

From 2212a55bad63f5cfc482d35db07267036160fdca Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sun, 1 Apr 2018 08:40:47 +0000
Subject: [PATCH 0972/1250] fix(package): update bootstrap-vue to version
 2.0.0-rc.4

Closes #1349
---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 14c89927e..d5fa1f3cb 100644
--- a/package.json
+++ b/package.json
@@ -89,7 +89,7 @@
 		"autwh": "0.0.1",
 		"bcryptjs": "2.4.3",
 		"body-parser": "1.18.2",
-		"bootstrap-vue": "2.0.0-rc.1",
+		"bootstrap-vue": "2.0.0-rc.4",
 		"cafy": "3.2.1",
 		"chai": "4.1.2",
 		"chai-http": "4.0.0",

From 65478160ca2a4570adaad47bfbe79f093c07fdb7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 1 Apr 2018 18:07:29 +0900
Subject: [PATCH 0973/1250] Add missing property

---
 src/models/post.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/models/post.ts b/src/models/post.ts
index 6c853e4f8..4daad306d 100644
--- a/src/models/post.ts
+++ b/src/models/post.ts
@@ -30,6 +30,7 @@ export type IPost = {
 	repostId: mongo.ObjectID;
 	poll: any; // todo
 	text: string;
+	tags: string[];
 	textHtml: string;
 	cw: string;
 	userId: mongo.ObjectID;

From f4c4ce83310323965d28bac8ad9deb7c6574cf4a Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Sun, 1 Apr 2018 18:12:51 +0900
Subject: [PATCH 0974/1250] Implement post Activity Streams

---
 src/common/remote/activitypub/context.ts |  5 ++
 src/models/post.ts                       |  1 +
 src/server/activitypub/index.ts          |  2 +
 src/server/activitypub/post.ts           | 85 ++++++++++++++++++++++++
 src/server/activitypub/user.ts           |  6 +-
 5 files changed, 95 insertions(+), 4 deletions(-)
 create mode 100644 src/common/remote/activitypub/context.ts
 create mode 100644 src/server/activitypub/post.ts

diff --git a/src/common/remote/activitypub/context.ts b/src/common/remote/activitypub/context.ts
new file mode 100644
index 000000000..b56f727ae
--- /dev/null
+++ b/src/common/remote/activitypub/context.ts
@@ -0,0 +1,5 @@
+export default [
+	'https://www.w3.org/ns/activitystreams',
+	'https://w3id.org/security/v1',
+	{ Hashtag: 'as:Hashtag' }
+];
diff --git a/src/models/post.ts b/src/models/post.ts
index 6c853e4f8..64c46c263 100644
--- a/src/models/post.ts
+++ b/src/models/post.ts
@@ -47,6 +47,7 @@ export type IPost = {
 		heading: number;
 		speed: number;
 	};
+	tags: string[];
 };
 
 /**
diff --git a/src/server/activitypub/index.ts b/src/server/activitypub/index.ts
index 07ff407a7..6618c291f 100644
--- a/src/server/activitypub/index.ts
+++ b/src/server/activitypub/index.ts
@@ -1,11 +1,13 @@
 import * as express from 'express';
 
+import post from './post';
 import user from './user';
 import inbox from './inbox';
 
 const app = express();
 app.disable('x-powered-by');
 
+app.use(post);
 app.use(user);
 app.use(inbox);
 
diff --git a/src/server/activitypub/post.ts b/src/server/activitypub/post.ts
new file mode 100644
index 000000000..4fb3a1319
--- /dev/null
+++ b/src/server/activitypub/post.ts
@@ -0,0 +1,85 @@
+import * as express from 'express';
+import context from '../../common/remote/activitypub/context';
+import parseAcct from '../../common/user/parse-acct';
+import config from '../../conf';
+import DriveFile from '../../models/drive-file';
+import Post from '../../models/post';
+import User from '../../models/user';
+
+const app = express();
+app.disable('x-powered-by');
+
+app.get('/@:user/:post', async (req, res, next) => {
+	const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']);
+	if (!(['application/activity+json', 'application/ld+json'] as Array<any>).includes(accepted)) {
+		return next();
+	}
+
+	const { username, host } = parseAcct(req.params.user);
+	if (host !== null) {
+		return res.sendStatus(422);
+	}
+
+	const user = await User.findOne({
+		usernameLower: username.toLowerCase(),
+		host: null
+	});
+	if (user === null) {
+		return res.sendStatus(404);
+	}
+
+	const post = await Post.findOne({
+		_id: req.params.post,
+		userId: user._id
+	});
+	if (post === null) {
+		return res.sendStatus(404);
+	}
+
+	const asyncFiles = DriveFile.find({ _id: { $in: post.mediaIds } });
+	let inReplyTo;
+
+	if (post.replyId) {
+		const inReplyToPost = await Post.findOne({
+			_id: post.replyId,
+		});
+
+		if (inReplyToPost !== null) {
+			const inReplyToUser = await User.findOne({
+				_id: post.userId,
+			});
+
+			if (inReplyToUser !== null) {
+				inReplyTo = `${config.url}@${inReplyToUser.username}/${inReplyToPost._id}`;
+			}
+		}
+	} else {
+		inReplyTo = null;
+	}
+
+	const attributedTo = `${config.url}/@${user.username}`;
+
+	res.json({
+		'@context': context,
+		id: `${attributedTo}/${post._id}`,
+		type: 'Note',
+		attributedTo,
+		content: post.textHtml,
+		published: post.createdAt.toISOString(),
+		to: 'https://www.w3.org/ns/activitystreams#Public',
+		cc: `${attributedTo}/followers`,
+		inReplyTo,
+		attachment: (await asyncFiles).map(({ _id, contentType }) => ({
+			type: 'Document',
+			mediaType: contentType,
+			url: `${config.drive_url}/${_id}`
+		})),
+		tag: post.tags.map(tag => ({
+			type: 'Hashtag',
+			href: `${config.url}/search?q=#${encodeURIComponent(tag)}`,
+			name: '#' + tag
+		}))
+	});
+});
+
+export default app;
diff --git a/src/server/activitypub/user.ts b/src/server/activitypub/user.ts
index 488de93a9..ef365c207 100644
--- a/src/server/activitypub/user.ts
+++ b/src/server/activitypub/user.ts
@@ -1,6 +1,7 @@
 import * as express from 'express';
 import config from '../../conf';
 import { extractPublic } from '../../crypto_key';
+import context from '../../common/remote/activitypub/context';
 import parseAcct from '../../common/user/parse-acct';
 import User, { ILocalAccount } from '../../models/user';
 
@@ -33,10 +34,7 @@ app.get('/@:user', async (req, res, next) => {
 	}
 
 	res.json({
-		'@context': [
-			'https://www.w3.org/ns/activitystreams',
-			'https://w3id.org/security/v1'
-		],
+		'@context': context,
 		type: 'Person',
 		id,
 		inbox: `${id}/inbox`,

From f53497e2c0a5a3fc53589c3ee3735d3ddea5584b Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Sun, 1 Apr 2018 18:16:47 +0900
Subject: [PATCH 0975/1250] Respond with 202 for inbox request

---
 src/server/activitypub/inbox.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/server/activitypub/inbox.ts b/src/server/activitypub/inbox.ts
index b4761d997..915129748 100644
--- a/src/server/activitypub/inbox.ts
+++ b/src/server/activitypub/inbox.ts
@@ -36,7 +36,7 @@ app.post('/@:user/inbox', async (req, res) => {
 		outbox: req.body,
 	}).save();
 
-	return res.sendStatus(200);
+	return res.status(202).end();
 });
 
 export default app;

From f9c290a9dfd00d4c6fce6ed990a89fd147be0b2d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 1 Apr 2018 18:17:04 +0900
Subject: [PATCH 0976/1250] Resolve conflict

---
 src/models/post.ts | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/models/post.ts b/src/models/post.ts
index e7b54180f..4daad306d 100644
--- a/src/models/post.ts
+++ b/src/models/post.ts
@@ -48,7 +48,6 @@ export type IPost = {
 		heading: number;
 		speed: number;
 	};
-	tags: string[];
 };
 
 /**

From 40c9adfef5d0c061a2d5c9c844cc3597d463a617 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 1 Apr 2018 18:20:17 +0900
Subject: [PATCH 0977/1250] Fix type annotation

---
 src/server/activitypub/post.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/server/activitypub/post.ts b/src/server/activitypub/post.ts
index 4fb3a1319..261d7ca4a 100644
--- a/src/server/activitypub/post.ts
+++ b/src/server/activitypub/post.ts
@@ -11,7 +11,7 @@ app.disable('x-powered-by');
 
 app.get('/@:user/:post', async (req, res, next) => {
 	const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']);
-	if (!(['application/activity+json', 'application/ld+json'] as Array<any>).includes(accepted)) {
+	if (!(['application/activity+json', 'application/ld+json'] as any[]).includes(accepted)) {
 		return next();
 	}
 

From e494fdd6da66ae1dabcd1262557f05b54ca2b0d2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 1 Apr 2018 18:28:10 +0900
Subject: [PATCH 0978/1250] Refactor: Better variable name

---
 src/server/activitypub/post.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/server/activitypub/post.ts b/src/server/activitypub/post.ts
index 261d7ca4a..bdfce0606 100644
--- a/src/server/activitypub/post.ts
+++ b/src/server/activitypub/post.ts
@@ -36,7 +36,7 @@ app.get('/@:user/:post', async (req, res, next) => {
 		return res.sendStatus(404);
 	}
 
-	const asyncFiles = DriveFile.find({ _id: { $in: post.mediaIds } });
+	const promisedFiles = DriveFile.find({ _id: { $in: post.mediaIds } });
 	let inReplyTo;
 
 	if (post.replyId) {
@@ -69,7 +69,7 @@ app.get('/@:user/:post', async (req, res, next) => {
 		to: 'https://www.w3.org/ns/activitystreams#Public',
 		cc: `${attributedTo}/followers`,
 		inReplyTo,
-		attachment: (await asyncFiles).map(({ _id, contentType }) => ({
+		attachment: (await promisedFiles).map(({ _id, contentType }) => ({
 			type: 'Document',
 			mediaType: contentType,
 			url: `${config.drive_url}/${_id}`

From 344c6b7b3977a486c046a040f90cd8106e72fd34 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Sun, 1 Apr 2018 19:18:36 +0900
Subject: [PATCH 0979/1250] Implement outbox

---
 .../activitypub/{ => renderer}/context.ts     |  0
 .../remote/activitypub/renderer/document.ts   |  7 +++
 .../remote/activitypub/renderer/hashtag.ts    |  7 +++
 .../remote/activitypub/renderer/image.ts      |  6 +++
 src/common/remote/activitypub/renderer/key.ts |  9 ++++
 .../remote/activitypub/renderer/note.ts       | 44 ++++++++++++++++
 .../renderer/ordered-collection.ts            |  6 +++
 .../remote/activitypub/renderer/person.ts     | 20 ++++++++
 src/server/activitypub/index.ts               |  6 ++-
 src/server/activitypub/outbox.ts              | 45 ++++++++++++++++
 src/server/activitypub/post.ts                | 51 ++-----------------
 src/server/activitypub/user.ts                | 36 +++----------
 12 files changed, 161 insertions(+), 76 deletions(-)
 rename src/common/remote/activitypub/{ => renderer}/context.ts (100%)
 create mode 100644 src/common/remote/activitypub/renderer/document.ts
 create mode 100644 src/common/remote/activitypub/renderer/hashtag.ts
 create mode 100644 src/common/remote/activitypub/renderer/image.ts
 create mode 100644 src/common/remote/activitypub/renderer/key.ts
 create mode 100644 src/common/remote/activitypub/renderer/note.ts
 create mode 100644 src/common/remote/activitypub/renderer/ordered-collection.ts
 create mode 100644 src/common/remote/activitypub/renderer/person.ts
 create mode 100644 src/server/activitypub/outbox.ts

diff --git a/src/common/remote/activitypub/context.ts b/src/common/remote/activitypub/renderer/context.ts
similarity index 100%
rename from src/common/remote/activitypub/context.ts
rename to src/common/remote/activitypub/renderer/context.ts
diff --git a/src/common/remote/activitypub/renderer/document.ts b/src/common/remote/activitypub/renderer/document.ts
new file mode 100644
index 000000000..4a456416a
--- /dev/null
+++ b/src/common/remote/activitypub/renderer/document.ts
@@ -0,0 +1,7 @@
+import config from '../../../../conf';
+
+export default ({ _id, contentType }) => ({
+	type: 'Document',
+	mediaType: contentType,
+	url: `${config.drive_url}/${_id}`
+});
diff --git a/src/common/remote/activitypub/renderer/hashtag.ts b/src/common/remote/activitypub/renderer/hashtag.ts
new file mode 100644
index 000000000..ad4270020
--- /dev/null
+++ b/src/common/remote/activitypub/renderer/hashtag.ts
@@ -0,0 +1,7 @@
+import config from '../../../../conf';
+
+export default tag => ({
+	type: 'Hashtag',
+	href: `${config.url}/search?q=#${encodeURIComponent(tag)}`,
+	name: '#' + tag
+});
diff --git a/src/common/remote/activitypub/renderer/image.ts b/src/common/remote/activitypub/renderer/image.ts
new file mode 100644
index 000000000..345fbbec5
--- /dev/null
+++ b/src/common/remote/activitypub/renderer/image.ts
@@ -0,0 +1,6 @@
+import config from '../../../../conf';
+
+export default ({ _id }) => ({
+	type: 'Image',
+	url: `${config.drive_url}/${_id}`
+});
diff --git a/src/common/remote/activitypub/renderer/key.ts b/src/common/remote/activitypub/renderer/key.ts
new file mode 100644
index 000000000..7148c5974
--- /dev/null
+++ b/src/common/remote/activitypub/renderer/key.ts
@@ -0,0 +1,9 @@
+import config from '../../../../conf';
+import { extractPublic } from '../../../../crypto_key';
+import { ILocalAccount } from '../../../../models/user';
+
+export default ({ username, account }) => ({
+	type: 'Key',
+	owner: `${config.url}/@${username}`,
+	publicKeyPem: extractPublic((account as ILocalAccount).keypair)
+});
diff --git a/src/common/remote/activitypub/renderer/note.ts b/src/common/remote/activitypub/renderer/note.ts
new file mode 100644
index 000000000..2fe20b213
--- /dev/null
+++ b/src/common/remote/activitypub/renderer/note.ts
@@ -0,0 +1,44 @@
+import renderDocument from './document';
+import renderHashtag from './hashtag';
+import config from '../../../../conf';
+import DriveFile from '../../../../models/drive-file';
+import Post from '../../../../models/post';
+import User from '../../../../models/user';
+
+export default async (user, post) => {
+	const promisedFiles = DriveFile.find({ _id: { $in: post.mediaIds } });
+	let inReplyTo;
+
+	if (post.replyId) {
+		const inReplyToPost = await Post.findOne({
+			_id: post.replyId,
+		});
+
+		if (inReplyToPost !== null) {
+			const inReplyToUser = await User.findOne({
+				_id: post.userId,
+			});
+
+			if (inReplyToUser !== null) {
+				inReplyTo = `${config.url}@${inReplyToUser.username}/${inReplyToPost._id}`;
+			}
+		}
+	} else {
+		inReplyTo = null;
+	}
+
+	const attributedTo = `${config.url}/@${user.username}`;
+
+	return {
+		id: `${attributedTo}/${post._id}`,
+		type: 'Note',
+		attributedTo,
+		content: post.textHtml,
+		published: post.createdAt.toISOString(),
+		to: 'https://www.w3.org/ns/activitystreams#Public',
+		cc: `${attributedTo}/followers`,
+		inReplyTo,
+		attachment: (await promisedFiles).map(renderDocument),
+		tag: post.tags.map(renderHashtag)
+	};
+};
diff --git a/src/common/remote/activitypub/renderer/ordered-collection.ts b/src/common/remote/activitypub/renderer/ordered-collection.ts
new file mode 100644
index 000000000..2ca0f7735
--- /dev/null
+++ b/src/common/remote/activitypub/renderer/ordered-collection.ts
@@ -0,0 +1,6 @@
+export default (id, totalItems, orderedItems) => ({
+	id,
+	type: 'OrderedCollection',
+	totalItems,
+	orderedItems
+});
diff --git a/src/common/remote/activitypub/renderer/person.ts b/src/common/remote/activitypub/renderer/person.ts
new file mode 100644
index 000000000..7303b3038
--- /dev/null
+++ b/src/common/remote/activitypub/renderer/person.ts
@@ -0,0 +1,20 @@
+import renderImage from './image';
+import renderKey from './key';
+import config from '../../../../conf';
+
+export default user => {
+	const id = `${config.url}/@${user.username}`;
+
+	return {
+		type: 'Person',
+		id,
+		inbox: `${id}/inbox`,
+		outbox: `${id}/outbox`,
+		preferredUsername: user.username,
+		name: user.name,
+		summary: user.description,
+		icon: user.avatarId && renderImage({ _id: user.avatarId }),
+		image: user.bannerId && renderImage({ _id: user.bannerId }),
+		publicKey: renderKey(user)
+	};
+};
diff --git a/src/server/activitypub/index.ts b/src/server/activitypub/index.ts
index 6618c291f..c81024d15 100644
--- a/src/server/activitypub/index.ts
+++ b/src/server/activitypub/index.ts
@@ -1,14 +1,16 @@
 import * as express from 'express';
 
-import post from './post';
 import user from './user';
 import inbox from './inbox';
+import outbox from './outbox';
+import post from './post';
 
 const app = express();
 app.disable('x-powered-by');
 
-app.use(post);
 app.use(user);
 app.use(inbox);
+app.use(outbox);
+app.use(post);
 
 export default app;
diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts
new file mode 100644
index 000000000..c5a42ae0a
--- /dev/null
+++ b/src/server/activitypub/outbox.ts
@@ -0,0 +1,45 @@
+import * as express from 'express';
+import context from '../../common/remote/activitypub/renderer/context';
+import renderNote from '../../common/remote/activitypub/renderer/note';
+import renderOrderedCollection from '../../common/remote/activitypub/renderer/ordered-collection';
+import parseAcct from '../../common/user/parse-acct';
+import config from '../../conf';
+import Post from '../../models/post';
+import User from '../../models/user';
+
+const app = express();
+app.disable('x-powered-by');
+
+app.get('/@:user/outbox', async (req, res) => {
+	const { username, host } = parseAcct(req.params.user);
+	if (host !== null) {
+		return res.sendStatus(422);
+	}
+
+	const user = await User.findOne({
+		usernameLower: username.toLowerCase(),
+		host: null
+	});
+	if (user === null) {
+		return res.sendStatus(404);
+	}
+
+	const id = `${config.url}/@${user.username}/inbox`;
+
+	if (username !== user.username) {
+		return res.redirect(id);
+	}
+
+	const posts = await Post.find({ userId: user._id }, {
+		limit: 20,
+		sort: { _id: -1 }
+	});
+
+	const renderedPosts = await Promise.all(posts.map(post => renderNote(user, post)));
+	const rendered = renderOrderedCollection(id, user.postsCount, renderedPosts);
+	rendered['@context'] = context;
+
+	res.json(rendered);
+});
+
+export default app;
diff --git a/src/server/activitypub/post.ts b/src/server/activitypub/post.ts
index bdfce0606..6644563d8 100644
--- a/src/server/activitypub/post.ts
+++ b/src/server/activitypub/post.ts
@@ -1,8 +1,7 @@
 import * as express from 'express';
-import context from '../../common/remote/activitypub/context';
+import context from '../../common/remote/activitypub/renderer/context';
+import render from '../../common/remote/activitypub/renderer/note';
 import parseAcct from '../../common/user/parse-acct';
-import config from '../../conf';
-import DriveFile from '../../models/drive-file';
 import Post from '../../models/post';
 import User from '../../models/user';
 
@@ -36,50 +35,10 @@ app.get('/@:user/:post', async (req, res, next) => {
 		return res.sendStatus(404);
 	}
 
-	const promisedFiles = DriveFile.find({ _id: { $in: post.mediaIds } });
-	let inReplyTo;
+	const rendered = await render(user, post);
+	rendered['@context'] = context;
 
-	if (post.replyId) {
-		const inReplyToPost = await Post.findOne({
-			_id: post.replyId,
-		});
-
-		if (inReplyToPost !== null) {
-			const inReplyToUser = await User.findOne({
-				_id: post.userId,
-			});
-
-			if (inReplyToUser !== null) {
-				inReplyTo = `${config.url}@${inReplyToUser.username}/${inReplyToPost._id}`;
-			}
-		}
-	} else {
-		inReplyTo = null;
-	}
-
-	const attributedTo = `${config.url}/@${user.username}`;
-
-	res.json({
-		'@context': context,
-		id: `${attributedTo}/${post._id}`,
-		type: 'Note',
-		attributedTo,
-		content: post.textHtml,
-		published: post.createdAt.toISOString(),
-		to: 'https://www.w3.org/ns/activitystreams#Public',
-		cc: `${attributedTo}/followers`,
-		inReplyTo,
-		attachment: (await promisedFiles).map(({ _id, contentType }) => ({
-			type: 'Document',
-			mediaType: contentType,
-			url: `${config.drive_url}/${_id}`
-		})),
-		tag: post.tags.map(tag => ({
-			type: 'Hashtag',
-			href: `${config.url}/search?q=#${encodeURIComponent(tag)}`,
-			name: '#' + tag
-		}))
-	});
+	res.json(rendered);
 });
 
 export default app;
diff --git a/src/server/activitypub/user.ts b/src/server/activitypub/user.ts
index ef365c207..d43a9793d 100644
--- a/src/server/activitypub/user.ts
+++ b/src/server/activitypub/user.ts
@@ -1,9 +1,9 @@
 import * as express from 'express';
 import config from '../../conf';
-import { extractPublic } from '../../crypto_key';
-import context from '../../common/remote/activitypub/context';
+import context from '../../common/remote/activitypub/renderer/context';
+import render from '../../common/remote/activitypub/renderer/person';
 import parseAcct from '../../common/user/parse-acct';
-import User, { ILocalAccount } from '../../models/user';
+import User from '../../models/user';
 
 const app = express();
 app.disable('x-powered-by');
@@ -27,34 +27,14 @@ app.get('/@:user', async (req, res, next) => {
 		return res.sendStatus(404);
 	}
 
-	const id = `${config.url}/@${user.username}`;
-
 	if (username !== user.username) {
-		return res.redirect(id);
+		return res.redirect(`${config.url}/@${user.username}`);
 	}
 
-	res.json({
-		'@context': context,
-		type: 'Person',
-		id,
-		inbox: `${id}/inbox`,
-		preferredUsername: user.username,
-		name: user.name,
-		summary: user.description,
-		icon: user.avatarId && {
-			type: 'Image',
-			url: `${config.drive_url}/${user.avatarId}`
-		},
-		image: user.bannerId && {
-			type: 'Image',
-			url: `${config.drive_url}/${user.bannerId}`
-		},
-		publicKey: {
-			type: 'Key',
-			owner: id,
-			publicKeyPem: extractPublic((user.account as ILocalAccount).keypair)
-		}
-	});
+	const rendered = render(user);
+	rendered['@context'] = context;
+
+	res.json(rendered);
 });
 
 export default app;

From 07543da1608dde3a93cc8d1ec4aaf5acec403e49 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 1 Apr 2018 19:43:27 +0900
Subject: [PATCH 0980/1250] Update README.md

---
 README.md | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index f7d67247a..4c0506709 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@ Misskey
 [![][dependencies-badge]][dependencies-link]
 [![][himawari-badge]][himasaku]
 [![][sakurako-badge]][himasaku]
-[![][agpl-3.0-badge]][AGPL-3.0]
+[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)
 
 > Lead Maintainer: [syuilo][syuilo-link]
 
@@ -50,6 +50,8 @@ If you want to donate to Misskey, please see [this](./docs/donate.ja.md).
 
 Misskey is an open-source software licensed under [GNU AGPLv3](LICENSE).
 
+[![][agpl-3.0-badge]][AGPL-3.0]
+
 [agpl-3.0]:           https://www.gnu.org/licenses/agpl-3.0.en.html
 [agpl-3.0-badge]:     https://img.shields.io/badge/license-AGPL--3.0-444444.svg?style=flat-square
 [travis-link]:        https://travis-ci.org/syuilo/misskey

From b2c29f630c9e48eaa8bb9bf6957a11a9f13c2ce3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 1 Apr 2018 19:53:35 +0900
Subject: [PATCH 0981/1250] Clean up

---
 tools/letsencrypt/get-cert.sh                 | 12 ---
 tools/migration/node.1509958623.use-gridfs.js | 71 ---------------
 ...change-gridfs-metadata-name-to-filename.js | 50 -----------
 tools/migration/node.1510056272.issue_882.js  | 47 ----------
 tools/migration/node.2017-11-08.js            | 88 -------------------
 tools/migration/node.2017-12-11.js            | 71 ---------------
 tools/migration/node.2017-12-22.hiseikika.js  | 67 --------------
 tools/migration/node.2018-03-13.othello.js    | 46 ----------
 .../shell.1487734995.user-profile.js          | 18 ----
 .../shell.1489951459.like-to-reactions.js     | 22 -----
 .../shell.1509507382.reply_to-to-reply.js     |  5 --
 11 files changed, 497 deletions(-)
 delete mode 100644 tools/letsencrypt/get-cert.sh
 delete mode 100644 tools/migration/node.1509958623.use-gridfs.js
 delete mode 100644 tools/migration/node.1510016282.change-gridfs-metadata-name-to-filename.js
 delete mode 100644 tools/migration/node.1510056272.issue_882.js
 delete mode 100644 tools/migration/node.2017-11-08.js
 delete mode 100644 tools/migration/node.2017-12-11.js
 delete mode 100644 tools/migration/node.2017-12-22.hiseikika.js
 delete mode 100644 tools/migration/node.2018-03-13.othello.js
 delete mode 100644 tools/migration/shell.1487734995.user-profile.js
 delete mode 100644 tools/migration/shell.1489951459.like-to-reactions.js
 delete mode 100644 tools/migration/shell.1509507382.reply_to-to-reply.js

diff --git a/tools/letsencrypt/get-cert.sh b/tools/letsencrypt/get-cert.sh
deleted file mode 100644
index d44deb144..000000000
--- a/tools/letsencrypt/get-cert.sh
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/bin/sh
-
-certbot certonly --standalone\
-  -d $1\
-  -d api.$1\
-  -d auth.$1\
-  -d docs.$1\
-  -d ch.$1\
-  -d stats.$1\
-  -d status.$1\
-  -d dev.$1\
-  -d file.$2\
diff --git a/tools/migration/node.1509958623.use-gridfs.js b/tools/migration/node.1509958623.use-gridfs.js
deleted file mode 100644
index a9d2b12e9..000000000
--- a/tools/migration/node.1509958623.use-gridfs.js
+++ /dev/null
@@ -1,71 +0,0 @@
-// for Node.js interpret
-
-const { default: db } = require('../../built/db/mongodb')
-const { default: DriveFile, getGridFSBucket } = require('../../built/api/models/drive-file')
-const { Duplex } = require('stream')
-const { default: zip } = require('@prezzemolo/zip')
-
-const writeToGridFS = (bucket, buffer, ...rest) => new Promise((resolve, reject) => {
-	const writeStream = bucket.openUploadStreamWithId(...rest)
-
-	const dataStream = new Duplex()
-	dataStream.push(buffer)
-	dataStream.push(null)
-
-	writeStream.once('finish', resolve)
-	writeStream.on('error', reject)
-
-	dataStream.pipe(writeStream)
-})
-
-const migrateToGridFS = async (doc) => {
-	const id = doc._id
-	const buffer = doc.data ? doc.data.buffer : Buffer.from([0x00]) // アップロードのバグなのか知らないけどなぜか data が存在しない drive_file ドキュメントがまれにあることがわかったので
-	const created_at = doc.created_at
-	const name = doc.name
-	const type = doc.type
-
-	delete doc._id
-	delete doc.created_at
-	delete doc.datasize
-	delete doc.hash
-	delete doc.data
-	delete doc.name
-	delete doc.type
-
-	const bucket = await getGridFSBucket()
-	const added = await writeToGridFS(bucket, buffer, id, name, { contentType: type, metadata: doc })
-
-	const result = await DriveFile.update(id, {
-		$set: {
-			uploadDate: created_at
-		}
-	})
-
-	return added && result.ok === 1
-}
-
-async function main() {
-	const count = await db.get('drive_files').count({});
-
-	console.log(`there are ${count} files.`)
-
-	const dop = Number.parseInt(process.argv[2]) || 5
-	const idop = ((count - (count % dop)) / dop) + 1
-
-	return zip(
-		1,
-		async (time) => {
-			console.log(`${time} / ${idop}`)
-			const doc = await db.get('drive_files').find({}, { limit: dop, skip: time * dop })
-			return Promise.all(doc.map(migrateToGridFS))
-		},
-		idop
-	).then(a => {
-		const rv = []
-		a.forEach(e => rv.push(...e))
-		return rv
-	})
-}
-
-main().then(console.dir).catch(console.error)
diff --git a/tools/migration/node.1510016282.change-gridfs-metadata-name-to-filename.js b/tools/migration/node.1510016282.change-gridfs-metadata-name-to-filename.js
deleted file mode 100644
index d7b2a6eff..000000000
--- a/tools/migration/node.1510016282.change-gridfs-metadata-name-to-filename.js
+++ /dev/null
@@ -1,50 +0,0 @@
-// for Node.js interpret
-/**
- * change usage of GridFS filename
- * see commit fb422b4d603c53a70712caba55b35a48a8c2e619
- */
-
-const { default: DriveFile } = require('../../built/api/models/drive-file')
-
-async function applyNewChange (doc) {
-	const result = await DriveFile.update(doc._id, {
-		$set: {
-			filename: doc.metadata.name
-		},
-		$unset: {
-			'metadata.name': ''
-		}
-	})
-	return result.ok === 1
-}
-
-async function main () {
-	const query = {
-		'metadata.name': {
-			$exists: true
-		}
-	}
-
-	const count = await DriveFile.count(query)
-
-	const dop = Number.parseInt(process.argv[2]) || 5
-	const idop = ((count - (count % dop)) / dop) + 1
-
-	return zip(
-		1,
-		async (time) => {
-			console.log(`${time} / ${idop}`)
-			const doc = await DriveFile.find(query, {
-				limit: dop, skip: time * dop
-			})
-			return Promise.all(doc.map(applyNewChange))
-		},
-		idop
-	).then(a => {
-		const rv = []
-		a.forEach(e => rv.push(...e))
-		return rv
-	})
-}
-
-main().then(console.dir).catch(console.error)
diff --git a/tools/migration/node.1510056272.issue_882.js b/tools/migration/node.1510056272.issue_882.js
deleted file mode 100644
index 302ef3de6..000000000
--- a/tools/migration/node.1510056272.issue_882.js
+++ /dev/null
@@ -1,47 +0,0 @@
-// for Node.js interpret
-
-const { default: DriveFile } = require('../../built/api/models/drive-file')
-const { default: zip } = require('@prezzemolo/zip')
-
-const migrate = async (doc) => {
-	const result = await DriveFile.update(doc._id, {
-		$set: {
-			contentType: doc.metadata.type
-		},
-		$unset: {
-			'metadata.type': ''
-		}
-	})
-	return result.ok === 1
-}
-
-async function main() {
-	const query = {
-		'metadata.type': {
-			$exists: true
-		}
-	}
-
-	const count = await DriveFile.count(query);
-
-	const dop = Number.parseInt(process.argv[2]) || 5
-	const idop = ((count - (count % dop)) / dop) + 1
-
-	return zip(
-		1,
-		async (time) => {
-			console.log(`${time} / ${idop}`)
-			const doc = await DriveFile.find(query, {
-				limit: dop, skip: time * dop
-			})
-			return Promise.all(doc.map(migrate))
-		},
-		idop
-	).then(a => {
-		const rv = []
-		a.forEach(e => rv.push(...e))
-		return rv
-	})
-}
-
-main().then(console.dir).catch(console.error)
diff --git a/tools/migration/node.2017-11-08.js b/tools/migration/node.2017-11-08.js
deleted file mode 100644
index 196a5a90c..000000000
--- a/tools/migration/node.2017-11-08.js
+++ /dev/null
@@ -1,88 +0,0 @@
-const uuid = require('uuid');
-const { default: User } = require('../../built/api/models/user')
-const { default: zip } = require('@prezzemolo/zip')
-
-const home = {
-	left: [
-		'profile',
-		'calendar',
-		'activity',
-		'rss-reader',
-		'trends',
-		'photo-stream',
-		'version'
-	],
-	right: [
-		'broadcast',
-		'notifications',
-		'user-recommendation',
-		'recommended-polls',
-		'server',
-		'donation',
-		'nav',
-		'tips'
-	]
-};
-
-const migrate = async (doc) => {
-
-	//#region Construct home data
-	const homeData = [];
-
-	home.left.forEach(widget => {
-		homeData.push({
-			name: widget,
-			id: uuid(),
-			place: 'left',
-			data: {}
-		});
-	});
-
-	home.right.forEach(widget => {
-		homeData.push({
-			name: widget,
-			id: uuid(),
-			place: 'right',
-			data: {}
-		});
-	});
-	//#endregion
-
-	const result = await User.update(doc._id, {
-		$unset: {
-			data: ''
-		},
-		$set: {
-			'settings': {},
-			'client_settings.home': homeData,
-			'client_settings.show_donation': false
-		}
-	})
-
-	return result.ok === 1
-}
-
-async function main() {
-	const count = await User.count();
-
-	console.log(`there are ${count} users.`)
-
-	const dop = Number.parseInt(process.argv[2]) || 5
-	const idop = ((count - (count % dop)) / dop) + 1
-
-	return zip(
-		1,
-		async (time) => {
-			console.log(`${time} / ${idop}`)
-			const docs = await User.find({}, { limit: dop, skip: time * dop })
-			return Promise.all(docs.map(migrate))
-		},
-		idop
-	).then(a => {
-		const rv = []
-		a.forEach(e => rv.push(...e))
-		return rv
-	})
-}
-
-main().then(console.dir).catch(console.error)
diff --git a/tools/migration/node.2017-12-11.js b/tools/migration/node.2017-12-11.js
deleted file mode 100644
index b9686b8b4..000000000
--- a/tools/migration/node.2017-12-11.js
+++ /dev/null
@@ -1,71 +0,0 @@
-// for Node.js interpret
-
-const { default: DriveFile, getGridFSBucket } = require('../../built/api/models/drive-file')
-const { default: zip } = require('@prezzemolo/zip')
-
-const _gm = require('gm');
-const gm = _gm.subClass({
-	imageMagick: true
-});
-
-const migrate = doc => new Promise(async (res, rej) => {
-	const bucket = await getGridFSBucket();
-
-	const readable = bucket.openDownloadStream(doc._id);
-
-	gm(readable)
-		.setFormat('ppm')
-		.resize(1, 1)
-		.toBuffer(async (err, buffer) => {
-			if (err) {
-				console.error(err);
-				res(false);
-				return;
-			}
-			const r = buffer.readUInt8(buffer.length - 3);
-			const g = buffer.readUInt8(buffer.length - 2);
-			const b = buffer.readUInt8(buffer.length - 1);
-
-			const result = await DriveFile.update(doc._id, {
-				$set: {
-					'metadata.properties.average_color': [r, g, b]
-				}
-			})
-
-			res(result.ok === 1);
-		});
-});
-
-async function main() {
-	const query = {
-		contentType: {
-			$in: [
-				'image/png',
-				'image/jpeg'
-			]
-		}
-	}
-
-	const count = await DriveFile.count(query);
-
-	const dop = Number.parseInt(process.argv[2]) || 5
-	const idop = ((count - (count % dop)) / dop) + 1
-
-	return zip(
-		1,
-		async (time) => {
-			console.log(`${time} / ${idop}`)
-			const doc = await DriveFile.find(query, {
-				limit: dop, skip: time * dop
-			})
-			return Promise.all(doc.map(migrate))
-		},
-		idop
-	).then(a => {
-		const rv = []
-		a.forEach(e => rv.push(...e))
-		return rv
-	})
-}
-
-main().then(console.dir).catch(console.error)
diff --git a/tools/migration/node.2017-12-22.hiseikika.js b/tools/migration/node.2017-12-22.hiseikika.js
deleted file mode 100644
index ff8294c8d..000000000
--- a/tools/migration/node.2017-12-22.hiseikika.js
+++ /dev/null
@@ -1,67 +0,0 @@
-// for Node.js interpret
-
-const { default: Post } = require('../../built/api/models/post')
-const { default: zip } = require('@prezzemolo/zip')
-
-const migrate = async (post) => {
-	const x = {};
-	if (post.reply_id != null) {
-		const reply = await Post.findOne({
-			_id: post.reply_id
-		});
-		x['_reply.user_id'] = reply.user_id;
-	}
-	if (post.repost_id != null) {
-		const repost = await Post.findOne({
-			_id: post.repost_id
-		});
-		x['_repost.user_id'] = repost.user_id;
-	}
-	if (post.reply_id != null || post.repost_id != null) {
-		const result = await Post.update(post._id, {
-			$set: x,
-		});
-		return result.ok === 1;
-	} else {
-		return true;
-	}
-}
-
-async function main() {
-	const query = {
-		$or: [{
-			reply_id: {
-				$exists: true,
-				$ne: null
-			}
-		}, {
-			repost_id: {
-				$exists: true,
-				$ne: null
-			}
-		}]
-	}
-
-	const count = await Post.count(query);
-
-	const dop = Number.parseInt(process.argv[2]) || 5
-	const idop = ((count - (count % dop)) / dop) + 1
-
-	return zip(
-		1,
-		async (time) => {
-			console.log(`${time} / ${idop}`)
-			const doc = await Post.find(query, {
-				limit: dop, skip: time * dop
-			})
-			return Promise.all(doc.map(migrate))
-		},
-		idop
-	).then(a => {
-		const rv = []
-		a.forEach(e => rv.push(...e))
-		return rv
-	})
-}
-
-main().then(console.dir).catch(console.error)
diff --git a/tools/migration/node.2018-03-13.othello.js b/tools/migration/node.2018-03-13.othello.js
deleted file mode 100644
index 4598f8d83..000000000
--- a/tools/migration/node.2018-03-13.othello.js
+++ /dev/null
@@ -1,46 +0,0 @@
-// for Node.js interpret
-
-const { default: Othello } = require('../../built/api/models/othello-game')
-const { default: zip } = require('@prezzemolo/zip')
-
-const migrate = async (doc) => {
-	const x = {};
-
-	doc.logs.forEach(log => {
-		log.color = log.color == 'black';
-	});
-
-	const result = await Othello.update(doc._id, {
-		$set: {
-			logs: doc.logs
-		}
-	});
-
-	return result.ok === 1;
-}
-
-async function main() {
-
-	const count = await Othello.count({});
-
-	const dop = Number.parseInt(process.argv[2]) || 5
-	const idop = ((count - (count % dop)) / dop) + 1
-
-	return zip(
-		1,
-		async (time) => {
-			console.log(`${time} / ${idop}`)
-			const doc = await Othello.find({}, {
-				limit: dop, skip: time * dop
-			})
-			return Promise.all(doc.map(migrate))
-		},
-		idop
-	).then(a => {
-		const rv = []
-		a.forEach(e => rv.push(...e))
-		return rv
-	})
-}
-
-main().then(console.dir).catch(console.error)
diff --git a/tools/migration/shell.1487734995.user-profile.js b/tools/migration/shell.1487734995.user-profile.js
deleted file mode 100644
index e6666319e..000000000
--- a/tools/migration/shell.1487734995.user-profile.js
+++ /dev/null
@@ -1,18 +0,0 @@
-db.users.find({}).forEach(function(user) {
-	print(user._id);
-	db.users.update({ _id: user._id }, {
-		$rename: {
-			bio: 'description'
-		},
-		$unset: {
-			location: '',
-			birthday: ''
-		},
-		$set: {
-			profile: {
-				location: user.location || null,
-				birthday: user.birthday || null
-			}
-		}
-	}, false, false);
-});
diff --git a/tools/migration/shell.1489951459.like-to-reactions.js b/tools/migration/shell.1489951459.like-to-reactions.js
deleted file mode 100644
index 962a0f00e..000000000
--- a/tools/migration/shell.1489951459.like-to-reactions.js
+++ /dev/null
@@ -1,22 +0,0 @@
-db.users.update({}, {
-	$unset: {
-		likes_count: 1,
-		liked_count: 1
-	}
-}, false, true)
-
-db.likes.renameCollection('post_reactions')
-
-db.post_reactions.update({}, {
-	$set: {
-		reaction: 'like'
-	}
-}, false, true)
-
-db.posts.update({}, {
-	$rename: {
-		likes_count: 'reaction_counts.like'
-	}
-}, false, true);
-
-db.notifications.remove({})
diff --git a/tools/migration/shell.1509507382.reply_to-to-reply.js b/tools/migration/shell.1509507382.reply_to-to-reply.js
deleted file mode 100644
index ceb272ebc..000000000
--- a/tools/migration/shell.1509507382.reply_to-to-reply.js
+++ /dev/null
@@ -1,5 +0,0 @@
-db.posts.update({}, {
-	$rename: {
-		reply_to_id: 'reply_id'
-	}
-}, false, true);

From 92b3501c6b233d1d636e2d0631ec96e17b20ab2b Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Sun, 1 Apr 2018 20:07:04 +0900
Subject: [PATCH 0982/1250] Implement account public key endpoint

---
 src/common/remote/activitypub/renderer/key.ts |  1 +
 src/server/activitypub/index.ts               |  2 +
 src/server/activitypub/outbox.ts              | 30 +++----------
 src/server/activitypub/publickey.ts           | 19 ++++++++
 src/server/activitypub/user.ts                | 43 +++++++------------
 src/server/activitypub/with-user.ts           | 23 ++++++++++
 6 files changed, 66 insertions(+), 52 deletions(-)
 create mode 100644 src/server/activitypub/publickey.ts
 create mode 100644 src/server/activitypub/with-user.ts

diff --git a/src/common/remote/activitypub/renderer/key.ts b/src/common/remote/activitypub/renderer/key.ts
index 7148c5974..692c71f88 100644
--- a/src/common/remote/activitypub/renderer/key.ts
+++ b/src/common/remote/activitypub/renderer/key.ts
@@ -3,6 +3,7 @@ import { extractPublic } from '../../../../crypto_key';
 import { ILocalAccount } from '../../../../models/user';
 
 export default ({ username, account }) => ({
+	id: `${config.url}/@${username}/publickey`,
 	type: 'Key',
 	owner: `${config.url}/@${username}`,
 	publicKeyPem: extractPublic((account as ILocalAccount).keypair)
diff --git a/src/server/activitypub/index.ts b/src/server/activitypub/index.ts
index c81024d15..ac7a184f2 100644
--- a/src/server/activitypub/index.ts
+++ b/src/server/activitypub/index.ts
@@ -3,6 +3,7 @@ import * as express from 'express';
 import user from './user';
 import inbox from './inbox';
 import outbox from './outbox';
+import publicKey from './publickey';
 import post from './post';
 
 const app = express();
@@ -11,6 +12,7 @@ app.disable('x-powered-by');
 app.use(user);
 app.use(inbox);
 app.use(outbox);
+app.use(publicKey);
 app.use(post);
 
 export default app;
diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts
index c5a42ae0a..c26c4df75 100644
--- a/src/server/activitypub/outbox.ts
+++ b/src/server/activitypub/outbox.ts
@@ -2,44 +2,26 @@ import * as express from 'express';
 import context from '../../common/remote/activitypub/renderer/context';
 import renderNote from '../../common/remote/activitypub/renderer/note';
 import renderOrderedCollection from '../../common/remote/activitypub/renderer/ordered-collection';
-import parseAcct from '../../common/user/parse-acct';
 import config from '../../conf';
 import Post from '../../models/post';
-import User from '../../models/user';
+import withUser from './with-user';
 
 const app = express();
 app.disable('x-powered-by');
 
-app.get('/@:user/outbox', async (req, res) => {
-	const { username, host } = parseAcct(req.params.user);
-	if (host !== null) {
-		return res.sendStatus(422);
-	}
-
-	const user = await User.findOne({
-		usernameLower: username.toLowerCase(),
-		host: null
-	});
-	if (user === null) {
-		return res.sendStatus(404);
-	}
-
-	const id = `${config.url}/@${user.username}/inbox`;
-
-	if (username !== user.username) {
-		return res.redirect(id);
-	}
-
+app.get('/@:user/outbox', withUser(username => {
+	return `${config.url}/@${username}/inbox`;
+}, async (user, req, res) => {
 	const posts = await Post.find({ userId: user._id }, {
 		limit: 20,
 		sort: { _id: -1 }
 	});
 
 	const renderedPosts = await Promise.all(posts.map(post => renderNote(user, post)));
-	const rendered = renderOrderedCollection(id, user.postsCount, renderedPosts);
+	const rendered = renderOrderedCollection(`${config.url}/@${user.username}/inbox`, user.postsCount, renderedPosts);
 	rendered['@context'] = context;
 
 	res.json(rendered);
-});
+}));
 
 export default app;
diff --git a/src/server/activitypub/publickey.ts b/src/server/activitypub/publickey.ts
new file mode 100644
index 000000000..e380309dc
--- /dev/null
+++ b/src/server/activitypub/publickey.ts
@@ -0,0 +1,19 @@
+import * as express from 'express';
+import context from '../../common/remote/activitypub/renderer/context';
+import render from '../../common/remote/activitypub/renderer/key';
+import config from '../../conf';
+import withUser from './with-user';
+
+const app = express();
+app.disable('x-powered-by');
+
+app.get('/@:user/publickey', withUser(username => {
+	return `${config.url}/@${username}/publickey`;
+}, (user, req, res) => {
+	const rendered = render(user);
+	rendered['@context'] = context;
+
+	res.json(rendered);
+}));
+
+export default app;
diff --git a/src/server/activitypub/user.ts b/src/server/activitypub/user.ts
index d43a9793d..8e8deca4a 100644
--- a/src/server/activitypub/user.ts
+++ b/src/server/activitypub/user.ts
@@ -2,39 +2,26 @@ import * as express from 'express';
 import config from '../../conf';
 import context from '../../common/remote/activitypub/renderer/context';
 import render from '../../common/remote/activitypub/renderer/person';
-import parseAcct from '../../common/user/parse-acct';
-import User from '../../models/user';
-
-const app = express();
-app.disable('x-powered-by');
-
-app.get('/@:user', async (req, res, next) => {
-	const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']);
-	if (!(['application/activity+json', 'application/ld+json'] as Array<any>).includes(accepted)) {
-		return next();
-	}
-
-	const { username, host } = parseAcct(req.params.user);
-	if (host !== null) {
-		return res.sendStatus(422);
-	}
-
-	const user = await User.findOne({
-		usernameLower: username.toLowerCase(),
-		host: null
-	});
-	if (user === null) {
-		return res.sendStatus(404);
-	}
-
-	if (username !== user.username) {
-		return res.redirect(`${config.url}/@${user.username}`);
-	}
+import withUser from './with-user';
 
+const respond = withUser(username => `${config.url}/@${username}`, (user, req, res) => {
 	const rendered = render(user);
 	rendered['@context'] = context;
 
 	res.json(rendered);
 });
 
+const app = express();
+app.disable('x-powered-by');
+
+app.get('/@:user', (req, res, next) => {
+	const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']);
+
+	if ((['application/activity+json', 'application/ld+json'] as Array<any>).includes(accepted)) {
+		respond(req, res, next);
+	} else {
+		next();
+	}
+});
+
 export default app;
diff --git a/src/server/activitypub/with-user.ts b/src/server/activitypub/with-user.ts
new file mode 100644
index 000000000..0bab47b78
--- /dev/null
+++ b/src/server/activitypub/with-user.ts
@@ -0,0 +1,23 @@
+import parseAcct from '../../common/user/parse-acct';
+import User from '../../models/user';
+
+export default (redirect, respond) => async (req, res, next) => {
+	const { username, host } = parseAcct(req.params.user);
+	if (host !== null) {
+		return res.sendStatus(422);
+	}
+
+	const user = await User.findOne({
+		usernameLower: username.toLowerCase(),
+		host: null
+	});
+	if (user === null) {
+		return res.sendStatus(404);
+	}
+
+	if (username !== user.username) {
+		return res.redirect(redirect(user.username));
+	}
+
+	return respond(user, req, res, next);
+}

From 4ebb22f37d09d4598e81708bc6735bfb510e9b8c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 1 Apr 2018 21:15:00 +0900
Subject: [PATCH 0983/1250] Add missing brackets

---
 src/common/remote/activitypub/act/index.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/common/remote/activitypub/act/index.ts b/src/common/remote/activitypub/act/index.ts
index 0f4084a61..49a9aee06 100644
--- a/src/common/remote/activitypub/act/index.ts
+++ b/src/common/remote/activitypub/act/index.ts
@@ -3,7 +3,7 @@ import createObject from '../create';
 import Resolver from '../resolver';
 
 export default (actor, value) => {
-	return (new Resolver).resolve(value).then(resolved => Promise.all(resolved.map(async asyncResult => {
+	return (new Resolver()).resolve(value).then(resolved => Promise.all(resolved.map(async asyncResult => {
 		const { resolver, object } = await asyncResult;
 		const created = await (await createObject(resolver, actor, [object]))[0];
 

From 823438f05b13d2939eee797572abab700c0e0c24 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 1 Apr 2018 21:16:37 +0900
Subject: [PATCH 0984/1250] Remove unnecessary brackets

---
 src/common/remote/activitypub/act/index.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/common/remote/activitypub/act/index.ts b/src/common/remote/activitypub/act/index.ts
index 49a9aee06..f8cf2f41b 100644
--- a/src/common/remote/activitypub/act/index.ts
+++ b/src/common/remote/activitypub/act/index.ts
@@ -3,7 +3,7 @@ import createObject from '../create';
 import Resolver from '../resolver';
 
 export default (actor, value) => {
-	return (new Resolver()).resolve(value).then(resolved => Promise.all(resolved.map(async asyncResult => {
+	return new Resolver().resolve(value).then(resolved => Promise.all(resolved.map(async asyncResult => {
 		const { resolver, object } = await asyncResult;
 		const created = await (await createObject(resolver, actor, [object]))[0];
 

From 0aee0bde271096009830db4aec156f71c0d84929 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 1 Apr 2018 21:17:46 +0900
Subject: [PATCH 0985/1250] Add missing semicolon

---
 src/common/remote/activitypub/act/index.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/common/remote/activitypub/act/index.ts b/src/common/remote/activitypub/act/index.ts
index f8cf2f41b..a4c5b4602 100644
--- a/src/common/remote/activitypub/act/index.ts
+++ b/src/common/remote/activitypub/act/index.ts
@@ -19,4 +19,4 @@ export default (actor, value) => {
 			return null;
 		}
 	})));
-}
+};

From 4841bc4642712d853771c5ff86c6e72811369a32 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 1 Apr 2018 21:19:39 +0900
Subject: [PATCH 0986/1250] Better variable name

---
 src/common/remote/activitypub/act/index.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/common/remote/activitypub/act/index.ts b/src/common/remote/activitypub/act/index.ts
index a4c5b4602..a76983638 100644
--- a/src/common/remote/activitypub/act/index.ts
+++ b/src/common/remote/activitypub/act/index.ts
@@ -3,8 +3,8 @@ import createObject from '../create';
 import Resolver from '../resolver';
 
 export default (actor, value) => {
-	return new Resolver().resolve(value).then(resolved => Promise.all(resolved.map(async asyncResult => {
-		const { resolver, object } = await asyncResult;
+	return new Resolver().resolve(value).then(resolved => Promise.all(resolved.map(async promisedResult => {
+		const { resolver, object } = await promisedResult;
 		const created = await (await createObject(resolver, actor, [object]))[0];
 
 		if (created !== null) {

From 28c8b843eeb4d56ee4bf86d53bb36ac850c79cd0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 1 Apr 2018 21:24:25 +0900
Subject: [PATCH 0987/1250] Some fixes and refactors

---
 src/common/remote/activitypub/act/create.ts     |  2 +-
 src/common/remote/activitypub/create.ts         | 11 ++++++-----
 src/common/remote/activitypub/resolve-person.ts |  6 +++---
 src/common/remote/activitypub/resolver.ts       | 12 ++++++------
 src/common/remote/activitypub/type.ts           |  2 +-
 src/common/remote/resolve-user.ts               |  2 +-
 src/common/remote/webfinger.ts                  |  6 +++---
 7 files changed, 21 insertions(+), 20 deletions(-)

diff --git a/src/common/remote/activitypub/act/create.ts b/src/common/remote/activitypub/act/create.ts
index 6c62f7ab9..9eb74800e 100644
--- a/src/common/remote/activitypub/act/create.ts
+++ b/src/common/remote/activitypub/act/create.ts
@@ -2,7 +2,7 @@ import create from '../create';
 
 export default (resolver, actor, activity) => {
 	if ('actor' in activity && actor.account.uri !== activity.actor) {
-		throw new Error;
+		throw new Error();
 	}
 
 	return create(resolver, actor, activity.object);
diff --git a/src/common/remote/activitypub/create.ts b/src/common/remote/activitypub/create.ts
index 4aaaeb306..ea780f01e 100644
--- a/src/common/remote/activitypub/create.ts
+++ b/src/common/remote/activitypub/create.ts
@@ -3,6 +3,7 @@ import config from '../../../conf';
 import Post from '../../../models/post';
 import RemoteUserObject, { IRemoteUserObject } from '../../../models/remote-user-object';
 import uploadFromUrl from '../../drive/upload_from_url';
+import Resolver from './resolver';
 const createDOMPurify = require('dompurify');
 
 function createRemoteUserObject($ref, $id, { id }) {
@@ -17,7 +18,7 @@ function createRemoteUserObject($ref, $id, { id }) {
 
 async function createImage(actor, object) {
 	if ('attributedTo' in object && actor.account.uri !== object.attributedTo) {
-		throw new Error;
+		throw new Error();
 	}
 
 	const { _id } = await uploadFromUrl(object.url, actor);
@@ -26,7 +27,7 @@ async function createImage(actor, object) {
 
 async function createNote(resolver, actor, object) {
 	if ('attributedTo' in object && actor.account.uri !== object.attributedTo) {
-		throw new Error;
+		throw new Error();
 	}
 
 	const mediaIds = 'attachment' in object &&
@@ -69,10 +70,10 @@ async function createNote(resolver, actor, object) {
 	return createRemoteUserObject('posts', _id, object);
 }
 
-export default async function create(parentResolver, actor, value): Promise<Promise<IRemoteUserObject>[]> {
+export default async function create(parentResolver: Resolver, actor, value): Promise<Array<Promise<IRemoteUserObject>>> {
 	const results = await parentResolver.resolveRemoteUserObjects(value);
 
-	return results.map(asyncResult => asyncResult.then(({ resolver, object }) => {
+	return results.map(promisedResult => promisedResult.then(({ resolver, object }) => {
 		switch (object.type) {
 		case 'Image':
 			return createImage(actor, object);
@@ -83,4 +84,4 @@ export default async function create(parentResolver, actor, value): Promise<Prom
 
 		return null;
 	}));
-};
+}
diff --git a/src/common/remote/activitypub/resolve-person.ts b/src/common/remote/activitypub/resolve-person.ts
index 999a37eea..b8c507d35 100644
--- a/src/common/remote/activitypub/resolve-person.ts
+++ b/src/common/remote/activitypub/resolve-person.ts
@@ -12,10 +12,10 @@ async function isCollection(collection) {
 
 export default async (value, usernameLower, hostLower, acctLower) => {
 	if (!validateUsername(usernameLower)) {
-		throw new Error;
+		throw new Error();
 	}
 
-	const { resolver, object } = await (new Resolver).resolveOne(value);
+	const { resolver, object } = await new Resolver().resolveOne(value);
 
 	if (
 		object === null ||
@@ -25,7 +25,7 @@ export default async (value, usernameLower, hostLower, acctLower) => {
 		!isValidName(object.name) ||
 		!isValidDescription(object.summary)
 	) {
-		throw new Error;
+		throw new Error();
 	}
 
 	const [followers, following, outbox, finger] = await Promise.all([
diff --git a/src/common/remote/activitypub/resolver.ts b/src/common/remote/activitypub/resolver.ts
index 50ac1b0b1..43f0d63cb 100644
--- a/src/common/remote/activitypub/resolver.ts
+++ b/src/common/remote/activitypub/resolver.ts
@@ -29,7 +29,7 @@ async function resolveUnrequestedOne(this: Resolver, value) {
 			!object['@context'].includes('https://www.w3.org/ns/activitystreams') :
 			object['@context'] !== 'https://www.w3.org/ns/activitystreams'
 	)) {
-		throw new Error;
+		throw new Error();
 	}
 
 	return { resolver, object };
@@ -57,13 +57,13 @@ async function resolveCollection(this: Resolver, value) {
 }
 
 export default class Resolver {
-	requesting: Set<string>;
+	private requesting: Set<string>;
 
 	constructor(iterable?: Iterable<string>) {
 		this.requesting = new Set(iterable);
 	}
 
-	async resolve(value): Promise<Promise<IResult>[]> {
+	public async resolve(value): Promise<Array<Promise<IResult>>> {
 		const collection = await resolveCollection.call(this, value);
 
 		return collection
@@ -71,15 +71,15 @@ export default class Resolver {
 			.map(resolveUnrequestedOne.bind(this));
 	}
 
-	resolveOne(value) {
+	public resolveOne(value) {
 		if (this.requesting.has(value)) {
-			throw new Error;
+			throw new Error();
 		}
 
 		return resolveUnrequestedOne.call(this, value);
 	}
 
-	async resolveRemoteUserObjects(value) {
+	public async resolveRemoteUserObjects(value) {
 		const collection = await resolveCollection.call(this, value);
 
 		return collection.filter(element => !this.requesting.has(element)).map(element => {
diff --git a/src/common/remote/activitypub/type.ts b/src/common/remote/activitypub/type.ts
index 5c4750e14..94e2c350a 100644
--- a/src/common/remote/activitypub/type.ts
+++ b/src/common/remote/activitypub/type.ts
@@ -1,3 +1,3 @@
 export type IObject = {
 	type: string;
-}
+};
diff --git a/src/common/remote/resolve-user.ts b/src/common/remote/resolve-user.ts
index 13d155830..4959539da 100644
--- a/src/common/remote/resolve-user.ts
+++ b/src/common/remote/resolve-user.ts
@@ -16,7 +16,7 @@ export default async (username, host, option) => {
 		const finger = await webFinger(acctLower, acctLower);
 		const self = finger.links.find(link => link.rel && link.rel.toLowerCase() === 'self');
 		if (!self) {
-			throw new Error;
+			throw new Error();
 		}
 
 		user = await resolvePerson(self.href, usernameLower, hostLower, acctLower);
diff --git a/src/common/remote/webfinger.ts b/src/common/remote/webfinger.ts
index 23f0aaa55..9f1b916c9 100644
--- a/src/common/remote/webfinger.ts
+++ b/src/common/remote/webfinger.ts
@@ -5,12 +5,12 @@ const webFinger = new WebFinger({});
 type ILink = {
   href: string;
   rel: string;
-}
+};
 
 type IWebFinger = {
-  links: Array<ILink>;
+  links: ILink[];
   subject: string;
-}
+};
 
 export default (query, verifier): Promise<IWebFinger> => new Promise((res, rej) => webFinger.lookup(query, (error, result) => {
 	if (error) {

From 0328ef96d9154464eb8de25427120756cb5b98ba Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 1 Apr 2018 21:35:38 +0900
Subject: [PATCH 0988/1250] Fix test

---
 test/text.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/text.js b/test/text.js
index 1c034d633..0338bcefb 100644
--- a/test/text.js
+++ b/test/text.js
@@ -5,7 +5,7 @@
 const assert = require('assert');
 
 const analyze = require('../built/common/text/parse').default;
-const syntaxhighlighter = require('../built/common/text/core/syntax-highlighter').default;
+const syntaxhighlighter = require('../built/common/text/parse/core/syntax-highlighter').default;
 
 describe('Text', () => {
 	it('can be analyzed', () => {

From 59701594909ff533b66d78a129ebf9bb8de779d9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 1 Apr 2018 21:53:44 +0900
Subject: [PATCH 0989/1250] oops

---
 src/models/drive-folder.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/models/drive-folder.ts b/src/models/drive-folder.ts
index ad27b151b..45cc9c964 100644
--- a/src/models/drive-folder.ts
+++ b/src/models/drive-folder.ts
@@ -3,7 +3,7 @@ import deepcopy = require('deepcopy');
 import db from '../db/mongodb';
 import DriveFile from './drive-file';
 
-const DriveFolder = db.get<IDriveFolder>('drive_folders');
+const DriveFolder = db.get<IDriveFolder>('driveFolders');
 export default DriveFolder;
 
 export type IDriveFolder = {

From a3229a23081c54ec7a816afff7619f28893bebcc Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 1 Apr 2018 21:56:11 +0900
Subject: [PATCH 0990/1250] Refactor

---
 src/common/remote/activitypub/resolver.ts | 108 +++++++++++-----------
 1 file changed, 54 insertions(+), 54 deletions(-)

diff --git a/src/common/remote/activitypub/resolver.ts b/src/common/remote/activitypub/resolver.ts
index 43f0d63cb..a167fa133 100644
--- a/src/common/remote/activitypub/resolver.ts
+++ b/src/common/remote/activitypub/resolver.ts
@@ -7,55 +7,6 @@ type IResult = {
   object: IObject;
 };
 
-async function resolveUnrequestedOne(this: Resolver, value) {
-	if (typeof value !== 'string') {
-		return { resolver: this, object: value };
-	}
-
-	const resolver = new Resolver(this.requesting);
-
-	resolver.requesting.add(value);
-
-	const object = await request({
-		url: value,
-		headers: {
-			Accept: 'application/activity+json, application/ld+json'
-		},
-		json: true
-	});
-
-	if (object === null || (
-		Array.isArray(object['@context']) ?
-			!object['@context'].includes('https://www.w3.org/ns/activitystreams') :
-			object['@context'] !== 'https://www.w3.org/ns/activitystreams'
-	)) {
-		throw new Error();
-	}
-
-	return { resolver, object };
-}
-
-async function resolveCollection(this: Resolver, value) {
-	if (Array.isArray(value)) {
-		return value;
-	}
-
-	const resolved = typeof value === 'string' ?
-		await resolveUnrequestedOne.call(this, value) :
-		value;
-
-	switch (resolved.type) {
-	case 'Collection':
-		return resolved.items;
-
-	case 'OrderedCollection':
-		return resolved.orderedItems;
-
-	default:
-		return [resolved];
-	}
-}
-
 export default class Resolver {
 	private requesting: Set<string>;
 
@@ -63,12 +14,61 @@ export default class Resolver {
 		this.requesting = new Set(iterable);
 	}
 
+	private async resolveUnrequestedOne(value) {
+		if (typeof value !== 'string') {
+			return { resolver: this, object: value };
+		}
+
+		const resolver = new Resolver(this.requesting);
+
+		resolver.requesting.add(value);
+
+		const object = await request({
+			url: value,
+			headers: {
+				Accept: 'application/activity+json, application/ld+json'
+			},
+			json: true
+		});
+
+		if (object === null || (
+			Array.isArray(object['@context']) ?
+				!object['@context'].includes('https://www.w3.org/ns/activitystreams') :
+				object['@context'] !== 'https://www.w3.org/ns/activitystreams'
+		)) {
+			throw new Error();
+		}
+
+		return { resolver, object };
+	}
+
+	private async resolveCollection(value) {
+		if (Array.isArray(value)) {
+			return value;
+		}
+
+		const resolved = typeof value === 'string' ?
+			await this.resolveUnrequestedOne(value) :
+			value;
+
+		switch (resolved.type) {
+		case 'Collection':
+			return resolved.items;
+
+		case 'OrderedCollection':
+			return resolved.orderedItems;
+
+		default:
+			return [resolved];
+		}
+	}
+
 	public async resolve(value): Promise<Array<Promise<IResult>>> {
-		const collection = await resolveCollection.call(this, value);
+		const collection = await this.resolveCollection(value);
 
 		return collection
 			.filter(element => !this.requesting.has(element))
-			.map(resolveUnrequestedOne.bind(this));
+			.map(this.resolveUnrequestedOne.bind(this));
 	}
 
 	public resolveOne(value) {
@@ -76,11 +76,11 @@ export default class Resolver {
 			throw new Error();
 		}
 
-		return resolveUnrequestedOne.call(this, value);
+		return this.resolveUnrequestedOne(value);
 	}
 
 	public async resolveRemoteUserObjects(value) {
-		const collection = await resolveCollection.call(this, value);
+		const collection = await this.resolveCollection(value);
 
 		return collection.filter(element => !this.requesting.has(element)).map(element => {
 			if (typeof element === 'string') {
@@ -91,7 +91,7 @@ export default class Resolver {
 				}
 			}
 
-			return resolveUnrequestedOne.call(this, element);
+			return this.resolveUnrequestedOne(element);
 		});
 	}
 }

From a60482e90a2c4907b8995d8d0a3d68e1c895e92a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 1 Apr 2018 22:09:25 +0900
Subject: [PATCH 0991/1250] Fix type annotation

---
 src/server/activitypub/user.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/server/activitypub/user.ts b/src/server/activitypub/user.ts
index 8e8deca4a..cfda409e2 100644
--- a/src/server/activitypub/user.ts
+++ b/src/server/activitypub/user.ts
@@ -17,7 +17,7 @@ app.disable('x-powered-by');
 app.get('/@:user', (req, res, next) => {
 	const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']);
 
-	if ((['application/activity+json', 'application/ld+json'] as Array<any>).includes(accepted)) {
+	if ((['application/activity+json', 'application/ld+json'] as any[]).includes(accepted)) {
 		respond(req, res, next);
 	} else {
 		next();

From 2037d1c602eedcdf37db6284ebc50305f4b0a58d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 1 Apr 2018 22:24:44 +0900
Subject: [PATCH 0992/1250] Add missing semicolon

---
 src/server/activitypub/with-user.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/server/activitypub/with-user.ts b/src/server/activitypub/with-user.ts
index 0bab47b78..ed289b641 100644
--- a/src/server/activitypub/with-user.ts
+++ b/src/server/activitypub/with-user.ts
@@ -20,4 +20,4 @@ export default (redirect, respond) => async (req, res, next) => {
 	}
 
 	return respond(user, req, res, next);
-}
+};

From dedd5bbdba3d491ccdc31f7ca84b36ee7ec39b70 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Sun, 1 Apr 2018 19:43:26 +0900
Subject: [PATCH 0993/1250] Implement remote follow

---
 src/{server/api => }/common/notify.ts         |  8 +-
 .../remote/activitypub/renderer/follow.ts     |  8 ++
 .../remote/activitypub/resolve-person.ts      |  1 +
 src/common/remote/webfinger.ts                |  2 +-
 src/models/user.ts                            |  1 +
 src/processor/http/follow.ts                  | 89 +++++++++++++++++++
 src/processor/http/index.ts                   |  2 +
 src/server/api/endpoints/following/create.ts  | 29 ++----
 src/server/api/endpoints/posts/create.ts      |  2 +-
 src/server/api/endpoints/posts/polls/vote.ts  |  2 +-
 .../api/endpoints/posts/reactions/create.ts   |  2 +-
 11 files changed, 114 insertions(+), 32 deletions(-)
 rename src/{server/api => }/common/notify.ts (86%)
 create mode 100644 src/common/remote/activitypub/renderer/follow.ts
 create mode 100644 src/processor/http/follow.ts

diff --git a/src/server/api/common/notify.ts b/src/common/notify.ts
similarity index 86%
rename from src/server/api/common/notify.ts
rename to src/common/notify.ts
index 69bf8480b..fc65820d3 100644
--- a/src/server/api/common/notify.ts
+++ b/src/common/notify.ts
@@ -1,8 +1,8 @@
 import * as mongo from 'mongodb';
-import Notification from '../../../models/notification';
-import Mute from '../../../models/mute';
-import event from '../../../common/event';
-import { pack } from '../../../models/notification';
+import Notification from '../models/notification';
+import Mute from '../models/mute';
+import event from './event';
+import { pack } from '../models/notification';
 
 export default (
 	notifiee: mongo.ObjectID,
diff --git a/src/common/remote/activitypub/renderer/follow.ts b/src/common/remote/activitypub/renderer/follow.ts
new file mode 100644
index 000000000..05c0ecca0
--- /dev/null
+++ b/src/common/remote/activitypub/renderer/follow.ts
@@ -0,0 +1,8 @@
+import config from '../../../../conf';
+import { IRemoteAccount } from '../../../../models/user';
+
+export default ({ username }, { account }) => ({
+	type: 'Follow',
+	actor: `${config.url}/@${username}`,
+	object: (account as IRemoteAccount).uri
+});
diff --git a/src/common/remote/activitypub/resolve-person.ts b/src/common/remote/activitypub/resolve-person.ts
index 999a37eea..c44911a57 100644
--- a/src/common/remote/activitypub/resolve-person.ts
+++ b/src/common/remote/activitypub/resolve-person.ts
@@ -66,6 +66,7 @@ export default async (value, usernameLower, hostLower, acctLower) => {
 				id: object.publicKey.id,
 				publicKeyPem: object.publicKey.publicKeyPem
 			},
+			inbox: object.inbox,
 			uri: object.id,
 		},
 	});
diff --git a/src/common/remote/webfinger.ts b/src/common/remote/webfinger.ts
index 23f0aaa55..f5e3d89e1 100644
--- a/src/common/remote/webfinger.ts
+++ b/src/common/remote/webfinger.ts
@@ -1,6 +1,6 @@
 const WebFinger = require('webfinger.js');
 
-const webFinger = new WebFinger({});
+const webFinger = new WebFinger({ tls_only: false });
 
 type ILink = {
   href: string;
diff --git a/src/models/user.ts b/src/models/user.ts
index 9588c4515..d9ac72b88 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -70,6 +70,7 @@ export type ILocalAccount = {
 };
 
 export type IRemoteAccount = {
+	inbox: string;
 	uri: string;
 	publicKey: {
 		id: string;
diff --git a/src/processor/http/follow.ts b/src/processor/http/follow.ts
new file mode 100644
index 000000000..adaa2f3f6
--- /dev/null
+++ b/src/processor/http/follow.ts
@@ -0,0 +1,89 @@
+import { request } from 'https';
+import { sign } from 'http-signature';
+import { URL } from 'url';
+import User, { ILocalAccount, IRemoteAccount, pack as packUser } from '../../models/user';
+import Following from '../../models/following';
+import event from '../../common/event';
+import notify from '../../common/notify';
+import context from '../../common/remote/activitypub/renderer/context';
+import render from '../../common/remote/activitypub/renderer/follow';
+import config from '../../conf';
+
+export default ({ data }, done) => Following.findOne({ _id: data.following }).then(({ followerId, followeeId }) => {
+	const promisedFollower = User.findOne({ _id: followerId });
+	const promisedFollowee = User.findOne({ _id: followeeId });
+
+	return Promise.all([
+		// Increment following count
+		User.update(followerId, {
+			$inc: {
+				followingCount: 1
+			}
+		}),
+
+		// Increment followers count
+		User.update({ _id: followeeId }, {
+			$inc: {
+				followersCount: 1
+			}
+		}),
+
+		// Notify
+		promisedFollowee.then(followee => followee.host === null ?
+			notify(followeeId, followerId, 'follow') : null),
+
+		// Publish follow event
+		Promise.all([promisedFollower, promisedFollowee]).then(([follower, followee]) => {
+			const followerEvent = packUser(followee, follower)
+				.then(packed => event(follower._id, 'follow', packed));
+			let followeeEvent;
+
+			if (followee.host === null) {
+				followeeEvent = packUser(follower, followee)
+					.then(packed => event(followee._id, 'followed', packed));
+			} else {
+				followeeEvent = new Promise((resolve, reject) => {
+					const {
+						protocol,
+						hostname,
+						port,
+						pathname,
+						search
+					} = new URL(followee.account as IRemoteAccount).inbox);
+
+					const req = request({
+						protocol,
+						hostname,
+						port,
+						method: 'POST',
+						path: pathname + search,
+					}, res => {
+						res.on('close', () => {
+							if (res.statusCode >= 200 && res.statusCode < 300) {
+								resolve();
+							} else {
+								reject(res);
+							}
+						});
+
+						res.on('data', () => {});
+						res.on('error', reject);
+					});
+
+					sign(req, {
+						authorizationHeaderName: 'Signature',
+						key: (follower.account as ILocalAccount).keypair,
+						keyId: `acct:${follower.username}@${config.host}`
+					});
+
+					const rendered = render(follower, followee);
+					rendered['@context'] = context;
+
+					req.end(JSON.stringify(rendered));
+				});
+			}
+
+			return Promise.all([followerEvent, followeeEvent]);
+		})
+	]);
+}).then(done, done);
diff --git a/src/processor/http/index.ts b/src/processor/http/index.ts
index da942ad2a..a001cf11f 100644
--- a/src/processor/http/index.ts
+++ b/src/processor/http/index.ts
@@ -1,7 +1,9 @@
+import follow from './follow';
 import performActivityPub from './perform-activitypub';
 import reportGitHubFailure from './report-github-failure';
 
 const handlers = {
+  follow,
   performActivityPub,
   reportGitHubFailure,
 };
diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts
index a689250e3..03c13ab7f 100644
--- a/src/server/api/endpoints/following/create.ts
+++ b/src/server/api/endpoints/following/create.ts
@@ -2,10 +2,9 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User, { pack as packUser } from '../../../../models/user';
+import User from '../../../../models/user';
 import Following from '../../../../models/following';
-import notify from '../../common/notify';
-import event from '../../../../common/event';
+import queue from '../../../../queue';
 
 /**
  * Follow a user
@@ -52,33 +51,15 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}
 
 	// Create following
-	await Following.insert({
+	const { _id } = await Following.insert({
 		createdAt: new Date(),
 		followerId: follower._id,
 		followeeId: followee._id
 	});
 
+	queue.create('http', { type: 'follow', following: _id }).save();
+
 	// Send response
 	res();
 
-	// Increment following count
-	User.update(follower._id, {
-		$inc: {
-			followingCount: 1
-		}
-	});
-
-	// Increment followers count
-	User.update({ _id: followee._id }, {
-		$inc: {
-			followersCount: 1
-		}
-	});
-
-	// Publish follow event
-	event(follower._id, 'follow', await packUser(followee, follower));
-	event(followee._id, 'followed', await packUser(follower, followee));
-
-	// Notify
-	notify(followee._id, follower._id, 'follow');
 });
diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts
index 42901ebcb..6e7d2329a 100644
--- a/src/server/api/endpoints/posts/create.ts
+++ b/src/server/api/endpoints/posts/create.ts
@@ -14,9 +14,9 @@ import DriveFile from '../../../../models/drive-file';
 import Watching from '../../../../models/post-watching';
 import ChannelWatching from '../../../../models/channel-watching';
 import { pack } from '../../../../models/post';
-import notify from '../../common/notify';
 import watch from '../../common/watch-post';
 import event, { pushSw, publishChannelStream } from '../../../../common/event';
+import notify from '../../../../common/notify';
 import getAcct from '../../../../common/user/get-acct';
 import parseAcct from '../../../../common/user/parse-acct';
 import config from '../../../../conf';
diff --git a/src/server/api/endpoints/posts/polls/vote.ts b/src/server/api/endpoints/posts/polls/vote.ts
index 98df074e5..59b1f099f 100644
--- a/src/server/api/endpoints/posts/polls/vote.ts
+++ b/src/server/api/endpoints/posts/polls/vote.ts
@@ -5,9 +5,9 @@ import $ from 'cafy';
 import Vote from '../../../../../models/poll-vote';
 import Post from '../../../../../models/post';
 import Watching from '../../../../../models/post-watching';
-import notify from '../../../common/notify';
 import watch from '../../../common/watch-post';
 import { publishPostStream } from '../../../../../common/event';
+import notify from '../../../../../common/notify';
 
 /**
  * Vote poll of a post
diff --git a/src/server/api/endpoints/posts/reactions/create.ts b/src/server/api/endpoints/posts/reactions/create.ts
index 8db76d643..441d56383 100644
--- a/src/server/api/endpoints/posts/reactions/create.ts
+++ b/src/server/api/endpoints/posts/reactions/create.ts
@@ -6,9 +6,9 @@ import Reaction from '../../../../../models/post-reaction';
 import Post, { pack as packPost } from '../../../../../models/post';
 import { pack as packUser } from '../../../../../models/user';
 import Watching from '../../../../../models/post-watching';
-import notify from '../../../common/notify';
 import watch from '../../../common/watch-post';
 import { publishPostStream, pushSw } from '../../../../../common/event';
+import notify from '../../../../../common/notify';
 
 /**
  * React to a post

From 40756d0b0ff915a7ebfff45fe0b55982e7076043 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Sun, 1 Apr 2018 23:33:48 +0900
Subject: [PATCH 0994/1250] Force TLS on WebFinger

---
 src/common/remote/webfinger.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/common/remote/webfinger.ts b/src/common/remote/webfinger.ts
index f5e3d89e1..641d88f97 100644
--- a/src/common/remote/webfinger.ts
+++ b/src/common/remote/webfinger.ts
@@ -1,6 +1,6 @@
 const WebFinger = require('webfinger.js');
 
-const webFinger = new WebFinger({ tls_only: false });
+const webFinger = new WebFinger({ });
 
 type ILink = {
   href: string;

From aaf8397128dcd554afee57e9fe559fcc80fc30c6 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Mon, 2 Apr 2018 00:36:36 +0900
Subject: [PATCH 0995/1250] Make inbox signature verification compatible with
 Mastodon

---
 src/server/activitypub/inbox.ts | 24 ++++++++++++++++++++----
 1 file changed, 20 insertions(+), 4 deletions(-)

diff --git a/src/server/activitypub/inbox.ts b/src/server/activitypub/inbox.ts
index 915129748..6d092e66b 100644
--- a/src/server/activitypub/inbox.ts
+++ b/src/server/activitypub/inbox.ts
@@ -11,16 +11,32 @@ app.use(bodyParser.json());
 app.post('/@:user/inbox', async (req, res) => {
 	let parsed;
 
+	req.headers.authorization = 'Signature ' + req.headers.signature;
+
 	try {
 		parsed = parseRequest(req);
 	} catch (exception) {
 		return res.sendStatus(401);
 	}
 
-	const user = await User.findOne({
-		host: { $ne: null },
-		'account.publicKey.id': parsed.keyId
-	});
+	const keyIdLower = parsed.keyId.toLowerCase();
+	let query;
+
+	if (keyIdLower.startsWith('acct:')) {
+		const { username, host } = parseAcct(keyIdLower.slice('acct:'.length));
+		if (host === null) {
+			return res.sendStatus(401);
+		}
+
+		query = { usernameLower: username, hostLower: host };
+	} else {
+		query = {
+			host: { $ne: null },
+			'account.publicKey.id': parsed.keyId
+		};
+	}
+
+	const user = await User.findOne(query);
 
 	if (user === null) {
 		return res.sendStatus(401);

From 030ceb1a11b2aef5d375fa671f12679bbc598e39 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 2 Apr 2018 01:26:17 +0900
Subject: [PATCH 0996/1250] Fix: Add missing bracket

---
 src/processor/http/follow.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/processor/http/follow.ts b/src/processor/http/follow.ts
index adaa2f3f6..a8f7ba78c 100644
--- a/src/processor/http/follow.ts
+++ b/src/processor/http/follow.ts
@@ -49,7 +49,7 @@ export default ({ data }, done) => Following.findOne({ _id: data.following }).th
 						port,
 						pathname,
 						search
-					} = new URL(followee.account as IRemoteAccount).inbox);
+					} = new URL((followee.account as IRemoteAccount).inbox);
 
 					const req = request({
 						protocol,

From da22a11340424e0172f8fe34b295ff37c71111a1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 2 Apr 2018 04:01:34 +0900
Subject: [PATCH 0997/1250] Refactor

---
 .../remote/activitypub/renderer/follow.ts     |  6 +++---
 src/common/remote/activitypub/renderer/key.ts | 10 +++++-----
 src/common/user/get-summary.ts                |  6 +++---
 src/models/user.ts                            | 20 ++++++++++++-------
 src/processor/http/follow.ts                  | 10 +++++-----
 src/server/activitypub/inbox.ts               |  7 ++++---
 src/server/api/bot/core.ts                    | 10 +++++-----
 src/server/api/endpoints/posts/create.ts      | 10 +++++-----
 src/server/api/private/signin.ts              |  8 ++++----
 9 files changed, 47 insertions(+), 40 deletions(-)

diff --git a/src/common/remote/activitypub/renderer/follow.ts b/src/common/remote/activitypub/renderer/follow.ts
index 05c0ecca0..86a3f8ced 100644
--- a/src/common/remote/activitypub/renderer/follow.ts
+++ b/src/common/remote/activitypub/renderer/follow.ts
@@ -1,8 +1,8 @@
 import config from '../../../../conf';
-import { IRemoteAccount } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/user';
 
-export default ({ username }, { account }) => ({
+export default ({ username }, followee: IRemoteUser) => ({
 	type: 'Follow',
 	actor: `${config.url}/@${username}`,
-	object: (account as IRemoteAccount).uri
+	object: followee.account.uri
 });
diff --git a/src/common/remote/activitypub/renderer/key.ts b/src/common/remote/activitypub/renderer/key.ts
index 692c71f88..3cac86b76 100644
--- a/src/common/remote/activitypub/renderer/key.ts
+++ b/src/common/remote/activitypub/renderer/key.ts
@@ -1,10 +1,10 @@
 import config from '../../../../conf';
 import { extractPublic } from '../../../../crypto_key';
-import { ILocalAccount } from '../../../../models/user';
+import { ILocalUser } from '../../../../models/user';
 
-export default ({ username, account }) => ({
-	id: `${config.url}/@${username}/publickey`,
+export default (user: ILocalUser) => ({
+	id: `${config.url}/@${user.username}/publickey`,
 	type: 'Key',
-	owner: `${config.url}/@${username}`,
-	publicKeyPem: extractPublic((account as ILocalAccount).keypair)
+	owner: `${config.url}/@${user.username}`,
+	publicKeyPem: extractPublic(user.account.keypair)
 });
diff --git a/src/common/user/get-summary.ts b/src/common/user/get-summary.ts
index 47592c86b..2c71d3eae 100644
--- a/src/common/user/get-summary.ts
+++ b/src/common/user/get-summary.ts
@@ -1,4 +1,4 @@
-import { ILocalAccount, IUser } from '../../models/user';
+import { IUser, isLocalUser } from '../../models/user';
 import getAcct from './get-acct';
 
 /**
@@ -9,8 +9,8 @@ export default function(user: IUser): string {
 	let string = `${user.name} (@${getAcct(user)})\n` +
 		`${user.postsCount}投稿、${user.followingCount}フォロー、${user.followersCount}フォロワー\n`;
 
-	if (user.host === null) {
-		const account = user.account as ILocalAccount;
+	if (isLocalUser(user)) {
+		const account = user.account;
 		string += `場所: ${account.profile.location}、誕生日: ${account.profile.birthday}\n`;
 	}
 
diff --git a/src/models/user.ts b/src/models/user.ts
index d9ac72b88..789b28b2f 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -39,7 +39,7 @@ export function isValidBirthday(birthday: string): boolean {
 	return typeof birthday == 'string' && /^([0-9]{4})\-([0-9]{2})-([0-9]{2})$/.test(birthday);
 }
 
-export type ILocalAccount = {
+type ILocalAccount = {
 	keypair: string;
 	email: string;
 	links: string[];
@@ -69,7 +69,7 @@ export type ILocalAccount = {
 	settings: any;
 };
 
-export type IRemoteAccount = {
+type IRemoteAccount = {
 	inbox: string;
 	uri: string;
 	publicKey: {
@@ -78,7 +78,7 @@ export type IRemoteAccount = {
 	};
 };
 
-export type IUser = {
+type IUserBase = {
 	_id: mongo.ObjectID;
 	createdAt: Date;
 	deletedAt: Date;
@@ -97,13 +97,19 @@ export type IUser = {
 	pinnedPostId: mongo.ObjectID;
 	isSuspended: boolean;
 	keywords: string[];
-	host: string;
 	hostLower: string;
-	account: ILocalAccount | IRemoteAccount;
 };
 
-export type ILocalUser = IUser & { account: ILocalAccount };
-export type IRemoteUser = IUser & { account: IRemoteAccount };
+export type IUser = ILocalUser | IRemoteUser;
+
+export interface ILocalUser extends IUserBase { host: null; account: ILocalAccount; }
+export interface IRemoteUser extends IUserBase { host: string; account: IRemoteAccount; }
+
+export const isLocalUser = (user: any): user is ILocalUser =>
+	user.host === null;
+
+export const isRemoteUser = (user: any): user is IRemoteUser =>
+	!isLocalUser(user);
 
 export function init(user): IUser {
 	user._id = new mongo.ObjectID(user._id);
diff --git a/src/processor/http/follow.ts b/src/processor/http/follow.ts
index a8f7ba78c..9b8337f2e 100644
--- a/src/processor/http/follow.ts
+++ b/src/processor/http/follow.ts
@@ -1,7 +1,7 @@
 import { request } from 'https';
 import { sign } from 'http-signature';
 import { URL } from 'url';
-import User, { ILocalAccount, IRemoteAccount, pack as packUser } from '../../models/user';
+import User, { isLocalUser, pack as packUser, ILocalUser } from '../../models/user';
 import Following from '../../models/following';
 import event from '../../common/event';
 import notify from '../../common/notify';
@@ -10,7 +10,7 @@ import render from '../../common/remote/activitypub/renderer/follow';
 import config from '../../conf';
 
 export default ({ data }, done) => Following.findOne({ _id: data.following }).then(({ followerId, followeeId }) => {
-	const promisedFollower = User.findOne({ _id: followerId });
+	const promisedFollower: Promise<ILocalUser> = User.findOne({ _id: followerId });
 	const promisedFollowee = User.findOne({ _id: followeeId });
 
 	return Promise.all([
@@ -38,7 +38,7 @@ export default ({ data }, done) => Following.findOne({ _id: data.following }).th
 				.then(packed => event(follower._id, 'follow', packed));
 			let followeeEvent;
 
-			if (followee.host === null) {
+			if (isLocalUser(followee)) {
 				followeeEvent = packUser(follower, followee)
 					.then(packed => event(followee._id, 'followed', packed));
 			} else {
@@ -49,7 +49,7 @@ export default ({ data }, done) => Following.findOne({ _id: data.following }).th
 						port,
 						pathname,
 						search
-					} = new URL((followee.account as IRemoteAccount).inbox);
+					} = new URL(followee.account.inbox);
 
 					const req = request({
 						protocol,
@@ -72,7 +72,7 @@ export default ({ data }, done) => Following.findOne({ _id: data.following }).th
 
 					sign(req, {
 						authorizationHeaderName: 'Signature',
-						key: (follower.account as ILocalAccount).keypair,
+						key: follower.account.keypair,
 						keyId: `acct:${follower.username}@${config.host}`
 					});
 
diff --git a/src/server/activitypub/inbox.ts b/src/server/activitypub/inbox.ts
index 6d092e66b..cb679dbf0 100644
--- a/src/server/activitypub/inbox.ts
+++ b/src/server/activitypub/inbox.ts
@@ -1,8 +1,9 @@
 import * as bodyParser from 'body-parser';
 import * as express from 'express';
 import { parseRequest, verifySignature } from 'http-signature';
-import User, { IRemoteAccount } from '../../models/user';
+import User, { IRemoteUser } from '../../models/user';
 import queue from '../../queue';
+import parseAcct from '../../common/user/parse-acct';
 
 const app = express();
 app.disable('x-powered-by');
@@ -36,13 +37,13 @@ app.post('/@:user/inbox', async (req, res) => {
 		};
 	}
 
-	const user = await User.findOne(query);
+	const user = await User.findOne(query) as IRemoteUser;
 
 	if (user === null) {
 		return res.sendStatus(401);
 	}
 
-	if (!verifySignature(parsed, (user.account as IRemoteAccount).publicKey.publicKeyPem)) {
+	if (!verifySignature(parsed, user.account.publicKey.publicKeyPem)) {
 		return res.sendStatus(401);
 	}
 
diff --git a/src/server/api/bot/core.ts b/src/server/api/bot/core.ts
index f84f1f5dc..d636cc26e 100644
--- a/src/server/api/bot/core.ts
+++ b/src/server/api/bot/core.ts
@@ -1,7 +1,7 @@
 import * as EventEmitter from 'events';
 import * as bcrypt from 'bcryptjs';
 
-import User, { ILocalAccount, IUser, init as initUser } from '../../../models/user';
+import User, { IUser, init as initUser, ILocalUser } from '../../../models/user';
 
 import getPostSummary from '../../../common/get-post-summary';
 import getUserSummary from '../../../common/user/get-summary';
@@ -198,7 +198,7 @@ abstract class Context extends EventEmitter {
 }
 
 class SigninContext extends Context {
-	private temporaryUser: IUser = null;
+	private temporaryUser: ILocalUser = null;
 
 	public async greet(): Promise<string> {
 		return 'まずユーザー名を教えてください:';
@@ -207,14 +207,14 @@ class SigninContext extends Context {
 	public async q(query: string): Promise<string> {
 		if (this.temporaryUser == null) {
 			// Fetch user
-			const user: IUser = await User.findOne({
+			const user = await User.findOne({
 				usernameLower: query.toLowerCase(),
 				host: null
 			}, {
 				fields: {
 					data: false
 				}
-			});
+			}) as ILocalUser;
 
 			if (user === null) {
 				return `${query}というユーザーは存在しませんでした... もう一度教えてください:`;
@@ -225,7 +225,7 @@ class SigninContext extends Context {
 			}
 		} else {
 			// Compare password
-			const same = await bcrypt.compare(query, (this.temporaryUser.account as ILocalAccount).password);
+			const same = await bcrypt.compare(query, this.temporaryUser.account.password);
 
 			if (same) {
 				this.bot.signin(this.temporaryUser);
diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts
index 6e7d2329a..4de917694 100644
--- a/src/server/api/endpoints/posts/create.ts
+++ b/src/server/api/endpoints/posts/create.ts
@@ -5,9 +5,9 @@ import $ from 'cafy';
 import deepEqual = require('deep-equal');
 import html from '../../../../common/text/html';
 import parse from '../../../../common/text/parse';
-import { default as Post, IPost, isValidText, isValidCw } from '../../../../models/post';
-import { default as User, ILocalAccount, IUser } from '../../../../models/user';
-import { default as Channel, IChannel } from '../../../../models/channel';
+import Post, { IPost, isValidText, isValidCw } from '../../../../models/post';
+import User, { ILocalUser } from '../../../../models/user';
+import Channel, { IChannel } from '../../../../models/channel';
 import Following from '../../../../models/following';
 import Mute from '../../../../models/mute';
 import DriveFile from '../../../../models/drive-file';
@@ -29,7 +29,7 @@ import config from '../../../../conf';
  * @param {any} app
  * @return {Promise<any>}
  */
-module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
+module.exports = (params, user: ILocalUser, app) => new Promise(async (res, rej) => {
 	// Get 'text' parameter
 	const [text, textErr] = $(params.text).optional.string().pipe(isValidText).$;
 	if (textErr) return rej('invalid text');
@@ -400,7 +400,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 			});
 
 		// この投稿をWatchする
-		if ((user.account as ILocalAccount).settings.autoWatch !== false) {
+		if (user.account.settings.autoWatch !== false) {
 			watch(user._id, reply);
 		}
 
diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts
index 4b7064491..4ad5097e5 100644
--- a/src/server/api/private/signin.ts
+++ b/src/server/api/private/signin.ts
@@ -1,7 +1,7 @@
 import * as express from 'express';
 import * as bcrypt from 'bcryptjs';
 import * as speakeasy from 'speakeasy';
-import { default as User, ILocalAccount, IUser } from '../../../models/user';
+import User, { ILocalUser } from '../../../models/user';
 import Signin, { pack } from '../../../models/signin';
 import event from '../../../common/event';
 import signin from '../common/signin';
@@ -31,7 +31,7 @@ export default async (req: express.Request, res: express.Response) => {
 	}
 
 	// Fetch user
-	const user: IUser = await User.findOne({
+	const user = await User.findOne({
 		usernameLower: username.toLowerCase(),
 		host: null
 	}, {
@@ -39,7 +39,7 @@ export default async (req: express.Request, res: express.Response) => {
 			data: false,
 			'account.profile': false
 		}
-	});
+	}) as ILocalUser;
 
 	if (user === null) {
 		res.status(404).send({
@@ -48,7 +48,7 @@ export default async (req: express.Request, res: express.Response) => {
 		return;
 	}
 
-	const account = user.account as ILocalAccount;
+	const account = user.account;
 
 	// Compare password
 	const same = await bcrypt.compare(password, account.password);

From 16771944fd72dc5fcafd8ee5422c5d11df32d1f5 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sun, 1 Apr 2018 19:10:28 +0000
Subject: [PATCH 0998/1250] fix(package): update jsdom to version 11.7.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index b3db8d677..ce06a6eeb 100644
--- a/package.json
+++ b/package.json
@@ -139,7 +139,7 @@
 		"is-root": "2.0.0",
 		"is-url": "1.2.4",
 		"js-yaml": "3.11.0",
-		"jsdom": "11.6.2",
+		"jsdom": "11.7.0",
 		"kue": "0.11.6",
 		"license-checker": "18.0.0",
 		"loader-utils": "1.1.0",

From 057c1e3c075d183f22a1718308965d12190d462c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 2 Apr 2018 04:15:27 +0900
Subject: [PATCH 0999/1250] Refactor

---
 src/client/app/ch/tags/channel.tag                        | 2 +-
 src/client/app/common/scripts/compose-notification.ts     | 4 ++--
 src/client/app/common/views/components/autocomplete.vue   | 2 +-
 .../common/views/components/messaging-room.message.vue    | 2 +-
 src/client/app/common/views/components/messaging.vue      | 2 +-
 src/client/app/common/views/components/othello.game.vue   | 2 +-
 src/client/app/common/views/components/othello.room.vue   | 2 +-
 src/client/app/common/views/components/post-html.ts       | 2 +-
 .../app/common/views/components/welcome-timeline.vue      | 2 +-
 src/client/app/desktop/views/components/friends-maker.vue | 2 +-
 .../desktop/views/components/messaging-room-window.vue    | 2 +-
 src/client/app/desktop/views/components/notifications.vue | 4 ++--
 .../app/desktop/views/components/post-detail.sub.vue      | 2 +-
 src/client/app/desktop/views/components/post-detail.vue   | 2 +-
 src/client/app/desktop/views/components/post-preview.vue  | 2 +-
 .../app/desktop/views/components/posts.post.sub.vue       | 2 +-
 src/client/app/desktop/views/components/posts.post.vue    | 2 +-
 src/client/app/desktop/views/components/settings.mute.vue | 2 +-
 src/client/app/desktop/views/components/user-preview.vue  | 4 ++--
 .../app/desktop/views/components/users-list.item.vue      | 2 +-
 src/client/app/desktop/views/pages/home.vue               | 2 +-
 src/client/app/desktop/views/pages/messaging-room.vue     | 2 +-
 .../desktop/views/pages/user/user.followers-you-know.vue  | 2 +-
 src/client/app/desktop/views/pages/user/user.friends.vue  | 2 +-
 src/client/app/desktop/views/pages/user/user.header.vue   | 2 +-
 src/client/app/desktop/views/pages/user/user.vue          | 2 +-
 src/client/app/desktop/views/pages/welcome.vue            | 2 +-
 .../app/desktop/views/widgets/channel.channel.post.vue    | 2 +-
 src/client/app/desktop/views/widgets/polls.vue            | 2 +-
 src/client/app/desktop/views/widgets/trends.vue           | 2 +-
 src/client/app/desktop/views/widgets/users.vue            | 2 +-
 src/client/app/mobile/api/post.ts                         | 2 +-
 .../app/mobile/views/components/notification-preview.vue  | 2 +-
 src/client/app/mobile/views/components/notification.vue   | 4 ++--
 src/client/app/mobile/views/components/post-card.vue      | 4 ++--
 .../app/mobile/views/components/post-detail.sub.vue       | 2 +-
 src/client/app/mobile/views/components/post-detail.vue    | 2 +-
 src/client/app/mobile/views/components/post-preview.vue   | 2 +-
 src/client/app/mobile/views/components/post.sub.vue       | 2 +-
 src/client/app/mobile/views/components/post.vue           | 2 +-
 src/client/app/mobile/views/components/user-card.vue      | 2 +-
 src/client/app/mobile/views/components/user-preview.vue   | 2 +-
 src/client/app/mobile/views/pages/followers.vue           | 2 +-
 src/client/app/mobile/views/pages/following.vue           | 2 +-
 src/client/app/mobile/views/pages/home.vue                | 2 +-
 src/client/app/mobile/views/pages/messaging-room.vue      | 2 +-
 src/client/app/mobile/views/pages/messaging.vue           | 2 +-
 src/client/app/mobile/views/pages/user.vue                | 4 ++--
 .../mobile/views/pages/user/home.followers-you-know.vue   | 2 +-
 src/client/app/mobile/views/pages/user/home.photos.vue    | 2 +-
 src/common/drive/add-file.ts                              | 2 +-
 .../drive/{upload_from_url.ts => upload-from-url.ts}      | 0
 src/common/text/parse/elements/mention.ts                 | 2 +-
 src/{common => misc}/get-notification-summary.ts          | 0
 src/{common => misc}/get-post-summary.ts                  | 0
 src/{common => misc}/get-reaction-emoji.ts                | 0
 src/{common => misc}/othello/ai/back.ts                   | 0
 src/{common => misc}/othello/ai/front.ts                  | 0
 src/{common => misc}/othello/ai/index.ts                  | 0
 src/{common => misc}/othello/core.ts                      | 0
 src/{common => misc}/othello/maps.ts                      | 0
 src/{common => misc}/user/get-acct.ts                     | 0
 src/{common => misc}/user/get-summary.ts                  | 0
 src/{common => misc}/user/parse-acct.ts                   | 0
 src/processor/http/follow.ts                              | 4 ++--
 src/processor/http/perform-activitypub.ts                 | 2 +-
 src/{common => }/remote/activitypub/act/create.ts         | 0
 src/{common => }/remote/activitypub/act/index.ts          | 0
 src/{common => }/remote/activitypub/create.ts             | 8 ++++----
 src/{common => }/remote/activitypub/renderer/context.ts   | 0
 src/{common => }/remote/activitypub/renderer/document.ts  | 2 +-
 src/{common => }/remote/activitypub/renderer/follow.ts    | 4 ++--
 src/{common => }/remote/activitypub/renderer/hashtag.ts   | 2 +-
 src/{common => }/remote/activitypub/renderer/image.ts     | 2 +-
 src/{common => }/remote/activitypub/renderer/key.ts       | 6 +++---
 src/{common => }/remote/activitypub/renderer/note.ts      | 8 ++++----
 .../remote/activitypub/renderer/ordered-collection.ts     | 0
 src/{common => }/remote/activitypub/renderer/person.ts    | 2 +-
 src/{common => }/remote/activitypub/resolve-person.ts     | 4 ++--
 src/{common => }/remote/activitypub/resolver.ts           | 2 +-
 src/{common => }/remote/activitypub/type.ts               | 0
 src/{common => }/remote/resolve-user.ts                   | 2 +-
 src/{common => }/remote/webfinger.ts                      | 0
 src/server/activitypub/inbox.ts                           | 2 +-
 src/server/activitypub/outbox.ts                          | 6 +++---
 src/server/activitypub/post.ts                            | 6 +++---
 src/server/activitypub/publickey.ts                       | 4 ++--
 src/server/activitypub/user.ts                            | 4 ++--
 src/server/activitypub/with-user.ts                       | 2 +-
 src/server/api/bot/core.ts                                | 8 ++++----
 src/server/api/bot/interfaces/line.ts                     | 6 +++---
 src/server/api/endpoints/drive/files/upload_from_url.ts   | 2 +-
 src/server/api/endpoints/othello/games/show.ts            | 2 +-
 src/server/api/endpoints/othello/match.ts                 | 2 +-
 src/server/api/endpoints/posts/create.ts                  | 4 ++--
 src/server/api/endpoints/users/show.ts                    | 2 +-
 src/server/api/limitter.ts                                | 2 +-
 src/server/api/stream/othello-game.ts                     | 4 ++--
 src/server/webfinger.ts                                   | 2 +-
 99 files changed, 111 insertions(+), 111 deletions(-)
 rename src/common/drive/{upload_from_url.ts => upload-from-url.ts} (100%)
 rename src/{common => misc}/get-notification-summary.ts (100%)
 rename src/{common => misc}/get-post-summary.ts (100%)
 rename src/{common => misc}/get-reaction-emoji.ts (100%)
 rename src/{common => misc}/othello/ai/back.ts (100%)
 rename src/{common => misc}/othello/ai/front.ts (100%)
 rename src/{common => misc}/othello/ai/index.ts (100%)
 rename src/{common => misc}/othello/core.ts (100%)
 rename src/{common => misc}/othello/maps.ts (100%)
 rename src/{common => misc}/user/get-acct.ts (100%)
 rename src/{common => misc}/user/get-summary.ts (100%)
 rename src/{common => misc}/user/parse-acct.ts (100%)
 rename src/{common => }/remote/activitypub/act/create.ts (100%)
 rename src/{common => }/remote/activitypub/act/index.ts (100%)
 rename src/{common => }/remote/activitypub/create.ts (90%)
 rename src/{common => }/remote/activitypub/renderer/context.ts (100%)
 rename src/{common => }/remote/activitypub/renderer/document.ts (76%)
 rename src/{common => }/remote/activitypub/renderer/follow.ts (61%)
 rename src/{common => }/remote/activitypub/renderer/hashtag.ts (76%)
 rename src/{common => }/remote/activitypub/renderer/image.ts (69%)
 rename src/{common => }/remote/activitypub/renderer/key.ts (57%)
 rename src/{common => }/remote/activitypub/renderer/note.ts (84%)
 rename src/{common => }/remote/activitypub/renderer/ordered-collection.ts (100%)
 rename src/{common => }/remote/activitypub/renderer/person.ts (92%)
 rename src/{common => }/remote/activitypub/resolve-person.ts (97%)
 rename src/{common => }/remote/activitypub/resolver.ts (96%)
 rename src/{common => }/remote/activitypub/type.ts (100%)
 rename src/{common => }/remote/resolve-user.ts (95%)
 rename src/{common => }/remote/webfinger.ts (100%)

diff --git a/src/client/app/ch/tags/channel.tag b/src/client/app/ch/tags/channel.tag
index 2abfb106a..70e494aed 100644
--- a/src/client/app/ch/tags/channel.tag
+++ b/src/client/app/ch/tags/channel.tag
@@ -229,7 +229,7 @@
 
 	</style>
 	<script lang="typescript">
-		import getAcct from '../../../../common/user/get-acct';
+		import getAcct from '../../../../misc/user/get-acct';
 
 		this.post = this.opts.post;
 		this.form = this.opts.form;
diff --git a/src/client/app/common/scripts/compose-notification.ts b/src/client/app/common/scripts/compose-notification.ts
index 273579cbc..56fbcb94f 100644
--- a/src/client/app/common/scripts/compose-notification.ts
+++ b/src/client/app/common/scripts/compose-notification.ts
@@ -1,5 +1,5 @@
-import getPostSummary from '../../../../common/get-post-summary';
-import getReactionEmoji from '../../../../common/get-reaction-emoji';
+import getPostSummary from '../../../../misc/get-post-summary';
+import getReactionEmoji from '../../../../misc/get-reaction-emoji';
 
 type Notification = {
 	title: string;
diff --git a/src/client/app/common/views/components/autocomplete.vue b/src/client/app/common/views/components/autocomplete.vue
index 79bd2ba02..edba47058 100644
--- a/src/client/app/common/views/components/autocomplete.vue
+++ b/src/client/app/common/views/components/autocomplete.vue
@@ -21,7 +21,7 @@
 import Vue from 'vue';
 import * as emojilib from 'emojilib';
 import contains from '../../../common/scripts/contains';
-import getAcct from '../../../../../common/user/get-acct';
+import getAcct from '../../../../../misc/user/get-acct';
 
 const lib = Object.entries(emojilib.lib).filter((x: any) => {
 	return x[1].category != 'flags';
diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue
index 91af26bff..cad6825f3 100644
--- a/src/client/app/common/views/components/messaging-room.message.vue
+++ b/src/client/app/common/views/components/messaging-room.message.vue
@@ -34,7 +34,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../common/user/get-acct';
+import getAcct from '../../../../../misc/user/get-acct';
 import parse from '../../../../../common/text/parse';
 
 export default Vue.extend({
diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue
index 8317c3738..f6709d02b 100644
--- a/src/client/app/common/views/components/messaging.vue
+++ b/src/client/app/common/views/components/messaging.vue
@@ -51,7 +51,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../common/user/get-acct';
+import getAcct from '../../../../../misc/user/get-acct';
 
 export default Vue.extend({
 	props: {
diff --git a/src/client/app/common/views/components/othello.game.vue b/src/client/app/common/views/components/othello.game.vue
index f08742ad1..aa5798d71 100644
--- a/src/client/app/common/views/components/othello.game.vue
+++ b/src/client/app/common/views/components/othello.game.vue
@@ -43,7 +43,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import * as CRC32 from 'crc-32';
-import Othello, { Color } from '../../../../../common/othello/core';
+import Othello, { Color } from '../../../../../misc/othello/core';
 import { url } from '../../../config';
 
 export default Vue.extend({
diff --git a/src/client/app/common/views/components/othello.room.vue b/src/client/app/common/views/components/othello.room.vue
index a32be6b74..eb1d3a0c6 100644
--- a/src/client/app/common/views/components/othello.room.vue
+++ b/src/client/app/common/views/components/othello.room.vue
@@ -94,7 +94,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import * as maps from '../../../../../common/othello/maps';
+import * as maps from '../../../../../misc/othello/maps';
 
 export default Vue.extend({
 	props: ['game', 'connection'],
diff --git a/src/client/app/common/views/components/post-html.ts b/src/client/app/common/views/components/post-html.ts
index c5c3b7275..5670ce036 100644
--- a/src/client/app/common/views/components/post-html.ts
+++ b/src/client/app/common/views/components/post-html.ts
@@ -1,7 +1,7 @@
 import Vue from 'vue';
 import * as emojilib from 'emojilib';
 import parse from '../../../../../common/text/parse';
-import getAcct from '../../../../../common/user/get-acct';
+import getAcct from '../../../../../misc/user/get-acct';
 import { url } from '../../../config';
 import MkUrl from './url.vue';
 
diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue
index 09b090bdc..036a77b1c 100644
--- a/src/client/app/common/views/components/welcome-timeline.vue
+++ b/src/client/app/common/views/components/welcome-timeline.vue
@@ -24,7 +24,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../common/user/get-acct';
+import getAcct from '../../../../../misc/user/get-acct';
 
 export default Vue.extend({
 	data() {
diff --git a/src/client/app/desktop/views/components/friends-maker.vue b/src/client/app/desktop/views/components/friends-maker.vue
index fd9914b15..68ecc6ad4 100644
--- a/src/client/app/desktop/views/components/friends-maker.vue
+++ b/src/client/app/desktop/views/components/friends-maker.vue
@@ -22,7 +22,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../common/user/get-acct';
+import getAcct from '../../../../../misc/user/get-acct';
 
 export default Vue.extend({
 	data() {
diff --git a/src/client/app/desktop/views/components/messaging-room-window.vue b/src/client/app/desktop/views/components/messaging-room-window.vue
index 373526781..610642204 100644
--- a/src/client/app/desktop/views/components/messaging-room-window.vue
+++ b/src/client/app/desktop/views/components/messaging-room-window.vue
@@ -8,7 +8,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import { url } from '../../../config';
-import getAcct from '../../../../../common/user/get-acct';
+import getAcct from '../../../../../misc/user/get-acct';
 
 export default Vue.extend({
 	props: ['user'],
diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
index 5e6db08c1..79b16d048 100644
--- a/src/client/app/desktop/views/components/notifications.vue
+++ b/src/client/app/desktop/views/components/notifications.vue
@@ -102,8 +102,8 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../common/user/get-acct';
-import getPostSummary from '../../../../../common/get-post-summary';
+import getAcct from '../../../../../misc/user/get-acct';
+import getPostSummary from '../../../../../misc/get-post-summary';
 
 export default Vue.extend({
 	data() {
diff --git a/src/client/app/desktop/views/components/post-detail.sub.vue b/src/client/app/desktop/views/components/post-detail.sub.vue
index 1d5649cf9..6fc569ada 100644
--- a/src/client/app/desktop/views/components/post-detail.sub.vue
+++ b/src/client/app/desktop/views/components/post-detail.sub.vue
@@ -28,7 +28,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
-import getAcct from '../../../../../common/user/get-acct';
+import getAcct from '../../../../../misc/user/get-acct';
 
 export default Vue.extend({
 	props: ['post'],
diff --git a/src/client/app/desktop/views/components/post-detail.vue b/src/client/app/desktop/views/components/post-detail.vue
index d6481e13d..309c88e70 100644
--- a/src/client/app/desktop/views/components/post-detail.vue
+++ b/src/client/app/desktop/views/components/post-detail.vue
@@ -78,7 +78,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
-import getAcct from '../../../../../common/user/get-acct';
+import getAcct from '../../../../../misc/user/get-acct';
 import parse from '../../../../../common/text/parse';
 
 import MkPostFormWindow from './post-form-window.vue';
diff --git a/src/client/app/desktop/views/components/post-preview.vue b/src/client/app/desktop/views/components/post-preview.vue
index 0ac3223be..eb62e4a63 100644
--- a/src/client/app/desktop/views/components/post-preview.vue
+++ b/src/client/app/desktop/views/components/post-preview.vue
@@ -21,7 +21,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
-import getAcct from '../../../../../common/user/get-acct';
+import getAcct from '../../../../../misc/user/get-acct';
 
 export default Vue.extend({
 	props: ['post'],
diff --git a/src/client/app/desktop/views/components/posts.post.sub.vue b/src/client/app/desktop/views/components/posts.post.sub.vue
index 65d3017d3..d3af0fef6 100644
--- a/src/client/app/desktop/views/components/posts.post.sub.vue
+++ b/src/client/app/desktop/views/components/posts.post.sub.vue
@@ -21,7 +21,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
-import getAcct from '../../../../../common/user/get-acct';
+import getAcct from '../../../../../misc/user/get-acct';
 
 export default Vue.extend({
 	props: ['post'],
diff --git a/src/client/app/desktop/views/components/posts.post.vue b/src/client/app/desktop/views/components/posts.post.vue
index c31e28d67..1325e3e4f 100644
--- a/src/client/app/desktop/views/components/posts.post.vue
+++ b/src/client/app/desktop/views/components/posts.post.vue
@@ -85,7 +85,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
-import getAcct from '../../../../../common/user/get-acct';
+import getAcct from '../../../../../misc/user/get-acct';
 import parse from '../../../../../common/text/parse';
 
 import MkPostFormWindow from './post-form-window.vue';
diff --git a/src/client/app/desktop/views/components/settings.mute.vue b/src/client/app/desktop/views/components/settings.mute.vue
index a8dfe1060..ce8f4b273 100644
--- a/src/client/app/desktop/views/components/settings.mute.vue
+++ b/src/client/app/desktop/views/components/settings.mute.vue
@@ -13,7 +13,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../common/user/get-acct';
+import getAcct from '../../../../../misc/user/get-acct';
 
 export default Vue.extend({
 	data() {
diff --git a/src/client/app/desktop/views/components/user-preview.vue b/src/client/app/desktop/views/components/user-preview.vue
index 8c86b2efe..595926a81 100644
--- a/src/client/app/desktop/views/components/user-preview.vue
+++ b/src/client/app/desktop/views/components/user-preview.vue
@@ -29,8 +29,8 @@
 <script lang="ts">
 import Vue from 'vue';
 import * as anime from 'animejs';
-import getAcct from '../../../../../common/user/get-acct';
-import parseAcct from '../../../../../common/user/parse-acct';
+import getAcct from '../../../../../misc/user/get-acct';
+import parseAcct from '../../../../../misc/user/parse-acct';
 
 export default Vue.extend({
 	props: {
diff --git a/src/client/app/desktop/views/components/users-list.item.vue b/src/client/app/desktop/views/components/users-list.item.vue
index d2bfc117d..1c40c247b 100644
--- a/src/client/app/desktop/views/components/users-list.item.vue
+++ b/src/client/app/desktop/views/components/users-list.item.vue
@@ -19,7 +19,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../common/user/get-acct';
+import getAcct from '../../../../../misc/user/get-acct';
 
 export default Vue.extend({
 	props: ['user'],
diff --git a/src/client/app/desktop/views/pages/home.vue b/src/client/app/desktop/views/pages/home.vue
index 69e134f79..fa7c19510 100644
--- a/src/client/app/desktop/views/pages/home.vue
+++ b/src/client/app/desktop/views/pages/home.vue
@@ -7,7 +7,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
-import getPostSummary from '../../../../../common/get-post-summary';
+import getPostSummary from '../../../../../misc/get-post-summary';
 
 export default Vue.extend({
 	props: {
diff --git a/src/client/app/desktop/views/pages/messaging-room.vue b/src/client/app/desktop/views/pages/messaging-room.vue
index 0cab1e0d1..244b0c904 100644
--- a/src/client/app/desktop/views/pages/messaging-room.vue
+++ b/src/client/app/desktop/views/pages/messaging-room.vue
@@ -7,7 +7,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
-import parseAcct from '../../../../../common/user/parse-acct';
+import parseAcct from '../../../../../misc/user/parse-acct';
 
 export default Vue.extend({
 	data() {
diff --git a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
index d0dab6c3d..e898b62a0 100644
--- a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
+++ b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
@@ -13,7 +13,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../../common/user/get-acct';
+import getAcct from '../../../../../../misc/user/get-acct';
 
 export default Vue.extend({
 	props: ['user'],
diff --git a/src/client/app/desktop/views/pages/user/user.friends.vue b/src/client/app/desktop/views/pages/user/user.friends.vue
index 3ec30fb43..b13eb528a 100644
--- a/src/client/app/desktop/views/pages/user/user.friends.vue
+++ b/src/client/app/desktop/views/pages/user/user.friends.vue
@@ -20,7 +20,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../../common/user/get-acct';
+import getAcct from '../../../../../../misc/user/get-acct';
 
 export default Vue.extend({
 	props: ['user'],
diff --git a/src/client/app/desktop/views/pages/user/user.header.vue b/src/client/app/desktop/views/pages/user/user.header.vue
index 54f431fd2..32e425f86 100644
--- a/src/client/app/desktop/views/pages/user/user.header.vue
+++ b/src/client/app/desktop/views/pages/user/user.header.vue
@@ -22,7 +22,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../../common/user/get-acct';
+import getAcct from '../../../../../../misc/user/get-acct';
 
 export default Vue.extend({
 	props: ['user'],
diff --git a/src/client/app/desktop/views/pages/user/user.vue b/src/client/app/desktop/views/pages/user/user.vue
index 67cef9326..6e68171a6 100644
--- a/src/client/app/desktop/views/pages/user/user.vue
+++ b/src/client/app/desktop/views/pages/user/user.vue
@@ -9,7 +9,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import parseAcct from '../../../../../../common/user/parse-acct';
+import parseAcct from '../../../../../../misc/user/parse-acct';
 import Progress from '../../../../common/scripts/loading';
 import XHeader from './user.header.vue';
 import XHome from './user.home.vue';
diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue
index 34c28854b..93bb59765 100644
--- a/src/client/app/desktop/views/pages/welcome.vue
+++ b/src/client/app/desktop/views/pages/welcome.vue
@@ -43,7 +43,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import { docsUrl, copyright, lang } from '../../../config';
-import getAcct from '../../../../../common/user/get-acct';
+import getAcct from '../../../../../misc/user/get-acct';
 
 const shares = [
 	'Everything!',
diff --git a/src/client/app/desktop/views/widgets/channel.channel.post.vue b/src/client/app/desktop/views/widgets/channel.channel.post.vue
index 433f9a00a..255d9a5e6 100644
--- a/src/client/app/desktop/views/widgets/channel.channel.post.vue
+++ b/src/client/app/desktop/views/widgets/channel.channel.post.vue
@@ -19,7 +19,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../common/user/get-acct';
+import getAcct from '../../../../../misc/user/get-acct';
 
 export default Vue.extend({
 	props: ['post'],
diff --git a/src/client/app/desktop/views/widgets/polls.vue b/src/client/app/desktop/views/widgets/polls.vue
index e5db34fc7..fd242a04b 100644
--- a/src/client/app/desktop/views/widgets/polls.vue
+++ b/src/client/app/desktop/views/widgets/polls.vue
@@ -16,7 +16,7 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
-import getAcct from '../../../../../common/user/get-acct';
+import getAcct from '../../../../../misc/user/get-acct';
 
 export default define({
 	name: 'polls',
diff --git a/src/client/app/desktop/views/widgets/trends.vue b/src/client/app/desktop/views/widgets/trends.vue
index 77779787e..6b22c123b 100644
--- a/src/client/app/desktop/views/widgets/trends.vue
+++ b/src/client/app/desktop/views/widgets/trends.vue
@@ -15,7 +15,7 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
-import getAcct from '../../../../../common/user/get-acct';
+import getAcct from '../../../../../misc/user/get-acct';
 
 export default define({
 	name: 'trends',
diff --git a/src/client/app/desktop/views/widgets/users.vue b/src/client/app/desktop/views/widgets/users.vue
index 7b8944112..102739bd0 100644
--- a/src/client/app/desktop/views/widgets/users.vue
+++ b/src/client/app/desktop/views/widgets/users.vue
@@ -23,7 +23,7 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
-import getAcct from '../../../../../common/user/get-acct';
+import getAcct from '../../../../../misc/user/get-acct';
 
 const limit = 3;
 
diff --git a/src/client/app/mobile/api/post.ts b/src/client/app/mobile/api/post.ts
index 841103fee..8090bbef2 100644
--- a/src/client/app/mobile/api/post.ts
+++ b/src/client/app/mobile/api/post.ts
@@ -1,6 +1,6 @@
 import PostForm from '../views/components/post-form.vue';
 //import RepostForm from '../views/components/repost-form.vue';
-import getPostSummary from '../../../../common/get-post-summary';
+import getPostSummary from '../../../../misc/get-post-summary';
 
 export default (os) => (opts) => {
 	const o = opts || {};
diff --git a/src/client/app/mobile/views/components/notification-preview.vue b/src/client/app/mobile/views/components/notification-preview.vue
index fce9ed82f..2a06d399c 100644
--- a/src/client/app/mobile/views/components/notification-preview.vue
+++ b/src/client/app/mobile/views/components/notification-preview.vue
@@ -59,7 +59,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getPostSummary from '../../../../../common/get-post-summary';
+import getPostSummary from '../../../../../misc/get-post-summary';
 
 export default Vue.extend({
 	props: ['notification'],
diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue
index e221fb3ac..93b1e8416 100644
--- a/src/client/app/mobile/views/components/notification.vue
+++ b/src/client/app/mobile/views/components/notification.vue
@@ -78,8 +78,8 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getPostSummary from '../../../../../common/get-post-summary';
-import getAcct from '../../../../../common/user/get-acct';
+import getPostSummary from '../../../../../misc/get-post-summary';
+import getAcct from '../../../../../misc/user/get-acct';
 
 export default Vue.extend({
 	props: ['notification'],
diff --git a/src/client/app/mobile/views/components/post-card.vue b/src/client/app/mobile/views/components/post-card.vue
index 10dfd9241..3f5beb9e0 100644
--- a/src/client/app/mobile/views/components/post-card.vue
+++ b/src/client/app/mobile/views/components/post-card.vue
@@ -14,8 +14,8 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import summary from '../../../../../common/get-post-summary';
-import getAcct from '../../../../../common/user/get-acct';
+import summary from '../../../../../misc/get-post-summary';
+import getAcct from '../../../../../misc/user/get-acct';
 
 export default Vue.extend({
 	props: ['post'],
diff --git a/src/client/app/mobile/views/components/post-detail.sub.vue b/src/client/app/mobile/views/components/post-detail.sub.vue
index db7567834..1fca0881d 100644
--- a/src/client/app/mobile/views/components/post-detail.sub.vue
+++ b/src/client/app/mobile/views/components/post-detail.sub.vue
@@ -20,7 +20,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../common/user/get-acct';
+import getAcct from '../../../../../misc/user/get-acct';
 
 export default Vue.extend({
 	props: ['post'],
diff --git a/src/client/app/mobile/views/components/post-detail.vue b/src/client/app/mobile/views/components/post-detail.vue
index 0a4e36fc6..a8ebb3b01 100644
--- a/src/client/app/mobile/views/components/post-detail.vue
+++ b/src/client/app/mobile/views/components/post-detail.vue
@@ -80,7 +80,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../common/user/get-acct';
+import getAcct from '../../../../../misc/user/get-acct';
 import parse from '../../../../../common/text/parse';
 
 import MkPostMenu from '../../../common/views/components/post-menu.vue';
diff --git a/src/client/app/mobile/views/components/post-preview.vue b/src/client/app/mobile/views/components/post-preview.vue
index a6141dc8e..05dd4a004 100644
--- a/src/client/app/mobile/views/components/post-preview.vue
+++ b/src/client/app/mobile/views/components/post-preview.vue
@@ -20,7 +20,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../common/user/get-acct';
+import getAcct from '../../../../../misc/user/get-acct';
 
 export default Vue.extend({
 	props: ['post'],
diff --git a/src/client/app/mobile/views/components/post.sub.vue b/src/client/app/mobile/views/components/post.sub.vue
index adf444a2d..cbd2fad88 100644
--- a/src/client/app/mobile/views/components/post.sub.vue
+++ b/src/client/app/mobile/views/components/post.sub.vue
@@ -20,7 +20,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../common/user/get-acct';
+import getAcct from '../../../../../misc/user/get-acct';
 
 export default Vue.extend({
 	props: ['post'],
diff --git a/src/client/app/mobile/views/components/post.vue b/src/client/app/mobile/views/components/post.vue
index f4f845b49..6cf24ed6a 100644
--- a/src/client/app/mobile/views/components/post.vue
+++ b/src/client/app/mobile/views/components/post.vue
@@ -77,7 +77,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../common/user/get-acct';
+import getAcct from '../../../../../misc/user/get-acct';
 import parse from '../../../../../common/text/parse';
 
 import MkPostMenu from '../../../common/views/components/post-menu.vue';
diff --git a/src/client/app/mobile/views/components/user-card.vue b/src/client/app/mobile/views/components/user-card.vue
index ffa110051..fb1b12ce5 100644
--- a/src/client/app/mobile/views/components/user-card.vue
+++ b/src/client/app/mobile/views/components/user-card.vue
@@ -13,7 +13,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../common/user/get-acct';
+import getAcct from '../../../../../misc/user/get-acct';
 
 export default Vue.extend({
 	props: ['user'],
diff --git a/src/client/app/mobile/views/components/user-preview.vue b/src/client/app/mobile/views/components/user-preview.vue
index e51e4353d..4bb830381 100644
--- a/src/client/app/mobile/views/components/user-preview.vue
+++ b/src/client/app/mobile/views/components/user-preview.vue
@@ -17,7 +17,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../common/user/get-acct';
+import getAcct from '../../../../../misc/user/get-acct';
 
 export default Vue.extend({
 	props: ['user'],
diff --git a/src/client/app/mobile/views/pages/followers.vue b/src/client/app/mobile/views/pages/followers.vue
index 8c058eb4e..28e866efb 100644
--- a/src/client/app/mobile/views/pages/followers.vue
+++ b/src/client/app/mobile/views/pages/followers.vue
@@ -19,7 +19,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
-import parseAcct from '../../../../../common/user/parse-acct';
+import parseAcct from '../../../../../misc/user/parse-acct';
 
 export default Vue.extend({
 	data() {
diff --git a/src/client/app/mobile/views/pages/following.vue b/src/client/app/mobile/views/pages/following.vue
index a73c9d171..7ee3b8628 100644
--- a/src/client/app/mobile/views/pages/following.vue
+++ b/src/client/app/mobile/views/pages/following.vue
@@ -19,7 +19,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
-import parseAcct from '../../../../../common/user/parse-acct';
+import parseAcct from '../../../../../misc/user/parse-acct';
 
 export default Vue.extend({
 	data() {
diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue
index f1f65f90c..f76fdbe19 100644
--- a/src/client/app/mobile/views/pages/home.vue
+++ b/src/client/app/mobile/views/pages/home.vue
@@ -64,7 +64,7 @@ import Vue from 'vue';
 import * as XDraggable from 'vuedraggable';
 import * as uuid from 'uuid';
 import Progress from '../../../common/scripts/loading';
-import getPostSummary from '../../../../../common/get-post-summary';
+import getPostSummary from '../../../../../misc/get-post-summary';
 
 export default Vue.extend({
 	components: {
diff --git a/src/client/app/mobile/views/pages/messaging-room.vue b/src/client/app/mobile/views/pages/messaging-room.vue
index 193c41179..400f5016e 100644
--- a/src/client/app/mobile/views/pages/messaging-room.vue
+++ b/src/client/app/mobile/views/pages/messaging-room.vue
@@ -10,7 +10,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import parseAcct from '../../../../../common/user/parse-acct';
+import parseAcct from '../../../../../misc/user/parse-acct';
 
 export default Vue.extend({
 	data() {
diff --git a/src/client/app/mobile/views/pages/messaging.vue b/src/client/app/mobile/views/pages/messaging.vue
index e92068eda..7ded67cdf 100644
--- a/src/client/app/mobile/views/pages/messaging.vue
+++ b/src/client/app/mobile/views/pages/messaging.vue
@@ -7,7 +7,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../common/user/get-acct';
+import getAcct from '../../../../../misc/user/get-acct';
 
 export default Vue.extend({
 	mounted() {
diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue
index 114decb8e..01eb408e6 100644
--- a/src/client/app/mobile/views/pages/user.vue
+++ b/src/client/app/mobile/views/pages/user.vue
@@ -60,8 +60,8 @@
 <script lang="ts">
 import Vue from 'vue';
 import * as age from 's-age';
-import getAcct from '../../../../../common/user/get-acct';
-import getAcct from '../../../../../common/user/parse-acct';
+import getAcct from '../../../../../misc/user/get-acct';
+import getAcct from '../../../../../misc/user/parse-acct';
 import Progress from '../../../common/scripts/loading';
 import XHome from './user/home.vue';
 
diff --git a/src/client/app/mobile/views/pages/user/home.followers-you-know.vue b/src/client/app/mobile/views/pages/user/home.followers-you-know.vue
index 8c84d2dbb..8dc4fbb85 100644
--- a/src/client/app/mobile/views/pages/user/home.followers-you-know.vue
+++ b/src/client/app/mobile/views/pages/user/home.followers-you-know.vue
@@ -12,7 +12,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../../common/user/get-acct';
+import getAcct from '../../../../../../misc/user/get-acct';
 
 export default Vue.extend({
 	props: ['user'],
diff --git a/src/client/app/mobile/views/pages/user/home.photos.vue b/src/client/app/mobile/views/pages/user/home.photos.vue
index f703f8a74..cc3527037 100644
--- a/src/client/app/mobile/views/pages/user/home.photos.vue
+++ b/src/client/app/mobile/views/pages/user/home.photos.vue
@@ -14,7 +14,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../../common/user/get-acct';
+import getAcct from '../../../../../../misc/user/get-acct';
 
 export default Vue.extend({
 	props: ['user'],
diff --git a/src/common/drive/add-file.ts b/src/common/drive/add-file.ts
index 52a7713dd..af85a23d2 100644
--- a/src/common/drive/add-file.ts
+++ b/src/common/drive/add-file.ts
@@ -14,7 +14,7 @@ import DriveFile, { getGridFSBucket } from '../../models/drive-file';
 import DriveFolder from '../../models/drive-folder';
 import { pack } from '../../models/drive-file';
 import event, { publishDriveStream } from '../event';
-import getAcct from '../user/get-acct';
+import getAcct from '../../misc/user/get-acct';
 import config from '../../conf';
 
 const gm = _gm.subClass({
diff --git a/src/common/drive/upload_from_url.ts b/src/common/drive/upload-from-url.ts
similarity index 100%
rename from src/common/drive/upload_from_url.ts
rename to src/common/drive/upload-from-url.ts
diff --git a/src/common/text/parse/elements/mention.ts b/src/common/text/parse/elements/mention.ts
index 2025dfdaa..3c81979d0 100644
--- a/src/common/text/parse/elements/mention.ts
+++ b/src/common/text/parse/elements/mention.ts
@@ -1,7 +1,7 @@
 /**
  * Mention
  */
-import parseAcct from '../../../../common/user/parse-acct';
+import parseAcct from '../../../../misc/user/parse-acct';
 
 module.exports = text => {
 	const match = text.match(/^(?:@[a-zA-Z0-9\-]+){1,2}/);
diff --git a/src/common/get-notification-summary.ts b/src/misc/get-notification-summary.ts
similarity index 100%
rename from src/common/get-notification-summary.ts
rename to src/misc/get-notification-summary.ts
diff --git a/src/common/get-post-summary.ts b/src/misc/get-post-summary.ts
similarity index 100%
rename from src/common/get-post-summary.ts
rename to src/misc/get-post-summary.ts
diff --git a/src/common/get-reaction-emoji.ts b/src/misc/get-reaction-emoji.ts
similarity index 100%
rename from src/common/get-reaction-emoji.ts
rename to src/misc/get-reaction-emoji.ts
diff --git a/src/common/othello/ai/back.ts b/src/misc/othello/ai/back.ts
similarity index 100%
rename from src/common/othello/ai/back.ts
rename to src/misc/othello/ai/back.ts
diff --git a/src/common/othello/ai/front.ts b/src/misc/othello/ai/front.ts
similarity index 100%
rename from src/common/othello/ai/front.ts
rename to src/misc/othello/ai/front.ts
diff --git a/src/common/othello/ai/index.ts b/src/misc/othello/ai/index.ts
similarity index 100%
rename from src/common/othello/ai/index.ts
rename to src/misc/othello/ai/index.ts
diff --git a/src/common/othello/core.ts b/src/misc/othello/core.ts
similarity index 100%
rename from src/common/othello/core.ts
rename to src/misc/othello/core.ts
diff --git a/src/common/othello/maps.ts b/src/misc/othello/maps.ts
similarity index 100%
rename from src/common/othello/maps.ts
rename to src/misc/othello/maps.ts
diff --git a/src/common/user/get-acct.ts b/src/misc/user/get-acct.ts
similarity index 100%
rename from src/common/user/get-acct.ts
rename to src/misc/user/get-acct.ts
diff --git a/src/common/user/get-summary.ts b/src/misc/user/get-summary.ts
similarity index 100%
rename from src/common/user/get-summary.ts
rename to src/misc/user/get-summary.ts
diff --git a/src/common/user/parse-acct.ts b/src/misc/user/parse-acct.ts
similarity index 100%
rename from src/common/user/parse-acct.ts
rename to src/misc/user/parse-acct.ts
diff --git a/src/processor/http/follow.ts b/src/processor/http/follow.ts
index 9b8337f2e..4804657fd 100644
--- a/src/processor/http/follow.ts
+++ b/src/processor/http/follow.ts
@@ -5,8 +5,8 @@ import User, { isLocalUser, pack as packUser, ILocalUser } from '../../models/us
 import Following from '../../models/following';
 import event from '../../common/event';
 import notify from '../../common/notify';
-import context from '../../common/remote/activitypub/renderer/context';
-import render from '../../common/remote/activitypub/renderer/follow';
+import context from '../../remote/activitypub/renderer/context';
+import render from '../../remote/activitypub/renderer/follow';
 import config from '../../conf';
 
 export default ({ data }, done) => Following.findOne({ _id: data.following }).then(({ followerId, followeeId }) => {
diff --git a/src/processor/http/perform-activitypub.ts b/src/processor/http/perform-activitypub.ts
index 5b1a02173..51e1ede14 100644
--- a/src/processor/http/perform-activitypub.ts
+++ b/src/processor/http/perform-activitypub.ts
@@ -1,5 +1,5 @@
 import User from '../../models/user';
-import act from '../../common/remote/activitypub/act';
+import act from '../../remote/activitypub/act';
 
 export default ({ data }, done) => User.findOne({ _id: data.actor })
 	.then(actor => act(actor, data.outbox))
diff --git a/src/common/remote/activitypub/act/create.ts b/src/remote/activitypub/act/create.ts
similarity index 100%
rename from src/common/remote/activitypub/act/create.ts
rename to src/remote/activitypub/act/create.ts
diff --git a/src/common/remote/activitypub/act/index.ts b/src/remote/activitypub/act/index.ts
similarity index 100%
rename from src/common/remote/activitypub/act/index.ts
rename to src/remote/activitypub/act/index.ts
diff --git a/src/common/remote/activitypub/create.ts b/src/remote/activitypub/create.ts
similarity index 90%
rename from src/common/remote/activitypub/create.ts
rename to src/remote/activitypub/create.ts
index ea780f01e..e610232f5 100644
--- a/src/common/remote/activitypub/create.ts
+++ b/src/remote/activitypub/create.ts
@@ -1,8 +1,8 @@
 import { JSDOM } from 'jsdom';
-import config from '../../../conf';
-import Post from '../../../models/post';
-import RemoteUserObject, { IRemoteUserObject } from '../../../models/remote-user-object';
-import uploadFromUrl from '../../drive/upload_from_url';
+import config from '../../conf';
+import Post from '../../models/post';
+import RemoteUserObject, { IRemoteUserObject } from '../../models/remote-user-object';
+import uploadFromUrl from '../../common/drive/upload-from-url';
 import Resolver from './resolver';
 const createDOMPurify = require('dompurify');
 
diff --git a/src/common/remote/activitypub/renderer/context.ts b/src/remote/activitypub/renderer/context.ts
similarity index 100%
rename from src/common/remote/activitypub/renderer/context.ts
rename to src/remote/activitypub/renderer/context.ts
diff --git a/src/common/remote/activitypub/renderer/document.ts b/src/remote/activitypub/renderer/document.ts
similarity index 76%
rename from src/common/remote/activitypub/renderer/document.ts
rename to src/remote/activitypub/renderer/document.ts
index 4a456416a..fdd52c1b6 100644
--- a/src/common/remote/activitypub/renderer/document.ts
+++ b/src/remote/activitypub/renderer/document.ts
@@ -1,4 +1,4 @@
-import config from '../../../../conf';
+import config from '../../../conf';
 
 export default ({ _id, contentType }) => ({
 	type: 'Document',
diff --git a/src/common/remote/activitypub/renderer/follow.ts b/src/remote/activitypub/renderer/follow.ts
similarity index 61%
rename from src/common/remote/activitypub/renderer/follow.ts
rename to src/remote/activitypub/renderer/follow.ts
index 86a3f8ced..c99bc375a 100644
--- a/src/common/remote/activitypub/renderer/follow.ts
+++ b/src/remote/activitypub/renderer/follow.ts
@@ -1,5 +1,5 @@
-import config from '../../../../conf';
-import { IRemoteUser } from '../../../../models/user';
+import config from '../../../conf';
+import { IRemoteUser } from '../../../models/user';
 
 export default ({ username }, followee: IRemoteUser) => ({
 	type: 'Follow',
diff --git a/src/common/remote/activitypub/renderer/hashtag.ts b/src/remote/activitypub/renderer/hashtag.ts
similarity index 76%
rename from src/common/remote/activitypub/renderer/hashtag.ts
rename to src/remote/activitypub/renderer/hashtag.ts
index ad4270020..c2d261ed2 100644
--- a/src/common/remote/activitypub/renderer/hashtag.ts
+++ b/src/remote/activitypub/renderer/hashtag.ts
@@ -1,4 +1,4 @@
-import config from '../../../../conf';
+import config from '../../../conf';
 
 export default tag => ({
 	type: 'Hashtag',
diff --git a/src/common/remote/activitypub/renderer/image.ts b/src/remote/activitypub/renderer/image.ts
similarity index 69%
rename from src/common/remote/activitypub/renderer/image.ts
rename to src/remote/activitypub/renderer/image.ts
index 345fbbec5..3d1c71cb9 100644
--- a/src/common/remote/activitypub/renderer/image.ts
+++ b/src/remote/activitypub/renderer/image.ts
@@ -1,4 +1,4 @@
-import config from '../../../../conf';
+import config from '../../../conf';
 
 export default ({ _id }) => ({
 	type: 'Image',
diff --git a/src/common/remote/activitypub/renderer/key.ts b/src/remote/activitypub/renderer/key.ts
similarity index 57%
rename from src/common/remote/activitypub/renderer/key.ts
rename to src/remote/activitypub/renderer/key.ts
index 3cac86b76..904a69e08 100644
--- a/src/common/remote/activitypub/renderer/key.ts
+++ b/src/remote/activitypub/renderer/key.ts
@@ -1,6 +1,6 @@
-import config from '../../../../conf';
-import { extractPublic } from '../../../../crypto_key';
-import { ILocalUser } from '../../../../models/user';
+import config from '../../../conf';
+import { extractPublic } from '../../../crypto_key';
+import { ILocalUser } from '../../../models/user';
 
 export default (user: ILocalUser) => ({
 	id: `${config.url}/@${user.username}/publickey`,
diff --git a/src/common/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts
similarity index 84%
rename from src/common/remote/activitypub/renderer/note.ts
rename to src/remote/activitypub/renderer/note.ts
index 2fe20b213..74806f14b 100644
--- a/src/common/remote/activitypub/renderer/note.ts
+++ b/src/remote/activitypub/renderer/note.ts
@@ -1,9 +1,9 @@
 import renderDocument from './document';
 import renderHashtag from './hashtag';
-import config from '../../../../conf';
-import DriveFile from '../../../../models/drive-file';
-import Post from '../../../../models/post';
-import User from '../../../../models/user';
+import config from '../../../conf';
+import DriveFile from '../../../models/drive-file';
+import Post from '../../../models/post';
+import User from '../../../models/user';
 
 export default async (user, post) => {
 	const promisedFiles = DriveFile.find({ _id: { $in: post.mediaIds } });
diff --git a/src/common/remote/activitypub/renderer/ordered-collection.ts b/src/remote/activitypub/renderer/ordered-collection.ts
similarity index 100%
rename from src/common/remote/activitypub/renderer/ordered-collection.ts
rename to src/remote/activitypub/renderer/ordered-collection.ts
diff --git a/src/common/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts
similarity index 92%
rename from src/common/remote/activitypub/renderer/person.ts
rename to src/remote/activitypub/renderer/person.ts
index 7303b3038..c6c789316 100644
--- a/src/common/remote/activitypub/renderer/person.ts
+++ b/src/remote/activitypub/renderer/person.ts
@@ -1,6 +1,6 @@
 import renderImage from './image';
 import renderKey from './key';
-import config from '../../../../conf';
+import config from '../../../conf';
 
 export default user => {
 	const id = `${config.url}/@${user.username}`;
diff --git a/src/common/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts
similarity index 97%
rename from src/common/remote/activitypub/resolve-person.ts
rename to src/remote/activitypub/resolve-person.ts
index 73584946e..d928e7ce1 100644
--- a/src/common/remote/activitypub/resolve-person.ts
+++ b/src/remote/activitypub/resolve-person.ts
@@ -1,7 +1,7 @@
 import { JSDOM } from 'jsdom';
 import { toUnicode } from 'punycode';
-import User, { validateUsername, isValidName, isValidDescription } from '../../../models/user';
-import queue from '../../../queue';
+import User, { validateUsername, isValidName, isValidDescription } from '../../models/user';
+import queue from '../../queue';
 import webFinger from '../webfinger';
 import create from './create';
 import Resolver from './resolver';
diff --git a/src/common/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts
similarity index 96%
rename from src/common/remote/activitypub/resolver.ts
rename to src/remote/activitypub/resolver.ts
index a167fa133..ebfe25fe7 100644
--- a/src/common/remote/activitypub/resolver.ts
+++ b/src/remote/activitypub/resolver.ts
@@ -1,4 +1,4 @@
-import RemoteUserObject from '../../../models/remote-user-object';
+import RemoteUserObject from '../../models/remote-user-object';
 import { IObject } from './type';
 const request = require('request-promise-native');
 
diff --git a/src/common/remote/activitypub/type.ts b/src/remote/activitypub/type.ts
similarity index 100%
rename from src/common/remote/activitypub/type.ts
rename to src/remote/activitypub/type.ts
diff --git a/src/common/remote/resolve-user.ts b/src/remote/resolve-user.ts
similarity index 95%
rename from src/common/remote/resolve-user.ts
rename to src/remote/resolve-user.ts
index 4959539da..a39309283 100644
--- a/src/common/remote/resolve-user.ts
+++ b/src/remote/resolve-user.ts
@@ -1,5 +1,5 @@
 import { toUnicode, toASCII } from 'punycode';
-import User from '../../models/user';
+import User from '../models/user';
 import resolvePerson from './activitypub/resolve-person';
 import webFinger from './webfinger';
 
diff --git a/src/common/remote/webfinger.ts b/src/remote/webfinger.ts
similarity index 100%
rename from src/common/remote/webfinger.ts
rename to src/remote/webfinger.ts
diff --git a/src/server/activitypub/inbox.ts b/src/server/activitypub/inbox.ts
index cb679dbf0..31a47e276 100644
--- a/src/server/activitypub/inbox.ts
+++ b/src/server/activitypub/inbox.ts
@@ -3,7 +3,7 @@ import * as express from 'express';
 import { parseRequest, verifySignature } from 'http-signature';
 import User, { IRemoteUser } from '../../models/user';
 import queue from '../../queue';
-import parseAcct from '../../common/user/parse-acct';
+import parseAcct from '../../misc/user/parse-acct';
 
 const app = express();
 app.disable('x-powered-by');
diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts
index c26c4df75..2018d8797 100644
--- a/src/server/activitypub/outbox.ts
+++ b/src/server/activitypub/outbox.ts
@@ -1,7 +1,7 @@
 import * as express from 'express';
-import context from '../../common/remote/activitypub/renderer/context';
-import renderNote from '../../common/remote/activitypub/renderer/note';
-import renderOrderedCollection from '../../common/remote/activitypub/renderer/ordered-collection';
+import context from '../../remote/activitypub/renderer/context';
+import renderNote from '../../remote/activitypub/renderer/note';
+import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
 import config from '../../conf';
 import Post from '../../models/post';
 import withUser from './with-user';
diff --git a/src/server/activitypub/post.ts b/src/server/activitypub/post.ts
index 6644563d8..ecbf9671f 100644
--- a/src/server/activitypub/post.ts
+++ b/src/server/activitypub/post.ts
@@ -1,7 +1,7 @@
 import * as express from 'express';
-import context from '../../common/remote/activitypub/renderer/context';
-import render from '../../common/remote/activitypub/renderer/note';
-import parseAcct from '../../common/user/parse-acct';
+import context from '../../remote/activitypub/renderer/context';
+import render from '../../remote/activitypub/renderer/note';
+import parseAcct from '../../misc/user/parse-acct';
 import Post from '../../models/post';
 import User from '../../models/user';
 
diff --git a/src/server/activitypub/publickey.ts b/src/server/activitypub/publickey.ts
index e380309dc..a23d11c15 100644
--- a/src/server/activitypub/publickey.ts
+++ b/src/server/activitypub/publickey.ts
@@ -1,6 +1,6 @@
 import * as express from 'express';
-import context from '../../common/remote/activitypub/renderer/context';
-import render from '../../common/remote/activitypub/renderer/key';
+import context from '../../remote/activitypub/renderer/context';
+import render from '../../remote/activitypub/renderer/key';
 import config from '../../conf';
 import withUser from './with-user';
 
diff --git a/src/server/activitypub/user.ts b/src/server/activitypub/user.ts
index cfda409e2..0d0b84bad 100644
--- a/src/server/activitypub/user.ts
+++ b/src/server/activitypub/user.ts
@@ -1,7 +1,7 @@
 import * as express from 'express';
 import config from '../../conf';
-import context from '../../common/remote/activitypub/renderer/context';
-import render from '../../common/remote/activitypub/renderer/person';
+import context from '../../remote/activitypub/renderer/context';
+import render from '../../remote/activitypub/renderer/person';
 import withUser from './with-user';
 
 const respond = withUser(username => `${config.url}/@${username}`, (user, req, res) => {
diff --git a/src/server/activitypub/with-user.ts b/src/server/activitypub/with-user.ts
index ed289b641..18f2e50ff 100644
--- a/src/server/activitypub/with-user.ts
+++ b/src/server/activitypub/with-user.ts
@@ -1,4 +1,4 @@
-import parseAcct from '../../common/user/parse-acct';
+import parseAcct from '../../misc/user/parse-acct';
 import User from '../../models/user';
 
 export default (redirect, respond) => async (req, res, next) => {
diff --git a/src/server/api/bot/core.ts b/src/server/api/bot/core.ts
index d636cc26e..0faf9b4e4 100644
--- a/src/server/api/bot/core.ts
+++ b/src/server/api/bot/core.ts
@@ -3,10 +3,10 @@ import * as bcrypt from 'bcryptjs';
 
 import User, { IUser, init as initUser, ILocalUser } from '../../../models/user';
 
-import getPostSummary from '../../../common/get-post-summary';
-import getUserSummary from '../../../common/user/get-summary';
-import parseAcct from '../../../common/user/parse-acct';
-import getNotificationSummary from '../../../common/get-notification-summary';
+import getPostSummary from '../../../misc/get-post-summary';
+import getUserSummary from '../../../misc/user/get-summary';
+import parseAcct from '../../../misc/user/parse-acct';
+import getNotificationSummary from '../../../misc/get-notification-summary';
 
 const hmm = [
 	'?',
diff --git a/src/server/api/bot/interfaces/line.ts b/src/server/api/bot/interfaces/line.ts
index 58fbb217b..a6e70a138 100644
--- a/src/server/api/bot/interfaces/line.ts
+++ b/src/server/api/bot/interfaces/line.ts
@@ -7,9 +7,9 @@ import config from '../../../../conf';
 import BotCore from '../core';
 import _redis from '../../../../db/redis';
 import prominence = require('prominence');
-import getAcct from '../../../../common/user/get-acct';
-import parseAcct from '../../../../common/user/parse-acct';
-import getPostSummary from '../../../../common/get-post-summary';
+import getAcct from '../../../../misc/user/get-acct';
+import parseAcct from '../../../../misc/user/parse-acct';
+import getPostSummary from '../../../../misc/get-post-summary';
 
 const redis = prominence(_redis);
 
diff --git a/src/server/api/endpoints/drive/files/upload_from_url.ts b/src/server/api/endpoints/drive/files/upload_from_url.ts
index 01d875055..ceb751a14 100644
--- a/src/server/api/endpoints/drive/files/upload_from_url.ts
+++ b/src/server/api/endpoints/drive/files/upload_from_url.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import { pack } from '../../../../../models/drive-file';
-import uploadFromUrl from '../../../../../common/drive/upload_from_url';
+import uploadFromUrl from '../../../../../common/drive/upload-from-url';
 
 /**
  * Create a file from a URL
diff --git a/src/server/api/endpoints/othello/games/show.ts b/src/server/api/endpoints/othello/games/show.ts
index 0d3b53965..0b1c9bc35 100644
--- a/src/server/api/endpoints/othello/games/show.ts
+++ b/src/server/api/endpoints/othello/games/show.ts
@@ -1,6 +1,6 @@
 import $ from 'cafy';
 import OthelloGame, { pack } from '../../../../../models/othello-game';
-import Othello from '../../../../../common/othello/core';
+import Othello from '../../../../../misc/othello/core';
 
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Get 'gameId' parameter
diff --git a/src/server/api/endpoints/othello/match.ts b/src/server/api/endpoints/othello/match.ts
index 992b93d41..d9c46aae1 100644
--- a/src/server/api/endpoints/othello/match.ts
+++ b/src/server/api/endpoints/othello/match.ts
@@ -3,7 +3,7 @@ import Matching, { pack as packMatching } from '../../../../models/othello-match
 import OthelloGame, { pack as packGame } from '../../../../models/othello-game';
 import User from '../../../../models/user';
 import publishUserStream, { publishOthelloStream } from '../../../../common/event';
-import { eighteight } from '../../../../common/othello/maps';
+import { eighteight } from '../../../../misc/othello/maps';
 
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Get 'userId' parameter
diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts
index 4de917694..c2e790a24 100644
--- a/src/server/api/endpoints/posts/create.ts
+++ b/src/server/api/endpoints/posts/create.ts
@@ -17,8 +17,8 @@ import { pack } from '../../../../models/post';
 import watch from '../../common/watch-post';
 import event, { pushSw, publishChannelStream } from '../../../../common/event';
 import notify from '../../../../common/notify';
-import getAcct from '../../../../common/user/get-acct';
-import parseAcct from '../../../../common/user/parse-acct';
+import getAcct from '../../../../misc/user/get-acct';
+import parseAcct from '../../../../misc/user/parse-acct';
 import config from '../../../../conf';
 
 /**
diff --git a/src/server/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts
index f7b37193b..2b0279937 100644
--- a/src/server/api/endpoints/users/show.ts
+++ b/src/server/api/endpoints/users/show.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import User, { pack } from '../../../../models/user';
-import resolveRemoteUser from '../../../../common/remote/resolve-user';
+import resolveRemoteUser from '../../../../remote/resolve-user';
 
 const cursorOption = { fields: { data: false } };
 
diff --git a/src/server/api/limitter.ts b/src/server/api/limitter.ts
index 88ea6c367..93e03f5e4 100644
--- a/src/server/api/limitter.ts
+++ b/src/server/api/limitter.ts
@@ -3,7 +3,7 @@ import * as debug from 'debug';
 import limiterDB from '../../db/redis';
 import { Endpoint } from './endpoints';
 import { IAuthContext } from './authenticate';
-import getAcct from '../../common/user/get-acct';
+import getAcct from '../../misc/user/get-acct';
 
 const log = debug('misskey:limitter');
 
diff --git a/src/server/api/stream/othello-game.ts b/src/server/api/stream/othello-game.ts
index b11915f8f..00367a824 100644
--- a/src/server/api/stream/othello-game.ts
+++ b/src/server/api/stream/othello-game.ts
@@ -3,8 +3,8 @@ import * as redis from 'redis';
 import * as CRC32 from 'crc-32';
 import OthelloGame, { pack } from '../../../models/othello-game';
 import { publishOthelloGameStream } from '../../../common/event';
-import Othello from '../../../common/othello/core';
-import * as maps from '../../../common/othello/maps';
+import Othello from '../../../misc/othello/core';
+import * as maps from '../../../misc/othello/maps';
 import { ParsedUrlQuery } from 'querystring';
 
 export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user?: any): void {
diff --git a/src/server/webfinger.ts b/src/server/webfinger.ts
index 864bb4af5..41aeb5642 100644
--- a/src/server/webfinger.ts
+++ b/src/server/webfinger.ts
@@ -1,5 +1,5 @@
 import config from '../conf';
-import parseAcct from '../common/user/parse-acct';
+import parseAcct from '../misc/user/parse-acct';
 import User from '../models/user';
 const express = require('express');
 

From dd5a773d3b9ad2d2b547f7379137fc4d8e8ffa03 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 2 Apr 2018 04:48:38 +0900
Subject: [PATCH 1000/1250] Refactor

---
 src/models/user.ts | 85 ++++++++++++++++++++++++----------------------
 1 file changed, 44 insertions(+), 41 deletions(-)

diff --git a/src/models/user.ts b/src/models/user.ts
index 789b28b2f..bbe9b5a53 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -39,45 +39,6 @@ export function isValidBirthday(birthday: string): boolean {
 	return typeof birthday == 'string' && /^([0-9]{4})\-([0-9]{2})-([0-9]{2})$/.test(birthday);
 }
 
-type ILocalAccount = {
-	keypair: string;
-	email: string;
-	links: string[];
-	password: string;
-	token: string;
-	twitter: {
-		accessToken: string;
-		accessTokenSecret: string;
-		userId: string;
-		screenName: string;
-	};
-	line: {
-		userId: string;
-	};
-	profile: {
-		location: string;
-		birthday: string; // 'YYYY-MM-DD'
-		tags: string[];
-	};
-	lastUsedAt: Date;
-	isBot: boolean;
-	isPro: boolean;
-	twoFactorSecret: string;
-	twoFactorEnabled: boolean;
-	twoFactorTempSecret: string;
-	clientSettings: any;
-	settings: any;
-};
-
-type IRemoteAccount = {
-	inbox: string;
-	uri: string;
-	publicKey: {
-		id: string;
-		publicKeyPem: string;
-	};
-};
-
 type IUserBase = {
 	_id: mongo.ObjectID;
 	createdAt: Date;
@@ -102,8 +63,50 @@ type IUserBase = {
 
 export type IUser = ILocalUser | IRemoteUser;
 
-export interface ILocalUser extends IUserBase { host: null; account: ILocalAccount; }
-export interface IRemoteUser extends IUserBase { host: string; account: IRemoteAccount; }
+export interface ILocalUser extends IUserBase {
+	host: null;
+	account: {
+		keypair: string;
+		email: string;
+		links: string[];
+		password: string;
+		token: string;
+		twitter: {
+			accessToken: string;
+			accessTokenSecret: string;
+			userId: string;
+			screenName: string;
+		};
+		line: {
+			userId: string;
+		};
+		profile: {
+			location: string;
+			birthday: string; // 'YYYY-MM-DD'
+			tags: string[];
+		};
+		lastUsedAt: Date;
+		isBot: boolean;
+		isPro: boolean;
+		twoFactorSecret: string;
+		twoFactorEnabled: boolean;
+		twoFactorTempSecret: string;
+		clientSettings: any;
+		settings: any;
+	};
+}
+
+export interface IRemoteUser extends IUserBase {
+	host: string;
+	account: {
+		inbox: string;
+		uri: string;
+		publicKey: {
+			id: string;
+			publicKeyPem: string;
+		};
+	};
+}
 
 export const isLocalUser = (user: any): user is ILocalUser =>
 	user.host === null;

From 15a42e5b34bb97b297d517d586eda54e93120969 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 2 Apr 2018 04:52:11 +0900
Subject: [PATCH 1001/1250] Refactor

---
 src/models/user.ts | 52 +++++++++++++++++++++++-----------------------
 1 file changed, 26 insertions(+), 26 deletions(-)

diff --git a/src/models/user.ts b/src/models/user.ts
index bbe9b5a53..2d1693573 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -15,30 +15,6 @@ User.createIndex('account.token');
 
 export default User;
 
-export function validateUsername(username: string): boolean {
-	return typeof username == 'string' && /^[a-zA-Z0-9\-]{3,20}$/.test(username);
-}
-
-export function validatePassword(password: string): boolean {
-	return typeof password == 'string' && password != '';
-}
-
-export function isValidName(name: string): boolean {
-	return typeof name == 'string' && name.length < 30 && name.trim() != '';
-}
-
-export function isValidDescription(description: string): boolean {
-	return typeof description == 'string' && description.length < 500 && description.trim() != '';
-}
-
-export function isValidLocation(location: string): boolean {
-	return typeof location == 'string' && location.length < 50 && location.trim() != '';
-}
-
-export function isValidBirthday(birthday: string): boolean {
-	return typeof birthday == 'string' && /^([0-9]{4})\-([0-9]{2})-([0-9]{2})$/.test(birthday);
-}
-
 type IUserBase = {
 	_id: mongo.ObjectID;
 	createdAt: Date;
@@ -61,8 +37,6 @@ type IUserBase = {
 	hostLower: string;
 };
 
-export type IUser = ILocalUser | IRemoteUser;
-
 export interface ILocalUser extends IUserBase {
 	host: null;
 	account: {
@@ -108,12 +82,38 @@ export interface IRemoteUser extends IUserBase {
 	};
 }
 
+export type IUser = ILocalUser | IRemoteUser;
+
 export const isLocalUser = (user: any): user is ILocalUser =>
 	user.host === null;
 
 export const isRemoteUser = (user: any): user is IRemoteUser =>
 	!isLocalUser(user);
 
+export function validateUsername(username: string): boolean {
+	return typeof username == 'string' && /^[a-zA-Z0-9\-]{3,20}$/.test(username);
+}
+
+export function validatePassword(password: string): boolean {
+	return typeof password == 'string' && password != '';
+}
+
+export function isValidName(name: string): boolean {
+	return typeof name == 'string' && name.length < 30 && name.trim() != '';
+}
+
+export function isValidDescription(description: string): boolean {
+	return typeof description == 'string' && description.length < 500 && description.trim() != '';
+}
+
+export function isValidLocation(location: string): boolean {
+	return typeof location == 'string' && location.length < 50 && location.trim() != '';
+}
+
+export function isValidBirthday(birthday: string): boolean {
+	return typeof birthday == 'string' && /^([0-9]{4})\-([0-9]{2})-([0-9]{2})$/.test(birthday);
+}
+
 export function init(user): IUser {
 	user._id = new mongo.ObjectID(user._id);
 	user.avatarId = new mongo.ObjectID(user.avatarId);

From b1f563fe8b431498af6303a1ecdcf9fcfc96d95a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 2 Apr 2018 11:14:45 +0900
Subject: [PATCH 1002/1250] Refactor

---
 src/models/user.ts | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/models/user.ts b/src/models/user.ts
index 2d1693573..c9a25a35e 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -34,6 +34,7 @@ type IUserBase = {
 	pinnedPostId: mongo.ObjectID;
 	isSuspended: boolean;
 	keywords: string[];
+	host: string;
 	hostLower: string;
 };
 
@@ -71,7 +72,6 @@ export interface ILocalUser extends IUserBase {
 }
 
 export interface IRemoteUser extends IUserBase {
-	host: string;
 	account: {
 		inbox: string;
 		uri: string;
@@ -90,6 +90,7 @@ export const isLocalUser = (user: any): user is ILocalUser =>
 export const isRemoteUser = (user: any): user is IRemoteUser =>
 	!isLocalUser(user);
 
+//#region Validators
 export function validateUsername(username: string): boolean {
 	return typeof username == 'string' && /^[a-zA-Z0-9\-]{3,20}$/.test(username);
 }
@@ -113,6 +114,7 @@ export function isValidLocation(location: string): boolean {
 export function isValidBirthday(birthday: string): boolean {
 	return typeof birthday == 'string' && /^([0-9]{4})\-([0-9]{2})-([0-9]{2})$/.test(birthday);
 }
+//#endregion
 
 export function init(user): IUser {
 	user._id = new mongo.ObjectID(user._id);

From e534990f7d805c3671b2f22ed638e8bb3a26ca68 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 2 Apr 2018 11:25:31 +0900
Subject: [PATCH 1003/1250] Fix config init script

---
 tools/init.js | 238 +++++++++++++++++++++++---------------------------
 1 file changed, 108 insertions(+), 130 deletions(-)

diff --git a/tools/init.js b/tools/init.js
index 39c45c82d..7a3e7d1b5 100644
--- a/tools/init.js
+++ b/tools/init.js
@@ -7,140 +7,119 @@ const chalk = require('chalk');
 const configDirPath = `${__dirname}/../.config`;
 const configPath = `${configDirPath}/default.yml`;
 
-const form = [
-	{
-		type: 'input',
-		name: 'maintainer',
-		message: 'Maintainer name(and email address):'
-	},
-	{
-		type: 'input',
-		name: 'url',
-		message: 'PRIMARY URL:'
-	},
-	{
-		type: 'input',
-		name: 'secondary_url',
-		message: 'SECONDARY URL:'
-	},
-	{
-		type: 'input',
-		name: 'port',
-		message: 'Listen port:'
-	},
-	{
-		type: 'confirm',
-		name: 'https',
-		message: 'Use TLS?',
-		default: false
-	},
-	{
-		type: 'input',
-		name: 'https_key',
-		message: 'Path of tls key:',
-		when: ctx => ctx.https
-	},
-	{
-		type: 'input',
-		name: 'https_cert',
-		message: 'Path of tls cert:',
-		when: ctx => ctx.https
-	},
-	{
-		type: 'input',
-		name: 'https_ca',
-		message: 'Path of tls ca:',
-		when: ctx => ctx.https
-	},
-	{
-		type: 'input',
-		name: 'mongo_host',
-		message: 'MongoDB\'s host:',
-		default: 'localhost'
-	},
-	{
-		type: 'input',
-		name: 'mongo_port',
-		message: 'MongoDB\'s port:',
-		default: '27017'
-	},
-	{
-		type: 'input',
-		name: 'mongo_db',
-		message: 'MongoDB\'s db:',
-		default: 'misskey'
-	},
-	{
-		type: 'input',
-		name: 'mongo_user',
-		message: 'MongoDB\'s user:'
-	},
-	{
-		type: 'password',
-		name: 'mongo_pass',
-		message: 'MongoDB\'s password:'
-	},
-	{
-		type: 'input',
-		name: 'redis_host',
-		message: 'Redis\'s host:',
-		default: 'localhost'
-	},
-	{
-		type: 'input',
-		name: 'redis_port',
-		message: 'Redis\'s port:',
-		default: '6379'
-	},
-	{
-		type: 'password',
-		name: 'redis_pass',
-		message: 'Redis\'s password:'
-	},
-	{
-		type: 'confirm',
-		name: 'elasticsearch',
-		message: 'Use Elasticsearch?',
-		default: false
-	},
-	{
-		type: 'input',
-		name: 'es_host',
-		message: 'Elasticsearch\'s host:',
-		default: 'localhost',
-		when: ctx => ctx.elasticsearch
-	},
-	{
-		type: 'input',
-		name: 'es_port',
-		message: 'Elasticsearch\'s port:',
-		default: '9200',
-		when: ctx => ctx.elasticsearch
-	},
-	{
-		type: 'password',
-		name: 'es_pass',
-		message: 'Elasticsearch\'s password:',
-		when: ctx => ctx.elasticsearch
-	},
-	{
-		type: 'input',
-		name: 'recaptcha_site',
-		message: 'reCAPTCHA\'s site key:'
-	},
-	{
-		type: 'input',
-		name: 'recaptcha_secret',
-		message: 'reCAPTCHA\'s secret key:'
-	}
-];
+const form = [{
+	type: 'input',
+	name: 'maintainerName',
+	message: 'Your name:'
+}, {
+	type: 'input',
+	name: 'maintainerUrl',
+	message: 'Your home page URL or your mailto URL:'
+}, {
+	type: 'input',
+	name: 'url',
+	message: 'URL you want to run Misskey:'
+}, {
+	type: 'input',
+	name: 'port',
+	message: 'Listen port (e.g. 443):'
+}, {
+	type: 'confirm',
+	name: 'https',
+	message: 'Use TLS?',
+	default: false
+}, {
+	type: 'input',
+	name: 'https_key',
+	message: 'Path of tls key:',
+	when: ctx => ctx.https
+}, {
+	type: 'input',
+	name: 'https_cert',
+	message: 'Path of tls cert:',
+	when: ctx => ctx.https
+}, {
+	type: 'input',
+	name: 'https_ca',
+	message: 'Path of tls ca:',
+	when: ctx => ctx.https
+}, {
+	type: 'input',
+	name: 'mongo_host',
+	message: 'MongoDB\'s host:',
+	default: 'localhost'
+}, {
+	type: 'input',
+	name: 'mongo_port',
+	message: 'MongoDB\'s port:',
+	default: '27017'
+}, {
+	type: 'input',
+	name: 'mongo_db',
+	message: 'MongoDB\'s db:',
+	default: 'misskey'
+}, {
+	type: 'input',
+	name: 'mongo_user',
+	message: 'MongoDB\'s user:'
+}, {
+	type: 'password',
+	name: 'mongo_pass',
+	message: 'MongoDB\'s password:'
+}, {
+	type: 'input',
+	name: 'redis_host',
+	message: 'Redis\'s host:',
+	default: 'localhost'
+}, {
+	type: 'input',
+	name: 'redis_port',
+	message: 'Redis\'s port:',
+	default: '6379'
+}, {
+	type: 'password',
+	name: 'redis_pass',
+	message: 'Redis\'s password:'
+}, {
+	type: 'confirm',
+	name: 'elasticsearch',
+	message: 'Use Elasticsearch?',
+	default: false
+}, {
+	type: 'input',
+	name: 'es_host',
+	message: 'Elasticsearch\'s host:',
+	default: 'localhost',
+	when: ctx => ctx.elasticsearch
+}, {
+	type: 'input',
+	name: 'es_port',
+	message: 'Elasticsearch\'s port:',
+	default: '9200',
+	when: ctx => ctx.elasticsearch
+}, {
+	type: 'password',
+	name: 'es_pass',
+	message: 'Elasticsearch\'s password:',
+	when: ctx => ctx.elasticsearch
+}, {
+	type: 'input',
+	name: 'recaptcha_site',
+	message: 'reCAPTCHA\'s site key:'
+}, {
+	type: 'input',
+	name: 'recaptcha_secret',
+	message: 'reCAPTCHA\'s secret key:'
+}];
 
 inquirer.prompt(form).then(as => {
 	// Mapping answers
 	const conf = {
-		maintainer: as['maintainer'],
+		maintainer: {
+			name: as['maintainerName'],
+			url: as['maintainerUrl']
+		},
 		url: as['url'],
-		secondary_url: as['secondary_url'],
 		port: parseInt(as['port'], 10),
 		https: {
 			enable: as['https'],
@@ -175,7 +154,6 @@ inquirer.prompt(form).then(as => {
 	console.log(`Thanks. Writing the configuration to ${chalk.bold(path.resolve(configPath))}`);
 
 	try {
-		fs.mkdirSync(configDirPath);
 		fs.writeFileSync(configPath, yaml.dump(conf));
 		console.log(chalk.green('Well done.'));
 	} catch (e) {

From 82958cfb5994590bc02d7d7a1a0ab85d58a7ecc4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 2 Apr 2018 11:32:27 +0900
Subject: [PATCH 1004/1250] Update setup docs

---
 docs/setup.en.md | 32 +++++++++++++++++++-------------
 docs/setup.ja.md | 33 ++++++++++++++++++++-------------
 2 files changed, 39 insertions(+), 26 deletions(-)

diff --git a/docs/setup.en.md b/docs/setup.en.md
index a436d751c..1d998b705 100644
--- a/docs/setup.en.md
+++ b/docs/setup.en.md
@@ -35,32 +35,38 @@ Please install and setup these softwares:
 ##### Optional
 * [Elasticsearch](https://www.elastic.co/) - used to provide searching feature instead of MongoDB
 
-*3.* Prepare configuration
+*3.* Install Misskey
+----------------------------------------------------------------
+1. `git clone -b master git://github.com/syuilo/misskey.git`
+2. `cd misskey`
+3. `npm install`
+
+*4.* Prepare configuration
 ----------------------------------------------------------------
 1. Copy `example.yml` of `.config` directory
 2. Rename it to `default.yml`
 3. Edit it
 
-*4.* Install and build Misskey
+---
+
+Or you can generate config file via `npm run config` command.
+
+*5.* Build Misskey
 ----------------------------------------------------------------
+1. `npm run build`
 
-1. `git clone -b master git://github.com/syuilo/misskey.git`
-2. `cd misskey`
-3. `npm install`
-4. `npm run build`
-
-#### Update
-1. `git reset --hard && git pull origin master`
-2. `npm install`
-3. `npm run build`
-
-*5.* That is it.
+*6.* That is it.
 ----------------------------------------------------------------
 Well done! Now, you have an environment that run to Misskey.
 
 ### Launch
 Just `sudo npm start`. GLHF!
 
+#### Way to Update to latest version of your Misskey
+1. `git reset --hard && git pull origin master`
+2. `npm install`
+3. `npm run build`
+
 ### Testing
 Run `npm test` after building
 
diff --git a/docs/setup.ja.md b/docs/setup.ja.md
index 6605461d9..ed63da26c 100644
--- a/docs/setup.ja.md
+++ b/docs/setup.ja.md
@@ -35,32 +35,39 @@ web-push generate-vapid-keys
 ##### オプション
 * [Elasticsearch](https://www.elastic.co/) - 検索機能を向上させるために用います。
 
-*3.* 設定ファイルを用意する
+*3.* Misskeyのインストール
+----------------------------------------------------------------
+1. `git clone -b master git://github.com/syuilo/misskey.git`
+2. `cd misskey`
+3. `npm install`
+
+*4.* 設定ファイルを用意する
 ----------------------------------------------------------------
 1. `.config`ディレクトリ内の`example.yml`をコピー
 2. `default.yml`にリネーム
 3. 編集する
 
-*4.* Misskeyのインストール(とビルド)
+---
+
+または、`npm run config`コマンドを利用して、ガイドに従って情報を
+入力して設定ファイルを生成することもできます。
+
+*5.* Misskeyのビルド
 ----------------------------------------------------------------
+1. `npm run build`
 
-1. `git clone -b master git://github.com/syuilo/misskey.git`
-2. `cd misskey`
-3. `npm install`
-4. `npm run build`
-
-#### アップデートするには:
-1. `git reset --hard && git pull origin master`
-2. `npm install`
-3. `npm run build`
-
-*5.* 以上です!
+*6.* 以上です!
 ----------------------------------------------------------------
 お疲れ様でした。これでMisskeyを動かす準備は整いました。
 
 ### 起動
 `sudo npm start`するだけです。GLHF!
 
+#### Misskeyを最新バージョンにアップデートする方法:
+1. `git reset --hard && git pull origin master`
+2. `npm install`
+3. `npm run build`
+
 ### テスト
 (ビルドされている状態で)`npm test`
 

From a0b8e08507d5999dabc488403ad0c0c7fd6c6247 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 2 Apr 2018 11:34:35 +0900
Subject: [PATCH 1005/1250] Docs: Fix heading level

---
 docs/setup.en.md | 2 +-
 docs/setup.ja.md | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/docs/setup.en.md b/docs/setup.en.md
index 1d998b705..a07c7b318 100644
--- a/docs/setup.en.md
+++ b/docs/setup.en.md
@@ -62,7 +62,7 @@ Well done! Now, you have an environment that run to Misskey.
 ### Launch
 Just `sudo npm start`. GLHF!
 
-#### Way to Update to latest version of your Misskey
+### Way to Update to latest version of your Misskey
 1. `git reset --hard && git pull origin master`
 2. `npm install`
 3. `npm run build`
diff --git a/docs/setup.ja.md b/docs/setup.ja.md
index ed63da26c..2dd44e545 100644
--- a/docs/setup.ja.md
+++ b/docs/setup.ja.md
@@ -63,7 +63,7 @@ web-push generate-vapid-keys
 ### 起動
 `sudo npm start`するだけです。GLHF!
 
-#### Misskeyを最新バージョンにアップデートする方法:
+### Misskeyを最新バージョンにアップデートする方法:
 1. `git reset --hard && git pull origin master`
 2. `npm install`
 3. `npm run build`

From 3b3568063e6dab206c93269e635643a9bce832e0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 2 Apr 2018 11:35:34 +0900
Subject: [PATCH 1006/1250] Docs: Clean up

---
 docs/setup.en.md | 7 -------
 docs/setup.ja.md | 7 -------
 2 files changed, 14 deletions(-)

diff --git a/docs/setup.en.md b/docs/setup.en.md
index a07c7b318..88e20f6bc 100644
--- a/docs/setup.en.md
+++ b/docs/setup.en.md
@@ -66,10 +66,3 @@ Just `sudo npm start`. GLHF!
 1. `git reset --hard && git pull origin master`
 2. `npm install`
 3. `npm run build`
-
-### Testing
-Run `npm test` after building
-
-### Debugging :bug:
-#### Show debug messages
-Misskey uses [debug](https://github.com/visionmedia/debug) and the namespace is `misskey:*`.
diff --git a/docs/setup.ja.md b/docs/setup.ja.md
index 2dd44e545..a46c38cb2 100644
--- a/docs/setup.ja.md
+++ b/docs/setup.ja.md
@@ -67,10 +67,3 @@ web-push generate-vapid-keys
 1. `git reset --hard && git pull origin master`
 2. `npm install`
 3. `npm run build`
-
-### テスト
-(ビルドされている状態で)`npm test`
-
-### デバッグ :bug:
-#### デバッグメッセージを表示するようにする
-Misskeyは[debug](https://github.com/visionmedia/debug)モジュールを利用しており、ネームスペースは`misskey:*`となっています。

From 1322889f7a7581dddf646bdd03868a1912c616fd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 2 Apr 2018 11:54:57 +0900
Subject: [PATCH 1007/1250] Update logo :art:

---
 assets/title.png | 4 ++--
 assets/title.svg | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/assets/title.png b/assets/title.png
index e91bbfa25..de3fd61ca 100644
--- a/assets/title.png
+++ b/assets/title.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:b81a639762b4f03976f5652a35e6eb14019890206b8f27d5d9247e47553cc0f9
-size 3195
+oid sha256:cbedad39ab1e68a5f2ebb4a965f04e5581d1927f29d95a19813402bf1e1a3a7e
+size 9666
diff --git a/assets/title.svg b/assets/title.svg
index fa81d9ad0..58ccf4f48 100644
--- a/assets/title.svg
+++ b/assets/title.svg
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:3b501af20f29f1148ef412b22ec012d46a5d81de69ca2e223e716265a96c33a1
-size 3304
+oid sha256:8a5cc1859b1fb8fe06ac956ef2d81890d99c74c0a81627ad2e287418cae3fcaf
+size 13058

From e42556af72e5784abc63cb65f00de36312116b1e Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 2 Apr 2018 11:55:57 +0900
Subject: [PATCH 1008/1250] Update README.md

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 4c0506709..46288e0c4 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 <img src="https://github.com/syuilo/misskey/blob/b3f42e62af698a67c2250533c437569559f1fdf9/src/himasaku/resources/himasaku.png?raw=true" align="right" width="320px"/>
 
-Misskey
+[![Misskey](/assets/title.png)](https://misskey.xyz/)
 ================================================================
 
 [![][travis-badge]][travis-link]

From cbd9d0a755f10e1943d290d1fe1559d7a21dace0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 2 Apr 2018 12:02:00 +0900
Subject: [PATCH 1009/1250] Update logo :art:

---
 assets/title.png | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/assets/title.png b/assets/title.png
index de3fd61ca..783d5eee9 100644
--- a/assets/title.png
+++ b/assets/title.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:cbedad39ab1e68a5f2ebb4a965f04e5581d1927f29d95a19813402bf1e1a3a7e
-size 9666
+oid sha256:a160068bd2f63c946af566ad8cb3307e7f0910e8cc00f27faf195971b81414b8
+size 6774

From a9c0dd5e53371b4c8bd3f6dd535848f04fffa86e Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Mon, 2 Apr 2018 12:58:53 +0900
Subject: [PATCH 1010/1250] Abolish common and misc directories

---
 src/client/app/ch/tags/channel.tag                   |  2 +-
 .../app/common/scripts/compose-notification.ts       |  4 ++--
 .../app/common/views/components/autocomplete.vue     |  2 +-
 .../views/components/messaging-room.message.vue      |  4 ++--
 src/client/app/common/views/components/messaging.vue |  2 +-
 .../app/common/views/components/othello.game.vue     |  2 +-
 .../app/common/views/components/othello.room.vue     |  2 +-
 src/client/app/common/views/components/post-html.ts  |  4 ++--
 .../app/common/views/components/welcome-timeline.vue |  2 +-
 .../app/desktop/views/components/friends-maker.vue   |  2 +-
 .../views/components/messaging-room-window.vue       |  2 +-
 .../app/desktop/views/components/notifications.vue   |  4 ++--
 .../app/desktop/views/components/post-detail.sub.vue |  2 +-
 .../app/desktop/views/components/post-detail.vue     |  4 ++--
 .../app/desktop/views/components/post-preview.vue    |  2 +-
 .../app/desktop/views/components/posts.post.sub.vue  |  2 +-
 .../app/desktop/views/components/posts.post.vue      |  4 ++--
 .../app/desktop/views/components/settings.mute.vue   |  2 +-
 .../app/desktop/views/components/user-preview.vue    |  4 ++--
 .../app/desktop/views/components/users-list.item.vue |  2 +-
 src/client/app/desktop/views/pages/home.vue          |  2 +-
 .../app/desktop/views/pages/messaging-room.vue       |  2 +-
 .../views/pages/user/user.followers-you-know.vue     |  2 +-
 .../app/desktop/views/pages/user/user.friends.vue    |  2 +-
 .../app/desktop/views/pages/user/user.header.vue     |  2 +-
 src/client/app/desktop/views/pages/user/user.vue     |  2 +-
 src/client/app/desktop/views/pages/welcome.vue       |  2 +-
 .../desktop/views/widgets/channel.channel.post.vue   |  2 +-
 src/client/app/desktop/views/widgets/polls.vue       |  2 +-
 src/client/app/desktop/views/widgets/trends.vue      |  2 +-
 src/client/app/desktop/views/widgets/users.vue       |  2 +-
 src/client/app/mobile/api/post.ts                    |  2 +-
 .../mobile/views/components/notification-preview.vue |  2 +-
 .../app/mobile/views/components/notification.vue     |  4 ++--
 src/client/app/mobile/views/components/post-card.vue |  4 ++--
 .../app/mobile/views/components/post-detail.sub.vue  |  2 +-
 .../app/mobile/views/components/post-detail.vue      |  4 ++--
 .../app/mobile/views/components/post-preview.vue     |  2 +-
 src/client/app/mobile/views/components/post.sub.vue  |  2 +-
 src/client/app/mobile/views/components/post.vue      |  4 ++--
 src/client/app/mobile/views/components/user-card.vue |  2 +-
 .../app/mobile/views/components/user-preview.vue     |  2 +-
 src/client/app/mobile/views/pages/followers.vue      |  2 +-
 src/client/app/mobile/views/pages/following.vue      |  2 +-
 src/client/app/mobile/views/pages/home.vue           |  2 +-
 src/client/app/mobile/views/pages/messaging-room.vue |  2 +-
 src/client/app/mobile/views/pages/messaging.vue      |  2 +-
 src/client/app/mobile/views/pages/user.vue           |  4 ++--
 .../views/pages/user/home.followers-you-know.vue     |  2 +-
 .../app/mobile/views/pages/user/home.photos.vue      |  2 +-
 src/{common => }/drive/add-file.ts                   | 12 ++++++------
 src/{common => }/drive/upload-from-url.ts            |  2 +-
 src/{common => }/event.ts                            |  2 +-
 src/{misc => }/get-notification-summary.ts           |  0
 src/{misc => }/get-post-summary.ts                   |  0
 src/{misc => }/get-reaction-emoji.ts                 |  0
 src/{common => }/notify.ts                           |  6 +++---
 src/{misc => }/othello/ai/back.ts                    |  2 +-
 src/{misc => }/othello/ai/front.ts                   |  2 +-
 src/{misc => }/othello/ai/index.ts                   |  0
 src/{misc => }/othello/core.ts                       |  0
 src/{misc => }/othello/maps.ts                       |  0
 src/processor/http/follow.ts                         |  4 ++--
 src/{common => }/push-sw.ts                          |  4 ++--
 src/remote/activitypub/create.ts                     |  2 +-
 src/server/activitypub/inbox.ts                      |  2 +-
 src/server/activitypub/post.ts                       |  2 +-
 src/server/activitypub/with-user.ts                  |  2 +-
 src/server/api/bot/core.ts                           |  8 ++++----
 src/server/api/bot/interfaces/line.ts                |  6 +++---
 src/server/api/common/read-messaging-message.ts      |  6 +++---
 src/server/api/common/read-notification.ts           |  2 +-
 src/server/api/endpoints/drive/files/create.ts       |  2 +-
 src/server/api/endpoints/drive/files/update.ts       |  2 +-
 .../api/endpoints/drive/files/upload_from_url.ts     |  2 +-
 src/server/api/endpoints/drive/folders/create.ts     |  2 +-
 src/server/api/endpoints/drive/folders/update.ts     |  2 +-
 src/server/api/endpoints/following/delete.ts         |  2 +-
 src/server/api/endpoints/i/regenerate_token.ts       |  2 +-
 src/server/api/endpoints/i/update.ts                 |  2 +-
 src/server/api/endpoints/i/update_client_setting.ts  |  2 +-
 src/server/api/endpoints/i/update_home.ts            |  2 +-
 src/server/api/endpoints/i/update_mobile_home.ts     |  2 +-
 .../api/endpoints/messaging/messages/create.ts       |  8 ++++----
 .../api/endpoints/notifications/mark_as_read_all.ts  |  2 +-
 src/server/api/endpoints/othello/games/show.ts       |  2 +-
 src/server/api/endpoints/othello/match.ts            |  4 ++--
 src/server/api/endpoints/posts/create.ts             | 12 ++++++------
 src/server/api/endpoints/posts/polls/vote.ts         |  4 ++--
 src/server/api/endpoints/posts/reactions/create.ts   |  4 ++--
 src/server/api/limitter.ts                           |  2 +-
 src/server/api/private/signin.ts                     |  2 +-
 src/server/api/service/twitter.ts                    |  2 +-
 src/server/api/stream/othello-game.ts                |  6 +++---
 src/server/api/stream/othello.ts                     |  2 +-
 src/server/webfinger.ts                              |  2 +-
 src/{common => }/text/html.ts                        |  0
 .../text/parse/core/syntax-highlighter.ts            |  0
 src/{common => }/text/parse/elements/bold.ts         |  0
 src/{common => }/text/parse/elements/code.ts         |  0
 src/{common => }/text/parse/elements/emoji.ts        |  0
 src/{common => }/text/parse/elements/hashtag.ts      |  0
 src/{common => }/text/parse/elements/inline-code.ts  |  0
 src/{common => }/text/parse/elements/link.ts         |  0
 src/{common => }/text/parse/elements/mention.ts      |  2 +-
 src/{common => }/text/parse/elements/quote.ts        |  0
 src/{common => }/text/parse/elements/url.ts          |  0
 src/{common => }/text/parse/index.ts                 |  0
 src/{misc => }/user/get-acct.ts                      |  0
 src/{misc => }/user/get-summary.ts                   |  2 +-
 src/{misc => }/user/parse-acct.ts                    |  0
 111 files changed, 133 insertions(+), 133 deletions(-)
 rename src/{common => }/drive/add-file.ts (96%)
 rename src/{common => }/drive/upload-from-url.ts (93%)
 rename src/{common => }/event.ts (98%)
 rename src/{misc => }/get-notification-summary.ts (100%)
 rename src/{misc => }/get-post-summary.ts (100%)
 rename src/{misc => }/get-reaction-emoji.ts (100%)
 rename src/{common => }/notify.ts (90%)
 rename src/{misc => }/othello/ai/back.ts (99%)
 rename src/{misc => }/othello/ai/front.ts (99%)
 rename src/{misc => }/othello/ai/index.ts (100%)
 rename src/{misc => }/othello/core.ts (100%)
 rename src/{misc => }/othello/maps.ts (100%)
 rename src/{common => }/push-sw.ts (93%)
 rename src/{common => }/text/html.ts (100%)
 rename src/{common => }/text/parse/core/syntax-highlighter.ts (100%)
 rename src/{common => }/text/parse/elements/bold.ts (100%)
 rename src/{common => }/text/parse/elements/code.ts (100%)
 rename src/{common => }/text/parse/elements/emoji.ts (100%)
 rename src/{common => }/text/parse/elements/hashtag.ts (100%)
 rename src/{common => }/text/parse/elements/inline-code.ts (100%)
 rename src/{common => }/text/parse/elements/link.ts (100%)
 rename src/{common => }/text/parse/elements/mention.ts (83%)
 rename src/{common => }/text/parse/elements/quote.ts (100%)
 rename src/{common => }/text/parse/elements/url.ts (100%)
 rename src/{common => }/text/parse/index.ts (100%)
 rename src/{misc => }/user/get-acct.ts (100%)
 rename src/{misc => }/user/get-summary.ts (90%)
 rename src/{misc => }/user/parse-acct.ts (100%)

diff --git a/src/client/app/ch/tags/channel.tag b/src/client/app/ch/tags/channel.tag
index 70e494aed..0c139ba26 100644
--- a/src/client/app/ch/tags/channel.tag
+++ b/src/client/app/ch/tags/channel.tag
@@ -229,7 +229,7 @@
 
 	</style>
 	<script lang="typescript">
-		import getAcct from '../../../../misc/user/get-acct';
+		import getAcct from '../../../../user/get-acct';
 
 		this.post = this.opts.post;
 		this.form = this.opts.form;
diff --git a/src/client/app/common/scripts/compose-notification.ts b/src/client/app/common/scripts/compose-notification.ts
index 56fbcb94f..4030d61ac 100644
--- a/src/client/app/common/scripts/compose-notification.ts
+++ b/src/client/app/common/scripts/compose-notification.ts
@@ -1,5 +1,5 @@
-import getPostSummary from '../../../../misc/get-post-summary';
-import getReactionEmoji from '../../../../misc/get-reaction-emoji';
+import getPostSummary from '../../../../get-post-summary';
+import getReactionEmoji from '../../../../get-reaction-emoji';
 
 type Notification = {
 	title: string;
diff --git a/src/client/app/common/views/components/autocomplete.vue b/src/client/app/common/views/components/autocomplete.vue
index edba47058..7bcfc07e9 100644
--- a/src/client/app/common/views/components/autocomplete.vue
+++ b/src/client/app/common/views/components/autocomplete.vue
@@ -21,7 +21,7 @@
 import Vue from 'vue';
 import * as emojilib from 'emojilib';
 import contains from '../../../common/scripts/contains';
-import getAcct from '../../../../../misc/user/get-acct';
+import getAcct from '../../../../../user/get-acct';
 
 const lib = Object.entries(emojilib.lib).filter((x: any) => {
 	return x[1].category != 'flags';
diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue
index cad6825f3..3de8321f8 100644
--- a/src/client/app/common/views/components/messaging-room.message.vue
+++ b/src/client/app/common/views/components/messaging-room.message.vue
@@ -34,8 +34,8 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../misc/user/get-acct';
-import parse from '../../../../../common/text/parse';
+import getAcct from '../../../../../user/get-acct';
+import parse from '../../../../../text/parse';
 
 export default Vue.extend({
 	props: {
diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue
index f6709d02b..bde2b2b90 100644
--- a/src/client/app/common/views/components/messaging.vue
+++ b/src/client/app/common/views/components/messaging.vue
@@ -51,7 +51,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../misc/user/get-acct';
+import getAcct from '../../../../../user/get-acct';
 
 export default Vue.extend({
 	props: {
diff --git a/src/client/app/common/views/components/othello.game.vue b/src/client/app/common/views/components/othello.game.vue
index aa5798d71..b9d946de9 100644
--- a/src/client/app/common/views/components/othello.game.vue
+++ b/src/client/app/common/views/components/othello.game.vue
@@ -43,7 +43,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import * as CRC32 from 'crc-32';
-import Othello, { Color } from '../../../../../misc/othello/core';
+import Othello, { Color } from '../../../../../othello/core';
 import { url } from '../../../config';
 
 export default Vue.extend({
diff --git a/src/client/app/common/views/components/othello.room.vue b/src/client/app/common/views/components/othello.room.vue
index eb1d3a0c6..86368b3cc 100644
--- a/src/client/app/common/views/components/othello.room.vue
+++ b/src/client/app/common/views/components/othello.room.vue
@@ -94,7 +94,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import * as maps from '../../../../../misc/othello/maps';
+import * as maps from '../../../../../othello/maps';
 
 export default Vue.extend({
 	props: ['game', 'connection'],
diff --git a/src/client/app/common/views/components/post-html.ts b/src/client/app/common/views/components/post-html.ts
index 5670ce036..4018a966e 100644
--- a/src/client/app/common/views/components/post-html.ts
+++ b/src/client/app/common/views/components/post-html.ts
@@ -1,7 +1,7 @@
 import Vue from 'vue';
 import * as emojilib from 'emojilib';
-import parse from '../../../../../common/text/parse';
-import getAcct from '../../../../../misc/user/get-acct';
+import parse from '../../../../../text/parse';
+import getAcct from '../../../../../user/get-acct';
 import { url } from '../../../config';
 import MkUrl from './url.vue';
 
diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue
index 036a77b1c..94b7f5889 100644
--- a/src/client/app/common/views/components/welcome-timeline.vue
+++ b/src/client/app/common/views/components/welcome-timeline.vue
@@ -24,7 +24,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../misc/user/get-acct';
+import getAcct from '../../../../../user/get-acct';
 
 export default Vue.extend({
 	data() {
diff --git a/src/client/app/desktop/views/components/friends-maker.vue b/src/client/app/desktop/views/components/friends-maker.vue
index 68ecc6ad4..bfa7503d2 100644
--- a/src/client/app/desktop/views/components/friends-maker.vue
+++ b/src/client/app/desktop/views/components/friends-maker.vue
@@ -22,7 +22,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../misc/user/get-acct';
+import getAcct from '../../../../../user/get-acct';
 
 export default Vue.extend({
 	data() {
diff --git a/src/client/app/desktop/views/components/messaging-room-window.vue b/src/client/app/desktop/views/components/messaging-room-window.vue
index 610642204..88eb28578 100644
--- a/src/client/app/desktop/views/components/messaging-room-window.vue
+++ b/src/client/app/desktop/views/components/messaging-room-window.vue
@@ -8,7 +8,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import { url } from '../../../config';
-import getAcct from '../../../../../misc/user/get-acct';
+import getAcct from '../../../../../user/get-acct';
 
 export default Vue.extend({
 	props: ['user'],
diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
index 79b16d048..8c4102494 100644
--- a/src/client/app/desktop/views/components/notifications.vue
+++ b/src/client/app/desktop/views/components/notifications.vue
@@ -102,8 +102,8 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../misc/user/get-acct';
-import getPostSummary from '../../../../../misc/get-post-summary';
+import getAcct from '../../../../../user/get-acct';
+import getPostSummary from '../../../../../get-post-summary';
 
 export default Vue.extend({
 	data() {
diff --git a/src/client/app/desktop/views/components/post-detail.sub.vue b/src/client/app/desktop/views/components/post-detail.sub.vue
index 6fc569ada..2719fee9d 100644
--- a/src/client/app/desktop/views/components/post-detail.sub.vue
+++ b/src/client/app/desktop/views/components/post-detail.sub.vue
@@ -28,7 +28,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
-import getAcct from '../../../../../misc/user/get-acct';
+import getAcct from '../../../../../user/get-acct';
 
 export default Vue.extend({
 	props: ['post'],
diff --git a/src/client/app/desktop/views/components/post-detail.vue b/src/client/app/desktop/views/components/post-detail.vue
index 309c88e70..5911a267f 100644
--- a/src/client/app/desktop/views/components/post-detail.vue
+++ b/src/client/app/desktop/views/components/post-detail.vue
@@ -78,8 +78,8 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
-import getAcct from '../../../../../misc/user/get-acct';
-import parse from '../../../../../common/text/parse';
+import getAcct from '../../../../../user/get-acct';
+import parse from '../../../../../text/parse';
 
 import MkPostFormWindow from './post-form-window.vue';
 import MkRepostFormWindow from './repost-form-window.vue';
diff --git a/src/client/app/desktop/views/components/post-preview.vue b/src/client/app/desktop/views/components/post-preview.vue
index eb62e4a63..e27f0b4cc 100644
--- a/src/client/app/desktop/views/components/post-preview.vue
+++ b/src/client/app/desktop/views/components/post-preview.vue
@@ -21,7 +21,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
-import getAcct from '../../../../../misc/user/get-acct';
+import getAcct from '../../../../../user/get-acct';
 
 export default Vue.extend({
 	props: ['post'],
diff --git a/src/client/app/desktop/views/components/posts.post.sub.vue b/src/client/app/desktop/views/components/posts.post.sub.vue
index d3af0fef6..16f5c4bee 100644
--- a/src/client/app/desktop/views/components/posts.post.sub.vue
+++ b/src/client/app/desktop/views/components/posts.post.sub.vue
@@ -21,7 +21,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
-import getAcct from '../../../../../misc/user/get-acct';
+import getAcct from '../../../../../user/get-acct';
 
 export default Vue.extend({
 	props: ['post'],
diff --git a/src/client/app/desktop/views/components/posts.post.vue b/src/client/app/desktop/views/components/posts.post.vue
index 1325e3e4f..b32ebe944 100644
--- a/src/client/app/desktop/views/components/posts.post.vue
+++ b/src/client/app/desktop/views/components/posts.post.vue
@@ -85,8 +85,8 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
-import getAcct from '../../../../../misc/user/get-acct';
-import parse from '../../../../../common/text/parse';
+import getAcct from '../../../../../user/get-acct';
+import parse from '../../../../../text/parse';
 
 import MkPostFormWindow from './post-form-window.vue';
 import MkRepostFormWindow from './repost-form-window.vue';
diff --git a/src/client/app/desktop/views/components/settings.mute.vue b/src/client/app/desktop/views/components/settings.mute.vue
index ce8f4b273..5c8c86222 100644
--- a/src/client/app/desktop/views/components/settings.mute.vue
+++ b/src/client/app/desktop/views/components/settings.mute.vue
@@ -13,7 +13,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../misc/user/get-acct';
+import getAcct from '../../../../../user/get-acct';
 
 export default Vue.extend({
 	data() {
diff --git a/src/client/app/desktop/views/components/user-preview.vue b/src/client/app/desktop/views/components/user-preview.vue
index 595926a81..cad7d455f 100644
--- a/src/client/app/desktop/views/components/user-preview.vue
+++ b/src/client/app/desktop/views/components/user-preview.vue
@@ -29,8 +29,8 @@
 <script lang="ts">
 import Vue from 'vue';
 import * as anime from 'animejs';
-import getAcct from '../../../../../misc/user/get-acct';
-import parseAcct from '../../../../../misc/user/parse-acct';
+import getAcct from '../../../../../user/get-acct';
+import parseAcct from '../../../../../user/parse-acct';
 
 export default Vue.extend({
 	props: {
diff --git a/src/client/app/desktop/views/components/users-list.item.vue b/src/client/app/desktop/views/components/users-list.item.vue
index 1c40c247b..a25d68c44 100644
--- a/src/client/app/desktop/views/components/users-list.item.vue
+++ b/src/client/app/desktop/views/components/users-list.item.vue
@@ -19,7 +19,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../misc/user/get-acct';
+import getAcct from '../../../../../user/get-acct';
 
 export default Vue.extend({
 	props: ['user'],
diff --git a/src/client/app/desktop/views/pages/home.vue b/src/client/app/desktop/views/pages/home.vue
index fa7c19510..ad9e2bc9d 100644
--- a/src/client/app/desktop/views/pages/home.vue
+++ b/src/client/app/desktop/views/pages/home.vue
@@ -7,7 +7,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
-import getPostSummary from '../../../../../misc/get-post-summary';
+import getPostSummary from '../../../../../get-post-summary';
 
 export default Vue.extend({
 	props: {
diff --git a/src/client/app/desktop/views/pages/messaging-room.vue b/src/client/app/desktop/views/pages/messaging-room.vue
index 244b0c904..b3d0ff149 100644
--- a/src/client/app/desktop/views/pages/messaging-room.vue
+++ b/src/client/app/desktop/views/pages/messaging-room.vue
@@ -7,7 +7,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
-import parseAcct from '../../../../../misc/user/parse-acct';
+import parseAcct from '../../../../../user/parse-acct';
 
 export default Vue.extend({
 	data() {
diff --git a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
index e898b62a0..a8bc67d3a 100644
--- a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
+++ b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
@@ -13,7 +13,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../../misc/user/get-acct';
+import getAcct from '../../../../../../user/get-acct';
 
 export default Vue.extend({
 	props: ['user'],
diff --git a/src/client/app/desktop/views/pages/user/user.friends.vue b/src/client/app/desktop/views/pages/user/user.friends.vue
index b13eb528a..6710e3793 100644
--- a/src/client/app/desktop/views/pages/user/user.friends.vue
+++ b/src/client/app/desktop/views/pages/user/user.friends.vue
@@ -20,7 +20,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../../misc/user/get-acct';
+import getAcct from '../../../../../../user/get-acct';
 
 export default Vue.extend({
 	props: ['user'],
diff --git a/src/client/app/desktop/views/pages/user/user.header.vue b/src/client/app/desktop/views/pages/user/user.header.vue
index 32e425f86..7b051224d 100644
--- a/src/client/app/desktop/views/pages/user/user.header.vue
+++ b/src/client/app/desktop/views/pages/user/user.header.vue
@@ -22,7 +22,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../../misc/user/get-acct';
+import getAcct from '../../../../../../user/get-acct';
 
 export default Vue.extend({
 	props: ['user'],
diff --git a/src/client/app/desktop/views/pages/user/user.vue b/src/client/app/desktop/views/pages/user/user.vue
index 6e68171a6..54d675bbd 100644
--- a/src/client/app/desktop/views/pages/user/user.vue
+++ b/src/client/app/desktop/views/pages/user/user.vue
@@ -9,7 +9,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import parseAcct from '../../../../../../misc/user/parse-acct';
+import parseAcct from '../../../../../../user/parse-acct';
 import Progress from '../../../../common/scripts/loading';
 import XHeader from './user.header.vue';
 import XHome from './user.home.vue';
diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue
index 93bb59765..1e1c49019 100644
--- a/src/client/app/desktop/views/pages/welcome.vue
+++ b/src/client/app/desktop/views/pages/welcome.vue
@@ -43,7 +43,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import { docsUrl, copyright, lang } from '../../../config';
-import getAcct from '../../../../../misc/user/get-acct';
+import getAcct from '../../../../../user/get-acct';
 
 const shares = [
 	'Everything!',
diff --git a/src/client/app/desktop/views/widgets/channel.channel.post.vue b/src/client/app/desktop/views/widgets/channel.channel.post.vue
index 255d9a5e6..587371a76 100644
--- a/src/client/app/desktop/views/widgets/channel.channel.post.vue
+++ b/src/client/app/desktop/views/widgets/channel.channel.post.vue
@@ -19,7 +19,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../misc/user/get-acct';
+import getAcct from '../../../../../user/get-acct';
 
 export default Vue.extend({
 	props: ['post'],
diff --git a/src/client/app/desktop/views/widgets/polls.vue b/src/client/app/desktop/views/widgets/polls.vue
index fd242a04b..ad7101e0d 100644
--- a/src/client/app/desktop/views/widgets/polls.vue
+++ b/src/client/app/desktop/views/widgets/polls.vue
@@ -16,7 +16,7 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
-import getAcct from '../../../../../misc/user/get-acct';
+import getAcct from '../../../../../user/get-acct';
 
 export default define({
 	name: 'polls',
diff --git a/src/client/app/desktop/views/widgets/trends.vue b/src/client/app/desktop/views/widgets/trends.vue
index 6b22c123b..be62faa42 100644
--- a/src/client/app/desktop/views/widgets/trends.vue
+++ b/src/client/app/desktop/views/widgets/trends.vue
@@ -15,7 +15,7 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
-import getAcct from '../../../../../misc/user/get-acct';
+import getAcct from '../../../../../user/get-acct';
 
 export default define({
 	name: 'trends',
diff --git a/src/client/app/desktop/views/widgets/users.vue b/src/client/app/desktop/views/widgets/users.vue
index 102739bd0..c4643f485 100644
--- a/src/client/app/desktop/views/widgets/users.vue
+++ b/src/client/app/desktop/views/widgets/users.vue
@@ -23,7 +23,7 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
-import getAcct from '../../../../../misc/user/get-acct';
+import getAcct from '../../../../../user/get-acct';
 
 const limit = 3;
 
diff --git a/src/client/app/mobile/api/post.ts b/src/client/app/mobile/api/post.ts
index 8090bbef2..6580cbf4b 100644
--- a/src/client/app/mobile/api/post.ts
+++ b/src/client/app/mobile/api/post.ts
@@ -1,6 +1,6 @@
 import PostForm from '../views/components/post-form.vue';
 //import RepostForm from '../views/components/repost-form.vue';
-import getPostSummary from '../../../../misc/get-post-summary';
+import getPostSummary from '../../../../get-post-summary';
 
 export default (os) => (opts) => {
 	const o = opts || {};
diff --git a/src/client/app/mobile/views/components/notification-preview.vue b/src/client/app/mobile/views/components/notification-preview.vue
index 2a06d399c..e7e1f75a8 100644
--- a/src/client/app/mobile/views/components/notification-preview.vue
+++ b/src/client/app/mobile/views/components/notification-preview.vue
@@ -59,7 +59,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getPostSummary from '../../../../../misc/get-post-summary';
+import getPostSummary from '../../../../../get-post-summary';
 
 export default Vue.extend({
 	props: ['notification'],
diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue
index 93b1e8416..d92b01944 100644
--- a/src/client/app/mobile/views/components/notification.vue
+++ b/src/client/app/mobile/views/components/notification.vue
@@ -78,8 +78,8 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getPostSummary from '../../../../../misc/get-post-summary';
-import getAcct from '../../../../../misc/user/get-acct';
+import getPostSummary from '../../../../../get-post-summary';
+import getAcct from '../../../../../user/get-acct';
 
 export default Vue.extend({
 	props: ['notification'],
diff --git a/src/client/app/mobile/views/components/post-card.vue b/src/client/app/mobile/views/components/post-card.vue
index 3f5beb9e0..893e4c307 100644
--- a/src/client/app/mobile/views/components/post-card.vue
+++ b/src/client/app/mobile/views/components/post-card.vue
@@ -14,8 +14,8 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import summary from '../../../../../misc/get-post-summary';
-import getAcct from '../../../../../misc/user/get-acct';
+import summary from '../../../../../get-post-summary';
+import getAcct from '../../../../../user/get-acct';
 
 export default Vue.extend({
 	props: ['post'],
diff --git a/src/client/app/mobile/views/components/post-detail.sub.vue b/src/client/app/mobile/views/components/post-detail.sub.vue
index 1fca0881d..c8e06e26f 100644
--- a/src/client/app/mobile/views/components/post-detail.sub.vue
+++ b/src/client/app/mobile/views/components/post-detail.sub.vue
@@ -20,7 +20,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../misc/user/get-acct';
+import getAcct from '../../../../../user/get-acct';
 
 export default Vue.extend({
 	props: ['post'],
diff --git a/src/client/app/mobile/views/components/post-detail.vue b/src/client/app/mobile/views/components/post-detail.vue
index a8ebb3b01..4a293562e 100644
--- a/src/client/app/mobile/views/components/post-detail.vue
+++ b/src/client/app/mobile/views/components/post-detail.vue
@@ -80,8 +80,8 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../misc/user/get-acct';
-import parse from '../../../../../common/text/parse';
+import getAcct from '../../../../../user/get-acct';
+import parse from '../../../../../text/parse';
 
 import MkPostMenu from '../../../common/views/components/post-menu.vue';
 import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
diff --git a/src/client/app/mobile/views/components/post-preview.vue b/src/client/app/mobile/views/components/post-preview.vue
index 05dd4a004..b6916edb4 100644
--- a/src/client/app/mobile/views/components/post-preview.vue
+++ b/src/client/app/mobile/views/components/post-preview.vue
@@ -20,7 +20,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../misc/user/get-acct';
+import getAcct from '../../../../../user/get-acct';
 
 export default Vue.extend({
 	props: ['post'],
diff --git a/src/client/app/mobile/views/components/post.sub.vue b/src/client/app/mobile/views/components/post.sub.vue
index cbd2fad88..7f132b0a4 100644
--- a/src/client/app/mobile/views/components/post.sub.vue
+++ b/src/client/app/mobile/views/components/post.sub.vue
@@ -20,7 +20,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../misc/user/get-acct';
+import getAcct from '../../../../../user/get-acct';
 
 export default Vue.extend({
 	props: ['post'],
diff --git a/src/client/app/mobile/views/components/post.vue b/src/client/app/mobile/views/components/post.vue
index 6cf24ed6a..7cfb97425 100644
--- a/src/client/app/mobile/views/components/post.vue
+++ b/src/client/app/mobile/views/components/post.vue
@@ -77,8 +77,8 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../misc/user/get-acct';
-import parse from '../../../../../common/text/parse';
+import getAcct from '../../../../../user/get-acct';
+import parse from '../../../../../text/parse';
 
 import MkPostMenu from '../../../common/views/components/post-menu.vue';
 import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
diff --git a/src/client/app/mobile/views/components/user-card.vue b/src/client/app/mobile/views/components/user-card.vue
index fb1b12ce5..542ad2031 100644
--- a/src/client/app/mobile/views/components/user-card.vue
+++ b/src/client/app/mobile/views/components/user-card.vue
@@ -13,7 +13,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../misc/user/get-acct';
+import getAcct from '../../../../../user/get-acct';
 
 export default Vue.extend({
 	props: ['user'],
diff --git a/src/client/app/mobile/views/components/user-preview.vue b/src/client/app/mobile/views/components/user-preview.vue
index 4bb830381..51c32b9fd 100644
--- a/src/client/app/mobile/views/components/user-preview.vue
+++ b/src/client/app/mobile/views/components/user-preview.vue
@@ -17,7 +17,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../misc/user/get-acct';
+import getAcct from '../../../../../user/get-acct';
 
 export default Vue.extend({
 	props: ['user'],
diff --git a/src/client/app/mobile/views/pages/followers.vue b/src/client/app/mobile/views/pages/followers.vue
index 28e866efb..2e984508d 100644
--- a/src/client/app/mobile/views/pages/followers.vue
+++ b/src/client/app/mobile/views/pages/followers.vue
@@ -19,7 +19,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
-import parseAcct from '../../../../../misc/user/parse-acct';
+import parseAcct from '../../../../../user/parse-acct';
 
 export default Vue.extend({
 	data() {
diff --git a/src/client/app/mobile/views/pages/following.vue b/src/client/app/mobile/views/pages/following.vue
index 7ee3b8628..f5c7223a1 100644
--- a/src/client/app/mobile/views/pages/following.vue
+++ b/src/client/app/mobile/views/pages/following.vue
@@ -19,7 +19,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
-import parseAcct from '../../../../../misc/user/parse-acct';
+import parseAcct from '../../../../../user/parse-acct';
 
 export default Vue.extend({
 	data() {
diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue
index f76fdbe19..b2edf5956 100644
--- a/src/client/app/mobile/views/pages/home.vue
+++ b/src/client/app/mobile/views/pages/home.vue
@@ -64,7 +64,7 @@ import Vue from 'vue';
 import * as XDraggable from 'vuedraggable';
 import * as uuid from 'uuid';
 import Progress from '../../../common/scripts/loading';
-import getPostSummary from '../../../../../misc/get-post-summary';
+import getPostSummary from '../../../../../get-post-summary';
 
 export default Vue.extend({
 	components: {
diff --git a/src/client/app/mobile/views/pages/messaging-room.vue b/src/client/app/mobile/views/pages/messaging-room.vue
index 400f5016e..14f12a99b 100644
--- a/src/client/app/mobile/views/pages/messaging-room.vue
+++ b/src/client/app/mobile/views/pages/messaging-room.vue
@@ -10,7 +10,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import parseAcct from '../../../../../misc/user/parse-acct';
+import parseAcct from '../../../../../user/parse-acct';
 
 export default Vue.extend({
 	data() {
diff --git a/src/client/app/mobile/views/pages/messaging.vue b/src/client/app/mobile/views/pages/messaging.vue
index 7ded67cdf..f3f6f9f9b 100644
--- a/src/client/app/mobile/views/pages/messaging.vue
+++ b/src/client/app/mobile/views/pages/messaging.vue
@@ -7,7 +7,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../misc/user/get-acct';
+import getAcct from '../../../../../user/get-acct';
 
 export default Vue.extend({
 	mounted() {
diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue
index 01eb408e6..2d8f9f8ec 100644
--- a/src/client/app/mobile/views/pages/user.vue
+++ b/src/client/app/mobile/views/pages/user.vue
@@ -60,8 +60,8 @@
 <script lang="ts">
 import Vue from 'vue';
 import * as age from 's-age';
-import getAcct from '../../../../../misc/user/get-acct';
-import getAcct from '../../../../../misc/user/parse-acct';
+import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../user/parse-acct';
 import Progress from '../../../common/scripts/loading';
 import XHome from './user/home.vue';
 
diff --git a/src/client/app/mobile/views/pages/user/home.followers-you-know.vue b/src/client/app/mobile/views/pages/user/home.followers-you-know.vue
index 8dc4fbb85..04d8538f7 100644
--- a/src/client/app/mobile/views/pages/user/home.followers-you-know.vue
+++ b/src/client/app/mobile/views/pages/user/home.followers-you-know.vue
@@ -12,7 +12,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../../misc/user/get-acct';
+import getAcct from '../../../../../../user/get-acct';
 
 export default Vue.extend({
 	props: ['user'],
diff --git a/src/client/app/mobile/views/pages/user/home.photos.vue b/src/client/app/mobile/views/pages/user/home.photos.vue
index cc3527037..491660711 100644
--- a/src/client/app/mobile/views/pages/user/home.photos.vue
+++ b/src/client/app/mobile/views/pages/user/home.photos.vue
@@ -14,7 +14,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../../misc/user/get-acct';
+import getAcct from '../../../../../../user/get-acct';
 
 export default Vue.extend({
 	props: ['user'],
diff --git a/src/common/drive/add-file.ts b/src/drive/add-file.ts
similarity index 96%
rename from src/common/drive/add-file.ts
rename to src/drive/add-file.ts
index af85a23d2..f356af00d 100644
--- a/src/common/drive/add-file.ts
+++ b/src/drive/add-file.ts
@@ -10,12 +10,12 @@ import * as debug from 'debug';
 import fileType = require('file-type');
 import prominence = require('prominence');
 
-import DriveFile, { getGridFSBucket } from '../../models/drive-file';
-import DriveFolder from '../../models/drive-folder';
-import { pack } from '../../models/drive-file';
+import DriveFile, { getGridFSBucket } from '../models/drive-file';
+import DriveFolder from '../models/drive-folder';
+import { pack } from '../models/drive-file';
 import event, { publishDriveStream } from '../event';
-import getAcct from '../../misc/user/get-acct';
-import config from '../../conf';
+import getAcct from '../user/get-acct';
+import config from '../conf';
 
 const gm = _gm.subClass({
 	imageMagick: true
@@ -290,7 +290,7 @@ export default (user: any, file: string | stream.Readable, ...args) => new Promi
 
 			// Register to search database
 			if (config.elasticsearch.enable) {
-				const es = require('../../db/elasticsearch');
+				const es = require('../db/elasticsearch');
 				es.index({
 					index: 'misskey',
 					type: 'drive_file',
diff --git a/src/common/drive/upload-from-url.ts b/src/drive/upload-from-url.ts
similarity index 93%
rename from src/common/drive/upload-from-url.ts
rename to src/drive/upload-from-url.ts
index 5dd969593..7ff16e9e4 100644
--- a/src/common/drive/upload-from-url.ts
+++ b/src/drive/upload-from-url.ts
@@ -1,5 +1,5 @@
 import * as URL from 'url';
-import { IDriveFile, validateFileName } from '../../models/drive-file';
+import { IDriveFile, validateFileName } from '../models/drive-file';
 import create from './add-file';
 import * as debug from 'debug';
 import * as tmp from 'tmp';
diff --git a/src/common/event.ts b/src/event.ts
similarity index 98%
rename from src/common/event.ts
rename to src/event.ts
index 53520f11c..df79f0c90 100644
--- a/src/common/event.ts
+++ b/src/event.ts
@@ -1,7 +1,7 @@
 import * as mongo from 'mongodb';
 import * as redis from 'redis';
 import swPush from './push-sw';
-import config from '../conf';
+import config from './conf';
 
 type ID = string | mongo.ObjectID;
 
diff --git a/src/misc/get-notification-summary.ts b/src/get-notification-summary.ts
similarity index 100%
rename from src/misc/get-notification-summary.ts
rename to src/get-notification-summary.ts
diff --git a/src/misc/get-post-summary.ts b/src/get-post-summary.ts
similarity index 100%
rename from src/misc/get-post-summary.ts
rename to src/get-post-summary.ts
diff --git a/src/misc/get-reaction-emoji.ts b/src/get-reaction-emoji.ts
similarity index 100%
rename from src/misc/get-reaction-emoji.ts
rename to src/get-reaction-emoji.ts
diff --git a/src/common/notify.ts b/src/notify.ts
similarity index 90%
rename from src/common/notify.ts
rename to src/notify.ts
index fc65820d3..228317b88 100644
--- a/src/common/notify.ts
+++ b/src/notify.ts
@@ -1,8 +1,8 @@
 import * as mongo from 'mongodb';
-import Notification from '../models/notification';
-import Mute from '../models/mute';
+import Notification from './models/notification';
+import Mute from './models/mute';
 import event from './event';
-import { pack } from '../models/notification';
+import { pack } from './models/notification';
 
 export default (
 	notifiee: mongo.ObjectID,
diff --git a/src/misc/othello/ai/back.ts b/src/othello/ai/back.ts
similarity index 99%
rename from src/misc/othello/ai/back.ts
rename to src/othello/ai/back.ts
index 0950adaa9..a67f6fe83 100644
--- a/src/misc/othello/ai/back.ts
+++ b/src/othello/ai/back.ts
@@ -8,7 +8,7 @@
 
 import * as request from 'request-promise-native';
 import Othello, { Color } from '../core';
-import conf from '../../../conf';
+import conf from '../../conf';
 
 let game;
 let form;
diff --git a/src/misc/othello/ai/front.ts b/src/othello/ai/front.ts
similarity index 99%
rename from src/misc/othello/ai/front.ts
rename to src/othello/ai/front.ts
index e5496132f..3414d6434 100644
--- a/src/misc/othello/ai/front.ts
+++ b/src/othello/ai/front.ts
@@ -10,7 +10,7 @@ import * as childProcess from 'child_process';
 const WebSocket = require('ws');
 import * as ReconnectingWebSocket from 'reconnecting-websocket';
 import * as request from 'request-promise-native';
-import conf from '../../../conf';
+import conf from '../../conf';
 
 // 設定 ////////////////////////////////////////////////////////
 
diff --git a/src/misc/othello/ai/index.ts b/src/othello/ai/index.ts
similarity index 100%
rename from src/misc/othello/ai/index.ts
rename to src/othello/ai/index.ts
diff --git a/src/misc/othello/core.ts b/src/othello/core.ts
similarity index 100%
rename from src/misc/othello/core.ts
rename to src/othello/core.ts
diff --git a/src/misc/othello/maps.ts b/src/othello/maps.ts
similarity index 100%
rename from src/misc/othello/maps.ts
rename to src/othello/maps.ts
diff --git a/src/processor/http/follow.ts b/src/processor/http/follow.ts
index 4804657fd..e81b410cb 100644
--- a/src/processor/http/follow.ts
+++ b/src/processor/http/follow.ts
@@ -3,8 +3,8 @@ import { sign } from 'http-signature';
 import { URL } from 'url';
 import User, { isLocalUser, pack as packUser, ILocalUser } from '../../models/user';
 import Following from '../../models/following';
-import event from '../../common/event';
-import notify from '../../common/notify';
+import event from '../../event';
+import notify from '../../notify';
 import context from '../../remote/activitypub/renderer/context';
 import render from '../../remote/activitypub/renderer/follow';
 import config from '../../conf';
diff --git a/src/common/push-sw.ts b/src/push-sw.ts
similarity index 93%
rename from src/common/push-sw.ts
rename to src/push-sw.ts
index 44c328e83..5b5ec5207 100644
--- a/src/common/push-sw.ts
+++ b/src/push-sw.ts
@@ -1,7 +1,7 @@
 const push = require('web-push');
 import * as mongo from 'mongodb';
-import Subscription from '../models/sw-subscription';
-import config from '../conf';
+import Subscription from './models/sw-subscription';
+import config from './conf';
 
 if (config.sw) {
 	// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録
diff --git a/src/remote/activitypub/create.ts b/src/remote/activitypub/create.ts
index e610232f5..6d135685e 100644
--- a/src/remote/activitypub/create.ts
+++ b/src/remote/activitypub/create.ts
@@ -2,7 +2,7 @@ import { JSDOM } from 'jsdom';
 import config from '../../conf';
 import Post from '../../models/post';
 import RemoteUserObject, { IRemoteUserObject } from '../../models/remote-user-object';
-import uploadFromUrl from '../../common/drive/upload-from-url';
+import uploadFromUrl from '../../drive/upload-from-url';
 import Resolver from './resolver';
 const createDOMPurify = require('dompurify');
 
diff --git a/src/server/activitypub/inbox.ts b/src/server/activitypub/inbox.ts
index 31a47e276..865797fae 100644
--- a/src/server/activitypub/inbox.ts
+++ b/src/server/activitypub/inbox.ts
@@ -3,7 +3,7 @@ import * as express from 'express';
 import { parseRequest, verifySignature } from 'http-signature';
 import User, { IRemoteUser } from '../../models/user';
 import queue from '../../queue';
-import parseAcct from '../../misc/user/parse-acct';
+import parseAcct from '../../user/parse-acct';
 
 const app = express();
 app.disable('x-powered-by');
diff --git a/src/server/activitypub/post.ts b/src/server/activitypub/post.ts
index ecbf9671f..2962a9b96 100644
--- a/src/server/activitypub/post.ts
+++ b/src/server/activitypub/post.ts
@@ -1,7 +1,7 @@
 import * as express from 'express';
 import context from '../../remote/activitypub/renderer/context';
 import render from '../../remote/activitypub/renderer/note';
-import parseAcct from '../../misc/user/parse-acct';
+import parseAcct from '../../user/parse-acct';
 import Post from '../../models/post';
 import User from '../../models/user';
 
diff --git a/src/server/activitypub/with-user.ts b/src/server/activitypub/with-user.ts
index 18f2e50ff..b4090786b 100644
--- a/src/server/activitypub/with-user.ts
+++ b/src/server/activitypub/with-user.ts
@@ -1,4 +1,4 @@
-import parseAcct from '../../misc/user/parse-acct';
+import parseAcct from '../../user/parse-acct';
 import User from '../../models/user';
 
 export default (redirect, respond) => async (req, res, next) => {
diff --git a/src/server/api/bot/core.ts b/src/server/api/bot/core.ts
index 0faf9b4e4..3beec33d1 100644
--- a/src/server/api/bot/core.ts
+++ b/src/server/api/bot/core.ts
@@ -3,10 +3,10 @@ import * as bcrypt from 'bcryptjs';
 
 import User, { IUser, init as initUser, ILocalUser } from '../../../models/user';
 
-import getPostSummary from '../../../misc/get-post-summary';
-import getUserSummary from '../../../misc/user/get-summary';
-import parseAcct from '../../../misc/user/parse-acct';
-import getNotificationSummary from '../../../misc/get-notification-summary';
+import getPostSummary from '../../../get-post-summary';
+import getUserSummary from '../../../user/get-summary';
+import parseAcct from '../../../user/parse-acct';
+import getNotificationSummary from '../../../get-notification-summary';
 
 const hmm = [
 	'?',
diff --git a/src/server/api/bot/interfaces/line.ts b/src/server/api/bot/interfaces/line.ts
index a6e70a138..847b13aa7 100644
--- a/src/server/api/bot/interfaces/line.ts
+++ b/src/server/api/bot/interfaces/line.ts
@@ -7,9 +7,9 @@ import config from '../../../../conf';
 import BotCore from '../core';
 import _redis from '../../../../db/redis';
 import prominence = require('prominence');
-import getAcct from '../../../../misc/user/get-acct';
-import parseAcct from '../../../../misc/user/parse-acct';
-import getPostSummary from '../../../../misc/get-post-summary';
+import getAcct from '../../../../user/get-acct';
+import parseAcct from '../../../../user/parse-acct';
+import getPostSummary from '../../../../get-post-summary';
 
 const redis = prominence(_redis);
 
diff --git a/src/server/api/common/read-messaging-message.ts b/src/server/api/common/read-messaging-message.ts
index 127ea1865..755cf1f50 100644
--- a/src/server/api/common/read-messaging-message.ts
+++ b/src/server/api/common/read-messaging-message.ts
@@ -1,9 +1,9 @@
 import * as mongo from 'mongodb';
 import Message from '../../../models/messaging-message';
 import { IMessagingMessage as IMessage } from '../../../models/messaging-message';
-import publishUserStream from '../../../common/event';
-import { publishMessagingStream } from '../../../common/event';
-import { publishMessagingIndexStream } from '../../../common/event';
+import publishUserStream from '../../../event';
+import { publishMessagingStream } from '../../../event';
+import { publishMessagingIndexStream } from '../../../event';
 
 /**
  * Mark as read message(s)
diff --git a/src/server/api/common/read-notification.ts b/src/server/api/common/read-notification.ts
index 9b2012182..b51c0ca00 100644
--- a/src/server/api/common/read-notification.ts
+++ b/src/server/api/common/read-notification.ts
@@ -1,6 +1,6 @@
 import * as mongo from 'mongodb';
 import { default as Notification, INotification } from '../../../models/notification';
-import publishUserStream from '../../../common/event';
+import publishUserStream from '../../../event';
 
 /**
  * Mark as read notification(s)
diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts
index cf4e35cd1..10ced1d8b 100644
--- a/src/server/api/endpoints/drive/files/create.ts
+++ b/src/server/api/endpoints/drive/files/create.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import { validateFileName, pack } from '../../../../../models/drive-file';
-import create from '../../../../../common/drive/add-file';
+import create from '../../../../../drive/add-file';
 
 /**
  * Create a file
diff --git a/src/server/api/endpoints/drive/files/update.ts b/src/server/api/endpoints/drive/files/update.ts
index 5d0b915f9..85bd2110f 100644
--- a/src/server/api/endpoints/drive/files/update.ts
+++ b/src/server/api/endpoints/drive/files/update.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import DriveFolder from '../../../../../models/drive-folder';
 import DriveFile, { validateFileName, pack } from '../../../../../models/drive-file';
-import { publishDriveStream } from '../../../../../common/event';
+import { publishDriveStream } from '../../../../../event';
 
 /**
  * Update a file
diff --git a/src/server/api/endpoints/drive/files/upload_from_url.ts b/src/server/api/endpoints/drive/files/upload_from_url.ts
index ceb751a14..acb67b2e0 100644
--- a/src/server/api/endpoints/drive/files/upload_from_url.ts
+++ b/src/server/api/endpoints/drive/files/upload_from_url.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import { pack } from '../../../../../models/drive-file';
-import uploadFromUrl from '../../../../../common/drive/upload-from-url';
+import uploadFromUrl from '../../../../../drive/upload-from-url';
 
 /**
  * Create a file from a URL
diff --git a/src/server/api/endpoints/drive/folders/create.ts b/src/server/api/endpoints/drive/folders/create.ts
index bd3b0a0b1..d9d39a918 100644
--- a/src/server/api/endpoints/drive/folders/create.ts
+++ b/src/server/api/endpoints/drive/folders/create.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import DriveFolder, { isValidFolderName, pack } from '../../../../../models/drive-folder';
-import { publishDriveStream } from '../../../../../common/event';
+import { publishDriveStream } from '../../../../../event';
 
 /**
  * Create drive folder
diff --git a/src/server/api/endpoints/drive/folders/update.ts b/src/server/api/endpoints/drive/folders/update.ts
index 5ac81e5b5..1cea05d71 100644
--- a/src/server/api/endpoints/drive/folders/update.ts
+++ b/src/server/api/endpoints/drive/folders/update.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import DriveFolder, { isValidFolderName, pack } from '../../../../../models/drive-folder';
-import { publishDriveStream } from '../../../../../common/event';
+import { publishDriveStream } from '../../../../../event';
 
 /**
  * Update a folder
diff --git a/src/server/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts
index ecca27d57..77a6cebee 100644
--- a/src/server/api/endpoints/following/delete.ts
+++ b/src/server/api/endpoints/following/delete.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import User, { pack as packUser } from '../../../../models/user';
 import Following from '../../../../models/following';
-import event from '../../../../common/event';
+import event from '../../../../event';
 
 /**
  * Unfollow a user
diff --git a/src/server/api/endpoints/i/regenerate_token.ts b/src/server/api/endpoints/i/regenerate_token.ts
index 0af622fd9..d7cb69784 100644
--- a/src/server/api/endpoints/i/regenerate_token.ts
+++ b/src/server/api/endpoints/i/regenerate_token.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import * as bcrypt from 'bcryptjs';
 import User from '../../../../models/user';
-import event from '../../../../common/event';
+import event from '../../../../event';
 import generateUserToken from '../../common/generate-native-user-token';
 
 /**
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index b465e763e..76ceede8d 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../../../models/user';
-import event from '../../../../common/event';
+import event from '../../../../event';
 import config from '../../../../conf';
 
 /**
diff --git a/src/server/api/endpoints/i/update_client_setting.ts b/src/server/api/endpoints/i/update_client_setting.ts
index 79789e664..263ac6d07 100644
--- a/src/server/api/endpoints/i/update_client_setting.ts
+++ b/src/server/api/endpoints/i/update_client_setting.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import User, { pack } from '../../../../models/user';
-import event from '../../../../common/event';
+import event from '../../../../event';
 
 /**
  * Update myself
diff --git a/src/server/api/endpoints/i/update_home.ts b/src/server/api/endpoints/i/update_home.ts
index 437f51d6f..227b8dd5a 100644
--- a/src/server/api/endpoints/i/update_home.ts
+++ b/src/server/api/endpoints/i/update_home.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import User from '../../../../models/user';
-import event from '../../../../common/event';
+import event from '../../../../event';
 
 module.exports = async (params, user) => new Promise(async (res, rej) => {
 	// Get 'home' parameter
diff --git a/src/server/api/endpoints/i/update_mobile_home.ts b/src/server/api/endpoints/i/update_mobile_home.ts
index 783ca09d1..007eb2eab 100644
--- a/src/server/api/endpoints/i/update_mobile_home.ts
+++ b/src/server/api/endpoints/i/update_mobile_home.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import User from '../../../../models/user';
-import event from '../../../../common/event';
+import event from '../../../../event';
 
 module.exports = async (params, user) => new Promise(async (res, rej) => {
 	// Get 'home' parameter
diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts
index b2b6c971d..e0ec1f216 100644
--- a/src/server/api/endpoints/messaging/messages/create.ts
+++ b/src/server/api/endpoints/messaging/messages/create.ts
@@ -9,10 +9,10 @@ import User from '../../../../../models/user';
 import Mute from '../../../../../models/mute';
 import DriveFile from '../../../../../models/drive-file';
 import { pack } from '../../../../../models/messaging-message';
-import publishUserStream from '../../../../../common/event';
-import { publishMessagingStream, publishMessagingIndexStream, pushSw } from '../../../../../common/event';
-import html from '../../../../../common/text/html';
-import parse from '../../../../../common/text/parse';
+import publishUserStream from '../../../../../event';
+import { publishMessagingStream, publishMessagingIndexStream, pushSw } from '../../../../../event';
+import html from '../../../../../text/html';
+import parse from '../../../../../text/parse';
 import config from '../../../../../conf';
 
 /**
diff --git a/src/server/api/endpoints/notifications/mark_as_read_all.ts b/src/server/api/endpoints/notifications/mark_as_read_all.ts
index f9bc6ebf7..3ba00a907 100644
--- a/src/server/api/endpoints/notifications/mark_as_read_all.ts
+++ b/src/server/api/endpoints/notifications/mark_as_read_all.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import Notification from '../../../../models/notification';
-import event from '../../../../common/event';
+import event from '../../../../event';
 
 /**
  * Mark as read all notifications
diff --git a/src/server/api/endpoints/othello/games/show.ts b/src/server/api/endpoints/othello/games/show.ts
index 0b1c9bc35..dd886936d 100644
--- a/src/server/api/endpoints/othello/games/show.ts
+++ b/src/server/api/endpoints/othello/games/show.ts
@@ -1,6 +1,6 @@
 import $ from 'cafy';
 import OthelloGame, { pack } from '../../../../../models/othello-game';
-import Othello from '../../../../../misc/othello/core';
+import Othello from '../../../../../othello/core';
 
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Get 'gameId' parameter
diff --git a/src/server/api/endpoints/othello/match.ts b/src/server/api/endpoints/othello/match.ts
index d9c46aae1..1fc46ab90 100644
--- a/src/server/api/endpoints/othello/match.ts
+++ b/src/server/api/endpoints/othello/match.ts
@@ -2,8 +2,8 @@ import $ from 'cafy';
 import Matching, { pack as packMatching } from '../../../../models/othello-matching';
 import OthelloGame, { pack as packGame } from '../../../../models/othello-game';
 import User from '../../../../models/user';
-import publishUserStream, { publishOthelloStream } from '../../../../common/event';
-import { eighteight } from '../../../../misc/othello/maps';
+import publishUserStream, { publishOthelloStream } from '../../../../event';
+import { eighteight } from '../../../../othello/maps';
 
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Get 'userId' parameter
diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts
index c2e790a24..e74ac7582 100644
--- a/src/server/api/endpoints/posts/create.ts
+++ b/src/server/api/endpoints/posts/create.ts
@@ -3,8 +3,8 @@
  */
 import $ from 'cafy';
 import deepEqual = require('deep-equal');
-import html from '../../../../common/text/html';
-import parse from '../../../../common/text/parse';
+import html from '../../../../text/html';
+import parse from '../../../../text/parse';
 import Post, { IPost, isValidText, isValidCw } from '../../../../models/post';
 import User, { ILocalUser } from '../../../../models/user';
 import Channel, { IChannel } from '../../../../models/channel';
@@ -15,10 +15,10 @@ import Watching from '../../../../models/post-watching';
 import ChannelWatching from '../../../../models/channel-watching';
 import { pack } from '../../../../models/post';
 import watch from '../../common/watch-post';
-import event, { pushSw, publishChannelStream } from '../../../../common/event';
-import notify from '../../../../common/notify';
-import getAcct from '../../../../misc/user/get-acct';
-import parseAcct from '../../../../misc/user/parse-acct';
+import event, { pushSw, publishChannelStream } from '../../../../event';
+import notify from '../../../../notify';
+import getAcct from '../../../../user/get-acct';
+import parseAcct from '../../../../user/parse-acct';
 import config from '../../../../conf';
 
 /**
diff --git a/src/server/api/endpoints/posts/polls/vote.ts b/src/server/api/endpoints/posts/polls/vote.ts
index 59b1f099f..be1fd7b5d 100644
--- a/src/server/api/endpoints/posts/polls/vote.ts
+++ b/src/server/api/endpoints/posts/polls/vote.ts
@@ -6,8 +6,8 @@ import Vote from '../../../../../models/poll-vote';
 import Post from '../../../../../models/post';
 import Watching from '../../../../../models/post-watching';
 import watch from '../../../common/watch-post';
-import { publishPostStream } from '../../../../../common/event';
-import notify from '../../../../../common/notify';
+import { publishPostStream } from '../../../../../event';
+import notify from '../../../../../notify';
 
 /**
  * Vote poll of a post
diff --git a/src/server/api/endpoints/posts/reactions/create.ts b/src/server/api/endpoints/posts/reactions/create.ts
index 441d56383..408b2483a 100644
--- a/src/server/api/endpoints/posts/reactions/create.ts
+++ b/src/server/api/endpoints/posts/reactions/create.ts
@@ -7,8 +7,8 @@ import Post, { pack as packPost } from '../../../../../models/post';
 import { pack as packUser } from '../../../../../models/user';
 import Watching from '../../../../../models/post-watching';
 import watch from '../../../common/watch-post';
-import { publishPostStream, pushSw } from '../../../../../common/event';
-import notify from '../../../../../common/notify';
+import { publishPostStream, pushSw } from '../../../../../event';
+import notify from '../../../../../notify';
 
 /**
  * React to a post
diff --git a/src/server/api/limitter.ts b/src/server/api/limitter.ts
index 93e03f5e4..81016457d 100644
--- a/src/server/api/limitter.ts
+++ b/src/server/api/limitter.ts
@@ -3,7 +3,7 @@ import * as debug from 'debug';
 import limiterDB from '../../db/redis';
 import { Endpoint } from './endpoints';
 import { IAuthContext } from './authenticate';
-import getAcct from '../../misc/user/get-acct';
+import getAcct from '../../user/get-acct';
 
 const log = debug('misskey:limitter');
 
diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts
index 4ad5097e5..99ba51b41 100644
--- a/src/server/api/private/signin.ts
+++ b/src/server/api/private/signin.ts
@@ -3,7 +3,7 @@ import * as bcrypt from 'bcryptjs';
 import * as speakeasy from 'speakeasy';
 import User, { ILocalUser } from '../../../models/user';
 import Signin, { pack } from '../../../models/signin';
-import event from '../../../common/event';
+import event from '../../../event';
 import signin from '../common/signin';
 import config from '../../../conf';
 
diff --git a/src/server/api/service/twitter.ts b/src/server/api/service/twitter.ts
index 73822b0bd..61a1d5639 100644
--- a/src/server/api/service/twitter.ts
+++ b/src/server/api/service/twitter.ts
@@ -6,7 +6,7 @@ import * as uuid from 'uuid';
 import autwh from 'autwh';
 import redis from '../../../db/redis';
 import User, { pack } from '../../../models/user';
-import event from '../../../common/event';
+import event from '../../../event';
 import config from '../../../conf';
 import signin from '../common/signin';
 
diff --git a/src/server/api/stream/othello-game.ts b/src/server/api/stream/othello-game.ts
index 00367a824..6eb610331 100644
--- a/src/server/api/stream/othello-game.ts
+++ b/src/server/api/stream/othello-game.ts
@@ -2,9 +2,9 @@ import * as websocket from 'websocket';
 import * as redis from 'redis';
 import * as CRC32 from 'crc-32';
 import OthelloGame, { pack } from '../../../models/othello-game';
-import { publishOthelloGameStream } from '../../../common/event';
-import Othello from '../../../misc/othello/core';
-import * as maps from '../../../misc/othello/maps';
+import { publishOthelloGameStream } from '../../../event';
+import Othello from '../../../othello/core';
+import * as maps from '../../../othello/maps';
 import { ParsedUrlQuery } from 'querystring';
 
 export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user?: any): void {
diff --git a/src/server/api/stream/othello.ts b/src/server/api/stream/othello.ts
index 1cf9a1494..4c292056d 100644
--- a/src/server/api/stream/othello.ts
+++ b/src/server/api/stream/othello.ts
@@ -2,7 +2,7 @@ import * as mongo from 'mongodb';
 import * as websocket from 'websocket';
 import * as redis from 'redis';
 import Matching, { pack } from '../../../models/othello-matching';
-import publishUserStream from '../../../common/event';
+import publishUserStream from '../../../event';
 
 export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void {
 	// Subscribe othello stream
diff --git a/src/server/webfinger.ts b/src/server/webfinger.ts
index 41aeb5642..7a28b1bec 100644
--- a/src/server/webfinger.ts
+++ b/src/server/webfinger.ts
@@ -1,5 +1,5 @@
 import config from '../conf';
-import parseAcct from '../misc/user/parse-acct';
+import parseAcct from '../user/parse-acct';
 import User from '../models/user';
 const express = require('express');
 
diff --git a/src/common/text/html.ts b/src/text/html.ts
similarity index 100%
rename from src/common/text/html.ts
rename to src/text/html.ts
diff --git a/src/common/text/parse/core/syntax-highlighter.ts b/src/text/parse/core/syntax-highlighter.ts
similarity index 100%
rename from src/common/text/parse/core/syntax-highlighter.ts
rename to src/text/parse/core/syntax-highlighter.ts
diff --git a/src/common/text/parse/elements/bold.ts b/src/text/parse/elements/bold.ts
similarity index 100%
rename from src/common/text/parse/elements/bold.ts
rename to src/text/parse/elements/bold.ts
diff --git a/src/common/text/parse/elements/code.ts b/src/text/parse/elements/code.ts
similarity index 100%
rename from src/common/text/parse/elements/code.ts
rename to src/text/parse/elements/code.ts
diff --git a/src/common/text/parse/elements/emoji.ts b/src/text/parse/elements/emoji.ts
similarity index 100%
rename from src/common/text/parse/elements/emoji.ts
rename to src/text/parse/elements/emoji.ts
diff --git a/src/common/text/parse/elements/hashtag.ts b/src/text/parse/elements/hashtag.ts
similarity index 100%
rename from src/common/text/parse/elements/hashtag.ts
rename to src/text/parse/elements/hashtag.ts
diff --git a/src/common/text/parse/elements/inline-code.ts b/src/text/parse/elements/inline-code.ts
similarity index 100%
rename from src/common/text/parse/elements/inline-code.ts
rename to src/text/parse/elements/inline-code.ts
diff --git a/src/common/text/parse/elements/link.ts b/src/text/parse/elements/link.ts
similarity index 100%
rename from src/common/text/parse/elements/link.ts
rename to src/text/parse/elements/link.ts
diff --git a/src/common/text/parse/elements/mention.ts b/src/text/parse/elements/mention.ts
similarity index 83%
rename from src/common/text/parse/elements/mention.ts
rename to src/text/parse/elements/mention.ts
index 3c81979d0..732554d8b 100644
--- a/src/common/text/parse/elements/mention.ts
+++ b/src/text/parse/elements/mention.ts
@@ -1,7 +1,7 @@
 /**
  * Mention
  */
-import parseAcct from '../../../../misc/user/parse-acct';
+import parseAcct from '../../../user/parse-acct';
 
 module.exports = text => {
 	const match = text.match(/^(?:@[a-zA-Z0-9\-]+){1,2}/);
diff --git a/src/common/text/parse/elements/quote.ts b/src/text/parse/elements/quote.ts
similarity index 100%
rename from src/common/text/parse/elements/quote.ts
rename to src/text/parse/elements/quote.ts
diff --git a/src/common/text/parse/elements/url.ts b/src/text/parse/elements/url.ts
similarity index 100%
rename from src/common/text/parse/elements/url.ts
rename to src/text/parse/elements/url.ts
diff --git a/src/common/text/parse/index.ts b/src/text/parse/index.ts
similarity index 100%
rename from src/common/text/parse/index.ts
rename to src/text/parse/index.ts
diff --git a/src/misc/user/get-acct.ts b/src/user/get-acct.ts
similarity index 100%
rename from src/misc/user/get-acct.ts
rename to src/user/get-acct.ts
diff --git a/src/misc/user/get-summary.ts b/src/user/get-summary.ts
similarity index 90%
rename from src/misc/user/get-summary.ts
rename to src/user/get-summary.ts
index 2c71d3eae..822081702 100644
--- a/src/misc/user/get-summary.ts
+++ b/src/user/get-summary.ts
@@ -1,4 +1,4 @@
-import { IUser, isLocalUser } from '../../models/user';
+import { IUser, isLocalUser } from '../models/user';
 import getAcct from './get-acct';
 
 /**
diff --git a/src/misc/user/parse-acct.ts b/src/user/parse-acct.ts
similarity index 100%
rename from src/misc/user/parse-acct.ts
rename to src/user/parse-acct.ts

From 8b3407e16c6ec8798133463e6cbf39e6adbac726 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Mon, 2 Apr 2018 13:15:53 +0900
Subject: [PATCH 1011/1250] Introduce config module

---
 gulpfile.ts                                   |   2 +-
 src/client/docs/api/gulpfile.ts               |   2 +-
 src/client/docs/vars.ts                       |   2 +-
 src/conf.ts                                   |   3 -
 src/config.ts                                 | 154 ------------------
 src/config/index.ts                           |   3 +
 src/config/load.ts                            |  57 +++++++
 src/config/types.ts                           |  97 +++++++++++
 src/db/elasticsearch.ts                       |   2 +-
 src/db/mongodb.ts                             |   2 +-
 src/db/redis.ts                               |   2 +-
 src/drive/add-file.ts                         |   2 +-
 src/event.ts                                  |   2 +-
 src/index.ts                                  |  19 ++-
 src/models/app.ts                             |   2 +-
 src/models/drive-file.ts                      |   2 +-
 src/models/user.ts                            |   2 +-
 src/othello/ai/back.ts                        |   2 +-
 src/othello/ai/front.ts                       |   2 +-
 src/processor/http/follow.ts                  |   2 +-
 src/push-sw.ts                                |   2 +-
 src/queue.ts                                  |   2 +-
 src/remote/activitypub/create.ts              |   2 +-
 src/remote/activitypub/renderer/document.ts   |   2 +-
 src/remote/activitypub/renderer/follow.ts     |   2 +-
 src/remote/activitypub/renderer/hashtag.ts    |   2 +-
 src/remote/activitypub/renderer/image.ts      |   2 +-
 src/remote/activitypub/renderer/key.ts        |   2 +-
 src/remote/activitypub/renderer/note.ts       |   2 +-
 src/remote/activitypub/renderer/person.ts     |   2 +-
 src/server/activitypub/outbox.ts              |   2 +-
 src/server/activitypub/publickey.ts           |   2 +-
 src/server/activitypub/user.ts                |   2 +-
 src/server/api/bot/interfaces/line.ts         |   2 +-
 src/server/api/common/signin.ts               |   2 +-
 .../api/endpoints/auth/session/generate.ts    |   2 +-
 src/server/api/endpoints/i/2fa/register.ts    |   2 +-
 src/server/api/endpoints/i/update.ts          |   2 +-
 .../endpoints/messaging/messages/create.ts    |   2 +-
 src/server/api/endpoints/meta.ts              |   2 +-
 src/server/api/endpoints/posts/create.ts      |   2 +-
 src/server/api/endpoints/users/search.ts      |   2 +-
 src/server/api/private/signin.ts              |   2 +-
 src/server/api/private/signup.ts              |   2 +-
 src/server/api/service/github.ts              |   2 +-
 src/server/api/service/twitter.ts             |   2 +-
 src/server/api/streaming.ts                   |   2 +-
 src/server/index.ts                           |   2 +-
 src/server/webfinger.ts                       |   2 +-
 webpack.config.ts                             |   2 +-
 50 files changed, 213 insertions(+), 208 deletions(-)
 delete mode 100644 src/conf.ts
 delete mode 100644 src/config.ts
 create mode 100644 src/config/index.ts
 create mode 100644 src/config/load.ts
 create mode 100644 src/config/types.ts

diff --git a/gulpfile.ts b/gulpfile.ts
index a6e9e53df..f372ed299 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -24,7 +24,7 @@ const uglifyes = require('uglify-es');
 
 import { fa } from './src/build/fa';
 import version from './src/version';
-import config from './src/conf';
+import config from './src/config';
 
 const uglify = uglifyComposer(uglifyes, console);
 
diff --git a/src/client/docs/api/gulpfile.ts b/src/client/docs/api/gulpfile.ts
index 4b962fe0c..c986e0353 100644
--- a/src/client/docs/api/gulpfile.ts
+++ b/src/client/docs/api/gulpfile.ts
@@ -13,7 +13,7 @@ import * as mkdirp from 'mkdirp';
 import locales from '../../../../locales';
 import I18nReplacer from '../../../build/i18n';
 import fa from '../../../build/fa';
-import config from './../../../conf';
+import config from './../../../config';
 
 import generateVars from '../vars';
 
diff --git a/src/client/docs/vars.ts b/src/client/docs/vars.ts
index 1a3b48bd7..dbdc88061 100644
--- a/src/client/docs/vars.ts
+++ b/src/client/docs/vars.ts
@@ -6,7 +6,7 @@ import * as licenseChecker from 'license-checker';
 import * as tmp from 'tmp';
 
 import { fa } from '../../build/fa';
-import config from '../../conf';
+import config from '../../config';
 import { licenseHtml } from '../../build/license';
 const constants = require('../../const.json');
 
diff --git a/src/conf.ts b/src/conf.ts
deleted file mode 100644
index b04a4c859..000000000
--- a/src/conf.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import load from './config';
-
-export default load();
diff --git a/src/config.ts b/src/config.ts
deleted file mode 100644
index 6d3e7740b..000000000
--- a/src/config.ts
+++ /dev/null
@@ -1,154 +0,0 @@
-/**
- * Config loader
- */
-
-import * as fs from 'fs';
-import { URL } from 'url';
-import * as yaml from 'js-yaml';
-import isUrl = require('is-url');
-
-/**
- * Path of configuration directory
- */
-const dir = `${__dirname}/../.config`;
-
-/**
- * Path of configuration file
- */
-export const path = process.env.NODE_ENV == 'test'
-	? `${dir}/test.yml`
-	: `${dir}/default.yml`;
-
-/**
- * ユーザーが設定する必要のある情報
- */
-type Source = {
-	/**
-	 * メンテナ情報
-	 */
-	maintainer: {
-		/**
-		 * メンテナの名前
-		 */
-		name: string;
-		/**
-		 * メンテナの連絡先(URLかmailto形式のURL)
-		 */
-		url: string;
-	};
-	url: string;
-	port: number;
-	https?: { [x: string]: string };
-	mongodb: {
-		host: string;
-		port: number;
-		db: string;
-		user: string;
-		pass: string;
-	};
-	redis: {
-		host: string;
-		port: number;
-		pass: string;
-	};
-	elasticsearch: {
-		enable: boolean;
-		host: string;
-		port: number;
-		pass: string;
-	};
-	recaptcha: {
-		site_key: string;
-		secret_key: string;
-	};
-	accesslog?: string;
-	accesses?: {
-		enable: boolean;
-		port: number;
-	};
-	twitter?: {
-		consumer_key: string;
-		consumer_secret: string;
-	};
-	github_bot?: {
-		hook_secret: string;
-		username: string;
-	};
-	othello_ai?: {
-		id: string;
-		i: string;
-	};
-	line_bot?: {
-		channel_secret: string;
-		channel_access_token: string;
-	};
-	analysis?: {
-		mecab_command?: string;
-	};
-
-	/**
-	 * Service Worker
-	 */
-	sw?: {
-		public_key: string;
-		private_key: string;
-	};
-
-	google_maps_api_key: string;
-};
-
-/**
- * Misskeyが自動的に(ユーザーが設定した情報から推論して)設定する情報
- */
-type Mixin = {
-	host: string;
-	hostname: string;
-	scheme: string;
-	ws_scheme: string;
-	api_url: string;
-	ws_url: string;
-	auth_url: string;
-	docs_url: string;
-	stats_url: string;
-	status_url: string;
-	dev_url: string;
-	drive_url: string;
-};
-
-export type Config = Source & Mixin;
-
-export default function load() {
-	const config = yaml.safeLoad(fs.readFileSync(path, 'utf-8')) as Source;
-
-	const mixin = {} as Mixin;
-
-	// Validate URLs
-	if (!isUrl(config.url)) urlError(config.url);
-
-	const url = new URL(config.url);
-	config.url = normalizeUrl(config.url);
-
-	mixin.host = url.host;
-	mixin.hostname = url.hostname;
-	mixin.scheme = url.protocol.replace(/:$/, '');
-	mixin.ws_scheme = mixin.scheme.replace('http', 'ws');
-	mixin.ws_url = `${mixin.ws_scheme}://${mixin.host}`;
-	mixin.api_url = `${mixin.scheme}://${mixin.host}/api`;
-	mixin.auth_url = `${mixin.scheme}://${mixin.host}/auth`;
-	mixin.dev_url = `${mixin.scheme}://${mixin.host}/dev`;
-	mixin.docs_url = `${mixin.scheme}://${mixin.host}/docs`;
-	mixin.stats_url = `${mixin.scheme}://${mixin.host}/stats`;
-	mixin.status_url = `${mixin.scheme}://${mixin.host}/status`;
-	mixin.drive_url = `${mixin.scheme}://${mixin.host}/files`;
-
-	return Object.assign(config, mixin);
-}
-
-function normalizeUrl(url: string) {
-	return url[url.length - 1] === '/' ? url.substr(0, url.length - 1) : url;
-}
-
-function urlError(url: string) {
-	console.error(`「${url}」は、正しいURLではありません。先頭に http:// または https:// をつけ忘れてないかなど確認してください。`);
-	process.exit();
-}
diff --git a/src/config/index.ts b/src/config/index.ts
new file mode 100644
index 000000000..7bfdca461
--- /dev/null
+++ b/src/config/index.ts
@@ -0,0 +1,3 @@
+import load from './load';
+
+export default load();
diff --git a/src/config/load.ts b/src/config/load.ts
new file mode 100644
index 000000000..9f4e2151f
--- /dev/null
+++ b/src/config/load.ts
@@ -0,0 +1,57 @@
+/**
+ * Config loader
+ */
+
+import * as fs from 'fs';
+import { URL } from 'url';
+import * as yaml from 'js-yaml';
+import { Source, Mixin } from './types';
+import isUrl = require('is-url');
+
+/**
+ * Path of configuration directory
+ */
+const dir = `${__dirname}/../../.config`;
+
+/**
+ * Path of configuration file
+ */
+const path = process.env.NODE_ENV == 'test'
+	? `${dir}/test.yml`
+	: `${dir}/default.yml`;
+
+export default function load() {
+	const config = yaml.safeLoad(fs.readFileSync(path, 'utf-8')) as Source;
+
+	const mixin = {} as Mixin;
+
+	// Validate URLs
+	if (!isUrl(config.url)) urlError(config.url);
+
+	const url = new URL(config.url);
+	config.url = normalizeUrl(config.url);
+
+	mixin.host = url.host;
+	mixin.hostname = url.hostname;
+	mixin.scheme = url.protocol.replace(/:$/, '');
+	mixin.ws_scheme = mixin.scheme.replace('http', 'ws');
+	mixin.ws_url = `${mixin.ws_scheme}://${mixin.host}`;
+	mixin.api_url = `${mixin.scheme}://${mixin.host}/api`;
+	mixin.auth_url = `${mixin.scheme}://${mixin.host}/auth`;
+	mixin.dev_url = `${mixin.scheme}://${mixin.host}/dev`;
+	mixin.docs_url = `${mixin.scheme}://${mixin.host}/docs`;
+	mixin.stats_url = `${mixin.scheme}://${mixin.host}/stats`;
+	mixin.status_url = `${mixin.scheme}://${mixin.host}/status`;
+	mixin.drive_url = `${mixin.scheme}://${mixin.host}/files`;
+
+	return Object.assign(config, mixin);
+}
+
+function normalizeUrl(url: string) {
+	return url[url.length - 1] === '/' ? url.substr(0, url.length - 1) : url;
+}
+
+function urlError(url: string) {
+	console.error(`「${url}」は、正しいURLではありません。先頭に http:// または https:// をつけ忘れてないかなど確認してください。`);
+	process.exit();
+}
diff --git a/src/config/types.ts b/src/config/types.ts
new file mode 100644
index 000000000..f802e70d1
--- /dev/null
+++ b/src/config/types.ts
@@ -0,0 +1,97 @@
+/**
+ * ユーザーが設定する必要のある情報
+ */
+export type Source = {
+	/**
+	 * メンテナ情報
+	 */
+	maintainer: {
+		/**
+		 * メンテナの名前
+		 */
+		name: string;
+		/**
+		 * メンテナの連絡先(URLかmailto形式のURL)
+		 */
+		url: string;
+	};
+	url: string;
+	port: number;
+	https?: { [x: string]: string };
+	mongodb: {
+		host: string;
+		port: number;
+		db: string;
+		user: string;
+		pass: string;
+	};
+	redis: {
+		host: string;
+		port: number;
+		pass: string;
+	};
+	elasticsearch: {
+		enable: boolean;
+		host: string;
+		port: number;
+		pass: string;
+	};
+	recaptcha: {
+		site_key: string;
+		secret_key: string;
+	};
+	accesslog?: string;
+	accesses?: {
+		enable: boolean;
+		port: number;
+	};
+	twitter?: {
+		consumer_key: string;
+		consumer_secret: string;
+	};
+	github_bot?: {
+		hook_secret: string;
+		username: string;
+	};
+	othello_ai?: {
+		id: string;
+		i: string;
+	};
+	line_bot?: {
+		channel_secret: string;
+		channel_access_token: string;
+	};
+	analysis?: {
+		mecab_command?: string;
+	};
+
+	/**
+	 * Service Worker
+	 */
+	sw?: {
+		public_key: string;
+		private_key: string;
+	};
+
+	google_maps_api_key: string;
+};
+
+/**
+ * Misskeyが自動的に(ユーザーが設定した情報から推論して)設定する情報
+ */
+export type Mixin = {
+	host: string;
+	hostname: string;
+	scheme: string;
+	ws_scheme: string;
+	api_url: string;
+	ws_url: string;
+	auth_url: string;
+	docs_url: string;
+	stats_url: string;
+	status_url: string;
+	dev_url: string;
+	drive_url: string;
+};
+
+export type Config = Source & Mixin;
diff --git a/src/db/elasticsearch.ts b/src/db/elasticsearch.ts
index 75054a31c..957b7ad97 100644
--- a/src/db/elasticsearch.ts
+++ b/src/db/elasticsearch.ts
@@ -1,5 +1,5 @@
 import * as elasticsearch from 'elasticsearch';
-import config from '../conf';
+import config from '../config';
 
 // Init ElasticSearch connection
 const client = new elasticsearch.Client({
diff --git a/src/db/mongodb.ts b/src/db/mongodb.ts
index 233f2f3d7..05bb72bfd 100644
--- a/src/db/mongodb.ts
+++ b/src/db/mongodb.ts
@@ -1,4 +1,4 @@
-import config from '../conf';
+import config from '../config';
 
 const u = config.mongodb.user ? encodeURIComponent(config.mongodb.user) : null;
 const p = config.mongodb.pass ? encodeURIComponent(config.mongodb.pass) : null;
diff --git a/src/db/redis.ts b/src/db/redis.ts
index 2e0867de6..f8d66ebda 100644
--- a/src/db/redis.ts
+++ b/src/db/redis.ts
@@ -1,5 +1,5 @@
 import * as redis from 'redis';
-import config from '../conf';
+import config from '../config';
 
 export default redis.createClient(
 	config.redis.port,
diff --git a/src/drive/add-file.ts b/src/drive/add-file.ts
index f356af00d..db13e04be 100644
--- a/src/drive/add-file.ts
+++ b/src/drive/add-file.ts
@@ -15,7 +15,7 @@ import DriveFolder from '../models/drive-folder';
 import { pack } from '../models/drive-file';
 import event, { publishDriveStream } from '../event';
 import getAcct from '../user/get-acct';
-import config from '../conf';
+import config from '../config';
 
 const gm = _gm.subClass({
 	imageMagick: true
diff --git a/src/event.ts b/src/event.ts
index df79f0c90..81876b3cf 100644
--- a/src/event.ts
+++ b/src/event.ts
@@ -1,7 +1,7 @@
 import * as mongo from 'mongodb';
 import * as redis from 'redis';
 import swPush from './push-sw';
-import config from './conf';
+import config from './config';
 
 type ID = string | mongo.ObjectID;
 
diff --git a/src/index.ts b/src/index.ts
index f86c768fd..b43e15285 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -4,7 +4,6 @@
 
 Error.stackTraceLimit = Infinity;
 
-import * as fs from 'fs';
 import * as os from 'os';
 import * as cluster from 'cluster';
 import * as debug from 'debug';
@@ -21,8 +20,8 @@ import MachineInfo from './utils/machineInfo';
 import DependencyInfo from './utils/dependencyInfo';
 import stats from './utils/stats';
 
-import { Config, path as configPath } from './config';
-import loadConfig from './config';
+import loadConfig from './config/load';
+import { Config } from './config/types';
 
 import parseOpt from './parse-opt';
 
@@ -116,11 +115,17 @@ async function init(): Promise<Config> {
 	new DependencyInfo().showAll();
 
 	const configLogger = new Logger('Config');
-	if (!fs.existsSync(configPath)) {
-		throw 'Configuration not found - Please run "npm run config" command.';
-	}
+	let config;
 
-	const config = loadConfig();
+	try {
+		config = loadConfig();
+	} catch (exception) {
+		if (exception.code === 'ENOENT') {
+			throw 'Configuration not found - Please run "npm run config" command.';
+		}
+
+		throw exception;
+	}
 
 	configLogger.info('Successfully loaded');
 	configLogger.info(`maintainer: ${config.maintainer}`);
diff --git a/src/models/app.ts b/src/models/app.ts
index 3b80a1602..83162a6b9 100644
--- a/src/models/app.ts
+++ b/src/models/app.ts
@@ -2,7 +2,7 @@ import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
 import AccessToken from './access-token';
 import db from '../db/mongodb';
-import config from '../conf';
+import config from '../config';
 
 const App = db.get<IApp>('apps');
 App.createIndex('nameId');
diff --git a/src/models/drive-file.ts b/src/models/drive-file.ts
index 9e0df58c4..fba1aebda 100644
--- a/src/models/drive-file.ts
+++ b/src/models/drive-file.ts
@@ -1,7 +1,7 @@
 import * as mongodb from 'mongodb';
 import deepcopy = require('deepcopy');
 import { pack as packFolder } from './drive-folder';
-import config from '../conf';
+import config from '../config';
 import monkDb, { nativeDbConn } from '../db/mongodb';
 
 const DriveFile = monkDb.get<IDriveFile>('driveFiles.files');
diff --git a/src/models/user.ts b/src/models/user.ts
index c9a25a35e..d3c94cab3 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -6,7 +6,7 @@ import { IPost, pack as packPost } from './post';
 import Following from './following';
 import Mute from './mute';
 import getFriends from '../server/api/common/get-friends';
-import config from '../conf';
+import config from '../config';
 
 const User = db.get<IUser>('users');
 
diff --git a/src/othello/ai/back.ts b/src/othello/ai/back.ts
index a67f6fe83..d6704b175 100644
--- a/src/othello/ai/back.ts
+++ b/src/othello/ai/back.ts
@@ -8,7 +8,7 @@
 
 import * as request from 'request-promise-native';
 import Othello, { Color } from '../core';
-import conf from '../../conf';
+import conf from '../../config';
 
 let game;
 let form;
diff --git a/src/othello/ai/front.ts b/src/othello/ai/front.ts
index 3414d6434..9d0b5f980 100644
--- a/src/othello/ai/front.ts
+++ b/src/othello/ai/front.ts
@@ -10,7 +10,7 @@ import * as childProcess from 'child_process';
 const WebSocket = require('ws');
 import * as ReconnectingWebSocket from 'reconnecting-websocket';
 import * as request from 'request-promise-native';
-import conf from '../../conf';
+import conf from '../../config';
 
 // 設定 ////////////////////////////////////////////////////////
 
diff --git a/src/processor/http/follow.ts b/src/processor/http/follow.ts
index e81b410cb..d6ce00006 100644
--- a/src/processor/http/follow.ts
+++ b/src/processor/http/follow.ts
@@ -7,7 +7,7 @@ import event from '../../event';
 import notify from '../../notify';
 import context from '../../remote/activitypub/renderer/context';
 import render from '../../remote/activitypub/renderer/follow';
-import config from '../../conf';
+import config from '../../config';
 
 export default ({ data }, done) => Following.findOne({ _id: data.following }).then(({ followerId, followeeId }) => {
 	const promisedFollower: Promise<ILocalUser> = User.findOne({ _id: followerId });
diff --git a/src/push-sw.ts b/src/push-sw.ts
index 5b5ec5207..fcef7796d 100644
--- a/src/push-sw.ts
+++ b/src/push-sw.ts
@@ -1,7 +1,7 @@
 const push = require('web-push');
 import * as mongo from 'mongodb';
 import Subscription from './models/sw-subscription';
-import config from './conf';
+import config from './config';
 
 if (config.sw) {
 	// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録
diff --git a/src/queue.ts b/src/queue.ts
index 6089e0a7f..08ea13c2a 100644
--- a/src/queue.ts
+++ b/src/queue.ts
@@ -1,5 +1,5 @@
 import { createQueue } from 'kue';
-import config from './conf';
+import config from './config';
 
 export default createQueue({
 	redis: {
diff --git a/src/remote/activitypub/create.ts b/src/remote/activitypub/create.ts
index 6d135685e..8ea8a85fd 100644
--- a/src/remote/activitypub/create.ts
+++ b/src/remote/activitypub/create.ts
@@ -1,5 +1,5 @@
 import { JSDOM } from 'jsdom';
-import config from '../../conf';
+import config from '../../config';
 import Post from '../../models/post';
 import RemoteUserObject, { IRemoteUserObject } from '../../models/remote-user-object';
 import uploadFromUrl from '../../drive/upload-from-url';
diff --git a/src/remote/activitypub/renderer/document.ts b/src/remote/activitypub/renderer/document.ts
index fdd52c1b6..91a9f7df3 100644
--- a/src/remote/activitypub/renderer/document.ts
+++ b/src/remote/activitypub/renderer/document.ts
@@ -1,4 +1,4 @@
-import config from '../../../conf';
+import config from '../../../config';
 
 export default ({ _id, contentType }) => ({
 	type: 'Document',
diff --git a/src/remote/activitypub/renderer/follow.ts b/src/remote/activitypub/renderer/follow.ts
index c99bc375a..6d1ded9a9 100644
--- a/src/remote/activitypub/renderer/follow.ts
+++ b/src/remote/activitypub/renderer/follow.ts
@@ -1,4 +1,4 @@
-import config from '../../../conf';
+import config from '../../../config';
 import { IRemoteUser } from '../../../models/user';
 
 export default ({ username }, followee: IRemoteUser) => ({
diff --git a/src/remote/activitypub/renderer/hashtag.ts b/src/remote/activitypub/renderer/hashtag.ts
index c2d261ed2..cf0b07b48 100644
--- a/src/remote/activitypub/renderer/hashtag.ts
+++ b/src/remote/activitypub/renderer/hashtag.ts
@@ -1,4 +1,4 @@
-import config from '../../../conf';
+import config from '../../../config';
 
 export default tag => ({
 	type: 'Hashtag',
diff --git a/src/remote/activitypub/renderer/image.ts b/src/remote/activitypub/renderer/image.ts
index 3d1c71cb9..d671a57e7 100644
--- a/src/remote/activitypub/renderer/image.ts
+++ b/src/remote/activitypub/renderer/image.ts
@@ -1,4 +1,4 @@
-import config from '../../../conf';
+import config from '../../../config';
 
 export default ({ _id }) => ({
 	type: 'Image',
diff --git a/src/remote/activitypub/renderer/key.ts b/src/remote/activitypub/renderer/key.ts
index 904a69e08..85be7b136 100644
--- a/src/remote/activitypub/renderer/key.ts
+++ b/src/remote/activitypub/renderer/key.ts
@@ -1,4 +1,4 @@
-import config from '../../../conf';
+import config from '../../../config';
 import { extractPublic } from '../../../crypto_key';
 import { ILocalUser } from '../../../models/user';
 
diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts
index 74806f14b..43531b121 100644
--- a/src/remote/activitypub/renderer/note.ts
+++ b/src/remote/activitypub/renderer/note.ts
@@ -1,6 +1,6 @@
 import renderDocument from './document';
 import renderHashtag from './hashtag';
-import config from '../../../conf';
+import config from '../../../config';
 import DriveFile from '../../../models/drive-file';
 import Post from '../../../models/post';
 import User from '../../../models/user';
diff --git a/src/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts
index c6c789316..7ea6f532f 100644
--- a/src/remote/activitypub/renderer/person.ts
+++ b/src/remote/activitypub/renderer/person.ts
@@ -1,6 +1,6 @@
 import renderImage from './image';
 import renderKey from './key';
-import config from '../../../conf';
+import config from '../../../config';
 
 export default user => {
 	const id = `${config.url}/@${user.username}`;
diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts
index 2018d8797..9ecb0c071 100644
--- a/src/server/activitypub/outbox.ts
+++ b/src/server/activitypub/outbox.ts
@@ -2,7 +2,7 @@ import * as express from 'express';
 import context from '../../remote/activitypub/renderer/context';
 import renderNote from '../../remote/activitypub/renderer/note';
 import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
-import config from '../../conf';
+import config from '../../config';
 import Post from '../../models/post';
 import withUser from './with-user';
 
diff --git a/src/server/activitypub/publickey.ts b/src/server/activitypub/publickey.ts
index a23d11c15..c564c437e 100644
--- a/src/server/activitypub/publickey.ts
+++ b/src/server/activitypub/publickey.ts
@@ -1,7 +1,7 @@
 import * as express from 'express';
 import context from '../../remote/activitypub/renderer/context';
 import render from '../../remote/activitypub/renderer/key';
-import config from '../../conf';
+import config from '../../config';
 import withUser from './with-user';
 
 const app = express();
diff --git a/src/server/activitypub/user.ts b/src/server/activitypub/user.ts
index 0d0b84bad..baf2dc9a0 100644
--- a/src/server/activitypub/user.ts
+++ b/src/server/activitypub/user.ts
@@ -1,5 +1,5 @@
 import * as express from 'express';
-import config from '../../conf';
+import config from '../../config';
 import context from '../../remote/activitypub/renderer/context';
 import render from '../../remote/activitypub/renderer/person';
 import withUser from './with-user';
diff --git a/src/server/api/bot/interfaces/line.ts b/src/server/api/bot/interfaces/line.ts
index 847b13aa7..27248c9b9 100644
--- a/src/server/api/bot/interfaces/line.ts
+++ b/src/server/api/bot/interfaces/line.ts
@@ -3,7 +3,7 @@ import * as express from 'express';
 import * as request from 'request';
 import * as crypto from 'crypto';
 import User from '../../../../models/user';
-import config from '../../../../conf';
+import config from '../../../../config';
 import BotCore from '../core';
 import _redis from '../../../../db/redis';
 import prominence = require('prominence');
diff --git a/src/server/api/common/signin.ts b/src/server/api/common/signin.ts
index a11ea56c0..f9688790c 100644
--- a/src/server/api/common/signin.ts
+++ b/src/server/api/common/signin.ts
@@ -1,4 +1,4 @@
-import config from '../../../conf';
+import config from '../../../config';
 
 export default function(res, user, redirect: boolean) {
 	const expires = 1000 * 60 * 60 * 24 * 365; // One Year
diff --git a/src/server/api/endpoints/auth/session/generate.ts b/src/server/api/endpoints/auth/session/generate.ts
index ad03e538c..9857b31d8 100644
--- a/src/server/api/endpoints/auth/session/generate.ts
+++ b/src/server/api/endpoints/auth/session/generate.ts
@@ -5,7 +5,7 @@ import * as uuid from 'uuid';
 import $ from 'cafy';
 import App from '../../../../../models/app';
 import AuthSess from '../../../../../models/auth-session';
-import config from '../../../../../conf';
+import config from '../../../../../config';
 
 /**
  * @swagger
diff --git a/src/server/api/endpoints/i/2fa/register.ts b/src/server/api/endpoints/i/2fa/register.ts
index d2683fb61..dc7fb959b 100644
--- a/src/server/api/endpoints/i/2fa/register.ts
+++ b/src/server/api/endpoints/i/2fa/register.ts
@@ -6,7 +6,7 @@ import * as bcrypt from 'bcryptjs';
 import * as speakeasy from 'speakeasy';
 import * as QRCode from 'qrcode';
 import User from '../../../../../models/user';
-import config from '../../../../../conf';
+import config from '../../../../../config';
 
 module.exports = async (params, user) => new Promise(async (res, rej) => {
 	// Get 'password' parameter
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index 76ceede8d..c4ec41339 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../../../models/user';
 import event from '../../../../event';
-import config from '../../../../conf';
+import config from '../../../../config';
 
 /**
  * Update myself
diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts
index e0ec1f216..604da32d2 100644
--- a/src/server/api/endpoints/messaging/messages/create.ts
+++ b/src/server/api/endpoints/messaging/messages/create.ts
@@ -13,7 +13,7 @@ import publishUserStream from '../../../../../event';
 import { publishMessagingStream, publishMessagingIndexStream, pushSw } from '../../../../../event';
 import html from '../../../../../text/html';
 import parse from '../../../../../text/parse';
-import config from '../../../../../conf';
+import config from '../../../../../config';
 
 /**
  * Create a message
diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts
index 4f0ae2a60..70ae7e99c 100644
--- a/src/server/api/endpoints/meta.ts
+++ b/src/server/api/endpoints/meta.ts
@@ -3,7 +3,7 @@
  */
 import * as os from 'os';
 import version from '../../../version';
-import config from '../../../conf';
+import config from '../../../config';
 import Meta from '../../../models/meta';
 
 /**
diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts
index e74ac7582..bf08fe283 100644
--- a/src/server/api/endpoints/posts/create.ts
+++ b/src/server/api/endpoints/posts/create.ts
@@ -19,7 +19,7 @@ import event, { pushSw, publishChannelStream } from '../../../../event';
 import notify from '../../../../notify';
 import getAcct from '../../../../user/get-acct';
 import parseAcct from '../../../../user/parse-acct';
-import config from '../../../../conf';
+import config from '../../../../config';
 
 /**
  * Create a post
diff --git a/src/server/api/endpoints/users/search.ts b/src/server/api/endpoints/users/search.ts
index 335043b02..da30f47c2 100644
--- a/src/server/api/endpoints/users/search.ts
+++ b/src/server/api/endpoints/users/search.ts
@@ -4,7 +4,7 @@
 import * as mongo from 'mongodb';
 import $ from 'cafy';
 import User, { pack } from '../../../../models/user';
-import config from '../../../../conf';
+import config from '../../../../config';
 const escapeRegexp = require('escape-regexp');
 
 /**
diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts
index 99ba51b41..bf883ee27 100644
--- a/src/server/api/private/signin.ts
+++ b/src/server/api/private/signin.ts
@@ -5,7 +5,7 @@ import User, { ILocalUser } from '../../../models/user';
 import Signin, { pack } from '../../../models/signin';
 import event from '../../../event';
 import signin from '../common/signin';
-import config from '../../../conf';
+import config from '../../../config';
 
 export default async (req: express.Request, res: express.Response) => {
 	res.header('Access-Control-Allow-Origin', config.url);
diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts
index 45b978d0b..4203ce526 100644
--- a/src/server/api/private/signup.ts
+++ b/src/server/api/private/signup.ts
@@ -5,7 +5,7 @@ import { generate as generateKeypair } from '../../../crypto_key';
 import recaptcha = require('recaptcha-promise');
 import User, { IUser, validateUsername, validatePassword, pack } from '../../../models/user';
 import generateUserToken from '../common/generate-native-user-token';
-import config from '../../../conf';
+import config from '../../../config';
 
 recaptcha.init({
 	secret_key: config.recaptcha.secret_key
diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts
index b4068c729..4fd59c2a9 100644
--- a/src/server/api/service/github.ts
+++ b/src/server/api/service/github.ts
@@ -2,7 +2,7 @@ import * as EventEmitter from 'events';
 import * as express from 'express';
 //const crypto = require('crypto');
 import User from '../../../models/user';
-import config from '../../../conf';
+import config from '../../../config';
 import queue from '../../../queue';
 
 module.exports = async (app: express.Application) => {
diff --git a/src/server/api/service/twitter.ts b/src/server/api/service/twitter.ts
index 61a1d5639..69fa5f3c6 100644
--- a/src/server/api/service/twitter.ts
+++ b/src/server/api/service/twitter.ts
@@ -7,7 +7,7 @@ import autwh from 'autwh';
 import redis from '../../../db/redis';
 import User, { pack } from '../../../models/user';
 import event from '../../../event';
-import config from '../../../conf';
+import config from '../../../config';
 import signin from '../common/signin';
 
 module.exports = (app: express.Application) => {
diff --git a/src/server/api/streaming.ts b/src/server/api/streaming.ts
index c86c6a8b4..edcf505d2 100644
--- a/src/server/api/streaming.ts
+++ b/src/server/api/streaming.ts
@@ -1,7 +1,7 @@
 import * as http from 'http';
 import * as websocket from 'websocket';
 import * as redis from 'redis';
-import config from '../../conf';
+import config from '../../config';
 import { default as User, IUser } from '../../models/user';
 import AccessToken from '../../models/access-token';
 import isNativeToken from './common/is-native-token';
diff --git a/src/server/index.ts b/src/server/index.ts
index 187479011..61a8739b4 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -12,7 +12,7 @@ import Accesses from 'accesses';
 import activityPub from './activitypub';
 import webFinger from './webfinger';
 import log from './log-request';
-import config from '../conf';
+import config from '../config';
 
 /**
  * Init app
diff --git a/src/server/webfinger.ts b/src/server/webfinger.ts
index 7a28b1bec..3b0f416d6 100644
--- a/src/server/webfinger.ts
+++ b/src/server/webfinger.ts
@@ -1,4 +1,4 @@
-import config from '../conf';
+import config from '../config';
 import parseAcct from '../user/parse-acct';
 import User from '../models/user';
 const express = require('express');
diff --git a/webpack.config.ts b/webpack.config.ts
index d486e100a..60dbfd2ff 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -14,7 +14,7 @@ const ProgressBarPlugin = require('progress-bar-webpack-plugin');
 import I18nReplacer from './src/build/i18n';
 import { pattern as faPattern, replacement as faReplacement } from './src/build/fa';
 const constants = require('./src/const.json');
-import config from './src/conf';
+import config from './src/config';
 import { licenseHtml } from './src/build/license';
 
 import locales from './locales';

From 8f593d27dcb79d5b3bba3b247510eefec025eed3 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Mon, 2 Apr 2018 13:20:03 +0900
Subject: [PATCH 1012/1250] Fix built module references

---
 test/text.js                   | 4 ++--
 tools/migration/nighthike/7.js | 4 ++--
 tools/migration/nighthike/8.js | 4 ++--
 3 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/test/text.js b/test/text.js
index 0338bcefb..711aad816 100644
--- a/test/text.js
+++ b/test/text.js
@@ -4,8 +4,8 @@
 
 const assert = require('assert');
 
-const analyze = require('../built/common/text/parse').default;
-const syntaxhighlighter = require('../built/common/text/parse/core/syntax-highlighter').default;
+const analyze = require('../built/text/parse').default;
+const syntaxhighlighter = require('../built/text/parse/core/syntax-highlighter').default;
 
 describe('Text', () => {
 	it('can be analyzed', () => {
diff --git a/tools/migration/nighthike/7.js b/tools/migration/nighthike/7.js
index c8efb8952..4463a6e9f 100644
--- a/tools/migration/nighthike/7.js
+++ b/tools/migration/nighthike/7.js
@@ -2,8 +2,8 @@
 
 const { default: Post } = require('../../../built/api/models/post');
 const { default: zip } = require('@prezzemolo/zip')
-const html = require('../../../built/common/text/html').default;
-const parse = require('../../../built/common/text/parse').default;
+const html = require('../../../built/text/html').default;
+const parse = require('../../../built/text/parse').default;
 
 const migrate = async (post) => {
 	const result = await Post.update(post._id, {
diff --git a/tools/migration/nighthike/8.js b/tools/migration/nighthike/8.js
index 5e0cf9507..e8743987b 100644
--- a/tools/migration/nighthike/8.js
+++ b/tools/migration/nighthike/8.js
@@ -2,8 +2,8 @@
 
 const { default: Message } = require('../../../built/api/models/message');
 const { default: zip } = require('@prezzemolo/zip')
-const html = require('../../../built/common/text/html').default;
-const parse = require('../../../built/common/text/parse').default;
+const html = require('../../../built/text/html').default;
+const parse = require('../../../built/text/parse').default;
 
 const migrate = async (message) => {
 	const result = await Message.update(message._id, {

From e8d54d0175662372d837ce9ede1194dfb16647ab Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Mon, 2 Apr 2018 13:33:46 +0900
Subject: [PATCH 1013/1250] Introduce publishers directory

---
 src/drive/add-file.ts                             |  2 +-
 src/processor/http/follow.ts                      |  4 ++--
 src/{ => publishers}/notify.ts                    | 12 ++++++------
 src/{ => publishers}/push-sw.ts                   |  4 ++--
 src/{event.ts => publishers/stream.ts}            |  9 +--------
 src/server/api/common/read-messaging-message.ts   |  6 +++---
 src/server/api/common/read-notification.ts        |  2 +-
 src/server/api/endpoints/drive/files/update.ts    |  2 +-
 src/server/api/endpoints/drive/folders/create.ts  |  2 +-
 src/server/api/endpoints/drive/folders/update.ts  |  2 +-
 src/server/api/endpoints/following/delete.ts      |  2 +-
 src/server/api/endpoints/i/regenerate_token.ts    |  2 +-
 src/server/api/endpoints/i/update.ts              |  2 +-
 .../api/endpoints/i/update_client_setting.ts      |  2 +-
 src/server/api/endpoints/i/update_home.ts         |  2 +-
 src/server/api/endpoints/i/update_mobile_home.ts  |  2 +-
 .../api/endpoints/messaging/messages/create.ts    |  5 +++--
 .../endpoints/notifications/mark_as_read_all.ts   |  2 +-
 src/server/api/endpoints/othello/match.ts         |  2 +-
 src/server/api/endpoints/posts/create.ts          | 15 ++++++++-------
 src/server/api/endpoints/posts/polls/vote.ts      |  4 ++--
 .../api/endpoints/posts/reactions/create.ts       |  5 +++--
 .../api/endpoints/posts/reactions/delete.ts       |  2 +-
 src/server/api/private/signin.ts                  |  2 +-
 src/server/api/service/twitter.ts                 |  2 +-
 src/server/api/stream/othello-game.ts             |  2 +-
 src/server/api/stream/othello.ts                  |  2 +-
 27 files changed, 48 insertions(+), 52 deletions(-)
 rename src/{ => publishers}/notify.ts (81%)
 rename src/{ => publishers}/push-sw.ts (92%)
 rename src/{event.ts => publishers/stream.ts} (92%)

diff --git a/src/drive/add-file.ts b/src/drive/add-file.ts
index db13e04be..4a718a9da 100644
--- a/src/drive/add-file.ts
+++ b/src/drive/add-file.ts
@@ -13,7 +13,7 @@ import prominence = require('prominence');
 import DriveFile, { getGridFSBucket } from '../models/drive-file';
 import DriveFolder from '../models/drive-folder';
 import { pack } from '../models/drive-file';
-import event, { publishDriveStream } from '../event';
+import event, { publishDriveStream } from '../publishers/stream';
 import getAcct from '../user/get-acct';
 import config from '../config';
 
diff --git a/src/processor/http/follow.ts b/src/processor/http/follow.ts
index d6ce00006..a7e4fa23d 100644
--- a/src/processor/http/follow.ts
+++ b/src/processor/http/follow.ts
@@ -3,8 +3,8 @@ import { sign } from 'http-signature';
 import { URL } from 'url';
 import User, { isLocalUser, pack as packUser, ILocalUser } from '../../models/user';
 import Following from '../../models/following';
-import event from '../../event';
-import notify from '../../notify';
+import event from '../../publishers/stream';
+import notify from '../../publishers/notify';
 import context from '../../remote/activitypub/renderer/context';
 import render from '../../remote/activitypub/renderer/follow';
 import config from '../../config';
diff --git a/src/notify.ts b/src/publishers/notify.ts
similarity index 81%
rename from src/notify.ts
rename to src/publishers/notify.ts
index 228317b88..2b89515d4 100644
--- a/src/notify.ts
+++ b/src/publishers/notify.ts
@@ -1,8 +1,8 @@
 import * as mongo from 'mongodb';
-import Notification from './models/notification';
-import Mute from './models/mute';
-import event from './event';
-import { pack } from './models/notification';
+import Notification from '../models/notification';
+import Mute from '../models/mute';
+import { pack } from '../models/notification';
+import stream from './stream';
 
 export default (
 	notifiee: mongo.ObjectID,
@@ -26,7 +26,7 @@ export default (
 	resolve(notification);
 
 	// Publish notification event
-	event(notifiee, 'notification',
+	stream(notifiee, 'notification',
 		await pack(notification));
 
 	// 3秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
@@ -44,7 +44,7 @@ export default (
 			}
 			//#endregion
 
-			event(notifiee, 'unread_notification', await pack(notification));
+			stream(notifiee, 'unread_notification', await pack(notification));
 		}
 	}, 3000);
 });
diff --git a/src/push-sw.ts b/src/publishers/push-sw.ts
similarity index 92%
rename from src/push-sw.ts
rename to src/publishers/push-sw.ts
index fcef7796d..aab91df62 100644
--- a/src/push-sw.ts
+++ b/src/publishers/push-sw.ts
@@ -1,7 +1,7 @@
 const push = require('web-push');
 import * as mongo from 'mongodb';
-import Subscription from './models/sw-subscription';
-import config from './config';
+import Subscription from '../models/sw-subscription';
+import config from '../config';
 
 if (config.sw) {
 	// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録
diff --git a/src/event.ts b/src/publishers/stream.ts
similarity index 92%
rename from src/event.ts
rename to src/publishers/stream.ts
index 81876b3cf..498ff33f3 100644
--- a/src/event.ts
+++ b/src/publishers/stream.ts
@@ -1,7 +1,6 @@
 import * as mongo from 'mongodb';
 import * as redis from 'redis';
-import swPush from './push-sw';
-import config from './config';
+import config from '../config';
 
 type ID = string | mongo.ObjectID;
 
@@ -18,10 +17,6 @@ class MisskeyEvent {
 		this.publish(`user-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
-	public publishSw(userId: ID, type: string, value?: any): void {
-		swPush(userId, type, value);
-	}
-
 	public publishDriveStream(userId: ID, type: string, value?: any): void {
 		this.publish(`drive-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
 	}
@@ -63,8 +58,6 @@ const ev = new MisskeyEvent();
 
 export default ev.publishUserStream.bind(ev);
 
-export const pushSw = ev.publishSw.bind(ev);
-
 export const publishDriveStream = ev.publishDriveStream.bind(ev);
 
 export const publishPostStream = ev.publishPostStream.bind(ev);
diff --git a/src/server/api/common/read-messaging-message.ts b/src/server/api/common/read-messaging-message.ts
index 755cf1f50..c52f9363b 100644
--- a/src/server/api/common/read-messaging-message.ts
+++ b/src/server/api/common/read-messaging-message.ts
@@ -1,9 +1,9 @@
 import * as mongo from 'mongodb';
 import Message from '../../../models/messaging-message';
 import { IMessagingMessage as IMessage } from '../../../models/messaging-message';
-import publishUserStream from '../../../event';
-import { publishMessagingStream } from '../../../event';
-import { publishMessagingIndexStream } from '../../../event';
+import publishUserStream from '../../../publishers/stream';
+import { publishMessagingStream } from '../../../publishers/stream';
+import { publishMessagingIndexStream } from '../../../publishers/stream';
 
 /**
  * Mark as read message(s)
diff --git a/src/server/api/common/read-notification.ts b/src/server/api/common/read-notification.ts
index b51c0ca00..9bd41519f 100644
--- a/src/server/api/common/read-notification.ts
+++ b/src/server/api/common/read-notification.ts
@@ -1,6 +1,6 @@
 import * as mongo from 'mongodb';
 import { default as Notification, INotification } from '../../../models/notification';
-import publishUserStream from '../../../event';
+import publishUserStream from '../../../publishers/stream';
 
 /**
  * Mark as read notification(s)
diff --git a/src/server/api/endpoints/drive/files/update.ts b/src/server/api/endpoints/drive/files/update.ts
index 85bd2110f..c783ad8b3 100644
--- a/src/server/api/endpoints/drive/files/update.ts
+++ b/src/server/api/endpoints/drive/files/update.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import DriveFolder from '../../../../../models/drive-folder';
 import DriveFile, { validateFileName, pack } from '../../../../../models/drive-file';
-import { publishDriveStream } from '../../../../../event';
+import { publishDriveStream } from '../../../../../publishers/stream';
 
 /**
  * Update a file
diff --git a/src/server/api/endpoints/drive/folders/create.ts b/src/server/api/endpoints/drive/folders/create.ts
index d9d39a918..f34d0019d 100644
--- a/src/server/api/endpoints/drive/folders/create.ts
+++ b/src/server/api/endpoints/drive/folders/create.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import DriveFolder, { isValidFolderName, pack } from '../../../../../models/drive-folder';
-import { publishDriveStream } from '../../../../../event';
+import { publishDriveStream } from '../../../../../publishers/stream';
 
 /**
  * Create drive folder
diff --git a/src/server/api/endpoints/drive/folders/update.ts b/src/server/api/endpoints/drive/folders/update.ts
index 1cea05d71..dd7e8f5c8 100644
--- a/src/server/api/endpoints/drive/folders/update.ts
+++ b/src/server/api/endpoints/drive/folders/update.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import DriveFolder, { isValidFolderName, pack } from '../../../../../models/drive-folder';
-import { publishDriveStream } from '../../../../../event';
+import { publishDriveStream } from '../../../../../publishers/stream';
 
 /**
  * Update a folder
diff --git a/src/server/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts
index 77a6cebee..3facfdcdd 100644
--- a/src/server/api/endpoints/following/delete.ts
+++ b/src/server/api/endpoints/following/delete.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import User, { pack as packUser } from '../../../../models/user';
 import Following from '../../../../models/following';
-import event from '../../../../event';
+import event from '../../../../publishers/stream';
 
 /**
  * Unfollow a user
diff --git a/src/server/api/endpoints/i/regenerate_token.ts b/src/server/api/endpoints/i/regenerate_token.ts
index d7cb69784..9aa6725f8 100644
--- a/src/server/api/endpoints/i/regenerate_token.ts
+++ b/src/server/api/endpoints/i/regenerate_token.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import * as bcrypt from 'bcryptjs';
 import User from '../../../../models/user';
-import event from '../../../../event';
+import event from '../../../../publishers/stream';
 import generateUserToken from '../../common/generate-native-user-token';
 
 /**
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index c4ec41339..279b062f5 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../../../models/user';
-import event from '../../../../event';
+import event from '../../../../publishers/stream';
 import config from '../../../../config';
 
 /**
diff --git a/src/server/api/endpoints/i/update_client_setting.ts b/src/server/api/endpoints/i/update_client_setting.ts
index 263ac6d07..10741aceb 100644
--- a/src/server/api/endpoints/i/update_client_setting.ts
+++ b/src/server/api/endpoints/i/update_client_setting.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import User, { pack } from '../../../../models/user';
-import event from '../../../../event';
+import event from '../../../../publishers/stream';
 
 /**
  * Update myself
diff --git a/src/server/api/endpoints/i/update_home.ts b/src/server/api/endpoints/i/update_home.ts
index 227b8dd5a..91be0714d 100644
--- a/src/server/api/endpoints/i/update_home.ts
+++ b/src/server/api/endpoints/i/update_home.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import User from '../../../../models/user';
-import event from '../../../../event';
+import event from '../../../../publishers/stream';
 
 module.exports = async (params, user) => new Promise(async (res, rej) => {
 	// Get 'home' parameter
diff --git a/src/server/api/endpoints/i/update_mobile_home.ts b/src/server/api/endpoints/i/update_mobile_home.ts
index 007eb2eab..1efda120d 100644
--- a/src/server/api/endpoints/i/update_mobile_home.ts
+++ b/src/server/api/endpoints/i/update_mobile_home.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import User from '../../../../models/user';
-import event from '../../../../event';
+import event from '../../../../publishers/stream';
 
 module.exports = async (params, user) => new Promise(async (res, rej) => {
 	// Get 'home' parameter
diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts
index 604da32d2..085e75e6c 100644
--- a/src/server/api/endpoints/messaging/messages/create.ts
+++ b/src/server/api/endpoints/messaging/messages/create.ts
@@ -9,8 +9,9 @@ import User from '../../../../../models/user';
 import Mute from '../../../../../models/mute';
 import DriveFile from '../../../../../models/drive-file';
 import { pack } from '../../../../../models/messaging-message';
-import publishUserStream from '../../../../../event';
-import { publishMessagingStream, publishMessagingIndexStream, pushSw } from '../../../../../event';
+import publishUserStream from '../../../../../publishers/stream';
+import { publishMessagingStream, publishMessagingIndexStream } from '../../../../../publishers/stream';
+import pushSw from '../../../../../publishers/push-sw';
 import html from '../../../../../text/html';
 import parse from '../../../../../text/parse';
 import config from '../../../../../config';
diff --git a/src/server/api/endpoints/notifications/mark_as_read_all.ts b/src/server/api/endpoints/notifications/mark_as_read_all.ts
index 3ba00a907..01c914583 100644
--- a/src/server/api/endpoints/notifications/mark_as_read_all.ts
+++ b/src/server/api/endpoints/notifications/mark_as_read_all.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import Notification from '../../../../models/notification';
-import event from '../../../../event';
+import event from '../../../../publishers/stream';
 
 /**
  * Mark as read all notifications
diff --git a/src/server/api/endpoints/othello/match.ts b/src/server/api/endpoints/othello/match.ts
index 1fc46ab90..d9274f8f9 100644
--- a/src/server/api/endpoints/othello/match.ts
+++ b/src/server/api/endpoints/othello/match.ts
@@ -2,7 +2,7 @@ import $ from 'cafy';
 import Matching, { pack as packMatching } from '../../../../models/othello-matching';
 import OthelloGame, { pack as packGame } from '../../../../models/othello-game';
 import User from '../../../../models/user';
-import publishUserStream, { publishOthelloStream } from '../../../../event';
+import publishUserStream, { publishOthelloStream } from '../../../../publishers/stream';
 import { eighteight } from '../../../../othello/maps';
 
 module.exports = (params, user) => new Promise(async (res, rej) => {
diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts
index bf08fe283..34a3aa190 100644
--- a/src/server/api/endpoints/posts/create.ts
+++ b/src/server/api/endpoints/posts/create.ts
@@ -15,8 +15,9 @@ import Watching from '../../../../models/post-watching';
 import ChannelWatching from '../../../../models/channel-watching';
 import { pack } from '../../../../models/post';
 import watch from '../../common/watch-post';
-import event, { pushSw, publishChannelStream } from '../../../../event';
-import notify from '../../../../notify';
+import stream, { publishChannelStream } from '../../../../publishers/stream';
+import notify from '../../../../publishers/notify';
+import pushSw from '../../../../publishers/push-sw';
 import getAcct from '../../../../user/get-acct';
 import parseAcct from '../../../../user/parse-acct';
 import config from '../../../../config';
@@ -306,7 +307,7 @@ module.exports = (params, user: ILocalUser, app) => new Promise(async (res, rej)
 			});
 			const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString());
 			if (mentioneesMutedUserIds.indexOf(user._id.toString()) == -1) {
-				event(mentionee, reason, postObj);
+				stream(mentionee, reason, postObj);
 				pushSw(mentionee, reason, postObj);
 			}
 		}
@@ -315,7 +316,7 @@ module.exports = (params, user: ILocalUser, app) => new Promise(async (res, rej)
 	// タイムラインへの投稿
 	if (!channel) {
 		// Publish event to myself's stream
-		event(user._id, 'post', postObj);
+		stream(user._id, 'post', postObj);
 
 		// Fetch all followers
 		const followers = await Following
@@ -330,7 +331,7 @@ module.exports = (params, user: ILocalUser, app) => new Promise(async (res, rej)
 
 		// Publish event to followers stream
 		followers.forEach(following =>
-			event(following.followerId, 'post', postObj));
+			stream(following.followerId, 'post', postObj));
 	}
 
 	// チャンネルへの投稿
@@ -354,7 +355,7 @@ module.exports = (params, user: ILocalUser, app) => new Promise(async (res, rej)
 
 		// チャンネルの視聴者(のタイムライン)に配信
 		watches.forEach(w => {
-			event(w.userId, 'post', postObj);
+			stream(w.userId, 'post', postObj);
 		});
 	}
 
@@ -448,7 +449,7 @@ module.exports = (params, user: ILocalUser, app) => new Promise(async (res, rej)
 		} else {
 			// Publish event
 			if (!user._id.equals(repost.userId)) {
-				event(repost.userId, 'repost', postObj);
+				stream(repost.userId, 'repost', postObj);
 			}
 		}
 
diff --git a/src/server/api/endpoints/posts/polls/vote.ts b/src/server/api/endpoints/posts/polls/vote.ts
index be1fd7b5d..029fb9323 100644
--- a/src/server/api/endpoints/posts/polls/vote.ts
+++ b/src/server/api/endpoints/posts/polls/vote.ts
@@ -6,8 +6,8 @@ import Vote from '../../../../../models/poll-vote';
 import Post from '../../../../../models/post';
 import Watching from '../../../../../models/post-watching';
 import watch from '../../../common/watch-post';
-import { publishPostStream } from '../../../../../event';
-import notify from '../../../../../notify';
+import { publishPostStream } from '../../../../../publishers/stream';
+import notify from '../../../../../publishers/notify';
 
 /**
  * Vote poll of a post
diff --git a/src/server/api/endpoints/posts/reactions/create.ts b/src/server/api/endpoints/posts/reactions/create.ts
index 408b2483a..8b5f1e57d 100644
--- a/src/server/api/endpoints/posts/reactions/create.ts
+++ b/src/server/api/endpoints/posts/reactions/create.ts
@@ -7,8 +7,9 @@ import Post, { pack as packPost } from '../../../../../models/post';
 import { pack as packUser } from '../../../../../models/user';
 import Watching from '../../../../../models/post-watching';
 import watch from '../../../common/watch-post';
-import { publishPostStream, pushSw } from '../../../../../event';
-import notify from '../../../../../notify';
+import { publishPostStream } from '../../../../../publishers/stream';
+import notify from '../../../../../publishers/notify';
+import pushSw from '../../../../../publishers/push-sw';
 
 /**
  * React to a post
diff --git a/src/server/api/endpoints/posts/reactions/delete.ts b/src/server/api/endpoints/posts/reactions/delete.ts
index 11f5c7daf..3a88bbd7c 100644
--- a/src/server/api/endpoints/posts/reactions/delete.ts
+++ b/src/server/api/endpoints/posts/reactions/delete.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import Reaction from '../../../../../models/post-reaction';
 import Post from '../../../../../models/post';
-// import event from '../../../event';
+// import event from '../../../publishers/stream';
 
 /**
  * Unreact to a post
diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts
index bf883ee27..e0bd67d1c 100644
--- a/src/server/api/private/signin.ts
+++ b/src/server/api/private/signin.ts
@@ -3,7 +3,7 @@ import * as bcrypt from 'bcryptjs';
 import * as speakeasy from 'speakeasy';
 import User, { ILocalUser } from '../../../models/user';
 import Signin, { pack } from '../../../models/signin';
-import event from '../../../event';
+import event from '../../../publishers/stream';
 import signin from '../common/signin';
 import config from '../../../config';
 
diff --git a/src/server/api/service/twitter.ts b/src/server/api/service/twitter.ts
index 69fa5f3c6..77b932b13 100644
--- a/src/server/api/service/twitter.ts
+++ b/src/server/api/service/twitter.ts
@@ -6,7 +6,7 @@ import * as uuid from 'uuid';
 import autwh from 'autwh';
 import redis from '../../../db/redis';
 import User, { pack } from '../../../models/user';
-import event from '../../../event';
+import event from '../../../publishers/stream';
 import config from '../../../config';
 import signin from '../common/signin';
 
diff --git a/src/server/api/stream/othello-game.ts b/src/server/api/stream/othello-game.ts
index 6eb610331..841e54261 100644
--- a/src/server/api/stream/othello-game.ts
+++ b/src/server/api/stream/othello-game.ts
@@ -2,7 +2,7 @@ import * as websocket from 'websocket';
 import * as redis from 'redis';
 import * as CRC32 from 'crc-32';
 import OthelloGame, { pack } from '../../../models/othello-game';
-import { publishOthelloGameStream } from '../../../event';
+import { publishOthelloGameStream } from '../../../publishers/stream';
 import Othello from '../../../othello/core';
 import * as maps from '../../../othello/maps';
 import { ParsedUrlQuery } from 'querystring';
diff --git a/src/server/api/stream/othello.ts b/src/server/api/stream/othello.ts
index 4c292056d..fa62b0583 100644
--- a/src/server/api/stream/othello.ts
+++ b/src/server/api/stream/othello.ts
@@ -2,7 +2,7 @@ import * as mongo from 'mongodb';
 import * as websocket from 'websocket';
 import * as redis from 'redis';
 import Matching, { pack } from '../../../models/othello-matching';
-import publishUserStream from '../../../event';
+import publishUserStream from '../../../publishers/stream';
 
 export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void {
 	// Subscribe othello stream

From 38d0f2b9afa3786d392f37bd9cdc25dbb2904fe3 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Mon, 2 Apr 2018 13:41:25 +0900
Subject: [PATCH 1014/1250] Introduce renderers directory

---
 src/client/app/common/scripts/compose-notification.ts       | 4 ++--
 src/client/app/desktop/views/components/notifications.vue   | 2 +-
 src/client/app/desktop/views/pages/home.vue                 | 2 +-
 src/client/app/mobile/api/post.ts                           | 2 +-
 .../app/mobile/views/components/notification-preview.vue    | 2 +-
 src/client/app/mobile/views/components/notification.vue     | 2 +-
 src/client/app/mobile/views/components/post-card.vue        | 2 +-
 src/client/app/mobile/views/pages/home.vue                  | 2 +-
 src/{ => renderers}/get-notification-summary.ts             | 0
 src/{ => renderers}/get-post-summary.ts                     | 0
 src/{ => renderers}/get-reaction-emoji.ts                   | 0
 src/{user/get-summary.ts => renderers/get-user-summary.ts}  | 2 +-
 src/server/api/bot/core.ts                                  | 6 +++---
 src/server/api/bot/interfaces/line.ts                       | 2 +-
 14 files changed, 14 insertions(+), 14 deletions(-)
 rename src/{ => renderers}/get-notification-summary.ts (100%)
 rename src/{ => renderers}/get-post-summary.ts (100%)
 rename src/{ => renderers}/get-reaction-emoji.ts (100%)
 rename src/{user/get-summary.ts => renderers/get-user-summary.ts} (93%)

diff --git a/src/client/app/common/scripts/compose-notification.ts b/src/client/app/common/scripts/compose-notification.ts
index 4030d61ac..ebc15952f 100644
--- a/src/client/app/common/scripts/compose-notification.ts
+++ b/src/client/app/common/scripts/compose-notification.ts
@@ -1,5 +1,5 @@
-import getPostSummary from '../../../../get-post-summary';
-import getReactionEmoji from '../../../../get-reaction-emoji';
+import getPostSummary from '../../../../renderers/get-post-summary';
+import getReactionEmoji from '../../../../renderers/get-reaction-emoji';
 
 type Notification = {
 	title: string;
diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
index 8c4102494..9bfe1560b 100644
--- a/src/client/app/desktop/views/components/notifications.vue
+++ b/src/client/app/desktop/views/components/notifications.vue
@@ -103,7 +103,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../user/get-acct';
-import getPostSummary from '../../../../../get-post-summary';
+import getPostSummary from '../../../../../renderers/get-post-summary';
 
 export default Vue.extend({
 	data() {
diff --git a/src/client/app/desktop/views/pages/home.vue b/src/client/app/desktop/views/pages/home.vue
index ad9e2bc9d..c209af4e4 100644
--- a/src/client/app/desktop/views/pages/home.vue
+++ b/src/client/app/desktop/views/pages/home.vue
@@ -7,7 +7,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
-import getPostSummary from '../../../../../get-post-summary';
+import getPostSummary from '../../../../../renderers/get-post-summary';
 
 export default Vue.extend({
 	props: {
diff --git a/src/client/app/mobile/api/post.ts b/src/client/app/mobile/api/post.ts
index 6580cbf4b..98309ba8d 100644
--- a/src/client/app/mobile/api/post.ts
+++ b/src/client/app/mobile/api/post.ts
@@ -1,6 +1,6 @@
 import PostForm from '../views/components/post-form.vue';
 //import RepostForm from '../views/components/repost-form.vue';
-import getPostSummary from '../../../../get-post-summary';
+import getPostSummary from '../../../../renderers/get-post-summary';
 
 export default (os) => (opts) => {
 	const o = opts || {};
diff --git a/src/client/app/mobile/views/components/notification-preview.vue b/src/client/app/mobile/views/components/notification-preview.vue
index e7e1f75a8..77abd3c0e 100644
--- a/src/client/app/mobile/views/components/notification-preview.vue
+++ b/src/client/app/mobile/views/components/notification-preview.vue
@@ -59,7 +59,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getPostSummary from '../../../../../get-post-summary';
+import getPostSummary from '../../../../../renderers/get-post-summary';
 
 export default Vue.extend({
 	props: ['notification'],
diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue
index d92b01944..d3e313756 100644
--- a/src/client/app/mobile/views/components/notification.vue
+++ b/src/client/app/mobile/views/components/notification.vue
@@ -78,7 +78,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getPostSummary from '../../../../../get-post-summary';
+import getPostSummary from '../../../../../renderers/get-post-summary';
 import getAcct from '../../../../../user/get-acct';
 
 export default Vue.extend({
diff --git a/src/client/app/mobile/views/components/post-card.vue b/src/client/app/mobile/views/components/post-card.vue
index 893e4c307..de42a01f7 100644
--- a/src/client/app/mobile/views/components/post-card.vue
+++ b/src/client/app/mobile/views/components/post-card.vue
@@ -14,7 +14,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import summary from '../../../../../get-post-summary';
+import summary from '../../../../../renderers/get-post-summary';
 import getAcct from '../../../../../user/get-acct';
 
 export default Vue.extend({
diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue
index b2edf5956..6fb1c6d4f 100644
--- a/src/client/app/mobile/views/pages/home.vue
+++ b/src/client/app/mobile/views/pages/home.vue
@@ -64,7 +64,7 @@ import Vue from 'vue';
 import * as XDraggable from 'vuedraggable';
 import * as uuid from 'uuid';
 import Progress from '../../../common/scripts/loading';
-import getPostSummary from '../../../../../get-post-summary';
+import getPostSummary from '../../../../../renderers/get-post-summary';
 
 export default Vue.extend({
 	components: {
diff --git a/src/get-notification-summary.ts b/src/renderers/get-notification-summary.ts
similarity index 100%
rename from src/get-notification-summary.ts
rename to src/renderers/get-notification-summary.ts
diff --git a/src/get-post-summary.ts b/src/renderers/get-post-summary.ts
similarity index 100%
rename from src/get-post-summary.ts
rename to src/renderers/get-post-summary.ts
diff --git a/src/get-reaction-emoji.ts b/src/renderers/get-reaction-emoji.ts
similarity index 100%
rename from src/get-reaction-emoji.ts
rename to src/renderers/get-reaction-emoji.ts
diff --git a/src/user/get-summary.ts b/src/renderers/get-user-summary.ts
similarity index 93%
rename from src/user/get-summary.ts
rename to src/renderers/get-user-summary.ts
index 822081702..2a9e8a5d0 100644
--- a/src/user/get-summary.ts
+++ b/src/renderers/get-user-summary.ts
@@ -1,5 +1,5 @@
 import { IUser, isLocalUser } from '../models/user';
-import getAcct from './get-acct';
+import getAcct from '../user/get-acct';
 
 /**
  * ユーザーを表す文字列を取得します。
diff --git a/src/server/api/bot/core.ts b/src/server/api/bot/core.ts
index 3beec33d1..7f9409660 100644
--- a/src/server/api/bot/core.ts
+++ b/src/server/api/bot/core.ts
@@ -3,10 +3,10 @@ import * as bcrypt from 'bcryptjs';
 
 import User, { IUser, init as initUser, ILocalUser } from '../../../models/user';
 
-import getPostSummary from '../../../get-post-summary';
-import getUserSummary from '../../../user/get-summary';
+import getPostSummary from '../../../renderers/get-post-summary';
+import getUserSummary from '../../../renderers/get-user-summary';
 import parseAcct from '../../../user/parse-acct';
-import getNotificationSummary from '../../../get-notification-summary';
+import getNotificationSummary from '../../../renderers/get-notification-summary';
 
 const hmm = [
 	'?',
diff --git a/src/server/api/bot/interfaces/line.ts b/src/server/api/bot/interfaces/line.ts
index 27248c9b9..17841a9f0 100644
--- a/src/server/api/bot/interfaces/line.ts
+++ b/src/server/api/bot/interfaces/line.ts
@@ -9,7 +9,7 @@ import _redis from '../../../../db/redis';
 import prominence = require('prominence');
 import getAcct from '../../../../user/get-acct';
 import parseAcct from '../../../../user/parse-acct';
-import getPostSummary from '../../../../get-post-summary';
+import getPostSummary from '../../../../renderers/get-post-summary';
 
 const redis = prominence(_redis);
 

From f47bcd1ebe5ba066d388944bed11916d8e4cafa9 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Mon, 2 Apr 2018 13:44:32 +0900
Subject: [PATCH 1015/1250] Introduce acct directory

---
 src/{user/parse-acct.ts => acct/parse.ts}                     | 0
 src/{user/get-acct.ts => acct/render.ts}                      | 0
 src/client/app/ch/tags/channel.tag                            | 2 +-
 src/client/app/common/views/components/autocomplete.vue       | 2 +-
 .../app/common/views/components/messaging-room.message.vue    | 2 +-
 src/client/app/common/views/components/messaging.vue          | 2 +-
 src/client/app/common/views/components/post-html.ts           | 2 +-
 src/client/app/common/views/components/welcome-timeline.vue   | 2 +-
 src/client/app/desktop/views/components/friends-maker.vue     | 2 +-
 .../app/desktop/views/components/messaging-room-window.vue    | 2 +-
 src/client/app/desktop/views/components/notifications.vue     | 2 +-
 src/client/app/desktop/views/components/post-detail.sub.vue   | 2 +-
 src/client/app/desktop/views/components/post-detail.vue       | 2 +-
 src/client/app/desktop/views/components/post-preview.vue      | 2 +-
 src/client/app/desktop/views/components/posts.post.sub.vue    | 2 +-
 src/client/app/desktop/views/components/posts.post.vue        | 2 +-
 src/client/app/desktop/views/components/settings.mute.vue     | 2 +-
 src/client/app/desktop/views/components/user-preview.vue      | 4 ++--
 src/client/app/desktop/views/components/users-list.item.vue   | 2 +-
 src/client/app/desktop/views/pages/messaging-room.vue         | 2 +-
 .../app/desktop/views/pages/user/user.followers-you-know.vue  | 2 +-
 src/client/app/desktop/views/pages/user/user.friends.vue      | 2 +-
 src/client/app/desktop/views/pages/user/user.header.vue       | 2 +-
 src/client/app/desktop/views/pages/user/user.vue              | 2 +-
 src/client/app/desktop/views/pages/welcome.vue                | 2 +-
 src/client/app/desktop/views/widgets/channel.channel.post.vue | 2 +-
 src/client/app/desktop/views/widgets/polls.vue                | 2 +-
 src/client/app/desktop/views/widgets/trends.vue               | 2 +-
 src/client/app/desktop/views/widgets/users.vue                | 2 +-
 src/client/app/mobile/views/components/notification.vue       | 2 +-
 src/client/app/mobile/views/components/post-card.vue          | 2 +-
 src/client/app/mobile/views/components/post-detail.sub.vue    | 2 +-
 src/client/app/mobile/views/components/post-detail.vue        | 2 +-
 src/client/app/mobile/views/components/post-preview.vue       | 2 +-
 src/client/app/mobile/views/components/post.sub.vue           | 2 +-
 src/client/app/mobile/views/components/post.vue               | 2 +-
 src/client/app/mobile/views/components/user-card.vue          | 2 +-
 src/client/app/mobile/views/components/user-preview.vue       | 2 +-
 src/client/app/mobile/views/pages/followers.vue               | 2 +-
 src/client/app/mobile/views/pages/following.vue               | 2 +-
 src/client/app/mobile/views/pages/messaging-room.vue          | 2 +-
 src/client/app/mobile/views/pages/messaging.vue               | 2 +-
 src/client/app/mobile/views/pages/user.vue                    | 4 ++--
 .../app/mobile/views/pages/user/home.followers-you-know.vue   | 2 +-
 src/client/app/mobile/views/pages/user/home.photos.vue        | 2 +-
 src/drive/add-file.ts                                         | 2 +-
 src/renderers/get-user-summary.ts                             | 2 +-
 src/server/activitypub/inbox.ts                               | 2 +-
 src/server/activitypub/post.ts                                | 2 +-
 src/server/activitypub/with-user.ts                           | 2 +-
 src/server/api/bot/core.ts                                    | 2 +-
 src/server/api/bot/interfaces/line.ts                         | 4 ++--
 src/server/api/endpoints/posts/create.ts                      | 4 ++--
 src/server/api/limitter.ts                                    | 2 +-
 src/server/webfinger.ts                                       | 2 +-
 src/text/parse/elements/mention.ts                            | 2 +-
 56 files changed, 58 insertions(+), 58 deletions(-)
 rename src/{user/parse-acct.ts => acct/parse.ts} (100%)
 rename src/{user/get-acct.ts => acct/render.ts} (100%)

diff --git a/src/user/parse-acct.ts b/src/acct/parse.ts
similarity index 100%
rename from src/user/parse-acct.ts
rename to src/acct/parse.ts
diff --git a/src/user/get-acct.ts b/src/acct/render.ts
similarity index 100%
rename from src/user/get-acct.ts
rename to src/acct/render.ts
diff --git a/src/client/app/ch/tags/channel.tag b/src/client/app/ch/tags/channel.tag
index 0c139ba26..1ebc3cceb 100644
--- a/src/client/app/ch/tags/channel.tag
+++ b/src/client/app/ch/tags/channel.tag
@@ -229,7 +229,7 @@
 
 	</style>
 	<script lang="typescript">
-		import getAcct from '../../../../user/get-acct';
+		import getAcct from '../../../../acct/render';
 
 		this.post = this.opts.post;
 		this.form = this.opts.form;
diff --git a/src/client/app/common/views/components/autocomplete.vue b/src/client/app/common/views/components/autocomplete.vue
index 7bcfc07e9..38eaf8650 100644
--- a/src/client/app/common/views/components/autocomplete.vue
+++ b/src/client/app/common/views/components/autocomplete.vue
@@ -21,7 +21,7 @@
 import Vue from 'vue';
 import * as emojilib from 'emojilib';
 import contains from '../../../common/scripts/contains';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 
 const lib = Object.entries(emojilib.lib).filter((x: any) => {
 	return x[1].category != 'flags';
diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue
index 3de8321f8..1518602c6 100644
--- a/src/client/app/common/views/components/messaging-room.message.vue
+++ b/src/client/app/common/views/components/messaging-room.message.vue
@@ -34,7 +34,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 import parse from '../../../../../text/parse';
 
 export default Vue.extend({
diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue
index bde2b2b90..4ab3e46e8 100644
--- a/src/client/app/common/views/components/messaging.vue
+++ b/src/client/app/common/views/components/messaging.vue
@@ -51,7 +51,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 
 export default Vue.extend({
 	props: {
diff --git a/src/client/app/common/views/components/post-html.ts b/src/client/app/common/views/components/post-html.ts
index 4018a966e..8d8531652 100644
--- a/src/client/app/common/views/components/post-html.ts
+++ b/src/client/app/common/views/components/post-html.ts
@@ -1,7 +1,7 @@
 import Vue from 'vue';
 import * as emojilib from 'emojilib';
 import parse from '../../../../../text/parse';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 import { url } from '../../../config';
 import MkUrl from './url.vue';
 
diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue
index 94b7f5889..ef0c7beaa 100644
--- a/src/client/app/common/views/components/welcome-timeline.vue
+++ b/src/client/app/common/views/components/welcome-timeline.vue
@@ -24,7 +24,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 
 export default Vue.extend({
 	data() {
diff --git a/src/client/app/desktop/views/components/friends-maker.vue b/src/client/app/desktop/views/components/friends-maker.vue
index bfa7503d2..351e9e1c5 100644
--- a/src/client/app/desktop/views/components/friends-maker.vue
+++ b/src/client/app/desktop/views/components/friends-maker.vue
@@ -22,7 +22,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 
 export default Vue.extend({
 	data() {
diff --git a/src/client/app/desktop/views/components/messaging-room-window.vue b/src/client/app/desktop/views/components/messaging-room-window.vue
index 88eb28578..f29f9b74e 100644
--- a/src/client/app/desktop/views/components/messaging-room-window.vue
+++ b/src/client/app/desktop/views/components/messaging-room-window.vue
@@ -8,7 +8,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import { url } from '../../../config';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 
 export default Vue.extend({
 	props: ['user'],
diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
index 9bfe1560b..c5ab284df 100644
--- a/src/client/app/desktop/views/components/notifications.vue
+++ b/src/client/app/desktop/views/components/notifications.vue
@@ -102,7 +102,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 import getPostSummary from '../../../../../renderers/get-post-summary';
 
 export default Vue.extend({
diff --git a/src/client/app/desktop/views/components/post-detail.sub.vue b/src/client/app/desktop/views/components/post-detail.sub.vue
index 2719fee9d..59bc9ce0c 100644
--- a/src/client/app/desktop/views/components/post-detail.sub.vue
+++ b/src/client/app/desktop/views/components/post-detail.sub.vue
@@ -28,7 +28,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 
 export default Vue.extend({
 	props: ['post'],
diff --git a/src/client/app/desktop/views/components/post-detail.vue b/src/client/app/desktop/views/components/post-detail.vue
index 5911a267f..8000ce2e6 100644
--- a/src/client/app/desktop/views/components/post-detail.vue
+++ b/src/client/app/desktop/views/components/post-detail.vue
@@ -78,7 +78,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 import parse from '../../../../../text/parse';
 
 import MkPostFormWindow from './post-form-window.vue';
diff --git a/src/client/app/desktop/views/components/post-preview.vue b/src/client/app/desktop/views/components/post-preview.vue
index e27f0b4cc..7129f67b3 100644
--- a/src/client/app/desktop/views/components/post-preview.vue
+++ b/src/client/app/desktop/views/components/post-preview.vue
@@ -21,7 +21,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 
 export default Vue.extend({
 	props: ['post'],
diff --git a/src/client/app/desktop/views/components/posts.post.sub.vue b/src/client/app/desktop/views/components/posts.post.sub.vue
index 16f5c4bee..dffecb89c 100644
--- a/src/client/app/desktop/views/components/posts.post.sub.vue
+++ b/src/client/app/desktop/views/components/posts.post.sub.vue
@@ -21,7 +21,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 
 export default Vue.extend({
 	props: ['post'],
diff --git a/src/client/app/desktop/views/components/posts.post.vue b/src/client/app/desktop/views/components/posts.post.vue
index b32ebe944..9a13dd687 100644
--- a/src/client/app/desktop/views/components/posts.post.vue
+++ b/src/client/app/desktop/views/components/posts.post.vue
@@ -85,7 +85,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 import parse from '../../../../../text/parse';
 
 import MkPostFormWindow from './post-form-window.vue';
diff --git a/src/client/app/desktop/views/components/settings.mute.vue b/src/client/app/desktop/views/components/settings.mute.vue
index 5c8c86222..c87f973fa 100644
--- a/src/client/app/desktop/views/components/settings.mute.vue
+++ b/src/client/app/desktop/views/components/settings.mute.vue
@@ -13,7 +13,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 
 export default Vue.extend({
 	data() {
diff --git a/src/client/app/desktop/views/components/user-preview.vue b/src/client/app/desktop/views/components/user-preview.vue
index cad7d455f..2f2e78ec6 100644
--- a/src/client/app/desktop/views/components/user-preview.vue
+++ b/src/client/app/desktop/views/components/user-preview.vue
@@ -29,8 +29,8 @@
 <script lang="ts">
 import Vue from 'vue';
 import * as anime from 'animejs';
-import getAcct from '../../../../../user/get-acct';
-import parseAcct from '../../../../../user/parse-acct';
+import getAcct from '../../../../../acct/render';
+import parseAcct from '../../../../../acct/parse';
 
 export default Vue.extend({
 	props: {
diff --git a/src/client/app/desktop/views/components/users-list.item.vue b/src/client/app/desktop/views/components/users-list.item.vue
index a25d68c44..2d7d4dc72 100644
--- a/src/client/app/desktop/views/components/users-list.item.vue
+++ b/src/client/app/desktop/views/components/users-list.item.vue
@@ -19,7 +19,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 
 export default Vue.extend({
 	props: ['user'],
diff --git a/src/client/app/desktop/views/pages/messaging-room.vue b/src/client/app/desktop/views/pages/messaging-room.vue
index b3d0ff149..1e61f3ce1 100644
--- a/src/client/app/desktop/views/pages/messaging-room.vue
+++ b/src/client/app/desktop/views/pages/messaging-room.vue
@@ -7,7 +7,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
-import parseAcct from '../../../../../user/parse-acct';
+import parseAcct from '../../../../../acct/parse';
 
 export default Vue.extend({
 	data() {
diff --git a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
index a8bc67d3a..7497acd0e 100644
--- a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
+++ b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
@@ -13,7 +13,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../../user/get-acct';
+import getAcct from '../../../../../../acct/render';
 
 export default Vue.extend({
 	props: ['user'],
diff --git a/src/client/app/desktop/views/pages/user/user.friends.vue b/src/client/app/desktop/views/pages/user/user.friends.vue
index 6710e3793..a726e2565 100644
--- a/src/client/app/desktop/views/pages/user/user.friends.vue
+++ b/src/client/app/desktop/views/pages/user/user.friends.vue
@@ -20,7 +20,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../../user/get-acct';
+import getAcct from '../../../../../../acct/render';
 
 export default Vue.extend({
 	props: ['user'],
diff --git a/src/client/app/desktop/views/pages/user/user.header.vue b/src/client/app/desktop/views/pages/user/user.header.vue
index 7b051224d..d30f423d5 100644
--- a/src/client/app/desktop/views/pages/user/user.header.vue
+++ b/src/client/app/desktop/views/pages/user/user.header.vue
@@ -22,7 +22,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../../user/get-acct';
+import getAcct from '../../../../../../acct/render';
 
 export default Vue.extend({
 	props: ['user'],
diff --git a/src/client/app/desktop/views/pages/user/user.vue b/src/client/app/desktop/views/pages/user/user.vue
index 54d675bbd..02ddc2421 100644
--- a/src/client/app/desktop/views/pages/user/user.vue
+++ b/src/client/app/desktop/views/pages/user/user.vue
@@ -9,7 +9,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import parseAcct from '../../../../../../user/parse-acct';
+import parseAcct from '../../../../../../acct/parse';
 import Progress from '../../../../common/scripts/loading';
 import XHeader from './user.header.vue';
 import XHome from './user.home.vue';
diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue
index 1e1c49019..41b015b8a 100644
--- a/src/client/app/desktop/views/pages/welcome.vue
+++ b/src/client/app/desktop/views/pages/welcome.vue
@@ -43,7 +43,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import { docsUrl, copyright, lang } from '../../../config';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 
 const shares = [
 	'Everything!',
diff --git a/src/client/app/desktop/views/widgets/channel.channel.post.vue b/src/client/app/desktop/views/widgets/channel.channel.post.vue
index 587371a76..e10e9c4f7 100644
--- a/src/client/app/desktop/views/widgets/channel.channel.post.vue
+++ b/src/client/app/desktop/views/widgets/channel.channel.post.vue
@@ -19,7 +19,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 
 export default Vue.extend({
 	props: ['post'],
diff --git a/src/client/app/desktop/views/widgets/polls.vue b/src/client/app/desktop/views/widgets/polls.vue
index ad7101e0d..c8ba17bd4 100644
--- a/src/client/app/desktop/views/widgets/polls.vue
+++ b/src/client/app/desktop/views/widgets/polls.vue
@@ -16,7 +16,7 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 
 export default define({
 	name: 'polls',
diff --git a/src/client/app/desktop/views/widgets/trends.vue b/src/client/app/desktop/views/widgets/trends.vue
index be62faa42..27c1860b3 100644
--- a/src/client/app/desktop/views/widgets/trends.vue
+++ b/src/client/app/desktop/views/widgets/trends.vue
@@ -15,7 +15,7 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 
 export default define({
 	name: 'trends',
diff --git a/src/client/app/desktop/views/widgets/users.vue b/src/client/app/desktop/views/widgets/users.vue
index c4643f485..6f6a10157 100644
--- a/src/client/app/desktop/views/widgets/users.vue
+++ b/src/client/app/desktop/views/widgets/users.vue
@@ -23,7 +23,7 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 
 const limit = 3;
 
diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue
index d3e313756..189d7195f 100644
--- a/src/client/app/mobile/views/components/notification.vue
+++ b/src/client/app/mobile/views/components/notification.vue
@@ -79,7 +79,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import getPostSummary from '../../../../../renderers/get-post-summary';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 
 export default Vue.extend({
 	props: ['notification'],
diff --git a/src/client/app/mobile/views/components/post-card.vue b/src/client/app/mobile/views/components/post-card.vue
index de42a01f7..38d4522d2 100644
--- a/src/client/app/mobile/views/components/post-card.vue
+++ b/src/client/app/mobile/views/components/post-card.vue
@@ -15,7 +15,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import summary from '../../../../../renderers/get-post-summary';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 
 export default Vue.extend({
 	props: ['post'],
diff --git a/src/client/app/mobile/views/components/post-detail.sub.vue b/src/client/app/mobile/views/components/post-detail.sub.vue
index c8e06e26f..7ff9f1aab 100644
--- a/src/client/app/mobile/views/components/post-detail.sub.vue
+++ b/src/client/app/mobile/views/components/post-detail.sub.vue
@@ -20,7 +20,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 
 export default Vue.extend({
 	props: ['post'],
diff --git a/src/client/app/mobile/views/components/post-detail.vue b/src/client/app/mobile/views/components/post-detail.vue
index 4a293562e..ed394acd3 100644
--- a/src/client/app/mobile/views/components/post-detail.vue
+++ b/src/client/app/mobile/views/components/post-detail.vue
@@ -80,7 +80,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 import parse from '../../../../../text/parse';
 
 import MkPostMenu from '../../../common/views/components/post-menu.vue';
diff --git a/src/client/app/mobile/views/components/post-preview.vue b/src/client/app/mobile/views/components/post-preview.vue
index b6916edb4..81b0c22bf 100644
--- a/src/client/app/mobile/views/components/post-preview.vue
+++ b/src/client/app/mobile/views/components/post-preview.vue
@@ -20,7 +20,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 
 export default Vue.extend({
 	props: ['post'],
diff --git a/src/client/app/mobile/views/components/post.sub.vue b/src/client/app/mobile/views/components/post.sub.vue
index 7f132b0a4..85ddb9188 100644
--- a/src/client/app/mobile/views/components/post.sub.vue
+++ b/src/client/app/mobile/views/components/post.sub.vue
@@ -20,7 +20,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 
 export default Vue.extend({
 	props: ['post'],
diff --git a/src/client/app/mobile/views/components/post.vue b/src/client/app/mobile/views/components/post.vue
index 7cfb97425..1454bc939 100644
--- a/src/client/app/mobile/views/components/post.vue
+++ b/src/client/app/mobile/views/components/post.vue
@@ -77,7 +77,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 import parse from '../../../../../text/parse';
 
 import MkPostMenu from '../../../common/views/components/post-menu.vue';
diff --git a/src/client/app/mobile/views/components/user-card.vue b/src/client/app/mobile/views/components/user-card.vue
index 542ad2031..46fa3b473 100644
--- a/src/client/app/mobile/views/components/user-card.vue
+++ b/src/client/app/mobile/views/components/user-card.vue
@@ -13,7 +13,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 
 export default Vue.extend({
 	props: ['user'],
diff --git a/src/client/app/mobile/views/components/user-preview.vue b/src/client/app/mobile/views/components/user-preview.vue
index 51c32b9fd..00f87e554 100644
--- a/src/client/app/mobile/views/components/user-preview.vue
+++ b/src/client/app/mobile/views/components/user-preview.vue
@@ -17,7 +17,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 
 export default Vue.extend({
 	props: ['user'],
diff --git a/src/client/app/mobile/views/pages/followers.vue b/src/client/app/mobile/views/pages/followers.vue
index 2e984508d..d2e6a9aea 100644
--- a/src/client/app/mobile/views/pages/followers.vue
+++ b/src/client/app/mobile/views/pages/followers.vue
@@ -19,7 +19,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
-import parseAcct from '../../../../../user/parse-acct';
+import parseAcct from '../../../../../acct/parse';
 
 export default Vue.extend({
 	data() {
diff --git a/src/client/app/mobile/views/pages/following.vue b/src/client/app/mobile/views/pages/following.vue
index f5c7223a1..3690536cf 100644
--- a/src/client/app/mobile/views/pages/following.vue
+++ b/src/client/app/mobile/views/pages/following.vue
@@ -19,7 +19,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
-import parseAcct from '../../../../../user/parse-acct';
+import parseAcct from '../../../../../acct/parse';
 
 export default Vue.extend({
 	data() {
diff --git a/src/client/app/mobile/views/pages/messaging-room.vue b/src/client/app/mobile/views/pages/messaging-room.vue
index 14f12a99b..172666eea 100644
--- a/src/client/app/mobile/views/pages/messaging-room.vue
+++ b/src/client/app/mobile/views/pages/messaging-room.vue
@@ -10,7 +10,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import parseAcct from '../../../../../user/parse-acct';
+import parseAcct from '../../../../../acct/parse';
 
 export default Vue.extend({
 	data() {
diff --git a/src/client/app/mobile/views/pages/messaging.vue b/src/client/app/mobile/views/pages/messaging.vue
index f3f6f9f9b..fa735a253 100644
--- a/src/client/app/mobile/views/pages/messaging.vue
+++ b/src/client/app/mobile/views/pages/messaging.vue
@@ -7,7 +7,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../user/get-acct';
+import getAcct from '../../../../../acct/render';
 
 export default Vue.extend({
 	mounted() {
diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue
index 2d8f9f8ec..be696484b 100644
--- a/src/client/app/mobile/views/pages/user.vue
+++ b/src/client/app/mobile/views/pages/user.vue
@@ -60,8 +60,8 @@
 <script lang="ts">
 import Vue from 'vue';
 import * as age from 's-age';
-import getAcct from '../../../../../user/get-acct';
-import getAcct from '../../../../../user/parse-acct';
+import getAcct from '../../../../../acct/render';
+import getAcct from '../../../../../acct/parse';
 import Progress from '../../../common/scripts/loading';
 import XHome from './user/home.vue';
 
diff --git a/src/client/app/mobile/views/pages/user/home.followers-you-know.vue b/src/client/app/mobile/views/pages/user/home.followers-you-know.vue
index 04d8538f7..ffdd9f178 100644
--- a/src/client/app/mobile/views/pages/user/home.followers-you-know.vue
+++ b/src/client/app/mobile/views/pages/user/home.followers-you-know.vue
@@ -12,7 +12,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../../user/get-acct';
+import getAcct from '../../../../../../acct/render';
 
 export default Vue.extend({
 	props: ['user'],
diff --git a/src/client/app/mobile/views/pages/user/home.photos.vue b/src/client/app/mobile/views/pages/user/home.photos.vue
index 491660711..ecf508207 100644
--- a/src/client/app/mobile/views/pages/user/home.photos.vue
+++ b/src/client/app/mobile/views/pages/user/home.photos.vue
@@ -14,7 +14,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../../user/get-acct';
+import getAcct from '../../../../../../acct/render';
 
 export default Vue.extend({
 	props: ['user'],
diff --git a/src/drive/add-file.ts b/src/drive/add-file.ts
index 4a718a9da..f48fada7e 100644
--- a/src/drive/add-file.ts
+++ b/src/drive/add-file.ts
@@ -14,7 +14,7 @@ import DriveFile, { getGridFSBucket } from '../models/drive-file';
 import DriveFolder from '../models/drive-folder';
 import { pack } from '../models/drive-file';
 import event, { publishDriveStream } from '../publishers/stream';
-import getAcct from '../user/get-acct';
+import getAcct from '../acct/render';
 import config from '../config';
 
 const gm = _gm.subClass({
diff --git a/src/renderers/get-user-summary.ts b/src/renderers/get-user-summary.ts
index 2a9e8a5d0..208987113 100644
--- a/src/renderers/get-user-summary.ts
+++ b/src/renderers/get-user-summary.ts
@@ -1,5 +1,5 @@
 import { IUser, isLocalUser } from '../models/user';
-import getAcct from '../user/get-acct';
+import getAcct from '../acct/render';
 
 /**
  * ユーザーを表す文字列を取得します。
diff --git a/src/server/activitypub/inbox.ts b/src/server/activitypub/inbox.ts
index 865797fae..6df636f89 100644
--- a/src/server/activitypub/inbox.ts
+++ b/src/server/activitypub/inbox.ts
@@ -3,7 +3,7 @@ import * as express from 'express';
 import { parseRequest, verifySignature } from 'http-signature';
 import User, { IRemoteUser } from '../../models/user';
 import queue from '../../queue';
-import parseAcct from '../../user/parse-acct';
+import parseAcct from '../../acct/parse';
 
 const app = express();
 app.disable('x-powered-by');
diff --git a/src/server/activitypub/post.ts b/src/server/activitypub/post.ts
index 2962a9b96..91d91aeb9 100644
--- a/src/server/activitypub/post.ts
+++ b/src/server/activitypub/post.ts
@@ -1,7 +1,7 @@
 import * as express from 'express';
 import context from '../../remote/activitypub/renderer/context';
 import render from '../../remote/activitypub/renderer/note';
-import parseAcct from '../../user/parse-acct';
+import parseAcct from '../../acct/parse';
 import Post from '../../models/post';
 import User from '../../models/user';
 
diff --git a/src/server/activitypub/with-user.ts b/src/server/activitypub/with-user.ts
index b4090786b..bdbbefb42 100644
--- a/src/server/activitypub/with-user.ts
+++ b/src/server/activitypub/with-user.ts
@@ -1,4 +1,4 @@
-import parseAcct from '../../user/parse-acct';
+import parseAcct from '../../acct/parse';
 import User from '../../models/user';
 
 export default (redirect, respond) => async (req, res, next) => {
diff --git a/src/server/api/bot/core.ts b/src/server/api/bot/core.ts
index 7f9409660..7e80f31e5 100644
--- a/src/server/api/bot/core.ts
+++ b/src/server/api/bot/core.ts
@@ -5,7 +5,7 @@ import User, { IUser, init as initUser, ILocalUser } from '../../../models/user'
 
 import getPostSummary from '../../../renderers/get-post-summary';
 import getUserSummary from '../../../renderers/get-user-summary';
-import parseAcct from '../../../user/parse-acct';
+import parseAcct from '../../../acct/parse';
 import getNotificationSummary from '../../../renderers/get-notification-summary';
 
 const hmm = [
diff --git a/src/server/api/bot/interfaces/line.ts b/src/server/api/bot/interfaces/line.ts
index 17841a9f0..7847cbdea 100644
--- a/src/server/api/bot/interfaces/line.ts
+++ b/src/server/api/bot/interfaces/line.ts
@@ -7,8 +7,8 @@ import config from '../../../../config';
 import BotCore from '../core';
 import _redis from '../../../../db/redis';
 import prominence = require('prominence');
-import getAcct from '../../../../user/get-acct';
-import parseAcct from '../../../../user/parse-acct';
+import getAcct from '../../../../acct/render';
+import parseAcct from '../../../../acct/parse';
 import getPostSummary from '../../../../renderers/get-post-summary';
 
 const redis = prominence(_redis);
diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts
index 34a3aa190..7c44e6f08 100644
--- a/src/server/api/endpoints/posts/create.ts
+++ b/src/server/api/endpoints/posts/create.ts
@@ -18,8 +18,8 @@ import watch from '../../common/watch-post';
 import stream, { publishChannelStream } from '../../../../publishers/stream';
 import notify from '../../../../publishers/notify';
 import pushSw from '../../../../publishers/push-sw';
-import getAcct from '../../../../user/get-acct';
-import parseAcct from '../../../../user/parse-acct';
+import getAcct from '../../../../acct/render';
+import parseAcct from '../../../../acct/parse';
 import config from '../../../../config';
 
 /**
diff --git a/src/server/api/limitter.ts b/src/server/api/limitter.ts
index 81016457d..638fac78b 100644
--- a/src/server/api/limitter.ts
+++ b/src/server/api/limitter.ts
@@ -3,7 +3,7 @@ import * as debug from 'debug';
 import limiterDB from '../../db/redis';
 import { Endpoint } from './endpoints';
 import { IAuthContext } from './authenticate';
-import getAcct from '../../user/get-acct';
+import getAcct from '../../acct/render';
 
 const log = debug('misskey:limitter');
 
diff --git a/src/server/webfinger.ts b/src/server/webfinger.ts
index 3b0f416d6..20057da31 100644
--- a/src/server/webfinger.ts
+++ b/src/server/webfinger.ts
@@ -1,5 +1,5 @@
 import config from '../config';
-import parseAcct from '../user/parse-acct';
+import parseAcct from '../acct/parse';
 import User from '../models/user';
 const express = require('express');
 
diff --git a/src/text/parse/elements/mention.ts b/src/text/parse/elements/mention.ts
index 732554d8b..4f2997b39 100644
--- a/src/text/parse/elements/mention.ts
+++ b/src/text/parse/elements/mention.ts
@@ -1,7 +1,7 @@
 /**
  * Mention
  */
-import parseAcct from '../../../user/parse-acct';
+import parseAcct from '../../../acct/parse';
 
 module.exports = text => {
 	const match = text.match(/^(?:@[a-zA-Z0-9\-]+){1,2}/);

From 39662b0e01d2c3fd05285cb6167f45981e32e9ea Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 2 Apr 2018 14:18:01 +0900
Subject: [PATCH 1016/1250] Add visibility property

---
 src/models/post.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/models/post.ts b/src/models/post.ts
index 4daad306d..798c18e4b 100644
--- a/src/models/post.ts
+++ b/src/models/post.ts
@@ -40,6 +40,7 @@ export type IPost = {
 	repliesCount: number;
 	reactionCounts: any;
 	mentions: mongo.ObjectID[];
+	visibility: 'public' | 'unlisted' | 'private' | 'direct';
 	geo: {
 		coordinates: number[];
 		altitude: number;

From d7323a78bf881a9eb68758869d5d58bc5c8acae9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 2 Apr 2018 15:05:46 +0900
Subject: [PATCH 1017/1250] Update icon :art:

---
 assets/favicon.ico     | 3 +++
 assets/favicon/128.png | 4 ++--
 assets/favicon/128.svg | 3 +++
 assets/favicon/16.png  | 4 ++--
 assets/favicon/16.svg  | 3 +++
 assets/favicon/256.png | 4 ++--
 assets/favicon/256.svg | 3 +++
 assets/favicon/32.png  | 4 ++--
 assets/favicon/32.svg  | 3 +++
 assets/favicon/64.png  | 4 ++--
 assets/favicon/64.svg  | 3 +++
 assets/mi.svg          | 3 +++
 12 files changed, 31 insertions(+), 10 deletions(-)
 create mode 100644 assets/favicon.ico
 create mode 100644 assets/favicon/128.svg
 create mode 100644 assets/favicon/16.svg
 create mode 100644 assets/favicon/256.svg
 create mode 100644 assets/favicon/32.svg
 create mode 100644 assets/favicon/64.svg
 create mode 100644 assets/mi.svg

diff --git a/assets/favicon.ico b/assets/favicon.ico
new file mode 100644
index 000000000..5fbb8790b
--- /dev/null
+++ b/assets/favicon.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:008d0abf6ea8e9ab827f72d808fc5bc64a652686aa8711646ddb3f5e94b61f33
+size 360414
diff --git a/assets/favicon/128.png b/assets/favicon/128.png
index 129c15b6c..6d22b18df 100644
--- a/assets/favicon/128.png
+++ b/assets/favicon/128.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:5fa7616da6ddc15fbf818fe2393edba92b921c4f8d6bc441c807b82040e54ef0
-size 1681
+oid sha256:51075db208c92f2f7ef6938e9a61487cfc71da0c0660a068d8bb0b88e1666656
+size 2325
diff --git a/assets/favicon/128.svg b/assets/favicon/128.svg
new file mode 100644
index 000000000..dd3f1b256
--- /dev/null
+++ b/assets/favicon/128.svg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6e0da2dcdee7371722c5325fd7165d510a02001037e6148cbed220bc3087254f
+size 7674
diff --git a/assets/favicon/16.png b/assets/favicon/16.png
index 112b14fd6..d10cb6f71 100644
--- a/assets/favicon/16.png
+++ b/assets/favicon/16.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:5806437941bc7b90e3bb5843d6b0c067edfc16948f8c816ee0dfe356674ff951
-size 203
+oid sha256:f692feda74ac5673f0322af7bbe5dbbfa79345704bf8c2f6ee6ed6bdcd2ea3fb
+size 518
diff --git a/assets/favicon/16.svg b/assets/favicon/16.svg
new file mode 100644
index 000000000..6b75b42f3
--- /dev/null
+++ b/assets/favicon/16.svg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6c84a59494c390831fd52f99075e3567daf763f95ec84ecd9f6257d22b7c3645
+size 7094
diff --git a/assets/favicon/256.png b/assets/favicon/256.png
index b3e13085e..b06bbe907 100644
--- a/assets/favicon/256.png
+++ b/assets/favicon/256.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:00f672b77e57e51fd8616603b037ea0b29da34e847954d6ebca404d83c8bf005
-size 3289
+oid sha256:d52f93b719d434dd11ab5d8b4b334402bfdb604ca99c018a1176e286cd42ae26
+size 4824
diff --git a/assets/favicon/256.svg b/assets/favicon/256.svg
new file mode 100644
index 000000000..4ee31d438
--- /dev/null
+++ b/assets/favicon/256.svg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:828c3c7f09d46f4e77ae7cc9be1ab8e69fd8f55f4ee398a92724f2e8b8eafcbd
+size 7674
diff --git a/assets/favicon/32.png b/assets/favicon/32.png
index cdbf18524..aae8c824b 100644
--- a/assets/favicon/32.png
+++ b/assets/favicon/32.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:fa25aaeb394a245fe5b7d4333f1f141d96e3ce02d6baab09d0dc10b84fe4e015
-size 329
+oid sha256:b0458fdb638416a898fa9a42b2410582c01d7dbe74dba3ada785c69501774e52
+size 761
diff --git a/assets/favicon/32.svg b/assets/favicon/32.svg
new file mode 100644
index 000000000..8802e42cb
--- /dev/null
+++ b/assets/favicon/32.svg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f684836ab630b7539f7f645df7c6187df5c2a47c8bac0688a5351f9fd48d876a
+size 7233
diff --git a/assets/favicon/64.png b/assets/favicon/64.png
index 60967c711..021345d06 100644
--- a/assets/favicon/64.png
+++ b/assets/favicon/64.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:bd7d8935d89cb3cdedf0b8841167c917c1dbca0ec15cbb14286354dad25952db
-size 818
+oid sha256:0a789d8371d6074ad90e3ac0d9028bf60b78ee92d037e29add0cdadb8b8217c2
+size 1258
diff --git a/assets/favicon/64.svg b/assets/favicon/64.svg
new file mode 100644
index 000000000..b137473f9
--- /dev/null
+++ b/assets/favicon/64.svg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d0fdc40155debac0377c0a51d45ffe0d5145c5e1f2e684a9c9550751183bdfa0
+size 7218
diff --git a/assets/mi.svg b/assets/mi.svg
new file mode 100644
index 000000000..ff5800096
--- /dev/null
+++ b/assets/mi.svg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a9e27c01560e1ee474ed73ee6e0b8236145e5efdc34edde6c9f4e45444ee0336
+size 7200

From 353423ceee3a945f336a421ce2ec71775c406160 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 2 Apr 2018 15:08:54 +0900
Subject: [PATCH 1018/1250] Update icon :art:

---
 assets/apple-touch-icon.png | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/assets/apple-touch-icon.png b/assets/apple-touch-icon.png
index b3e13085e..b06bbe907 100644
--- a/assets/apple-touch-icon.png
+++ b/assets/apple-touch-icon.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:00f672b77e57e51fd8616603b037ea0b29da34e847954d6ebca404d83c8bf005
-size 3289
+oid sha256:d52f93b719d434dd11ab5d8b4b334402bfdb604ca99c018a1176e286cd42ae26
+size 4824

From 59f39ab3030f24f18d7ccd0c96b9fe3159bf6fc3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 2 Apr 2018 15:09:20 +0900
Subject: [PATCH 1019/1250] Clean up

---
 assets/icon.ai | 3 ---
 assets/ss.jpg  | 3 ---
 2 files changed, 6 deletions(-)
 delete mode 100644 assets/icon.ai
 delete mode 100644 assets/ss.jpg

diff --git a/assets/icon.ai b/assets/icon.ai
deleted file mode 100644
index 82eceb537..000000000
--- a/assets/icon.ai
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:8eb7dea6464cebfa901c46d62bd4182f98072420055e836a961865a9691b50e6
-size 1097030
diff --git a/assets/ss.jpg b/assets/ss.jpg
deleted file mode 100644
index a569ebc90..000000000
--- a/assets/ss.jpg
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:1af75c7294071320a3fe19b320e2e93c47b9cf6c9daa3a934a87e20f976a2050
-size 307446

From 4b0df0923e737b01f36fd569eadb23efd13a20f4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 2 Apr 2018 15:19:12 +0900
Subject: [PATCH 1020/1250] Clean up

---
 src/client/assets/favicon.ico | 3 ---
 1 file changed, 3 deletions(-)
 delete mode 100644 src/client/assets/favicon.ico

diff --git a/src/client/assets/favicon.ico b/src/client/assets/favicon.ico
deleted file mode 100644
index 927a60e0f..000000000
--- a/src/client/assets/favicon.ico
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:d8d9f6d631000e6de39f02813b3771c6de1742e94ee8312e140fd53511ff8cbc
-size 360414

From 756c1e974e2356479565c952827954eb9400e09b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 2 Apr 2018 15:22:32 +0900
Subject: [PATCH 1021/1250] Present icons

---
 src/client/assets/manifest.json | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/src/client/assets/manifest.json b/src/client/assets/manifest.json
index 783d0539a..a0f6745b0 100644
--- a/src/client/assets/manifest.json
+++ b/src/client/assets/manifest.json
@@ -3,5 +3,12 @@
 	"name": "Misskey",
 	"start_url": "/",
 	"display": "standalone",
-	"background_color": "#313a42"
+	"background_color": "#313a42",
+	"icons": {
+		"16": "/assets/favicon/16.png",
+		"32": "/assets/favicon/32.png",
+		"64": "/assets/favicon/64.png",
+		"128": "/assets/favicon/128.png",
+		"256": "/assets/favicon/256.png"
+	}
 }

From 9480d5f01aa7dc0653469369c906bf0ad9245398 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 2 Apr 2018 07:52:37 +0000
Subject: [PATCH 1022/1250] fix(package): update bootstrap-vue to version
 2.0.0-rc.6

Closes #1367
---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index ce06a6eeb..07b30e16b 100644
--- a/package.json
+++ b/package.json
@@ -89,7 +89,7 @@
 		"autwh": "0.0.1",
 		"bcryptjs": "2.4.3",
 		"body-parser": "1.18.2",
-		"bootstrap-vue": "2.0.0-rc.4",
+		"bootstrap-vue": "2.0.0-rc.6",
 		"cafy": "3.2.1",
 		"chai": "4.1.2",
 		"chai-http": "4.0.0",

From ebae97a1febb6294636e7715e8c7e72a84d5a073 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Mon, 2 Apr 2018 17:11:14 +0900
Subject: [PATCH 1023/1250] Distribute posts from remote

---
 src/post/create.ts                            |  50 +++
 src/post/distribute.ts                        | 216 ++++++++++++
 .../common/watch-post.ts => post/watch.ts}    |   2 +-
 src/processor/http/perform-activitypub.ts     |   2 +-
 src/remote/activitypub/act/create.ts          |   4 +-
 src/remote/activitypub/act/index.ts           |   6 +-
 src/remote/activitypub/create.ts              | 159 +++++----
 src/remote/activitypub/resolve-person.ts      |   6 +-
 src/server/activitypub/inbox.ts               |  11 +-
 src/server/api/endpoints/posts/create.ts      | 310 +++---------------
 src/server/api/endpoints/posts/polls/vote.ts  |   2 +-
 .../api/endpoints/posts/reactions/create.ts   |   2 +-
 12 files changed, 417 insertions(+), 353 deletions(-)
 create mode 100644 src/post/create.ts
 create mode 100644 src/post/distribute.ts
 rename src/{server/api/common/watch-post.ts => post/watch.ts} (89%)

diff --git a/src/post/create.ts b/src/post/create.ts
new file mode 100644
index 000000000..ecea37382
--- /dev/null
+++ b/src/post/create.ts
@@ -0,0 +1,50 @@
+import parseAcct from '../acct/parse';
+import Post from '../models/post';
+import User from '../models/user';
+
+export default async (post, reply, repost, atMentions) => {
+	post.mentions = [];
+
+	function addMention(mentionee) {
+		// Reject if already added
+		if (post.mentions.some(x => x.equals(mentionee))) return;
+
+		// Add mention
+		post.mentions.push(mentionee);
+	}
+
+	if (reply) {
+		// Add mention
+		addMention(reply.userId);
+		post.replyId = reply._id;
+		post._reply = { userId: reply.userId };
+	} else {
+		post.replyId = null;
+		post._reply = null;
+	}
+
+	if (repost) {
+		if (post.text) {
+			// Add mention
+			addMention(repost.userId);
+		}
+
+		post.repostId = repost._id;
+		post._repost = { userId: repost.userId };
+	} else {
+		post.repostId = null;
+		post._repost = null;
+	}
+
+	await Promise.all(atMentions.map(async mention => {
+		// Fetch mentioned user
+		// SELECT _id
+		const { _id } = await User
+			.findOne(parseAcct(mention), { _id: true });
+
+		// Add mention
+		addMention(_id);
+	}));
+
+	return Post.insert(post);
+};
diff --git a/src/post/distribute.ts b/src/post/distribute.ts
new file mode 100644
index 000000000..3925d4128
--- /dev/null
+++ b/src/post/distribute.ts
@@ -0,0 +1,216 @@
+import Channel from '../models/channel';
+import Mute from '../models/mute';
+import Following from '../models/following';
+import Post from '../models/post';
+import Watching from '../models/post-watching';
+import ChannelWatching from '../models/channel-watching';
+import User from '../models/user';
+import stream, { publishChannelStream } from '../publishers/stream';
+import notify from '../publishers/notify';
+import pushSw from '../publishers/push-sw';
+import watch from './watch';
+
+export default async (user, mentions, post) => {
+	const promises = [
+		User.update({ _id: user._id }, {
+			// Increment my posts count
+			$inc: {
+				postsCount: 1
+			},
+
+			$set: {
+				latestPost: post._id
+			}
+		}),
+	] as Array<Promise<any>>;
+
+	function addMention(mentionee, reason) {
+		// Publish event
+		if (!user._id.equals(mentionee)) {
+			promises.push(Mute.find({
+				muterId: mentionee,
+				deletedAt: { $exists: false }
+			}).then(mentioneeMutes => {
+				const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString());
+				if (mentioneesMutedUserIds.indexOf(user._id.toString()) == -1) {
+					stream(mentionee, reason, post);
+					pushSw(mentionee, reason, post);
+				}
+			}));
+		}
+	}
+
+	// タイムラインへの投稿
+	if (!post.channelId) {
+		// Publish event to myself's stream
+		stream(user._id, 'post', post);
+
+		// Fetch all followers
+		const followers = await Following
+			.find({
+				followeeId: user._id,
+				// 削除されたドキュメントは除く
+				deletedAt: { $exists: false }
+			}, {
+				followerId: true,
+				_id: false
+			});
+
+		// Publish event to followers stream
+		followers.forEach(following =>
+			stream(following.followerId, 'post', post));
+	}
+
+	// チャンネルへの投稿
+	if (post.channelId) {
+		// Increment channel index(posts count)
+		promises.push(Channel.update({ _id: post.channelId }, {
+			$inc: {
+				index: 1
+			}
+		}));
+
+		// Publish event to channel
+		publishChannelStream(post.channelId, 'post', post);
+
+		// Get channel watchers
+		const watches = await ChannelWatching.find({
+			channelId: post.channelId,
+			// 削除されたドキュメントは除く
+			deletedAt: { $exists: false }
+		});
+
+		// チャンネルの視聴者(のタイムライン)に配信
+		watches.forEach(w => {
+			stream(w.userId, 'post', post);
+		});
+	}
+
+	// If has in reply to post
+	if (post.replyId) {
+		promises.push(
+			// Increment replies count
+			Post.update({ _id: post.replyId }, {
+				$inc: {
+					repliesCount: 1
+				}
+			}),
+
+			// 自分自身へのリプライでない限りは通知を作成
+			notify(post.reply.userId, user._id, 'reply', {
+				postId: post._id
+			}),
+
+			// Fetch watchers
+			Watching
+				.find({
+					postId: post.replyId,
+					userId: { $ne: user._id },
+					// 削除されたドキュメントは除く
+					deletedAt: { $exists: false }
+				}, {
+					fields: {
+						userId: true
+					}
+				})
+				.then(watchers => {
+					watchers.forEach(watcher => {
+						notify(watcher.userId, user._id, 'reply', {
+							postId: post._id
+						});
+					});
+				})
+		);
+
+		// Add mention
+		addMention(post.reply.userId, 'reply');
+
+		// この投稿をWatchする
+		if (user.account.settings.autoWatch !== false) {
+			promises.push(watch(user._id, post.reply));
+		}
+	}
+
+	// If it is repost
+	if (post.repostId) {
+		const type = post.text ? 'quote' : 'repost';
+
+		promises.push(
+			// Notify
+			notify(post.repost.userId, user._id, type, {
+				postId: post._id
+			}),
+
+			// Fetch watchers
+			Watching
+				.find({
+					postId: post.repostId,
+					userId: { $ne: user._id },
+					// 削除されたドキュメントは除く
+					deletedAt: { $exists: false }
+				}, {
+					fields: {
+						userId: true
+					}
+				})
+				.then(watchers => {
+					watchers.forEach(watcher => {
+						notify(watcher.userId, user._id, type, {
+							postId: post._id
+						});
+					});
+				}),
+
+			// この投稿をWatchする
+			// TODO: ユーザーが「Repostしたときに自動でWatchする」設定を
+			//       オフにしていた場合はしない
+			watch(user._id, post.repost)
+		);
+
+		// If it is quote repost
+		if (post.text) {
+			// Add mention
+			addMention(post.repost.userId, 'quote');
+		} else {
+			// Publish event
+			if (!user._id.equals(post.repost.userId)) {
+				stream(post.repost.userId, 'repost', post);
+			}
+		}
+
+		// 今までで同じ投稿をRepostしているか
+		const existRepost = await Post.findOne({
+			userId: user._id,
+			repostId: post.repostId,
+			_id: {
+				$ne: post._id
+			}
+		});
+
+		if (!existRepost) {
+			// Update repostee status
+			promises.push(Post.update({ _id: post.repostId }, {
+				$inc: {
+					repostCount: 1
+				}
+			}));
+		}
+	}
+
+	// Resolve all mentions
+	await Promise.all(mentions.map(async mention => {
+		// 既に言及されたユーザーに対する返信や引用repostの場合も無視
+		if (post.reply && post.reply.userId.equals(mention)) return;
+		if (post.repost && post.repost.userId.equals(mention)) return;
+
+		// Add mention
+		addMention(mention, 'mention');
+
+		// Create notification
+		await notify(mention, user._id, 'mention', {
+			postId: post._id
+		});
+	}));
+
+	return Promise.all(promises);
+};
diff --git a/src/server/api/common/watch-post.ts b/src/post/watch.ts
similarity index 89%
rename from src/server/api/common/watch-post.ts
rename to src/post/watch.ts
index 83c9b94f3..61ea44443 100644
--- a/src/server/api/common/watch-post.ts
+++ b/src/post/watch.ts
@@ -1,5 +1,5 @@
 import * as mongodb from 'mongodb';
-import Watching from '../../../models/post-watching';
+import Watching from '../models/post-watching';
 
 export default async (me: mongodb.ObjectID, post: object) => {
 	// 自分の投稿はwatchできない
diff --git a/src/processor/http/perform-activitypub.ts b/src/processor/http/perform-activitypub.ts
index 51e1ede14..d8981ea12 100644
--- a/src/processor/http/perform-activitypub.ts
+++ b/src/processor/http/perform-activitypub.ts
@@ -2,5 +2,5 @@ import User from '../../models/user';
 import act from '../../remote/activitypub/act';
 
 export default ({ data }, done) => User.findOne({ _id: data.actor })
-	.then(actor => act(actor, data.outbox))
+	.then(actor => act(actor, data.outbox, data.distribute))
 	.then(() => done(), done);
diff --git a/src/remote/activitypub/act/create.ts b/src/remote/activitypub/act/create.ts
index 9eb74800e..a6ba9a1d2 100644
--- a/src/remote/activitypub/act/create.ts
+++ b/src/remote/activitypub/act/create.ts
@@ -1,9 +1,9 @@
 import create from '../create';
 
-export default (resolver, actor, activity) => {
+export default (resolver, actor, activity, distribute) => {
 	if ('actor' in activity && actor.account.uri !== activity.actor) {
 		throw new Error();
 	}
 
-	return create(resolver, actor, activity.object);
+	return create(resolver, actor, activity.object, distribute);
 };
diff --git a/src/remote/activitypub/act/index.ts b/src/remote/activitypub/act/index.ts
index a76983638..06d662c19 100644
--- a/src/remote/activitypub/act/index.ts
+++ b/src/remote/activitypub/act/index.ts
@@ -2,10 +2,10 @@ import create from './create';
 import createObject from '../create';
 import Resolver from '../resolver';
 
-export default (actor, value) => {
+export default (actor, value, distribute) => {
 	return new Resolver().resolve(value).then(resolved => Promise.all(resolved.map(async promisedResult => {
 		const { resolver, object } = await promisedResult;
-		const created = await (await createObject(resolver, actor, [object]))[0];
+		const created = await (await createObject(resolver, actor, [object], distribute))[0];
 
 		if (created !== null) {
 			return created;
@@ -13,7 +13,7 @@ export default (actor, value) => {
 
 		switch (object.type) {
 		case 'Create':
-			return create(resolver, actor, object);
+			return create(resolver, actor, object, distribute);
 
 		default:
 			return null;
diff --git a/src/remote/activitypub/create.ts b/src/remote/activitypub/create.ts
index 8ea8a85fd..dd3f7b022 100644
--- a/src/remote/activitypub/create.ts
+++ b/src/remote/activitypub/create.ts
@@ -1,8 +1,11 @@
 import { JSDOM } from 'jsdom';
 import config from '../../config';
-import Post from '../../models/post';
+import { pack as packPost } from '../../models/post';
 import RemoteUserObject, { IRemoteUserObject } from '../../models/remote-user-object';
+import { IRemoteUser } from '../../models/user';
 import uploadFromUrl from '../../drive/upload-from-url';
+import createPost from '../../post/create';
+import distributePost from '../../post/distribute';
 import Resolver from './resolver';
 const createDOMPurify = require('dompurify');
 
@@ -16,72 +19,98 @@ function createRemoteUserObject($ref, $id, { id }) {
 	return RemoteUserObject.insert({ uri: id, object });
 }
 
-async function createImage(actor, object) {
-	if ('attributedTo' in object && actor.account.uri !== object.attributedTo) {
-		throw new Error();
+class Creator {
+	private actor: IRemoteUser;
+	private distribute: boolean;
+
+	constructor(actor, distribute) {
+		this.actor = actor;
+		this.distribute = distribute;
 	}
 
-	const { _id } = await uploadFromUrl(object.url, actor);
-	return createRemoteUserObject('driveFiles.files', _id, object);
-}
-
-async function createNote(resolver, actor, object) {
-	if ('attributedTo' in object && actor.account.uri !== object.attributedTo) {
-		throw new Error();
-	}
-
-	const mediaIds = 'attachment' in object &&
-		(await Promise.all(await create(resolver, actor, object.attachment)))
-			.filter(media => media !== null && media.object.$ref === 'driveFiles.files')
-			.map(({ object }) => object.$id);
-
-	const { window } = new JSDOM(object.content);
-
-	const { _id } = await Post.insert({
-		channelId: undefined,
-		index: undefined,
-		createdAt: new Date(object.published),
-		mediaIds,
-		replyId: undefined,
-		repostId: undefined,
-		poll: undefined,
-		text: window.document.body.textContent,
-		textHtml: object.content && createDOMPurify(window).sanitize(object.content),
-		userId: actor._id,
-		appId: null,
-		viaMobile: false,
-		geo: undefined
-	});
-
-	// Register to search database
-	if (object.content && config.elasticsearch.enable) {
-		const es = require('../../db/elasticsearch');
-
-		es.index({
-			index: 'misskey',
-			type: 'post',
-			id: _id.toString(),
-			body: {
-				text: window.document.body.textContent
-			}
-		});
-	}
-
-	return createRemoteUserObject('posts', _id, object);
-}
-
-export default async function create(parentResolver: Resolver, actor, value): Promise<Array<Promise<IRemoteUserObject>>> {
-	const results = await parentResolver.resolveRemoteUserObjects(value);
-
-	return results.map(promisedResult => promisedResult.then(({ resolver, object }) => {
-		switch (object.type) {
-		case 'Image':
-			return createImage(actor, object);
-
-		case 'Note':
-			return createNote(resolver, actor, object);
+	private async createImage(object) {
+		if ('attributedTo' in object && this.actor.account.uri !== object.attributedTo) {
+			throw new Error();
 		}
 
-		return null;
-	}));
+		const { _id } = await uploadFromUrl(object.url, this.actor);
+		return createRemoteUserObject('driveFiles.files', _id, object);
+	}
+
+	private async createNote(resolver, object) {
+		if ('attributedTo' in object && this.actor.account.uri !== object.attributedTo) {
+			throw new Error();
+		}
+
+		const mediaIds = 'attachment' in object &&
+			(await Promise.all(await this.create(resolver, object.attachment)))
+				.filter(media => media !== null && media.object.$ref === 'driveFiles.files')
+				.map(({ object }) => object.$id);
+
+		const { window } = new JSDOM(object.content);
+
+		const inserted = await createPost({
+			channelId: undefined,
+			index: undefined,
+			createdAt: new Date(object.published),
+			mediaIds,
+			replyId: undefined,
+			repostId: undefined,
+			poll: undefined,
+			text: window.document.body.textContent,
+			textHtml: object.content && createDOMPurify(window).sanitize(object.content),
+			userId: this.actor._id,
+			appId: null,
+			viaMobile: false,
+			geo: undefined
+		}, null, null, []);
+
+		const promisedRemoteUserObject = createRemoteUserObject('posts', inserted._id, object);
+		const promises = [];
+
+		if (this.distribute) {
+			promises.push(distributePost(this.actor, inserted.mentions, packPost(inserted)));
+		}
+
+		// Register to search database
+		if (object.content && config.elasticsearch.enable) {
+			const es = require('../../db/elasticsearch');
+
+			promises.push(new Promise((resolve, reject) => {
+				es.index({
+					index: 'misskey',
+					type: 'post',
+					id: inserted._id.toString(),
+					body: {
+						text: window.document.body.textContent
+					}
+				}, resolve);
+			}));
+		}
+
+		await Promise.all(promises);
+
+		return promisedRemoteUserObject;
+	}
+
+	public async create(parentResolver, value): Promise<Array<Promise<IRemoteUserObject>>> {
+		const results = await parentResolver.resolveRemoteUserObjects(value);
+
+		return results.map(promisedResult => promisedResult.then(({ resolver, object }) => {
+			switch (object.type) {
+			case 'Image':
+				return this.createImage(object);
+
+			case 'Note':
+				return this.createNote(resolver, object);
+			}
+
+			return null;
+		}));
+	}
 }
+
+export default (resolver: Resolver, actor, value, distribute?: boolean) => {
+	const creator = new Creator(actor, distribute);
+	return creator.create(resolver, value);
+};
diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts
index d928e7ce1..4a2636b2f 100644
--- a/src/remote/activitypub/resolve-person.ts
+++ b/src/remote/activitypub/resolve-person.ts
@@ -52,10 +52,10 @@ export default async (value, usernameLower, hostLower, acctLower) => {
 		bannerId: null,
 		createdAt: Date.parse(object.published),
 		description: summaryDOM.textContent,
-		followersCount: followers.totalItem,
-		followingCount: following.totalItem,
+		followersCount: followers ? followers.totalItem || 0 : 0,
+		followingCount: following ? following.totalItem || 0 : 0,
 		name: object.name,
-		postsCount: outbox.totalItem,
+		postsCount: outbox ? outbox.totalItem || 0 : 0,
 		driveCapacity: 1024 * 1024 * 8, // 8MiB
 		username: object.preferredUsername,
 		usernameLower,
diff --git a/src/server/activitypub/inbox.ts b/src/server/activitypub/inbox.ts
index 6df636f89..2de2bd964 100644
--- a/src/server/activitypub/inbox.ts
+++ b/src/server/activitypub/inbox.ts
@@ -6,10 +6,14 @@ import queue from '../../queue';
 import parseAcct from '../../acct/parse';
 
 const app = express();
-app.disable('x-powered-by');
-app.use(bodyParser.json());
 
-app.post('/@:user/inbox', async (req, res) => {
+app.disable('x-powered-by');
+
+app.post('/@:user/inbox', bodyParser.json({
+	type() {
+		return true;
+	}
+}), async (req, res) => {
 	let parsed;
 
 	req.headers.authorization = 'Signature ' + req.headers.signature;
@@ -51,6 +55,7 @@ app.post('/@:user/inbox', async (req, res) => {
 		type: 'performActivityPub',
 		actor: user._id,
 		outbox: req.body,
+		distribute: true,
 	}).save();
 
 	return res.status(202).end();
diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts
index 7c44e6f08..b633494a3 100644
--- a/src/server/api/endpoints/posts/create.ts
+++ b/src/server/api/endpoints/posts/create.ts
@@ -3,24 +3,16 @@
  */
 import $ from 'cafy';
 import deepEqual = require('deep-equal');
+import renderAcct from '../../../../acct/render';
+import config from '../../../../config';
 import html from '../../../../text/html';
 import parse from '../../../../text/parse';
-import Post, { IPost, isValidText, isValidCw } from '../../../../models/post';
-import User, { ILocalUser } from '../../../../models/user';
+import Post, { IPost, isValidText, isValidCw, pack } from '../../../../models/post';
+import { ILocalUser } from '../../../../models/user';
 import Channel, { IChannel } from '../../../../models/channel';
-import Following from '../../../../models/following';
-import Mute from '../../../../models/mute';
 import DriveFile from '../../../../models/drive-file';
-import Watching from '../../../../models/post-watching';
-import ChannelWatching from '../../../../models/channel-watching';
-import { pack } from '../../../../models/post';
-import watch from '../../common/watch-post';
-import stream, { publishChannelStream } from '../../../../publishers/stream';
-import notify from '../../../../publishers/notify';
-import pushSw from '../../../../publishers/push-sw';
-import getAcct from '../../../../acct/render';
-import parseAcct from '../../../../acct/parse';
-import config from '../../../../config';
+import create from '../../../../post/create';
+import distribute from '../../../../post/distribute';
 
 /**
  * Create a post
@@ -251,226 +243,7 @@ module.exports = (params, user: ILocalUser, app) => new Promise(async (res, rej)
 		});
 	}
 
-	// 投稿を作成
-	const post = await Post.insert({
-		createdAt: new Date(),
-		channelId: channel ? channel._id : undefined,
-		index: channel ? channel.index + 1 : undefined,
-		mediaIds: files ? files.map(file => file._id) : [],
-		replyId: reply ? reply._id : undefined,
-		repostId: repost ? repost._id : undefined,
-		poll: poll,
-		text: text,
-		textHtml: tokens === null ? null : html(tokens),
-		cw: cw,
-		tags: tags,
-		userId: user._id,
-		appId: app ? app._id : null,
-		viaMobile: viaMobile,
-		geo,
-
-		// 以下非正規化データ
-		_reply: reply ? { userId: reply.userId } : undefined,
-		_repost: repost ? { userId: repost.userId } : undefined,
-	});
-
-	// Serialize
-	const postObj = await pack(post);
-
-	// Reponse
-	res({
-		createdPost: postObj
-	});
-
-	//#region Post processes
-
-	User.update({ _id: user._id }, {
-		$set: {
-			latestPost: post
-		}
-	});
-
-	const mentions = [];
-
-	async function addMention(mentionee, reason) {
-		// Reject if already added
-		if (mentions.some(x => x.equals(mentionee))) return;
-
-		// Add mention
-		mentions.push(mentionee);
-
-		// Publish event
-		if (!user._id.equals(mentionee)) {
-			const mentioneeMutes = await Mute.find({
-				muterId: mentionee,
-				deletedAt: { $exists: false }
-			});
-			const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString());
-			if (mentioneesMutedUserIds.indexOf(user._id.toString()) == -1) {
-				stream(mentionee, reason, postObj);
-				pushSw(mentionee, reason, postObj);
-			}
-		}
-	}
-
-	// タイムラインへの投稿
-	if (!channel) {
-		// Publish event to myself's stream
-		stream(user._id, 'post', postObj);
-
-		// Fetch all followers
-		const followers = await Following
-			.find({
-				followeeId: user._id,
-				// 削除されたドキュメントは除く
-				deletedAt: { $exists: false }
-			}, {
-				followerId: true,
-				_id: false
-			});
-
-		// Publish event to followers stream
-		followers.forEach(following =>
-			stream(following.followerId, 'post', postObj));
-	}
-
-	// チャンネルへの投稿
-	if (channel) {
-		// Increment channel index(posts count)
-		Channel.update({ _id: channel._id }, {
-			$inc: {
-				index: 1
-			}
-		});
-
-		// Publish event to channel
-		publishChannelStream(channel._id, 'post', postObj);
-
-		// Get channel watchers
-		const watches = await ChannelWatching.find({
-			channelId: channel._id,
-			// 削除されたドキュメントは除く
-			deletedAt: { $exists: false }
-		});
-
-		// チャンネルの視聴者(のタイムライン)に配信
-		watches.forEach(w => {
-			stream(w.userId, 'post', postObj);
-		});
-	}
-
-	// Increment my posts count
-	User.update({ _id: user._id }, {
-		$inc: {
-			postsCount: 1
-		}
-	});
-
-	// If has in reply to post
-	if (reply) {
-		// Increment replies count
-		Post.update({ _id: reply._id }, {
-			$inc: {
-				repliesCount: 1
-			}
-		});
-
-		// 自分自身へのリプライでない限りは通知を作成
-		notify(reply.userId, user._id, 'reply', {
-			postId: post._id
-		});
-
-		// Fetch watchers
-		Watching
-			.find({
-				postId: reply._id,
-				userId: { $ne: user._id },
-				// 削除されたドキュメントは除く
-				deletedAt: { $exists: false }
-			}, {
-				fields: {
-					userId: true
-				}
-			})
-			.then(watchers => {
-				watchers.forEach(watcher => {
-					notify(watcher.userId, user._id, 'reply', {
-						postId: post._id
-					});
-				});
-			});
-
-		// この投稿をWatchする
-		if (user.account.settings.autoWatch !== false) {
-			watch(user._id, reply);
-		}
-
-		// Add mention
-		addMention(reply.userId, 'reply');
-	}
-
-	// If it is repost
-	if (repost) {
-		// Notify
-		const type = text ? 'quote' : 'repost';
-		notify(repost.userId, user._id, type, {
-			postId: post._id
-		});
-
-		// Fetch watchers
-		Watching
-			.find({
-				postId: repost._id,
-				userId: { $ne: user._id },
-				// 削除されたドキュメントは除く
-				deletedAt: { $exists: false }
-			}, {
-				fields: {
-					userId: true
-				}
-			})
-			.then(watchers => {
-				watchers.forEach(watcher => {
-					notify(watcher.userId, user._id, type, {
-						postId: post._id
-					});
-				});
-			});
-
-		// この投稿をWatchする
-		// TODO: ユーザーが「Repostしたときに自動でWatchする」設定を
-		//       オフにしていた場合はしない
-		watch(user._id, repost);
-
-		// If it is quote repost
-		if (text) {
-			// Add mention
-			addMention(repost.userId, 'quote');
-		} else {
-			// Publish event
-			if (!user._id.equals(repost.userId)) {
-				stream(repost.userId, 'repost', postObj);
-			}
-		}
-
-		// 今までで同じ投稿をRepostしているか
-		const existRepost = await Post.findOne({
-			userId: user._id,
-			repostId: repost._id,
-			_id: {
-				$ne: post._id
-			}
-		});
-
-		if (!existRepost) {
-			// Update repostee status
-			Post.update({ _id: repost._id }, {
-				$inc: {
-					repostCount: 1
-				}
-			});
-		}
-	}
+	let atMentions = [];
 
 	// If has text content
 	if (text) {
@@ -486,40 +259,42 @@ module.exports = (params, user: ILocalUser, app) => new Promise(async (res, rej)
 				registerHashtags(user, hashtags);
 		*/
 		// Extract an '@' mentions
-		const atMentions = tokens
+		atMentions = tokens
 			.filter(t => t.type == 'mention')
-			.map(getAcct)
+			.map(renderAcct)
 			// Drop dupulicates
 			.filter((v, i, s) => s.indexOf(v) == i);
-
-		// Resolve all mentions
-		await Promise.all(atMentions.map(async (mention) => {
-			// Fetch mentioned user
-			// SELECT _id
-			const mentionee = await User
-				.findOne(parseAcct(mention), { _id: true });
-
-			// When mentioned user not found
-			if (mentionee == null) return;
-
-			// 既に言及されたユーザーに対する返信や引用repostの場合も無視
-			if (reply && reply.userId.equals(mentionee._id)) return;
-			if (repost && repost.userId.equals(mentionee._id)) return;
-
-			// Add mention
-			addMention(mentionee._id, 'mention');
-
-			// Create notification
-			notify(mentionee._id, user._id, 'mention', {
-				postId: post._id
-			});
-
-			return;
-		}));
 	}
 
+	// 投稿を作成
+	const post = await create({
+		createdAt: new Date(),
+		channelId: channel ? channel._id : undefined,
+		index: channel ? channel.index + 1 : undefined,
+		mediaIds: files ? files.map(file => file._id) : [],
+		poll: poll,
+		text: text,
+		textHtml: tokens === null ? null : html(tokens),
+		cw: cw,
+		tags: tags,
+		userId: user._id,
+		appId: app ? app._id : null,
+		viaMobile: viaMobile,
+		geo
+	}, reply, repost, atMentions);
+
+	// Serialize
+	const postObj = await pack(post);
+
+	// Reponse
+	res({
+		createdPost: postObj
+	});
+
+	distribute(user, post.mentions, postObj);
+
 	// Register to search database
-	if (text && config.elasticsearch.enable) {
+	if (post.text && config.elasticsearch.enable) {
 		const es = require('../../../db/elasticsearch');
 
 		es.index({
@@ -531,15 +306,4 @@ module.exports = (params, user: ILocalUser, app) => new Promise(async (res, rej)
 			}
 		});
 	}
-
-	// Append mentions data
-	if (mentions.length > 0) {
-		Post.update({ _id: post._id }, {
-			$set: {
-				mentions: mentions
-			}
-		});
-	}
-
-	//#endregion
 });
diff --git a/src/server/api/endpoints/posts/polls/vote.ts b/src/server/api/endpoints/posts/polls/vote.ts
index 029fb9323..c270cd09a 100644
--- a/src/server/api/endpoints/posts/polls/vote.ts
+++ b/src/server/api/endpoints/posts/polls/vote.ts
@@ -5,7 +5,7 @@ import $ from 'cafy';
 import Vote from '../../../../../models/poll-vote';
 import Post from '../../../../../models/post';
 import Watching from '../../../../../models/post-watching';
-import watch from '../../../common/watch-post';
+import watch from '../../../../../post/watch';
 import { publishPostStream } from '../../../../../publishers/stream';
 import notify from '../../../../../publishers/notify';
 
diff --git a/src/server/api/endpoints/posts/reactions/create.ts b/src/server/api/endpoints/posts/reactions/create.ts
index 8b5f1e57d..f1b0c7dd2 100644
--- a/src/server/api/endpoints/posts/reactions/create.ts
+++ b/src/server/api/endpoints/posts/reactions/create.ts
@@ -6,7 +6,7 @@ import Reaction from '../../../../../models/post-reaction';
 import Post, { pack as packPost } from '../../../../../models/post';
 import { pack as packUser } from '../../../../../models/user';
 import Watching from '../../../../../models/post-watching';
-import watch from '../../../common/watch-post';
+import watch from '../../../../../post/watch';
 import { publishPostStream } from '../../../../../publishers/stream';
 import notify from '../../../../../publishers/notify';
 import pushSw from '../../../../../publishers/push-sw';

From a8f7156fb1136312830c2fc9e86856040bed902e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 2 Apr 2018 18:36:07 +0900
Subject: [PATCH 1024/1250] Fix bug

---
 tools/migration/nighthike/6.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tools/migration/nighthike/6.js b/tools/migration/nighthike/6.js
index 8b97e9f7b..27fff2ec1 100644
--- a/tools/migration/nighthike/6.js
+++ b/tools/migration/nighthike/6.js
@@ -3,7 +3,7 @@ db.posts.update({
 		mediaIds: null
 	}, {
 		mediaIds: {
-			$exist: false
+			$exists: false
 		}
 	}]
 }, {

From 00110e37f2c51618b93940f76382f69c586d6081 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Mon, 2 Apr 2018 18:36:47 +0900
Subject: [PATCH 1025/1250] Resolve account by signature in inbox

---
 src/processor/http/index.ts               |  2 ++
 src/processor/http/perform-activitypub.ts |  2 +-
 src/processor/http/process-inbox.ts       | 38 ++++++++++++++++++++
 src/remote/activitypub/resolve-person.ts  | 16 ++++-----
 src/remote/resolve-user.ts                |  2 +-
 src/remote/webfinger.ts                   | 26 +++++++++-----
 src/server/activitypub/inbox.ts           | 42 ++++-------------------
 7 files changed, 72 insertions(+), 56 deletions(-)
 create mode 100644 src/processor/http/process-inbox.ts

diff --git a/src/processor/http/index.ts b/src/processor/http/index.ts
index a001cf11f..b3161cb99 100644
--- a/src/processor/http/index.ts
+++ b/src/processor/http/index.ts
@@ -1,10 +1,12 @@
 import follow from './follow';
 import performActivityPub from './perform-activitypub';
+import processInbox from './process-inbox';
 import reportGitHubFailure from './report-github-failure';
 
 const handlers = {
   follow,
   performActivityPub,
+  processInbox,
   reportGitHubFailure,
 };
 
diff --git a/src/processor/http/perform-activitypub.ts b/src/processor/http/perform-activitypub.ts
index d8981ea12..420ed9ec7 100644
--- a/src/processor/http/perform-activitypub.ts
+++ b/src/processor/http/perform-activitypub.ts
@@ -2,5 +2,5 @@ import User from '../../models/user';
 import act from '../../remote/activitypub/act';
 
 export default ({ data }, done) => User.findOne({ _id: data.actor })
-	.then(actor => act(actor, data.outbox, data.distribute))
+	.then(actor => act(actor, data.outbox, false))
 	.then(() => done(), done);
diff --git a/src/processor/http/process-inbox.ts b/src/processor/http/process-inbox.ts
new file mode 100644
index 000000000..78c20f8a7
--- /dev/null
+++ b/src/processor/http/process-inbox.ts
@@ -0,0 +1,38 @@
+import { verifySignature } from 'http-signature';
+import parseAcct from '../../acct/parse';
+import User, { IRemoteUser } from '../../models/user';
+import act from '../../remote/activitypub/act';
+import resolvePerson from '../../remote/activitypub/resolve-person';
+
+export default ({ data }, done) => (async () => {
+	const keyIdLower = data.signature.keyId.toLowerCase();
+	let user;
+
+	if (keyIdLower.startsWith('acct:')) {
+		const { username, host } = parseAcct(keyIdLower.slice('acct:'.length));
+		if (host === null) {
+			throw 'request was made by local user';
+		}
+
+		user = await User.findOne({ usernameLower: username, hostLower: host }) as IRemoteUser;
+	} else {
+		user = await User.findOne({
+			host: { $ne: null },
+			'account.publicKey.id': data.signature.keyId
+		}) as IRemoteUser;
+
+		if (user === null) {
+			user = await resolvePerson(data.signature.keyId);
+		}
+	}
+
+	if (user === null) {
+		throw 'failed to resolve user';
+	}
+
+	if (!verifySignature(data.signature, user.account.publicKey.publicKeyPem)) {
+		throw 'signature verification failed';
+	}
+
+	await act(user, data.inbox, true);
+})().then(done, done);
diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts
index 4a2636b2f..59be65908 100644
--- a/src/remote/activitypub/resolve-person.ts
+++ b/src/remote/activitypub/resolve-person.ts
@@ -10,18 +10,14 @@ async function isCollection(collection) {
 	return ['Collection', 'OrderedCollection'].includes(collection.type);
 }
 
-export default async (value, usernameLower, hostLower, acctLower) => {
-	if (!validateUsername(usernameLower)) {
-		throw new Error();
-	}
-
+export default async (value, verifier?: string) => {
 	const { resolver, object } = await new Resolver().resolveOne(value);
 
 	if (
 		object === null ||
 		object.type !== 'Person' ||
 		typeof object.preferredUsername !== 'string' ||
-		object.preferredUsername.toLowerCase() !== usernameLower ||
+		!validateUsername(object.preferredUsername) ||
 		!isValidName(object.name) ||
 		!isValidDescription(object.summary)
 	) {
@@ -41,9 +37,11 @@ export default async (value, usernameLower, hostLower, acctLower) => {
 			resolved => isCollection(resolved.object) ? resolved.object : null,
 			() => null
 		),
-		webFinger(object.id, acctLower),
+		webFinger(object.id, verifier),
 	]);
 
+	const host = toUnicode(finger.subject.replace(/^.*?@/, ''));
+	const hostLower = host.replace(/[A-Z]+/, matched => matched.toLowerCase());
 	const summaryDOM = JSDOM.fragment(object.summary);
 
 	// Create user
@@ -58,8 +56,8 @@ export default async (value, usernameLower, hostLower, acctLower) => {
 		postsCount: outbox ? outbox.totalItem || 0 : 0,
 		driveCapacity: 1024 * 1024 * 8, // 8MiB
 		username: object.preferredUsername,
-		usernameLower,
-		host: toUnicode(finger.subject.replace(/^.*?@/, '')),
+		usernameLower: object.preferredUsername.toLowerCase(),
+		host,
 		hostLower,
 		account: {
 			publicKey: {
diff --git a/src/remote/resolve-user.ts b/src/remote/resolve-user.ts
index a39309283..48219e8cb 100644
--- a/src/remote/resolve-user.ts
+++ b/src/remote/resolve-user.ts
@@ -19,7 +19,7 @@ export default async (username, host, option) => {
 			throw new Error();
 		}
 
-		user = await resolvePerson(self.href, usernameLower, hostLower, acctLower);
+		user = await resolvePerson(self.href, acctLower);
 	}
 
 	return user;
diff --git a/src/remote/webfinger.ts b/src/remote/webfinger.ts
index fec5da689..4c0304e3f 100644
--- a/src/remote/webfinger.ts
+++ b/src/remote/webfinger.ts
@@ -12,14 +12,22 @@ type IWebFinger = {
   subject: string;
 };
 
-export default (query, verifier): Promise<IWebFinger> => new Promise((res, rej) => webFinger.lookup(query, (error, result) => {
-	if (error) {
-		return rej(error);
+export default async function resolve(query, verifier?: string): Promise<IWebFinger> {
+	const finger = await new Promise((res, rej) => webFinger.lookup(query, (error, result) => {
+		if (error) {
+			return rej(error);
+		}
+
+		res(result.object);
+	})) as IWebFinger;
+
+	if (verifier) {
+		if (finger.subject.toLowerCase().replace(/^acct:/, '') !== verifier) {
+			throw 'WebFinger verfification failed';
+		}
+
+		return finger;
 	}
 
-	if (result.object.subject.toLowerCase().replace(/^acct:/, '') !== verifier) {
-		return rej('WebFinger verfification failed');
-	}
-
-	res(result.object);
-}));
+	return resolve(finger.subject, finger.subject.toLowerCase());
+}
diff --git a/src/server/activitypub/inbox.ts b/src/server/activitypub/inbox.ts
index 2de2bd964..5de843385 100644
--- a/src/server/activitypub/inbox.ts
+++ b/src/server/activitypub/inbox.ts
@@ -1,9 +1,7 @@
 import * as bodyParser from 'body-parser';
 import * as express from 'express';
-import { parseRequest, verifySignature } from 'http-signature';
-import User, { IRemoteUser } from '../../models/user';
+import { parseRequest } from 'http-signature';
 import queue from '../../queue';
-import parseAcct from '../../acct/parse';
 
 const app = express();
 
@@ -14,48 +12,20 @@ app.post('/@:user/inbox', bodyParser.json({
 		return true;
 	}
 }), async (req, res) => {
-	let parsed;
+	let signature;
 
 	req.headers.authorization = 'Signature ' + req.headers.signature;
 
 	try {
-		parsed = parseRequest(req);
+		signature = parseRequest(req);
 	} catch (exception) {
 		return res.sendStatus(401);
 	}
 
-	const keyIdLower = parsed.keyId.toLowerCase();
-	let query;
-
-	if (keyIdLower.startsWith('acct:')) {
-		const { username, host } = parseAcct(keyIdLower.slice('acct:'.length));
-		if (host === null) {
-			return res.sendStatus(401);
-		}
-
-		query = { usernameLower: username, hostLower: host };
-	} else {
-		query = {
-			host: { $ne: null },
-			'account.publicKey.id': parsed.keyId
-		};
-	}
-
-	const user = await User.findOne(query) as IRemoteUser;
-
-	if (user === null) {
-		return res.sendStatus(401);
-	}
-
-	if (!verifySignature(parsed, user.account.publicKey.publicKeyPem)) {
-		return res.sendStatus(401);
-	}
-
 	queue.create('http', {
-		type: 'performActivityPub',
-		actor: user._id,
-		outbox: req.body,
-		distribute: true,
+		type: 'processInbox',
+		inbox: req.body,
+		signature,
 	}).save();
 
 	return res.status(202).end();

From 0a3f45713980b9cf5caaea43efac8aa7a0bf5455 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 2 Apr 2018 18:40:59 +0900
Subject: [PATCH 1026/1250] Fix bug

---
 tools/migration/nighthike/1.js | 2 +-
 tools/migration/nighthike/2.js | 2 +-
 tools/migration/nighthike/3.js | 2 +-
 tools/migration/nighthike/5.js | 2 +-
 tools/migration/nighthike/7.js | 2 +-
 tools/migration/nighthike/8.js | 2 +-
 6 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/tools/migration/nighthike/1.js b/tools/migration/nighthike/1.js
index d7e011c5b..6ae30ad4f 100644
--- a/tools/migration/nighthike/1.js
+++ b/tools/migration/nighthike/1.js
@@ -1,6 +1,6 @@
 // for Node.js interpret
 
-const { default: User } = require('../../../built/api/models/user');
+const { default: User } = require('../../../built/models/user');
 const { default: zip } = require('@prezzemolo/zip')
 
 const migrate = async (user) => {
diff --git a/tools/migration/nighthike/2.js b/tools/migration/nighthike/2.js
index 8fb5bbb08..e8f622902 100644
--- a/tools/migration/nighthike/2.js
+++ b/tools/migration/nighthike/2.js
@@ -1,6 +1,6 @@
 // for Node.js interpret
 
-const { default: App } = require('../../../built/api/models/app');
+const { default: App } = require('../../../built/models/app');
 const { default: zip } = require('@prezzemolo/zip')
 
 const migrate = async (app) => {
diff --git a/tools/migration/nighthike/3.js b/tools/migration/nighthike/3.js
index cc0603d9e..bde4f773d 100644
--- a/tools/migration/nighthike/3.js
+++ b/tools/migration/nighthike/3.js
@@ -1,6 +1,6 @@
 // for Node.js interpret
 
-const { default: User } = require('../../../built/api/models/user');
+const { default: User } = require('../../../built/models/user');
 const { generate } = require('../../../built/crypto_key');
 const { default: zip } = require('@prezzemolo/zip')
 
diff --git a/tools/migration/nighthike/5.js b/tools/migration/nighthike/5.js
index fb72b6907..97f507733 100644
--- a/tools/migration/nighthike/5.js
+++ b/tools/migration/nighthike/5.js
@@ -1,6 +1,6 @@
 // for Node.js interpret
 
-const { default: Post } = require('../../../built/api/models/post');
+const { default: Post } = require('../../../built/models/post');
 const { default: zip } = require('@prezzemolo/zip')
 
 const migrate = async (post) => {
diff --git a/tools/migration/nighthike/7.js b/tools/migration/nighthike/7.js
index 4463a6e9f..f1102d13e 100644
--- a/tools/migration/nighthike/7.js
+++ b/tools/migration/nighthike/7.js
@@ -1,6 +1,6 @@
 // for Node.js interpret
 
-const { default: Post } = require('../../../built/api/models/post');
+const { default: Post } = require('../../../built/models/post');
 const { default: zip } = require('@prezzemolo/zip')
 const html = require('../../../built/text/html').default;
 const parse = require('../../../built/text/parse').default;
diff --git a/tools/migration/nighthike/8.js b/tools/migration/nighthike/8.js
index e8743987b..e4f4482db 100644
--- a/tools/migration/nighthike/8.js
+++ b/tools/migration/nighthike/8.js
@@ -1,6 +1,6 @@
 // for Node.js interpret
 
-const { default: Message } = require('../../../built/api/models/message');
+const { default: Message } = require('../../../built/models/messaging-message');
 const { default: zip } = require('@prezzemolo/zip')
 const html = require('../../../built/text/html').default;
 const parse = require('../../../built/text/parse').default;

From 2536d58d72a1bef4a2315a464a33a92d2241afe0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 2 Apr 2018 18:59:48 +0900
Subject: [PATCH 1027/1250] :v:

---
 package.json                    | 1 +
 src/client/docs/api/gulpfile.ts | 2 +-
 src/client/docs/vars.ts         | 4 ++--
 src/index.ts                    | 3 +++
 4 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/package.json b/package.json
index 07b30e16b..802074365 100644
--- a/package.json
+++ b/package.json
@@ -56,6 +56,7 @@
 		"@types/is-root": "1.0.0",
 		"@types/is-url": "1.2.28",
 		"@types/js-yaml": "3.11.1",
+		"@types/kue": "^0.11.8",
 		"@types/license-checker": "15.0.0",
 		"@types/mkdirp": "0.5.2",
 		"@types/mocha": "5.0.0",
diff --git a/src/client/docs/api/gulpfile.ts b/src/client/docs/api/gulpfile.ts
index c986e0353..9980ede23 100644
--- a/src/client/docs/api/gulpfile.ts
+++ b/src/client/docs/api/gulpfile.ts
@@ -151,7 +151,7 @@ gulp.task('doc:api:entities', async () => {
 			return;
 		}
 		files.forEach(file => {
-			const entity = yaml.safeLoad(fs.readFileSync(file, 'utf-8'));
+			const entity = yaml.safeLoad(fs.readFileSync(file, 'utf-8')) as any;
 			const vars = {
 				name: entity.name,
 				desc: entity.desc,
diff --git a/src/client/docs/vars.ts b/src/client/docs/vars.ts
index dbdc88061..32b961aaa 100644
--- a/src/client/docs/vars.ts
+++ b/src/client/docs/vars.ts
@@ -15,13 +15,13 @@ export default async function(): Promise<{ [key: string]: any }> {
 
 	const endpoints = glob.sync('./src/client/docs/api/endpoints/**/*.yaml');
 	vars['endpoints'] = endpoints.map(ep => {
-		const _ep = yaml.safeLoad(fs.readFileSync(ep, 'utf-8'));
+		const _ep = yaml.safeLoad(fs.readFileSync(ep, 'utf-8')) as any;
 		return _ep.endpoint;
 	});
 
 	const entities = glob.sync('./src/client/docs/api/entities/**/*.yaml');
 	vars['entities'] = entities.map(x => {
-		const _x = yaml.safeLoad(fs.readFileSync(x, 'utf-8'));
+		const _x = yaml.safeLoad(fs.readFileSync(x, 'utf-8')) as any;
 		return _x.name;
 	});
 
diff --git a/src/index.ts b/src/index.ts
index b43e15285..29c4f3431 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -30,6 +30,9 @@ const ev = new Xev();
 
 process.title = 'Misskey';
 
+// https://github.com/Automattic/kue/issues/822
+require('events').EventEmitter.prototype._maxListeners = 256;
+
 // Start app
 main();
 

From cf195b8e9213d05cb0ae1fa289e3f305c53e6bad Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Mon, 2 Apr 2018 19:37:00 +0900
Subject: [PATCH 1028/1250] Improve WebFinger verification

---
 src/remote/webfinger.ts | 13 +++++++++----
 1 file changed, 9 insertions(+), 4 deletions(-)

diff --git a/src/remote/webfinger.ts b/src/remote/webfinger.ts
index 4c0304e3f..c84beb099 100644
--- a/src/remote/webfinger.ts
+++ b/src/remote/webfinger.ts
@@ -20,14 +20,19 @@ export default async function resolve(query, verifier?: string): Promise<IWebFin
 
 		res(result.object);
 	})) as IWebFinger;
+	const subject = finger.subject.toLowerCase().replace(/^acct:/, '');
 
-	if (verifier) {
-		if (finger.subject.toLowerCase().replace(/^acct:/, '') !== verifier) {
-			throw 'WebFinger verfification failed';
+	if (typeof verifier === 'string') {
+		if (subject !== verifier) {
+			throw new Error;
 		}
 
 		return finger;
 	}
 
-	return resolve(finger.subject, finger.subject.toLowerCase());
+	if (typeof subject === 'string') {
+		return resolve(subject, subject);
+	}
+
+	throw new Error;
 }

From 00253eb355302544a819367e01c931902999535f Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Mon, 2 Apr 2018 19:50:40 +0900
Subject: [PATCH 1029/1250] Implement Follow activity

---
 src/models/following.ts              |  1 +
 src/processor/http/follow.ts         | 14 +++++---
 src/remote/activitypub/act/follow.ts | 51 ++++++++++++++++++++++++++++
 src/remote/activitypub/act/index.ts  |  4 +++
 4 files changed, 65 insertions(+), 5 deletions(-)
 create mode 100644 src/remote/activitypub/act/follow.ts

diff --git a/src/models/following.ts b/src/models/following.ts
index 3f8a9be50..fe9ce550d 100644
--- a/src/models/following.ts
+++ b/src/models/following.ts
@@ -2,6 +2,7 @@ import * as mongo from 'mongodb';
 import db from '../db/mongodb';
 
 const Following = db.get<IFollowing>('following');
+Following.createIndex(['followerId', 'followeeId'], { unique: true });
 export default Following;
 
 export type IFollowing = {
diff --git a/src/processor/http/follow.ts b/src/processor/http/follow.ts
index a7e4fa23d..7ec1ee675 100644
--- a/src/processor/http/follow.ts
+++ b/src/processor/http/follow.ts
@@ -1,7 +1,7 @@
 import { request } from 'https';
 import { sign } from 'http-signature';
 import { URL } from 'url';
-import User, { isLocalUser, pack as packUser, ILocalUser } from '../../models/user';
+import User, { isLocalUser, pack as packUser } from '../../models/user';
 import Following from '../../models/following';
 import event from '../../publishers/stream';
 import notify from '../../publishers/notify';
@@ -10,7 +10,7 @@ import render from '../../remote/activitypub/renderer/follow';
 import config from '../../config';
 
 export default ({ data }, done) => Following.findOne({ _id: data.following }).then(({ followerId, followeeId }) => {
-	const promisedFollower: Promise<ILocalUser> = User.findOne({ _id: followerId });
+	const promisedFollower = User.findOne({ _id: followerId });
 	const promisedFollowee = User.findOne({ _id: followeeId });
 
 	return Promise.all([
@@ -34,14 +34,18 @@ export default ({ data }, done) => Following.findOne({ _id: data.following }).th
 
 		// Publish follow event
 		Promise.all([promisedFollower, promisedFollowee]).then(([follower, followee]) => {
-			const followerEvent = packUser(followee, follower)
-				.then(packed => event(follower._id, 'follow', packed));
+			let followerEvent;
 			let followeeEvent;
 
+			if (isLocalUser(follower)) {
+				followerEvent = packUser(followee, follower)
+					.then(packed => event(follower._id, 'follow', packed));
+			}
+
 			if (isLocalUser(followee)) {
 				followeeEvent = packUser(follower, followee)
 					.then(packed => event(followee._id, 'followed', packed));
-			} else {
+			} else if (isLocalUser(follower)) {
 				followeeEvent = new Promise((resolve, reject) => {
 					const {
 						protocol,
diff --git a/src/remote/activitypub/act/follow.ts b/src/remote/activitypub/act/follow.ts
new file mode 100644
index 000000000..ec9e080df
--- /dev/null
+++ b/src/remote/activitypub/act/follow.ts
@@ -0,0 +1,51 @@
+import { MongoError } from 'mongodb';
+import parseAcct from '../../../acct/parse';
+import Following from '../../../models/following';
+import User from '../../../models/user';
+import config from '../../../config';
+import queue from '../../../queue';
+
+export default async (actor, activity) => {
+	const prefix = config.url + '/@';
+	const id = activity.object.id || activity.object;
+	let following;
+
+	if (!id.startsWith(prefix)) {
+		return null;
+	}
+
+	const { username, host } = parseAcct(id.slice(prefix.length));
+	if (host !== null) {
+		throw new Error();
+	}
+
+	const followee = await User.findOne({ username, host });
+	if (followee === null) {
+		throw new Error();
+	}
+
+	try {
+		following = await Following.insert({
+			createdAt: new Date(),
+			followerId: actor._id,
+			followeeId: followee._id
+		});
+	} catch (exception) {
+		// duplicate key error
+		if (exception instanceof MongoError && exception.code === 11000) {
+			return null;
+		}
+
+		throw exception;
+	}
+
+	await new Promise((resolve, reject) => {
+		queue.create('http', { type: 'follow', following: following._id }).save(error => {
+			if (error) {
+				reject(error);
+			} else {
+				resolve(null);
+			}
+		});
+	});
+};
diff --git a/src/remote/activitypub/act/index.ts b/src/remote/activitypub/act/index.ts
index 06d662c19..24320dcb1 100644
--- a/src/remote/activitypub/act/index.ts
+++ b/src/remote/activitypub/act/index.ts
@@ -1,4 +1,5 @@
 import create from './create';
+import follow from './follow';
 import createObject from '../create';
 import Resolver from '../resolver';
 
@@ -15,6 +16,9 @@ export default (actor, value, distribute) => {
 		case 'Create':
 			return create(resolver, actor, object, distribute);
 
+		case 'Follow':
+			return follow(actor, object);
+
 		default:
 			return null;
 		}

From f3e3c58fe1bfb4dd1ced96c96ddf6f24c6c58bda Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Mon, 2 Apr 2018 21:57:36 +0900
Subject: [PATCH 1030/1250] Introduce followed log and following log

---
 src/models/followed-log.ts                    | 11 ++++
 src/models/following-log.ts                   | 11 ++++
 src/models/following.ts                       |  1 -
 src/models/user.ts                            | 12 ++--
 src/post/distribute.ts                        |  4 +-
 src/processor/http/follow.ts                  | 12 ++++
 src/server/api/common/get-friends.ts          |  4 +-
 .../endpoints/aggregation/users/followers.ts  | 66 ++++++++-----------
 .../endpoints/aggregation/users/following.ts  | 65 ++++++++----------
 src/server/api/endpoints/following/create.ts  |  3 +-
 src/server/api/endpoints/following/delete.ts  |  9 +--
 src/server/api/endpoints/users/followers.ts   |  3 +-
 src/server/api/endpoints/users/following.ts   |  3 +-
 test/api.js                                   | 14 ----
 14 files changed, 101 insertions(+), 117 deletions(-)
 create mode 100644 src/models/followed-log.ts
 create mode 100644 src/models/following-log.ts

diff --git a/src/models/followed-log.ts b/src/models/followed-log.ts
new file mode 100644
index 000000000..4d8ecf684
--- /dev/null
+++ b/src/models/followed-log.ts
@@ -0,0 +1,11 @@
+import { ObjectID } from 'mongodb';
+import db from '../db/mongodb';
+
+const FollowedLog = db.get<IFollowedLog>('followedLogs');
+export default FollowedLog;
+
+export type IFollowedLog = {
+	_id: ObjectID;
+	userId: ObjectID;
+	count: number;
+};
diff --git a/src/models/following-log.ts b/src/models/following-log.ts
new file mode 100644
index 000000000..f18707db8
--- /dev/null
+++ b/src/models/following-log.ts
@@ -0,0 +1,11 @@
+import { ObjectID } from 'mongodb';
+import db from '../db/mongodb';
+
+const FollowingLog = db.get<IFollowingLog>('followingLogs');
+export default FollowingLog;
+
+export type IFollowingLog = {
+	_id: ObjectID;
+	userId: ObjectID;
+	count: number;
+};
diff --git a/src/models/following.ts b/src/models/following.ts
index fe9ce550d..b4090d8c7 100644
--- a/src/models/following.ts
+++ b/src/models/following.ts
@@ -8,7 +8,6 @@ export default Following;
 export type IFollowing = {
 	_id: mongo.ObjectID;
 	createdAt: Date;
-	deletedAt: Date;
 	followeeId: mongo.ObjectID;
 	followerId: mongo.ObjectID;
 };
diff --git a/src/models/user.ts b/src/models/user.ts
index d3c94cab3..f817c33aa 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -234,8 +234,7 @@ export const pack = (
 		_user.isFollowing = (async () => {
 			const follow = await Following.findOne({
 				followerId: meId,
-				followeeId: _user.id,
-				deletedAt: { $exists: false }
+				followeeId: _user.id
 			});
 			return follow !== null;
 		})();
@@ -244,8 +243,7 @@ export const pack = (
 		_user.isFollowed = (async () => {
 			const follow2 = await Following.findOne({
 				followerId: _user.id,
-				followeeId: meId,
-				deletedAt: { $exists: false }
+				followeeId: meId
 			});
 			return follow2 !== null;
 		})();
@@ -275,15 +273,13 @@ export const pack = (
 			// Get following you know count
 			_user.followingYouKnowCount = Following.count({
 				followeeId: { $in: myFollowingIds },
-				followerId: _user.id,
-				deletedAt: { $exists: false }
+				followerId: _user.id
 			});
 
 			// Get followers you know count
 			_user.followersYouKnowCount = Following.count({
 				followeeId: _user.id,
-				followerId: { $in: myFollowingIds },
-				deletedAt: { $exists: false }
+				followerId: { $in: myFollowingIds }
 			});
 		}
 	}
diff --git a/src/post/distribute.ts b/src/post/distribute.ts
index 3925d4128..4def2c27f 100644
--- a/src/post/distribute.ts
+++ b/src/post/distribute.ts
@@ -48,9 +48,7 @@ export default async (user, mentions, post) => {
 		// Fetch all followers
 		const followers = await Following
 			.find({
-				followeeId: user._id,
-				// 削除されたドキュメントは除く
-				deletedAt: { $exists: false }
+				followeeId: user._id
 			}, {
 				followerId: true,
 				_id: false
diff --git a/src/processor/http/follow.ts b/src/processor/http/follow.ts
index 7ec1ee675..29ac9fa55 100644
--- a/src/processor/http/follow.ts
+++ b/src/processor/http/follow.ts
@@ -3,6 +3,8 @@ import { sign } from 'http-signature';
 import { URL } from 'url';
 import User, { isLocalUser, pack as packUser } from '../../models/user';
 import Following from '../../models/following';
+import FollowingLog from '../../models/following-log';
+import FollowedLog from '../../models/followed-log';
 import event from '../../publishers/stream';
 import notify from '../../publishers/notify';
 import context from '../../remote/activitypub/renderer/context';
@@ -21,6 +23,11 @@ export default ({ data }, done) => Following.findOne({ _id: data.following }).th
 			}
 		}),
 
+		promisedFollower.then(({ followingCount }) => FollowingLog.insert({
+			userId: followerId,
+			count: followingCount + 1
+		})),
+
 		// Increment followers count
 		User.update({ _id: followeeId }, {
 			$inc: {
@@ -28,6 +35,11 @@ export default ({ data }, done) => Following.findOne({ _id: data.following }).th
 			}
 		}),
 
+		promisedFollowee.then(({ followersCount }) => FollowedLog.insert({
+			userId: followerId,
+			count: followersCount + 1
+		})),
+
 		// Notify
 		promisedFollowee.then(followee => followee.host === null ?
 			notify(followeeId, followerId, 'follow') : null),
diff --git a/src/server/api/common/get-friends.ts b/src/server/api/common/get-friends.ts
index e0942e029..c1cc3957d 100644
--- a/src/server/api/common/get-friends.ts
+++ b/src/server/api/common/get-friends.ts
@@ -6,9 +6,7 @@ export default async (me: mongodb.ObjectID, includeMe: boolean = true) => {
 	// SELECT followee
 	const myfollowing = await Following
 		.find({
-			followerId: me,
-			// 削除されたドキュメントは除く
-			deletedAt: { $exists: false }
+			followerId: me
 		}, {
 			fields: {
 				followeeId: true
diff --git a/src/server/api/endpoints/aggregation/users/followers.ts b/src/server/api/endpoints/aggregation/users/followers.ts
index dda34ed7b..580d31a3f 100644
--- a/src/server/api/endpoints/aggregation/users/followers.ts
+++ b/src/server/api/endpoints/aggregation/users/followers.ts
@@ -2,8 +2,9 @@
  * Module dependencies
  */
 import $ from 'cafy';
+import { ObjectID } from 'mongodb';
 import User from '../../../../../models/user';
-import Following from '../../../../../models/following';
+import FollowedLog from '../../../../../models/followed-log';
 
 /**
  * Aggregate followers of a user
@@ -29,47 +30,36 @@ module.exports = (params) => new Promise(async (res, rej) => {
 		return rej('user not found');
 	}
 
-	const startTime = new Date(new Date().setMonth(new Date().getMonth() - 1));
-
-	const following = await Following
-		.find({
-			followeeId: user._id,
-			$or: [
-				{ deletedAt: { $exists: false } },
-				{ deletedAt: { $gt: startTime } }
-			]
-		}, {
-			sort: { createdAt: -1 },
-			fields: {
-				_id: false,
-				followerId: false,
-				followeeId: false
-			}
-		});
-
+	const today = new Date();
 	const graph = [];
 
+	today.setMinutes(0);
+	today.setSeconds(0);
+	today.setMilliseconds(0);
+
+	let cursorDate = new Date(today.getTime());
+	let cursorTime = cursorDate.setDate(new Date(today.getTime()).getDate() + 1);
+
 	for (let i = 0; i < 30; i++) {
-		let day = new Date(new Date().setDate(new Date().getDate() - i));
-		day = new Date(day.setMilliseconds(999));
-		day = new Date(day.setSeconds(59));
-		day = new Date(day.setMinutes(59));
-		day = new Date(day.setHours(23));
-		// day = day.getTime();
+		graph.push(FollowedLog.findOne({
+			_id: { $lt: ObjectID.createFromTime(cursorTime / 1000) },
+			userId: user._id
+		}, {
+			sort: { _id: -1 },
+		}).then(log => {
+			cursorDate = new Date(today.getTime());
+			cursorTime = cursorDate.setDate(today.getDate() - i);
 
-		const count = following.filter(f =>
-			f.createdAt < day && (f.deletedAt == null || f.deletedAt > day)
-		).length;
-
-		graph.push({
-			date: {
-				year: day.getFullYear(),
-				month: day.getMonth() + 1, // In JavaScript, month is zero-based.
-				day: day.getDate()
-			},
-			count: count
-		});
+			return {
+				date: {
+					year: cursorDate.getFullYear(),
+					month: cursorDate.getMonth() + 1, // In JavaScript, month is zero-based.
+					day: cursorDate.getDate()
+				},
+				count: log ? log.count : 0
+			};
+		}));
 	}
 
-	res(graph);
+	res(await Promise.all(graph));
 });
diff --git a/src/server/api/endpoints/aggregation/users/following.ts b/src/server/api/endpoints/aggregation/users/following.ts
index cd08d89e4..3ac0e3a53 100644
--- a/src/server/api/endpoints/aggregation/users/following.ts
+++ b/src/server/api/endpoints/aggregation/users/following.ts
@@ -2,8 +2,9 @@
  * Module dependencies
  */
 import $ from 'cafy';
+import { ObjectID } from 'mongodb';
 import User from '../../../../../models/user';
-import Following from '../../../../../models/following';
+import FollowingLog from '../../../../../models/following-log';
 
 /**
  * Aggregate following of a user
@@ -29,46 +30,36 @@ module.exports = (params) => new Promise(async (res, rej) => {
 		return rej('user not found');
 	}
 
-	const startTime = new Date(new Date().setMonth(new Date().getMonth() - 1));
-
-	const following = await Following
-		.find({
-			followerId: user._id,
-			$or: [
-				{ deletedAt: { $exists: false } },
-				{ deletedAt: { $gt: startTime } }
-			]
-		}, {
-			sort: { createdAt: -1 },
-			fields: {
-				_id: false,
-				followerId: false,
-				followeeId: false
-			}
-		});
-
+	const today = new Date();
 	const graph = [];
 
+	today.setMinutes(0);
+	today.setSeconds(0);
+	today.setMilliseconds(0);
+
+	let cursorDate = new Date(today.getTime());
+	let cursorTime = cursorDate.setDate(new Date(today.getTime()).getDate() + 1);
+
 	for (let i = 0; i < 30; i++) {
-		let day = new Date(new Date().setDate(new Date().getDate() - i));
-		day = new Date(day.setMilliseconds(999));
-		day = new Date(day.setSeconds(59));
-		day = new Date(day.setMinutes(59));
-		day = new Date(day.setHours(23));
+		graph.push(FollowingLog.findOne({
+			_id: { $lt: ObjectID.createFromTime(cursorTime / 1000) },
+			userId: user._id
+		}, {
+			sort: { _id: -1 },
+		}).then(log => {
+			cursorDate = new Date(today.getTime());
+			cursorTime = cursorDate.setDate(today.getDate() - i);
 
-		const count = following.filter(f =>
-			f.createdAt < day && (f.deletedAt == null || f.deletedAt > day)
-		).length;
-
-		graph.push({
-			date: {
-				year: day.getFullYear(),
-				month: day.getMonth() + 1, // In JavaScript, month is zero-based.
-				day: day.getDate()
-			},
-			count: count
-		});
+			return {
+				date: {
+					year: cursorDate.getFullYear(),
+					month: cursorDate.getMonth() + 1, // In JavaScript, month is zero-based.
+					day: cursorDate.getDate()
+				},
+				count: log ? log.count : 0
+			};
+		}));
 	}
 
-	res(graph);
+	res(await Promise.all(graph));
 });
diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts
index 03c13ab7f..e56859521 100644
--- a/src/server/api/endpoints/following/create.ts
+++ b/src/server/api/endpoints/following/create.ts
@@ -42,8 +42,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Check if already following
 	const exist = await Following.findOne({
 		followerId: follower._id,
-		followeeId: followee._id,
-		deletedAt: { $exists: false }
+		followeeId: followee._id
 	});
 
 	if (exist !== null) {
diff --git a/src/server/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts
index 3facfdcdd..5deddc919 100644
--- a/src/server/api/endpoints/following/delete.ts
+++ b/src/server/api/endpoints/following/delete.ts
@@ -42,8 +42,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Check not following
 	const exist = await Following.findOne({
 		followerId: follower._id,
-		followeeId: followee._id,
-		deletedAt: { $exists: false }
+		followeeId: followee._id
 	});
 
 	if (exist === null) {
@@ -51,12 +50,8 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}
 
 	// Delete following
-	await Following.update({
+	await Following.findOneAndDelete({
 		_id: exist._id
-	}, {
-		$set: {
-			deletedAt: new Date()
-		}
 	});
 
 	// Send response
diff --git a/src/server/api/endpoints/users/followers.ts b/src/server/api/endpoints/users/followers.ts
index 39b69a6aa..0222313e8 100644
--- a/src/server/api/endpoints/users/followers.ts
+++ b/src/server/api/endpoints/users/followers.ts
@@ -46,8 +46,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Construct query
 	const query = {
-		followeeId: user._id,
-		deletedAt: { $exists: false }
+		followeeId: user._id
 	} as any;
 
 	// ログインしていてかつ iknow フラグがあるとき
diff --git a/src/server/api/endpoints/users/following.ts b/src/server/api/endpoints/users/following.ts
index aa6628dde..2372f57fb 100644
--- a/src/server/api/endpoints/users/following.ts
+++ b/src/server/api/endpoints/users/following.ts
@@ -46,8 +46,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	// Construct query
 	const query = {
-		followerId: user._id,
-		deletedAt: { $exists: false }
+		followerId: user._id
 	} as any;
 
 	// ログインしていてかつ iknow フラグがあるとき
diff --git a/test/api.js b/test/api.js
index 962730836..98d3f2faf 100644
--- a/test/api.js
+++ b/test/api.js
@@ -609,20 +609,6 @@ describe('API', () => {
 			res.should.have.status(204);
 		}));
 
-		it('過去にフォロー歴があった状態でフォローできる', async(async () => {
-			const hima = await insertHimawari();
-			const me = await insertSakurako();
-			await db.get('following').insert({
-				followeeId: hima._id,
-				followerId: me._id,
-				deletedAt: new Date()
-			});
-			const res = await request('/following/create', {
-				userId: hima._id.toString()
-			}, me);
-			res.should.have.status(204);
-		}));
-
 		it('既にフォローしている場合は怒る', async(async () => {
 			const hima = await insertHimawari();
 			const me = await insertSakurako();

From a83184f1f59d4b3a29b32e485bc2b0574099a9d0 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 2 Apr 2018 13:43:13 +0000
Subject: [PATCH 1031/1250] fix(package): update ws to version 5.1.1

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 802074365..365cf88c5 100644
--- a/package.json
+++ b/package.json
@@ -213,7 +213,7 @@
 		"webpack-cli": "2.0.13",
 		"webpack-replace-loader": "1.3.0",
 		"websocket": "1.0.25",
-		"ws": "5.1.0",
+		"ws": "5.1.1",
 		"xev": "2.0.0"
 	}
 }

From 6a3cd6842303f1a517b1a232589c21dbe411e6cd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 2 Apr 2018 23:19:07 +0900
Subject: [PATCH 1032/1250] Make migration scripts

and use createdAt instead of _id
---
 src/models/followed-log.ts                    |  1 +
 src/models/following-log.ts                   |  1 +
 src/processor/http/follow.ts                  |  2 +
 .../endpoints/aggregation/users/followers.ts  |  4 +-
 .../endpoints/aggregation/users/following.ts  |  4 +-
 tools/migration/nighthike/10.js               |  3 +
 tools/migration/nighthike/9.js                | 79 +++++++++++++++++++
 7 files changed, 90 insertions(+), 4 deletions(-)
 create mode 100644 tools/migration/nighthike/10.js
 create mode 100644 tools/migration/nighthike/9.js

diff --git a/src/models/followed-log.ts b/src/models/followed-log.ts
index 4d8ecf684..9e3ca1782 100644
--- a/src/models/followed-log.ts
+++ b/src/models/followed-log.ts
@@ -6,6 +6,7 @@ export default FollowedLog;
 
 export type IFollowedLog = {
 	_id: ObjectID;
+	createdAt: Date;
 	userId: ObjectID;
 	count: number;
 };
diff --git a/src/models/following-log.ts b/src/models/following-log.ts
index f18707db8..045ff7bf0 100644
--- a/src/models/following-log.ts
+++ b/src/models/following-log.ts
@@ -6,6 +6,7 @@ export default FollowingLog;
 
 export type IFollowingLog = {
 	_id: ObjectID;
+	createdAt: Date;
 	userId: ObjectID;
 	count: number;
 };
diff --git a/src/processor/http/follow.ts b/src/processor/http/follow.ts
index 29ac9fa55..cbb7838c6 100644
--- a/src/processor/http/follow.ts
+++ b/src/processor/http/follow.ts
@@ -24,6 +24,7 @@ export default ({ data }, done) => Following.findOne({ _id: data.following }).th
 		}),
 
 		promisedFollower.then(({ followingCount }) => FollowingLog.insert({
+			createdAt: data.following.createdAt,
 			userId: followerId,
 			count: followingCount + 1
 		})),
@@ -36,6 +37,7 @@ export default ({ data }, done) => Following.findOne({ _id: data.following }).th
 		}),
 
 		promisedFollowee.then(({ followersCount }) => FollowedLog.insert({
+			createdAt: data.following.createdAt,
 			userId: followerId,
 			count: followersCount + 1
 		})),
diff --git a/src/server/api/endpoints/aggregation/users/followers.ts b/src/server/api/endpoints/aggregation/users/followers.ts
index 580d31a3f..f9a5e8dca 100644
--- a/src/server/api/endpoints/aggregation/users/followers.ts
+++ b/src/server/api/endpoints/aggregation/users/followers.ts
@@ -42,10 +42,10 @@ module.exports = (params) => new Promise(async (res, rej) => {
 
 	for (let i = 0; i < 30; i++) {
 		graph.push(FollowedLog.findOne({
-			_id: { $lt: ObjectID.createFromTime(cursorTime / 1000) },
+			createdAt: { $lt: new Date(cursorTime / 1000) },
 			userId: user._id
 		}, {
-			sort: { _id: -1 },
+			sort: { createdAt: -1 },
 		}).then(log => {
 			cursorDate = new Date(today.getTime());
 			cursorTime = cursorDate.setDate(today.getDate() - i);
diff --git a/src/server/api/endpoints/aggregation/users/following.ts b/src/server/api/endpoints/aggregation/users/following.ts
index 3ac0e3a53..b30b1282b 100644
--- a/src/server/api/endpoints/aggregation/users/following.ts
+++ b/src/server/api/endpoints/aggregation/users/following.ts
@@ -42,10 +42,10 @@ module.exports = (params) => new Promise(async (res, rej) => {
 
 	for (let i = 0; i < 30; i++) {
 		graph.push(FollowingLog.findOne({
-			_id: { $lt: ObjectID.createFromTime(cursorTime / 1000) },
+			createdAt: { $lt: new Date(cursorTime / 1000) },
 			userId: user._id
 		}, {
-			sort: { _id: -1 },
+			sort: { createdAt: -1 },
 		}).then(log => {
 			cursorDate = new Date(today.getTime());
 			cursorTime = cursorDate.setDate(today.getDate() - i);
diff --git a/tools/migration/nighthike/10.js b/tools/migration/nighthike/10.js
new file mode 100644
index 000000000..3c57b8d48
--- /dev/null
+++ b/tools/migration/nighthike/10.js
@@ -0,0 +1,3 @@
+db.following.remove({
+	deletedAt: { $exists: true }
+});
diff --git a/tools/migration/nighthike/9.js b/tools/migration/nighthike/9.js
new file mode 100644
index 000000000..a904feb06
--- /dev/null
+++ b/tools/migration/nighthike/9.js
@@ -0,0 +1,79 @@
+// for Node.js interpret
+
+const { default: Following } = require('../../../built/models/following');
+const { default: FollowingLog } = require('../../../built/models/following-log');
+const { default: FollowedLog } = require('../../../built/models/followed-log');
+const { default: zip } = require('@prezzemolo/zip')
+const html = require('../../../built/text/html').default;
+const parse = require('../../../built/text/parse').default;
+
+const migrate = async (following) => {
+	const followingCount = await Following.count({
+		followerId: following.followerId,
+		_id: { $lt: following._id },
+		$or: [
+			{ deletedAt: { $exists: false } },
+			{ deletedAt: { $gt: following.createdAt } }
+		]
+	});
+	await FollowingLog.insert({
+		createdAt: following.createdAt,
+		userId: following.followerId,
+		count: followingCount + 1
+	});
+
+	const followersCount = await Following.count({
+		followeeId: following.followeeId,
+		_id: { $lt: following._id },
+		$or: [
+			{ deletedAt: { $exists: false } },
+			{ deletedAt: { $gt: following.createdAt } }
+		]
+	});
+	await FollowedLog.insert({
+		createdAt: following.createdAt,
+		userId: following.followeeId,
+		count: followersCount + 1
+	});
+
+	if (following.deletedAt) {
+		await FollowingLog.insert({
+			createdAt: following.deletedAt,
+			userId: following.followerId,
+			count: followingCount - 1
+		});
+
+		await FollowedLog.insert({
+			createdAt: following.deletedAt,
+			userId: following.followeeId,
+			count: followersCount - 1
+		});
+	}
+
+	return true;
+}
+
+async function main() {
+	const count = await Following.count({});
+
+	const dop = Number.parseInt(process.argv[2]) || 5
+	const idop = ((count - (count % dop)) / dop) + 1
+
+	return zip(
+		1,
+		async (time) => {
+			console.log(`${time} / ${idop}`)
+			const doc = await Following.find({}, {
+				limit: dop, skip: time * dop, sort: { _id: 1 }
+			})
+			return Promise.all(doc.map(migrate))
+		},
+		idop
+	).then(a => {
+		const rv = []
+		a.forEach(e => rv.push(...e))
+		return rv
+	})
+}
+
+main().then(console.dir).catch(console.error)

From 757eca395b11024815cab88161ae0120d4c089a5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 2 Apr 2018 23:21:04 +0900
Subject: [PATCH 1033/1250] Fix: Remove unused imports

---
 src/server/api/endpoints/aggregation/users/followers.ts | 1 -
 src/server/api/endpoints/aggregation/users/following.ts | 1 -
 2 files changed, 2 deletions(-)

diff --git a/src/server/api/endpoints/aggregation/users/followers.ts b/src/server/api/endpoints/aggregation/users/followers.ts
index f9a5e8dca..7ccb2a306 100644
--- a/src/server/api/endpoints/aggregation/users/followers.ts
+++ b/src/server/api/endpoints/aggregation/users/followers.ts
@@ -2,7 +2,6 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import { ObjectID } from 'mongodb';
 import User from '../../../../../models/user';
 import FollowedLog from '../../../../../models/followed-log';
 
diff --git a/src/server/api/endpoints/aggregation/users/following.ts b/src/server/api/endpoints/aggregation/users/following.ts
index b30b1282b..45e246495 100644
--- a/src/server/api/endpoints/aggregation/users/following.ts
+++ b/src/server/api/endpoints/aggregation/users/following.ts
@@ -2,7 +2,6 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import { ObjectID } from 'mongodb';
 import User from '../../../../../models/user';
 import FollowingLog from '../../../../../models/following-log';
 

From 92bdf8b2e2a51265dd7e0dda3a8360a9c1e63e57 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 2 Apr 2018 23:26:47 +0900
Subject: [PATCH 1034/1250] Fix bug

---
 tools/migration/nighthike/9.js | 26 ++++++++++++++++++++------
 1 file changed, 20 insertions(+), 6 deletions(-)

diff --git a/tools/migration/nighthike/9.js b/tools/migration/nighthike/9.js
index a904feb06..f4e1ab341 100644
--- a/tools/migration/nighthike/9.js
+++ b/tools/migration/nighthike/9.js
@@ -4,13 +4,11 @@ const { default: Following } = require('../../../built/models/following');
 const { default: FollowingLog } = require('../../../built/models/following-log');
 const { default: FollowedLog } = require('../../../built/models/followed-log');
 const { default: zip } = require('@prezzemolo/zip')
-const html = require('../../../built/text/html').default;
-const parse = require('../../../built/text/parse').default;
 
 const migrate = async (following) => {
 	const followingCount = await Following.count({
 		followerId: following.followerId,
-		_id: { $lt: following._id },
+		createdAt: { $lt: following.createdAt },
 		$or: [
 			{ deletedAt: { $exists: false } },
 			{ deletedAt: { $gt: following.createdAt } }
@@ -24,7 +22,7 @@ const migrate = async (following) => {
 
 	const followersCount = await Following.count({
 		followeeId: following.followeeId,
-		_id: { $lt: following._id },
+		createdAt: { $lt: following.createdAt },
 		$or: [
 			{ deletedAt: { $exists: false } },
 			{ deletedAt: { $gt: following.createdAt } }
@@ -37,16 +35,32 @@ const migrate = async (following) => {
 	});
 
 	if (following.deletedAt) {
+		const followingCount2 = await Following.count({
+			followerId: following.followerId,
+			createdAt: { $lt: following.deletedAt },
+			$or: [
+				{ deletedAt: { $exists: false } },
+				{ deletedAt: { $gt: following.createdAt } }
+			]
+		});
 		await FollowingLog.insert({
 			createdAt: following.deletedAt,
 			userId: following.followerId,
-			count: followingCount - 1
+			count: followingCount2 - 1
 		});
 
+		const followersCount2 = await Following.count({
+			followeeId: following.followeeId,
+			createdAt: { $lt: following.deletedAt },
+			$or: [
+				{ deletedAt: { $exists: false } },
+				{ deletedAt: { $gt: following.createdAt } }
+			]
+		});
 		await FollowedLog.insert({
 			createdAt: following.deletedAt,
 			userId: following.followeeId,
-			count: followersCount - 1
+			count: followersCount2 - 1
 		});
 	}
 

From 507b9fdfd727a37520c9a3490dfed035ab3b9414 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 3 Apr 2018 00:59:38 +0900
Subject: [PATCH 1035/1250] [wip] Implement like activity

---
 src/models/post-reaction.ts             |  2 +-
 src/remote/activitypub/act/like.ts      | 48 +++++++++++++++++++++++++
 src/remote/activitypub/renderer/note.ts |  2 +-
 3 files changed, 50 insertions(+), 2 deletions(-)
 create mode 100644 src/remote/activitypub/act/like.ts

diff --git a/src/models/post-reaction.ts b/src/models/post-reaction.ts
index 3fc33411f..81be95b8d 100644
--- a/src/models/post-reaction.ts
+++ b/src/models/post-reaction.ts
@@ -5,12 +5,12 @@ import Reaction from './post-reaction';
 import { pack as packUser } from './user';
 
 const PostReaction = db.get<IPostReaction>('postReactions');
+PostReaction.createIndex(['userId', 'postId'], { unique: true });
 export default PostReaction;
 
 export interface IPostReaction {
 	_id: mongo.ObjectID;
 	createdAt: Date;
-	deletedAt: Date;
 	postId: mongo.ObjectID;
 	userId: mongo.ObjectID;
 	reaction: string;
diff --git a/src/remote/activitypub/act/like.ts b/src/remote/activitypub/act/like.ts
new file mode 100644
index 000000000..d2ddba89a
--- /dev/null
+++ b/src/remote/activitypub/act/like.ts
@@ -0,0 +1,48 @@
+import { MongoError } from 'mongodb';
+import Post from '../../../models/post';
+import Reaction from '../../../models/post-reaction';
+import config from '../../../config';
+import queue from '../../../queue';
+
+export default async (actor, activity) => {
+	const prefix = config.url + '/posts';
+	const id = activity.object.id || activity.object;
+	let reaction;
+
+	if (!id.startsWith(prefix)) {
+		return null;
+	}
+
+	const postId = id.slice(prefix.length);
+
+	const post = await Post.findOne({ _id: postId });
+	if (post === null) {
+		throw new Error();
+	}
+
+	try {
+		reaction = await Reaction.insert({
+			createdAt: new Date(),
+			postId,
+			userId: actor._id,
+			reaction: 'pudding'
+		});
+	} catch (exception) {
+		// duplicate key error
+		if (exception instanceof MongoError && exception.code === 11000) {
+			return null;
+		}
+
+		throw exception;
+	}
+
+	await new Promise((resolve, reject) => {
+		queue.create('http', { type: 'like', reaction: reaction._id }).save(error => {
+			if (error) {
+				reject(error);
+			} else {
+				resolve(null);
+			}
+		});
+	});
+};
diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts
index 43531b121..36f8578ec 100644
--- a/src/remote/activitypub/renderer/note.ts
+++ b/src/remote/activitypub/renderer/note.ts
@@ -30,7 +30,7 @@ export default async (user, post) => {
 	const attributedTo = `${config.url}/@${user.username}`;
 
 	return {
-		id: `${attributedTo}/${post._id}`,
+		id: `${config.url}/posts/${post._id}}`,
 		type: 'Note',
 		attributedTo,
 		content: post.textHtml,

From 4762ab417d9a16ea353f4f5a7ae9139f3e088ff6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 3 Apr 2018 01:02:04 +0900
Subject: [PATCH 1036/1250] Fix typo

---
 src/remote/activitypub/renderer/note.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts
index 36f8578ec..935ccb8e5 100644
--- a/src/remote/activitypub/renderer/note.ts
+++ b/src/remote/activitypub/renderer/note.ts
@@ -30,7 +30,7 @@ export default async (user, post) => {
 	const attributedTo = `${config.url}/@${user.username}`;
 
 	return {
-		id: `${config.url}/posts/${post._id}}`,
+		id: `${config.url}/posts/${post._id}`,
 		type: 'Note',
 		attributedTo,
 		content: post.textHtml,

From 3f3e6d0ba06999b30f2237072171b09a84652087 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 3 Apr 2018 01:22:23 +0900
Subject: [PATCH 1037/1250] Fix

---
 src/server/activitypub/post.ts | 22 +++++++---------------
 1 file changed, 7 insertions(+), 15 deletions(-)

diff --git a/src/server/activitypub/post.ts b/src/server/activitypub/post.ts
index 91d91aeb9..1dadad0db 100644
--- a/src/server/activitypub/post.ts
+++ b/src/server/activitypub/post.ts
@@ -1,37 +1,29 @@
 import * as express from 'express';
 import context from '../../remote/activitypub/renderer/context';
 import render from '../../remote/activitypub/renderer/note';
-import parseAcct from '../../acct/parse';
 import Post from '../../models/post';
 import User from '../../models/user';
 
 const app = express();
 app.disable('x-powered-by');
 
-app.get('/@:user/:post', async (req, res, next) => {
+app.get('/posts/:post', async (req, res, next) => {
 	const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']);
 	if (!(['application/activity+json', 'application/ld+json'] as any[]).includes(accepted)) {
 		return next();
 	}
 
-	const { username, host } = parseAcct(req.params.user);
-	if (host !== null) {
-		return res.sendStatus(422);
-	}
-
-	const user = await User.findOne({
-		usernameLower: username.toLowerCase(),
-		host: null
+	const post = await Post.findOne({
+		_id: req.params.post
 	});
-	if (user === null) {
+	if (post === null) {
 		return res.sendStatus(404);
 	}
 
-	const post = await Post.findOne({
-		_id: req.params.post,
-		userId: user._id
+	const user = await User.findOne({
+		_id: post.userId
 	});
-	if (post === null) {
+	if (user === null) {
 		return res.sendStatus(404);
 	}
 

From ece470fd232283991f7a4655badbe9c702c1d0f5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 3 Apr 2018 01:36:55 +0900
Subject: [PATCH 1038/1250] Revert "Fix"

This reverts commit 8afdb5aef91bea931c1ab7ecfd9f5ba1e977652c.
---
 src/server/activitypub/post.ts | 22 +++++++++++++++-------
 1 file changed, 15 insertions(+), 7 deletions(-)

diff --git a/src/server/activitypub/post.ts b/src/server/activitypub/post.ts
index 1dadad0db..91d91aeb9 100644
--- a/src/server/activitypub/post.ts
+++ b/src/server/activitypub/post.ts
@@ -1,32 +1,40 @@
 import * as express from 'express';
 import context from '../../remote/activitypub/renderer/context';
 import render from '../../remote/activitypub/renderer/note';
+import parseAcct from '../../acct/parse';
 import Post from '../../models/post';
 import User from '../../models/user';
 
 const app = express();
 app.disable('x-powered-by');
 
-app.get('/posts/:post', async (req, res, next) => {
+app.get('/@:user/:post', async (req, res, next) => {
 	const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']);
 	if (!(['application/activity+json', 'application/ld+json'] as any[]).includes(accepted)) {
 		return next();
 	}
 
-	const post = await Post.findOne({
-		_id: req.params.post
-	});
-	if (post === null) {
-		return res.sendStatus(404);
+	const { username, host } = parseAcct(req.params.user);
+	if (host !== null) {
+		return res.sendStatus(422);
 	}
 
 	const user = await User.findOne({
-		_id: post.userId
+		usernameLower: username.toLowerCase(),
+		host: null
 	});
 	if (user === null) {
 		return res.sendStatus(404);
 	}
 
+	const post = await Post.findOne({
+		_id: req.params.post,
+		userId: user._id
+	});
+	if (post === null) {
+		return res.sendStatus(404);
+	}
+
 	const rendered = await render(user, post);
 	rendered['@context'] = context;
 

From 6eac6f8ddd7c46a32b486a9ca58d1c9f1c6a422f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 3 Apr 2018 01:37:02 +0900
Subject: [PATCH 1039/1250] Revert "Fix typo"

This reverts commit f3f6dce60b9ace954a26b0e045d8bf8f49571482.
---
 src/remote/activitypub/renderer/note.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts
index 935ccb8e5..36f8578ec 100644
--- a/src/remote/activitypub/renderer/note.ts
+++ b/src/remote/activitypub/renderer/note.ts
@@ -30,7 +30,7 @@ export default async (user, post) => {
 	const attributedTo = `${config.url}/@${user.username}`;
 
 	return {
-		id: `${config.url}/posts/${post._id}`,
+		id: `${config.url}/posts/${post._id}}`,
 		type: 'Note',
 		attributedTo,
 		content: post.textHtml,

From f4cb6ba0e03ca235cfd418fe295d1f64acde09f9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 3 Apr 2018 01:37:11 +0900
Subject: [PATCH 1040/1250] Revert "[wip] Implement like activity"

This reverts commit 7da191396469642de14655c30eba86926882e98c.
---
 src/models/post-reaction.ts             |  2 +-
 src/remote/activitypub/act/like.ts      | 48 -------------------------
 src/remote/activitypub/renderer/note.ts |  2 +-
 3 files changed, 2 insertions(+), 50 deletions(-)
 delete mode 100644 src/remote/activitypub/act/like.ts

diff --git a/src/models/post-reaction.ts b/src/models/post-reaction.ts
index 81be95b8d..3fc33411f 100644
--- a/src/models/post-reaction.ts
+++ b/src/models/post-reaction.ts
@@ -5,12 +5,12 @@ import Reaction from './post-reaction';
 import { pack as packUser } from './user';
 
 const PostReaction = db.get<IPostReaction>('postReactions');
-PostReaction.createIndex(['userId', 'postId'], { unique: true });
 export default PostReaction;
 
 export interface IPostReaction {
 	_id: mongo.ObjectID;
 	createdAt: Date;
+	deletedAt: Date;
 	postId: mongo.ObjectID;
 	userId: mongo.ObjectID;
 	reaction: string;
diff --git a/src/remote/activitypub/act/like.ts b/src/remote/activitypub/act/like.ts
deleted file mode 100644
index d2ddba89a..000000000
--- a/src/remote/activitypub/act/like.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import { MongoError } from 'mongodb';
-import Post from '../../../models/post';
-import Reaction from '../../../models/post-reaction';
-import config from '../../../config';
-import queue from '../../../queue';
-
-export default async (actor, activity) => {
-	const prefix = config.url + '/posts';
-	const id = activity.object.id || activity.object;
-	let reaction;
-
-	if (!id.startsWith(prefix)) {
-		return null;
-	}
-
-	const postId = id.slice(prefix.length);
-
-	const post = await Post.findOne({ _id: postId });
-	if (post === null) {
-		throw new Error();
-	}
-
-	try {
-		reaction = await Reaction.insert({
-			createdAt: new Date(),
-			postId,
-			userId: actor._id,
-			reaction: 'pudding'
-		});
-	} catch (exception) {
-		// duplicate key error
-		if (exception instanceof MongoError && exception.code === 11000) {
-			return null;
-		}
-
-		throw exception;
-	}
-
-	await new Promise((resolve, reject) => {
-		queue.create('http', { type: 'like', reaction: reaction._id }).save(error => {
-			if (error) {
-				reject(error);
-			} else {
-				resolve(null);
-			}
-		});
-	});
-};
diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts
index 36f8578ec..43531b121 100644
--- a/src/remote/activitypub/renderer/note.ts
+++ b/src/remote/activitypub/renderer/note.ts
@@ -30,7 +30,7 @@ export default async (user, post) => {
 	const attributedTo = `${config.url}/@${user.username}`;
 
 	return {
-		id: `${config.url}/posts/${post._id}}`,
+		id: `${attributedTo}/${post._id}`,
 		type: 'Note',
 		attributedTo,
 		content: post.textHtml,

From 342cba4c546c4d607120389d4bab0c5fd2790821 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 3 Apr 2018 01:37:43 +0900
Subject: [PATCH 1041/1250] Use index

---
 src/models/post-reaction.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/models/post-reaction.ts b/src/models/post-reaction.ts
index 3fc33411f..81be95b8d 100644
--- a/src/models/post-reaction.ts
+++ b/src/models/post-reaction.ts
@@ -5,12 +5,12 @@ import Reaction from './post-reaction';
 import { pack as packUser } from './user';
 
 const PostReaction = db.get<IPostReaction>('postReactions');
+PostReaction.createIndex(['userId', 'postId'], { unique: true });
 export default PostReaction;
 
 export interface IPostReaction {
 	_id: mongo.ObjectID;
 	createdAt: Date;
-	deletedAt: Date;
 	postId: mongo.ObjectID;
 	userId: mongo.ObjectID;
 	reaction: string;

From 1420358efd6d43b729ac6adf5a5c524680a62eb7 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Tue, 3 Apr 2018 02:09:50 +0900
Subject: [PATCH 1042/1250] Fix job processor interfaces

---
 src/processor/http/follow.ts                |  4 +--
 src/processor/http/perform-activitypub.ts   |  5 ++--
 src/processor/http/process-inbox.ts         |  4 +--
 src/processor/http/report-github-failure.ts | 31 +++++++++------------
 4 files changed, 19 insertions(+), 25 deletions(-)

diff --git a/src/processor/http/follow.ts b/src/processor/http/follow.ts
index cbb7838c6..6b2a39d51 100644
--- a/src/processor/http/follow.ts
+++ b/src/processor/http/follow.ts
@@ -11,7 +11,7 @@ import context from '../../remote/activitypub/renderer/context';
 import render from '../../remote/activitypub/renderer/follow';
 import config from '../../config';
 
-export default ({ data }, done) => Following.findOne({ _id: data.following }).then(({ followerId, followeeId }) => {
+export default ({ data }) => Following.findOne({ _id: data.following }).then(({ followerId, followeeId }) => {
 	const promisedFollower = User.findOne({ _id: followerId });
 	const promisedFollowee = User.findOne({ _id: followeeId });
 
@@ -104,4 +104,4 @@ export default ({ data }, done) => Following.findOne({ _id: data.following }).th
 			return Promise.all([followerEvent, followeeEvent]);
 		})
 	]);
-}).then(done, done);
+});
diff --git a/src/processor/http/perform-activitypub.ts b/src/processor/http/perform-activitypub.ts
index 420ed9ec7..adf4e65a7 100644
--- a/src/processor/http/perform-activitypub.ts
+++ b/src/processor/http/perform-activitypub.ts
@@ -1,6 +1,5 @@
 import User from '../../models/user';
 import act from '../../remote/activitypub/act';
 
-export default ({ data }, done) => User.findOne({ _id: data.actor })
-	.then(actor => act(actor, data.outbox, false))
-	.then(() => done(), done);
+export default ({ data }) => User.findOne({ _id: data.actor })
+	.then(actor => act(actor, data.outbox, false));
diff --git a/src/processor/http/process-inbox.ts b/src/processor/http/process-inbox.ts
index 78c20f8a7..11801409c 100644
--- a/src/processor/http/process-inbox.ts
+++ b/src/processor/http/process-inbox.ts
@@ -4,7 +4,7 @@ import User, { IRemoteUser } from '../../models/user';
 import act from '../../remote/activitypub/act';
 import resolvePerson from '../../remote/activitypub/resolve-person';
 
-export default ({ data }, done) => (async () => {
+export default async ({ data }) => {
 	const keyIdLower = data.signature.keyId.toLowerCase();
 	let user;
 
@@ -35,4 +35,4 @@ export default ({ data }, done) => (async () => {
 	}
 
 	await act(user, data.inbox, true);
-})().then(done, done);
+};
diff --git a/src/processor/http/report-github-failure.ts b/src/processor/http/report-github-failure.ts
index 53924a0fb..4f6f5ccee 100644
--- a/src/processor/http/report-github-failure.ts
+++ b/src/processor/http/report-github-failure.ts
@@ -1,29 +1,24 @@
-import * as request from 'request';
+import * as request from 'request-promise-native';
 import User from '../../models/user';
 const createPost = require('../../server/api/endpoints/posts/create');
 
-export default ({ data }, done) => {
+export default async ({ data }) => {
 	const asyncBot = User.findOne({ _id: data.userId });
 
 	// Fetch parent status
-	request({
+	const parentStatuses = await request({
 		url: `${data.parentUrl}/statuses`,
 		headers: {
 			'User-Agent': 'misskey'
-		}
-	}, async (err, res, body) => {
-		if (err) {
-			console.error(err);
-			return;
-		}
-		const parentStatuses = JSON.parse(body);
-		const parentState = parentStatuses[0].state;
-		const stillFailed = parentState == 'failure' || parentState == 'error';
-		const text = stillFailed ?
-			`**⚠️BUILD STILL FAILED⚠️**: ?[${data.message}](${data.htmlUrl})` :
-			`**🚨BUILD FAILED🚨**: →→→?[${data.message}](${data.htmlUrl})←←←`;
-
-		createPost({ text }, await asyncBot);
-		done();
+		},
+		json: true
 	});
+
+	const parentState = parentStatuses[0].state;
+	const stillFailed = parentState == 'failure' || parentState == 'error';
+	const text = stillFailed ?
+		`**⚠️BUILD STILL FAILED⚠️**: ?[${data.message}](${data.htmlUrl})` :
+		`**🚨BUILD FAILED🚨**: →→→?[${data.message}](${data.htmlUrl})←←←`;
+
+	createPost({ text }, await asyncBot);
 };

From 1d62b15889a4928abbac9a680006798eb9f6206e Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Mon, 2 Apr 2018 20:16:13 +0900
Subject: [PATCH 1043/1250] Deliver posts to remote followers

---
 src/post/distribute.ts                   | 138 ++++++++++-------------
 src/processor/http/deliver-post.ts       |  94 +++++++++++++++
 src/processor/http/follow.ts             |  46 +-------
 src/processor/http/index.ts              |   2 +
 src/remote/activitypub/create.ts         |   3 +-
 src/remote/request.ts                    |  35 ++++++
 src/server/api/endpoints/posts/create.ts |   7 +-
 7 files changed, 195 insertions(+), 130 deletions(-)
 create mode 100644 src/processor/http/deliver-post.ts
 create mode 100644 src/remote/request.ts

diff --git a/src/post/distribute.ts b/src/post/distribute.ts
index 4def2c27f..49c6eb22d 100644
--- a/src/post/distribute.ts
+++ b/src/post/distribute.ts
@@ -1,16 +1,15 @@
-import Channel from '../models/channel';
 import Mute from '../models/mute';
-import Following from '../models/following';
-import Post from '../models/post';
+import Post, { pack } from '../models/post';
 import Watching from '../models/post-watching';
-import ChannelWatching from '../models/channel-watching';
 import User from '../models/user';
-import stream, { publishChannelStream } from '../publishers/stream';
+import stream from '../publishers/stream';
 import notify from '../publishers/notify';
 import pushSw from '../publishers/push-sw';
+import queue from '../queue';
 import watch from './watch';
 
 export default async (user, mentions, post) => {
+	const promisedPostObj = pack(post);
 	const promises = [
 		User.update({ _id: user._id }, {
 			// Increment my posts count
@@ -22,66 +21,33 @@ export default async (user, mentions, post) => {
 				latestPost: post._id
 			}
 		}),
+		new Promise((resolve, reject) => queue.create('http', {
+			type: 'deliverPost',
+			id: post._id,
+		}).save(error => error ? reject(error) : resolve())),
 	] as Array<Promise<any>>;
 
-	function addMention(mentionee, reason) {
+	function addMention(promisedMentionee, reason) {
 		// Publish event
-		if (!user._id.equals(mentionee)) {
-			promises.push(Mute.find({
-				muterId: mentionee,
-				deletedAt: { $exists: false }
-			}).then(mentioneeMutes => {
+		promises.push(promisedMentionee.then(mentionee => {
+			if (user._id.equals(mentionee)) {
+				return Promise.resolve();
+			}
+
+			return Promise.all([
+				promisedPostObj,
+				Mute.find({
+					muterId: mentionee,
+					deletedAt: { $exists: false }
+				})
+			]).then(([postObj, mentioneeMutes]) => {
 				const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString());
 				if (mentioneesMutedUserIds.indexOf(user._id.toString()) == -1) {
-					stream(mentionee, reason, post);
-					pushSw(mentionee, reason, post);
+					stream(mentionee, reason, postObj);
+					pushSw(mentionee, reason, postObj);
 				}
-			}));
-		}
-	}
-
-	// タイムラインへの投稿
-	if (!post.channelId) {
-		// Publish event to myself's stream
-		stream(user._id, 'post', post);
-
-		// Fetch all followers
-		const followers = await Following
-			.find({
-				followeeId: user._id
-			}, {
-				followerId: true,
-				_id: false
 			});
-
-		// Publish event to followers stream
-		followers.forEach(following =>
-			stream(following.followerId, 'post', post));
-	}
-
-	// チャンネルへの投稿
-	if (post.channelId) {
-		// Increment channel index(posts count)
-		promises.push(Channel.update({ _id: post.channelId }, {
-			$inc: {
-				index: 1
-			}
 		}));
-
-		// Publish event to channel
-		publishChannelStream(post.channelId, 'post', post);
-
-		// Get channel watchers
-		const watches = await ChannelWatching.find({
-			channelId: post.channelId,
-			// 削除されたドキュメントは除く
-			deletedAt: { $exists: false }
-		});
-
-		// チャンネルの視聴者(のタイムライン)に配信
-		watches.forEach(w => {
-			stream(w.userId, 'post', post);
-		});
 	}
 
 	// If has in reply to post
@@ -95,8 +61,10 @@ export default async (user, mentions, post) => {
 			}),
 
 			// 自分自身へのリプライでない限りは通知を作成
-			notify(post.reply.userId, user._id, 'reply', {
-				postId: post._id
+			promisedPostObj.then(({ reply }) => {
+				return notify(reply.userId, user._id, 'reply', {
+					postId: post._id
+				});
 			}),
 
 			// Fetch watchers
@@ -121,11 +89,13 @@ export default async (user, mentions, post) => {
 		);
 
 		// Add mention
-		addMention(post.reply.userId, 'reply');
+		addMention(promisedPostObj.then(({ reply }) => reply.userId), 'reply');
 
 		// この投稿をWatchする
 		if (user.account.settings.autoWatch !== false) {
-			promises.push(watch(user._id, post.reply));
+			promises.push(promisedPostObj.then(({ reply }) => {
+				return watch(user._id, reply);
+			}));
 		}
 	}
 
@@ -134,10 +104,17 @@ export default async (user, mentions, post) => {
 		const type = post.text ? 'quote' : 'repost';
 
 		promises.push(
-			// Notify
-			notify(post.repost.userId, user._id, type, {
-				postId: post._id
-			}),
+			promisedPostObj.then(({ repost }) => Promise.all([
+				// Notify
+				notify(repost.userId, user._id, type, {
+					postId: post._id
+				}),
+
+				// この投稿をWatchする
+				// TODO: ユーザーが「Repostしたときに自動でWatchする」設定を
+				//       オフにしていた場合はしない
+				watch(user._id, repost)
+			])),
 
 			// Fetch watchers
 			Watching
@@ -157,23 +134,20 @@ export default async (user, mentions, post) => {
 							postId: post._id
 						});
 					});
-				}),
-
-			// この投稿をWatchする
-			// TODO: ユーザーが「Repostしたときに自動でWatchする」設定を
-			//       オフにしていた場合はしない
-			watch(user._id, post.repost)
+				})
 		);
 
 		// If it is quote repost
 		if (post.text) {
 			// Add mention
-			addMention(post.repost.userId, 'quote');
+			addMention(promisedPostObj.then(({ repost }) => repost.userId), 'quote');
 		} else {
-			// Publish event
-			if (!user._id.equals(post.repost.userId)) {
-				stream(post.repost.userId, 'repost', post);
-			}
+			promises.push(promisedPostObj.then(postObj => {
+				// Publish event
+				if (!user._id.equals(postObj.repost.userId)) {
+					stream(postObj.repost.userId, 'repost', postObj);
+				}
+			}));
 		}
 
 		// 今までで同じ投稿をRepostしているか
@@ -196,10 +170,10 @@ export default async (user, mentions, post) => {
 	}
 
 	// Resolve all mentions
-	await Promise.all(mentions.map(async mention => {
+	await promisedPostObj.then(({ reply, repost }) => Promise.all(mentions.map(async mention => {
 		// 既に言及されたユーザーに対する返信や引用repostの場合も無視
-		if (post.reply && post.reply.userId.equals(mention)) return;
-		if (post.repost && post.repost.userId.equals(mention)) return;
+		if (reply && reply.userId.equals(mention)) return;
+		if (repost && repost.userId.equals(mention)) return;
 
 		// Add mention
 		addMention(mention, 'mention');
@@ -208,7 +182,9 @@ export default async (user, mentions, post) => {
 		await notify(mention, user._id, 'mention', {
 			postId: post._id
 		});
-	}));
+	})));
 
-	return Promise.all(promises);
+	await Promise.all(promises);
+
+	return promisedPostObj;
 };
diff --git a/src/processor/http/deliver-post.ts b/src/processor/http/deliver-post.ts
new file mode 100644
index 000000000..83ac8281f
--- /dev/null
+++ b/src/processor/http/deliver-post.ts
@@ -0,0 +1,94 @@
+import Channel from '../../models/channel';
+import Following from '../../models/following';
+import ChannelWatching from '../../models/channel-watching';
+import Post, { pack } from '../../models/post';
+import User, { isLocalUser } from '../../models/user';
+import stream, { publishChannelStream } from '../../publishers/stream';
+import context from '../../remote/activitypub/renderer/context';
+import renderNote from '../../remote/activitypub/renderer/note';
+import request from '../../remote/request';
+
+export default ({ data }) => Post.findOne({ _id: data.id }).then(post => {
+	const promisedPostObj = pack(post);
+	const promises = [];
+
+	// タイムラインへの投稿
+	if (!post.channelId) {
+		promises.push(
+			// Publish event to myself's stream
+			promisedPostObj.then(postObj => {
+				stream(post.userId, 'post', postObj);
+			}),
+
+			Promise.all([
+				User.findOne({ _id: post.userId }),
+
+				// Fetch all followers
+				Following.aggregate([
+					{
+						$lookup: {
+							from: 'users',
+							localField: 'followerId',
+							foreignField: '_id',
+							as: 'follower'
+						}
+					},
+					{
+					$match: {
+							followeeId: post.userId
+						}
+					}
+				], {
+					_id: false
+				})
+			]).then(([user, followers]) => Promise.all(followers.map(following => {
+				if (isLocalUser(following.follower)) {
+					// Publish event to followers stream
+					return promisedPostObj.then(postObj => {
+						stream(following.followerId, 'post', postObj);
+					});
+				}
+
+				return renderNote(user, post).then(rendered => {
+					rendered['@context'] = context;
+					return request(user, following.follower[0].account.inbox, rendered);
+				});
+			})))
+		);
+	}
+
+	// チャンネルへの投稿
+	if (post.channelId) {
+		promises.push(
+			// Increment channel index(posts count)
+			Channel.update({ _id: post.channelId }, {
+				$inc: {
+					index: 1
+				}
+			}),
+
+			// Publish event to channel
+			promisedPostObj.then(postObj => {
+				publishChannelStream(post.channelId, 'post', postObj);
+			}),
+
+			Promise.all([
+				promisedPostObj,
+
+				// Get channel watchers
+				ChannelWatching.find({
+					channelId: post.channelId,
+					// 削除されたドキュメントは除く
+					deletedAt: { $exists: false }
+				})
+			]).then(([postObj, watches]) => {
+				// チャンネルの視聴者(のタイムライン)に配信
+				watches.forEach(w => {
+					stream(w.userId, 'post', postObj);
+				});
+			})
+		);
+	}
+
+	return Promise.all(promises);
+});
diff --git a/src/processor/http/follow.ts b/src/processor/http/follow.ts
index 6b2a39d51..8bf890efb 100644
--- a/src/processor/http/follow.ts
+++ b/src/processor/http/follow.ts
@@ -1,6 +1,3 @@
-import { request } from 'https';
-import { sign } from 'http-signature';
-import { URL } from 'url';
 import User, { isLocalUser, pack as packUser } from '../../models/user';
 import Following from '../../models/following';
 import FollowingLog from '../../models/following-log';
@@ -9,7 +6,7 @@ import event from '../../publishers/stream';
 import notify from '../../publishers/notify';
 import context from '../../remote/activitypub/renderer/context';
 import render from '../../remote/activitypub/renderer/follow';
-import config from '../../config';
+import request from '../../remote/request';
 
 export default ({ data }) => Following.findOne({ _id: data.following }).then(({ followerId, followeeId }) => {
 	const promisedFollower = User.findOne({ _id: followerId });
@@ -60,45 +57,10 @@ export default ({ data }) => Following.findOne({ _id: data.following }).then(({
 				followeeEvent = packUser(follower, followee)
 					.then(packed => event(followee._id, 'followed', packed));
 			} else if (isLocalUser(follower)) {
-				followeeEvent = new Promise((resolve, reject) => {
-					const {
-						protocol,
-						hostname,
-						port,
-						pathname,
-						search
-					} = new URL(followee.account.inbox);
+				const rendered = render(follower, followee);
+				rendered['@context'] = context;
 
-					const req = request({
-						protocol,
-						hostname,
-						port,
-						method: 'POST',
-						path: pathname + search,
-					}, res => {
-						res.on('close', () => {
-							if (res.statusCode >= 200 && res.statusCode < 300) {
-								resolve();
-							} else {
-								reject(res);
-							}
-						});
-
-						res.on('data', () => {});
-						res.on('error', reject);
-					});
-
-					sign(req, {
-						authorizationHeaderName: 'Signature',
-						key: follower.account.keypair,
-						keyId: `acct:${follower.username}@${config.host}`
-					});
-
-					const rendered = render(follower, followee);
-					rendered['@context'] = context;
-
-					req.end(JSON.stringify(rendered));
-				});
+				followeeEvent = request(follower, followee.account.inbox, rendered);
 			}
 
 			return Promise.all([followerEvent, followeeEvent]);
diff --git a/src/processor/http/index.ts b/src/processor/http/index.ts
index b3161cb99..0301b472c 100644
--- a/src/processor/http/index.ts
+++ b/src/processor/http/index.ts
@@ -1,9 +1,11 @@
+import deliverPost from './deliver-post';
 import follow from './follow';
 import performActivityPub from './perform-activitypub';
 import processInbox from './process-inbox';
 import reportGitHubFailure from './report-github-failure';
 
 const handlers = {
+  deliverPost,
   follow,
   performActivityPub,
   processInbox,
diff --git a/src/remote/activitypub/create.ts b/src/remote/activitypub/create.ts
index dd3f7b022..f70f56b79 100644
--- a/src/remote/activitypub/create.ts
+++ b/src/remote/activitypub/create.ts
@@ -1,6 +1,5 @@
 import { JSDOM } from 'jsdom';
 import config from '../../config';
-import { pack as packPost } from '../../models/post';
 import RemoteUserObject, { IRemoteUserObject } from '../../models/remote-user-object';
 import { IRemoteUser } from '../../models/user';
 import uploadFromUrl from '../../drive/upload-from-url';
@@ -69,7 +68,7 @@ class Creator {
 		const promises = [];
 
 		if (this.distribute) {
-			promises.push(distributePost(this.actor, inserted.mentions, packPost(inserted)));
+			promises.push(distributePost(this.actor, inserted.mentions, inserted));
 		}
 
 		// Register to search database
diff --git a/src/remote/request.ts b/src/remote/request.ts
new file mode 100644
index 000000000..72262cbf6
--- /dev/null
+++ b/src/remote/request.ts
@@ -0,0 +1,35 @@
+import { request } from 'https';
+import { sign } from 'http-signature';
+import { URL } from 'url';
+import config from '../config';
+
+export default ({ account, username }, url, object) => new Promise((resolve, reject) => {
+	const { protocol, hostname, port, pathname, search } = new URL(url);
+
+	const req = request({
+		protocol,
+		hostname,
+		port,
+		method: 'POST',
+		path: pathname + search,
+	}, res => {
+		res.on('end', () => {
+			if (res.statusCode >= 200 && res.statusCode < 300) {
+				resolve();
+			} else {
+				reject(res);
+			}
+		});
+
+		res.on('data', () => {});
+		res.on('error', reject);
+	});
+
+	sign(req, {
+		authorizationHeaderName: 'Signature',
+		key: account.keypair,
+		keyId: `acct:${username}@${config.host}`
+	});
+
+	req.end(JSON.stringify(object));
+});
diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts
index b633494a3..ccd617545 100644
--- a/src/server/api/endpoints/posts/create.ts
+++ b/src/server/api/endpoints/posts/create.ts
@@ -7,7 +7,7 @@ import renderAcct from '../../../../acct/render';
 import config from '../../../../config';
 import html from '../../../../text/html';
 import parse from '../../../../text/parse';
-import Post, { IPost, isValidText, isValidCw, pack } from '../../../../models/post';
+import Post, { IPost, isValidText, isValidCw } from '../../../../models/post';
 import { ILocalUser } from '../../../../models/user';
 import Channel, { IChannel } from '../../../../models/channel';
 import DriveFile from '../../../../models/drive-file';
@@ -283,16 +283,13 @@ module.exports = (params, user: ILocalUser, app) => new Promise(async (res, rej)
 		geo
 	}, reply, repost, atMentions);
 
-	// Serialize
-	const postObj = await pack(post);
+	const postObj = await distribute(user, post.mentions, post);
 
 	// Reponse
 	res({
 		createdPost: postObj
 	});
 
-	distribute(user, post.mentions, postObj);
-
 	// Register to search database
 	if (post.text && config.elasticsearch.enable) {
 		const es = require('../../../db/elasticsearch');

From c8077538f2636edadba755010e7ae3b4ef8d79e6 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Tue, 3 Apr 2018 12:07:29 +0900
Subject: [PATCH 1044/1250] Accept remote follow

---
 src/remote/activitypub/act/follow.ts      | 49 +++++++++++++----------
 src/remote/activitypub/renderer/accept.ts |  4 ++
 2 files changed, 32 insertions(+), 21 deletions(-)
 create mode 100644 src/remote/activitypub/renderer/accept.ts

diff --git a/src/remote/activitypub/act/follow.ts b/src/remote/activitypub/act/follow.ts
index ec9e080df..d7ea15fa3 100644
--- a/src/remote/activitypub/act/follow.ts
+++ b/src/remote/activitypub/act/follow.ts
@@ -4,11 +4,13 @@ import Following from '../../../models/following';
 import User from '../../../models/user';
 import config from '../../../config';
 import queue from '../../../queue';
+import context from '../renderer/context';
+import renderAccept from '../renderer/accept';
+import request from '../../request';
 
 export default async (actor, activity) => {
 	const prefix = config.url + '/@';
 	const id = activity.object.id || activity.object;
-	let following;
 
 	if (!id.startsWith(prefix)) {
 		return null;
@@ -24,28 +26,33 @@ export default async (actor, activity) => {
 		throw new Error();
 	}
 
-	try {
-		following = await Following.insert({
+	const accept = renderAccept(activity);
+	accept['@context'] = context;
+
+	await Promise.all([
+		request(followee, actor.account.inbox, accept),
+
+		Following.insert({
 			createdAt: new Date(),
 			followerId: actor._id,
 			followeeId: followee._id
-		});
-	} catch (exception) {
-		// duplicate key error
-		if (exception instanceof MongoError && exception.code === 11000) {
-			return null;
-		}
-
-		throw exception;
-	}
-
-	await new Promise((resolve, reject) => {
-		queue.create('http', { type: 'follow', following: following._id }).save(error => {
-			if (error) {
-				reject(error);
-			} else {
-				resolve(null);
+		}).then(following => new Promise((resolve, reject) => {
+			queue.create('http', { type: 'follow', following: following._id }).save(error => {
+				if (error) {
+					reject(error);
+				} else {
+					resolve();
+				}
+			});
+		}), error => {
+			// duplicate key error
+			if (error instanceof MongoError && error.code === 11000) {
+				return;
 			}
-		});
-	});
+
+			throw error;
+		})
+	]);
+
+	return null;
 };
diff --git a/src/remote/activitypub/renderer/accept.ts b/src/remote/activitypub/renderer/accept.ts
new file mode 100644
index 000000000..00c76883a
--- /dev/null
+++ b/src/remote/activitypub/renderer/accept.ts
@@ -0,0 +1,4 @@
+export default object => ({
+	type: 'Accept',
+	object
+});

From 655846f5cf1a1b5c2d97802ea9068ab8067991a2 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Tue, 3 Apr 2018 12:25:13 +0900
Subject: [PATCH 1045/1250] Explicityly wrap objects with Create activity

---
 src/processor/http/deliver-post.ts        | 8 +++++---
 src/remote/activitypub/renderer/create.ts | 4 ++++
 2 files changed, 9 insertions(+), 3 deletions(-)
 create mode 100644 src/remote/activitypub/renderer/create.ts

diff --git a/src/processor/http/deliver-post.ts b/src/processor/http/deliver-post.ts
index 83ac8281f..1389aede8 100644
--- a/src/processor/http/deliver-post.ts
+++ b/src/processor/http/deliver-post.ts
@@ -5,6 +5,7 @@ import Post, { pack } from '../../models/post';
 import User, { isLocalUser } from '../../models/user';
 import stream, { publishChannelStream } from '../../publishers/stream';
 import context from '../../remote/activitypub/renderer/context';
+import renderCreate from '../../remote/activitypub/renderer/create';
 import renderNote from '../../remote/activitypub/renderer/note';
 import request from '../../remote/request';
 
@@ -49,9 +50,10 @@ export default ({ data }) => Post.findOne({ _id: data.id }).then(post => {
 					});
 				}
 
-				return renderNote(user, post).then(rendered => {
-					rendered['@context'] = context;
-					return request(user, following.follower[0].account.inbox, rendered);
+				return renderNote(user, post).then(note => {
+					const create = renderCreate(note);
+					create['@context'] = context;
+					return request(user, following.follower[0].account.inbox, create);
 				});
 			})))
 		);
diff --git a/src/remote/activitypub/renderer/create.ts b/src/remote/activitypub/renderer/create.ts
new file mode 100644
index 000000000..de411e195
--- /dev/null
+++ b/src/remote/activitypub/renderer/create.ts
@@ -0,0 +1,4 @@
+export default object => ({
+	type: 'Create',
+	object
+});

From 634a1be9504430e19b859696a639cd56fae142f3 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Tue, 3 Apr 2018 12:28:34 +0900
Subject: [PATCH 1046/1250] Invoke a constructor with parentheses

---
 src/remote/webfinger.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/remote/webfinger.ts b/src/remote/webfinger.ts
index c84beb099..bfca8d1c8 100644
--- a/src/remote/webfinger.ts
+++ b/src/remote/webfinger.ts
@@ -24,7 +24,7 @@ export default async function resolve(query, verifier?: string): Promise<IWebFin
 
 	if (typeof verifier === 'string') {
 		if (subject !== verifier) {
-			throw new Error;
+			throw new Error();
 		}
 
 		return finger;
@@ -34,5 +34,5 @@ export default async function resolve(query, verifier?: string): Promise<IWebFin
 		return resolve(subject, subject);
 	}
 
-	throw new Error;
+	throw new Error();
 }

From 1d8888554e84b7c03efc6a6ba2e0044107a7830b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 3 Apr 2018 13:03:37 +0900
Subject: [PATCH 1047/1250] Use typescript in tests

---
 gulpfile.ts               |  4 ++--
 test/{api.js => api.ts}   | 38 ++++++++++++--------------------------
 test/{text.js => text.ts} |  0
 3 files changed, 14 insertions(+), 28 deletions(-)
 rename test/{api.js => api.ts} (97%)
 rename test/{text.js => text.ts} (100%)

diff --git a/gulpfile.ts b/gulpfile.ts
index f372ed299..fe3b04023 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -91,8 +91,8 @@ gulp.src('./src/**/*.ts')
 gulp.task('mocha', () =>
 	gulp.src([])
 		.pipe(mocha({
-			exit: true
-			//compilers: 'ts:ts-node/register'
+			exit: true,
+			compilers: 'ts:ts-node/register'
 		} as any))
 );
 
diff --git a/test/api.js b/test/api.ts
similarity index 97%
rename from test/api.js
rename to test/api.ts
index 98d3f2faf..c794869e4 100644
--- a/test/api.js
+++ b/test/api.ts
@@ -2,6 +2,8 @@
  * API TESTS
  */
 
+import * as merge from 'object-assign-deep';
+
 Error.stackTraceLimit = Infinity;
 
 // During the test the env variable is set to test
@@ -28,7 +30,7 @@ const async = fn => (done) => {
 	});
 };
 
-const request = (endpoint, params, me) => new Promise((ok, ng) => {
+const request = (endpoint, params, me?) => new Promise<any>((ok, ng) => {
 	const auth = me ? {
 		i: me.account.token
 	} : {};
@@ -1126,24 +1128,8 @@ describe('API', () => {
 	});
 });
 
-function deepAssign(destination, ...sources) {
-	for (const source of sources) {
-		for (const key in source) {
-			const destinationChild = destination[key];
-
-			if (typeof destinationChild === 'object' && destinationChild != null) {
-				deepAssign(destinationChild, source[key]);
-			} else {
-				destination[key] = source[key];
-			}
-		}
-	}
-
-	return destination;
-}
-
-function insertSakurako(opts) {
-	return db.get('users').insert(deepAssign({
+function insertSakurako(opts?) {
+	return db.get('users').insert(merge({
 		username: 'sakurako',
 		usernameLower: 'sakurako',
 		account: {
@@ -1157,8 +1143,8 @@ function insertSakurako(opts) {
 	}, opts));
 }
 
-function insertHimawari(opts) {
-	return db.get('users').insert(deepAssign({
+function insertHimawari(opts?) {
+	return db.get('users').insert(merge({
 		username: 'himawari',
 		usernameLower: 'himawari',
 		account: {
@@ -1172,7 +1158,7 @@ function insertHimawari(opts) {
 	}, opts));
 }
 
-function insertDriveFile(opts) {
+function insertDriveFile(opts?) {
 	return db.get('driveFiles.files').insert({
 		length: opts.datasize,
 		filename: 'strawberry-pasta.png',
@@ -1180,15 +1166,15 @@ function insertDriveFile(opts) {
 	});
 }
 
-function insertDriveFolder(opts) {
-	return db.get('driveFolders').insert(deepAssign({
+function insertDriveFolder(opts?) {
+	return db.get('driveFolders').insert(merge({
 		name: 'my folder',
 		parentId: null
 	}, opts));
 }
 
-function insertApp(opts) {
-	return db.get('apps').insert(deepAssign({
+function insertApp(opts?) {
+	return db.get('apps').insert(merge({
 		name: 'my app',
 		secret: 'mysecret'
 	}, opts));
diff --git a/test/text.js b/test/text.ts
similarity index 100%
rename from test/text.js
rename to test/text.ts

From 433a93b77244db4da9912349c2050768cd43be62 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 3 Apr 2018 13:05:52 +0900
Subject: [PATCH 1048/1250] Clean up: Remove unused files

---
 .travis/.gitignore-release | 10 ----------
 .travis/release.sh         | 17 -----------------
 .travis/shapeup.js         | 13 -------------
 .travis/travis_rsa.enc     |  3 ---
 4 files changed, 43 deletions(-)
 delete mode 100644 .travis/.gitignore-release
 delete mode 100644 .travis/release.sh
 delete mode 100644 .travis/shapeup.js
 delete mode 100644 .travis/travis_rsa.enc

diff --git a/.travis/.gitignore-release b/.travis/.gitignore-release
deleted file mode 100644
index ae1157b33..000000000
--- a/.travis/.gitignore-release
+++ /dev/null
@@ -1,10 +0,0 @@
-# Realizing whitelist by excluding everything and specifying exceptions.
-
-/*
-
-!/built
-!/tools
-!/elasticsearch
-!/package.json
-!/.travis.yml
-!/appveyor.yml
diff --git a/.travis/release.sh b/.travis/release.sh
deleted file mode 100644
index 5def2ab03..000000000
--- a/.travis/release.sh
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/bin/bash
-
-echo "Starting releasing task"
-openssl aes-256-cbc -K $encrypted_ceda82069128_key -iv $encrypted_ceda82069128_iv -in ./.travis/travis_rsa.enc -out travis_rsa -d
-cp travis_rsa ~/.ssh/id_rsa
-chmod 600 ~/.ssh/id_rsa
-echo -e "Host github.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config
-git checkout -b release
-cp -f ./.travis/.gitignore-release .gitignore
-node ./.travis/shapeup.js
-git add --all
-git rm --cached `git ls-files --full-name -i --exclude-standard`
-git config --global user.email "AyaMorisawa4869@gmail.com"
-git config --global user.name "Aya Morisawa"
-git commit -m "Release build for $TRAVIS_COMMIT"
-git push -f git@github.com:syuilo/misskey release
-echo "Finished releasing task"
diff --git a/.travis/shapeup.js b/.travis/shapeup.js
deleted file mode 100644
index 9a5d85a18..000000000
--- a/.travis/shapeup.js
+++ /dev/null
@@ -1,13 +0,0 @@
-'use strict'
-
-const fs = require('fs')
-const filename = process.argv[2] || 'package.json'
-
-fs.readFile(filename, (err, data) => {
-    if (err) process.exit(2)
-    const object = JSON.parse(data)
-    delete object.devDependencies
-    fs.writeFile(filename, JSON.stringify(object, null, '\t') + '\n', err => {
-        if (err) process.exit(3)
-    })
-})
diff --git a/.travis/travis_rsa.enc b/.travis/travis_rsa.enc
deleted file mode 100644
index dd7a81f08..000000000
--- a/.travis/travis_rsa.enc
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:b93bfbfccbcda293382730b5b9cc5953e3564e80d27027a2f0dd624f42b1b0a3
-size 1680

From 6e5e1611fa2aa5c5a599dc3bdf2722e04e45fe5d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 3 Apr 2018 13:08:21 +0900
Subject: [PATCH 1049/1250] Clean up

---
 src/client/assets/recover.html | 36 ----------------------------------
 src/server/web/index.ts        |  2 --
 2 files changed, 38 deletions(-)
 delete mode 100644 src/client/assets/recover.html

diff --git a/src/client/assets/recover.html b/src/client/assets/recover.html
deleted file mode 100644
index 4922b68d3..000000000
--- a/src/client/assets/recover.html
+++ /dev/null
@@ -1,36 +0,0 @@
-<!DOCTYPE html>
-
-<html>
-	<head>
-		<meta charset="utf-8">
-		<title>Misskeyのリカバリ</title>
-		<script>
-
-			const yn = window.confirm('キャッシュをクリアしますか?(他のタブでMisskeyを開いている状態だと正常にクリアできないので、他のMisskeyのタブをすべて閉じてから行ってください)\n\nDo you want to clear caches?');
-
-			if (yn) {
-				try {
-					navigator.serviceWorker.controller.postMessage('clear');
-
-					navigator.serviceWorker.getRegistrations().then(registrations => {
-						registrations.forEach(registration => registration.unregister());
-					});
-
-				} catch (e) {
-					console.error(e);
-				}
-
-				alert('キャッシュをクリアしました。');
-
-				alert('まもなくページを再度読み込みします。再度読み込みが終わると、再度キャッシュをクリアするか尋ねられるので、「キャンセル」を選択して抜けてください。');
-
-				setTimeout(() => {
-					location.reload(true);
-				}, 100);
-			} else {
-				location.href = '/';
-			}
-
-		</script>
-	</head>
-</html>
diff --git a/src/server/web/index.ts b/src/server/web/index.ts
index 445f03de1..1445d1aef 100644
--- a/src/server/web/index.ts
+++ b/src/server/web/index.ts
@@ -42,8 +42,6 @@ app.use('/assets', (req, res) => {
 	res.sendStatus(404);
 });
 
-app.use('/recover', (req, res) => res.sendFile(`${client}/assets/recover.html`));
-
 // ServiceWroker
 app.get(/^\/sw\.(.+?)\.js$/, (req, res) =>
 	res.sendFile(`${client}/assets/sw.${req.params[0]}.js`));

From c2a8b684d03876edd002f4b6b81758b07d92130c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 3 Apr 2018 15:32:53 +0900
Subject: [PATCH 1050/1250] Test: Remove needless test

---
 test/api.ts | 18 ------------------
 1 file changed, 18 deletions(-)

diff --git a/test/api.ts b/test/api.ts
index c794869e4..953c5aea0 100644
--- a/test/api.ts
+++ b/test/api.ts
@@ -669,24 +669,6 @@ describe('API', () => {
 			res.should.have.status(204);
 		}));
 
-		it('過去にフォロー歴があった状態でフォロー解除できる', async(async () => {
-			const hima = await insertHimawari();
-			const me = await insertSakurako();
-			await db.get('following').insert({
-				followeeId: hima._id,
-				followerId: me._id,
-				deletedAt: new Date()
-			});
-			await db.get('following').insert({
-				followeeId: hima._id,
-				followerId: me._id
-			});
-			const res = await request('/following/delete', {
-				userId: hima._id.toString()
-			}, me);
-			res.should.have.status(204);
-		}));
-
 		it('フォローしていない場合は怒る', async(async () => {
 			const hima = await insertHimawari();
 			const me = await insertSakurako();

From 54e128df4f1a2c79cd79a5a1986823c6bd649bcc Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Tue, 3 Apr 2018 16:32:54 +0900
Subject: [PATCH 1051/1250] Fix ActivityStreams collection resolution

---
 src/remote/activitypub/resolver.ts | 35 ++++++++++++++++--------------
 1 file changed, 19 insertions(+), 16 deletions(-)

diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts
index ebfe25fe7..b7e431b91 100644
--- a/src/remote/activitypub/resolver.ts
+++ b/src/remote/activitypub/resolver.ts
@@ -43,32 +43,35 @@ export default class Resolver {
 	}
 
 	private async resolveCollection(value) {
-		if (Array.isArray(value)) {
-			return value;
-		}
-
 		const resolved = typeof value === 'string' ?
 			await this.resolveUnrequestedOne(value) :
-			value;
+			{ resolver: this, object: value };
 
-		switch (resolved.type) {
+		switch (resolved.object.type) {
 		case 'Collection':
-			return resolved.items;
+			resolved.object = resolved.object.items;
+			break;
 
 		case 'OrderedCollection':
-			return resolved.orderedItems;
+			resolved.object = resolved.object.orderedItems;
+			break;
 
 		default:
-			return [resolved];
+			if (!Array.isArray(value)) {
+				resolved.object = [resolved.object];
+			}
+			break;
 		}
+
+		return resolved;
 	}
 
 	public async resolve(value): Promise<Array<Promise<IResult>>> {
-		const collection = await this.resolveCollection(value);
+		const { resolver, object } = await this.resolveCollection(value);
 
-		return collection
-			.filter(element => !this.requesting.has(element))
-			.map(this.resolveUnrequestedOne.bind(this));
+		return object
+			.filter(element => !resolver.requesting.has(element))
+			.map(resolver.resolveUnrequestedOne.bind(resolver));
 	}
 
 	public resolveOne(value) {
@@ -80,9 +83,9 @@ export default class Resolver {
 	}
 
 	public async resolveRemoteUserObjects(value) {
-		const collection = await this.resolveCollection(value);
+		const { resolver, object } = await this.resolveCollection(value);
 
-		return collection.filter(element => !this.requesting.has(element)).map(element => {
+		return object.filter(element => !resolver.requesting.has(element)).map(element => {
 			if (typeof element === 'string') {
 				const object = RemoteUserObject.findOne({ uri: element });
 
@@ -91,7 +94,7 @@ export default class Resolver {
 				}
 			}
 
-			return this.resolveUnrequestedOne(element);
+			return resolver.resolveUnrequestedOne(element);
 		});
 	}
 }

From bd935c56967a7f2aeec7edf00f5d95318c46f9da Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Tue, 3 Apr 2018 16:33:16 +0900
Subject: [PATCH 1052/1250] Implement unfollow by remote account

---
 src/processor/http/perform-activitypub.ts   |  3 +-
 src/processor/http/process-inbox.ts         |  3 +-
 src/remote/activitypub/act/follow.ts        | 73 +++++++++++++--------
 src/remote/activitypub/act/index.ts         | 19 +++---
 src/remote/activitypub/act/undo/index.ts    | 23 +++++++
 src/remote/activitypub/act/undo/unfollow.ts | 24 +++++++
 6 files changed, 108 insertions(+), 37 deletions(-)
 create mode 100644 src/remote/activitypub/act/undo/index.ts
 create mode 100644 src/remote/activitypub/act/undo/unfollow.ts

diff --git a/src/processor/http/perform-activitypub.ts b/src/processor/http/perform-activitypub.ts
index adf4e65a7..4fdbb901d 100644
--- a/src/processor/http/perform-activitypub.ts
+++ b/src/processor/http/perform-activitypub.ts
@@ -1,5 +1,6 @@
 import User from '../../models/user';
 import act from '../../remote/activitypub/act';
+import Resolver from '../../remote/activitypub/resolver';
 
 export default ({ data }) => User.findOne({ _id: data.actor })
-	.then(actor => act(actor, data.outbox, false));
+	.then(actor => act(new Resolver(), actor, data.outbox));
diff --git a/src/processor/http/process-inbox.ts b/src/processor/http/process-inbox.ts
index 11801409c..e75c0b5c5 100644
--- a/src/processor/http/process-inbox.ts
+++ b/src/processor/http/process-inbox.ts
@@ -3,6 +3,7 @@ import parseAcct from '../../acct/parse';
 import User, { IRemoteUser } from '../../models/user';
 import act from '../../remote/activitypub/act';
 import resolvePerson from '../../remote/activitypub/resolve-person';
+import Resolver from '../../remote/activitypub/resolver';
 
 export default async ({ data }) => {
 	const keyIdLower = data.signature.keyId.toLowerCase();
@@ -34,5 +35,5 @@ export default async ({ data }) => {
 		throw 'signature verification failed';
 	}
 
-	await act(user, data.inbox, true);
+	await act(new Resolver(), user, data.inbox, true);
 };
diff --git a/src/remote/activitypub/act/follow.ts b/src/remote/activitypub/act/follow.ts
index d7ea15fa3..ec00b78bf 100644
--- a/src/remote/activitypub/act/follow.ts
+++ b/src/remote/activitypub/act/follow.ts
@@ -1,6 +1,6 @@
 import { MongoError } from 'mongodb';
 import parseAcct from '../../../acct/parse';
-import Following from '../../../models/following';
+import Following, { IFollowing } from '../../../models/following';
 import User from '../../../models/user';
 import config from '../../../config';
 import queue from '../../../queue';
@@ -8,7 +8,7 @@ import context from '../renderer/context';
 import renderAccept from '../renderer/accept';
 import request from '../../request';
 
-export default async (actor, activity) => {
+export default async (resolver, actor, activity, distribute) => {
 	const prefix = config.url + '/@';
 	const id = activity.object.id || activity.object;
 
@@ -26,33 +26,52 @@ export default async (actor, activity) => {
 		throw new Error();
 	}
 
+	if (!distribute) {
+		const { _id } = await Following.findOne({
+			followerId: actor._id,
+			followeeId: followee._id
+		})
+
+		return {
+			resolver,
+			object: { $ref: 'following', $id: _id }
+		};
+	}
+
+	const promisedFollowing = Following.insert({
+		createdAt: new Date(),
+		followerId: actor._id,
+		followeeId: followee._id
+	}).then(following => new Promise((resolve, reject) => {
+		queue.create('http', {
+			type: 'follow',
+			following: following._id
+		}).save(error => {
+			if (error) {
+				reject(error);
+			} else {
+				resolve(following);
+			}
+		});
+	}) as Promise<IFollowing>, async error => {
+		// duplicate key error
+		if (error instanceof MongoError && error.code === 11000) {
+			return Following.findOne({
+				followerId: actor._id,
+				followeeId: followee._id
+			});
+		}
+
+		throw error;
+	});
+
 	const accept = renderAccept(activity);
 	accept['@context'] = context;
 
-	await Promise.all([
-		request(followee, actor.account.inbox, accept),
+	await request(followee, actor.account.inbox, accept);
 
-		Following.insert({
-			createdAt: new Date(),
-			followerId: actor._id,
-			followeeId: followee._id
-		}).then(following => new Promise((resolve, reject) => {
-			queue.create('http', { type: 'follow', following: following._id }).save(error => {
-				if (error) {
-					reject(error);
-				} else {
-					resolve();
-				}
-			});
-		}), error => {
-			// duplicate key error
-			if (error instanceof MongoError && error.code === 11000) {
-				return;
-			}
-
-			throw error;
-		})
-	]);
-
-	return null;
+	return promisedFollowing.then(({ _id }) => ({
+		resolver,
+		object: { $ref: 'following', $id: _id }
+	}));
 };
diff --git a/src/remote/activitypub/act/index.ts b/src/remote/activitypub/act/index.ts
index 24320dcb1..3b7dd5b24 100644
--- a/src/remote/activitypub/act/index.ts
+++ b/src/remote/activitypub/act/index.ts
@@ -1,23 +1,26 @@
 import create from './create';
 import follow from './follow';
+import undo from './undo';
 import createObject from '../create';
-import Resolver from '../resolver';
 
-export default (actor, value, distribute) => {
-	return new Resolver().resolve(value).then(resolved => Promise.all(resolved.map(async promisedResult => {
-		const { resolver, object } = await promisedResult;
-		const created = await (await createObject(resolver, actor, [object], distribute))[0];
+export default (resolver, actor, value, distribute?: boolean) => {
+	return resolver.resolve(value).then(resolved => Promise.all(resolved.map(async promisedResult => {
+		const result = await promisedResult;
+		const created = await (await createObject(result.resolver, actor, [result.object], distribute))[0];
 
 		if (created !== null) {
 			return created;
 		}
 
-		switch (object.type) {
+		switch (result.object.type) {
 		case 'Create':
-			return create(resolver, actor, object, distribute);
+			return create(result.resolver, actor, result.object, distribute);
 
 		case 'Follow':
-			return follow(actor, object);
+			return follow(result.resolver, actor, result.object, distribute);
+
+		case 'Undo':
+			return undo(result.resolver, actor, result.object);
 
 		default:
 			return null;
diff --git a/src/remote/activitypub/act/undo/index.ts b/src/remote/activitypub/act/undo/index.ts
new file mode 100644
index 000000000..b43ae8617
--- /dev/null
+++ b/src/remote/activitypub/act/undo/index.ts
@@ -0,0 +1,23 @@
+import act from '../../act';
+import unfollow from './unfollow';
+
+export default async (resolver, actor, activity) => {
+	if ('actor' in activity && actor.account.uri !== activity.actor) {
+		throw new Error();
+	}
+
+	const results = await act(resolver, actor, activity.object);
+
+	await Promise.all(results.map(async result => {
+		if (result === null) {
+			return;
+		}
+
+		switch (result.object.$ref) {
+		case 'following':
+			await unfollow(result.resolver, result.object);
+		}
+	}));
+
+	return null;
+};
diff --git a/src/remote/activitypub/act/undo/unfollow.ts b/src/remote/activitypub/act/undo/unfollow.ts
new file mode 100644
index 000000000..0523699bf
--- /dev/null
+++ b/src/remote/activitypub/act/undo/unfollow.ts
@@ -0,0 +1,24 @@
+import FollowedLog from '../../../../models/followed-log';
+import Following from '../../../../models/following';
+import FollowingLog from '../../../../models/following-log';
+import User from '../../../../models/user';
+
+export default async (resolver, { $id }) => {
+	const following = await Following.findOneAndDelete({ _id: $id });
+	if (following === null) {
+		return;
+	}
+
+	await Promise.all([
+		User.update({ _id: following.followerId }, { $inc: { followingCount: -1 } }),
+		User.findOne({ _id: following.followerId }).then(({ followingCount }) => FollowingLog.insert({
+			userId: following.followerId,
+			count: followingCount - 1
+		})),
+		User.update({ _id: following.followeeId }, { $inc: { followersCount: -1 } }),
+		User.findOne({ _id: following.followeeId }).then(({ followersCount }) => FollowedLog.insert({
+			userId: following.followeeId,
+			count: followersCount - 1
+		})),
+	]);
+};

From 749f0fd6cb37d0ffb32c6f9c24ce43cd44602e84 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Tue, 3 Apr 2018 17:18:06 +0900
Subject: [PATCH 1053/1250] Implement remote account unfollow

---
 src/processor/http/index.ts                  |  2 +
 src/processor/http/unfollow.ts               | 54 ++++++++++++++++++++
 src/remote/activitypub/act/undo/index.ts     |  2 +-
 src/remote/activitypub/act/undo/unfollow.ts  | 33 ++++--------
 src/remote/activitypub/renderer/undo.ts      |  4 ++
 src/server/api/endpoints/following/delete.ts | 33 ++++--------
 6 files changed, 81 insertions(+), 47 deletions(-)
 create mode 100644 src/processor/http/unfollow.ts
 create mode 100644 src/remote/activitypub/renderer/undo.ts

diff --git a/src/processor/http/index.ts b/src/processor/http/index.ts
index 0301b472c..8f9aa717c 100644
--- a/src/processor/http/index.ts
+++ b/src/processor/http/index.ts
@@ -3,6 +3,7 @@ import follow from './follow';
 import performActivityPub from './perform-activitypub';
 import processInbox from './process-inbox';
 import reportGitHubFailure from './report-github-failure';
+import unfollow from './unfollow';
 
 const handlers = {
   deliverPost,
@@ -10,6 +11,7 @@ const handlers = {
   performActivityPub,
   processInbox,
   reportGitHubFailure,
+  unfollow
 };
 
 export default (job, done) => handlers[job.data.type](job).then(() => done(), done);
diff --git a/src/processor/http/unfollow.ts b/src/processor/http/unfollow.ts
new file mode 100644
index 000000000..4a8f03391
--- /dev/null
+++ b/src/processor/http/unfollow.ts
@@ -0,0 +1,54 @@
+import FollowedLog from '../../models/followed-log';
+import Following from '../../models/following';
+import FollowingLog from '../../models/following-log';
+import User, { isRemoteUser, pack as packUser } from '../../models/user';
+import stream from '../../publishers/stream';
+import renderFollow from '../../remote/activitypub/renderer/follow';
+import renderUndo from '../../remote/activitypub/renderer/undo';
+import context from '../../remote/activitypub/renderer/context';
+import request from '../../remote/request';
+
+export default async ({ data }) => {
+	// Delete following
+	const following = await Following.findOneAndDelete({ _id: data.id });
+	if (following === null) {
+		return;
+	}
+
+	const promisedFollower = User.findOne({ _id: following.followerId });
+	const promisedFollowee = User.findOne({ _id: following.followeeId });
+
+	await Promise.all([
+		// Decrement following count
+		User.update({ _id: following.followerId }, { $inc: { followingCount: -1 } }),
+		promisedFollower.then(({ followingCount }) => FollowingLog.insert({
+			userId: following.followerId,
+			count: followingCount - 1
+		})),
+
+		// Decrement followers count
+		User.update({ _id: following.followeeId }, { $inc: { followersCount: -1 } }),
+		promisedFollowee.then(({ followersCount }) => FollowedLog.insert({
+			userId: following.followeeId,
+			count: followersCount - 1
+		})),
+
+		// Publish follow event
+		Promise.all([promisedFollower, promisedFollowee]).then(async ([follower, followee]) => {
+			if (isRemoteUser(follower)) {
+				return;
+			}
+
+			const promisedPackedUser = packUser(followee, follower);
+
+			if (isRemoteUser(followee)) {
+				const undo = renderUndo(renderFollow(follower, followee));
+				undo['@context'] = context;
+
+				await request(follower, followee.account.inbox, undo);
+			}
+
+			stream(follower._id, 'unfollow', promisedPackedUser);
+		})
+	]);
+};
diff --git a/src/remote/activitypub/act/undo/index.ts b/src/remote/activitypub/act/undo/index.ts
index b43ae8617..c34d56e70 100644
--- a/src/remote/activitypub/act/undo/index.ts
+++ b/src/remote/activitypub/act/undo/index.ts
@@ -15,7 +15,7 @@ export default async (resolver, actor, activity) => {
 
 		switch (result.object.$ref) {
 		case 'following':
-			await unfollow(result.resolver, result.object);
+			await unfollow(result.object);
 		}
 	}));
 
diff --git a/src/remote/activitypub/act/undo/unfollow.ts b/src/remote/activitypub/act/undo/unfollow.ts
index 0523699bf..c17e06e8a 100644
--- a/src/remote/activitypub/act/undo/unfollow.ts
+++ b/src/remote/activitypub/act/undo/unfollow.ts
@@ -1,24 +1,11 @@
-import FollowedLog from '../../../../models/followed-log';
-import Following from '../../../../models/following';
-import FollowingLog from '../../../../models/following-log';
-import User from '../../../../models/user';
+import queue from '../../../../queue';
 
-export default async (resolver, { $id }) => {
-	const following = await Following.findOneAndDelete({ _id: $id });
-	if (following === null) {
-		return;
-	}
-
-	await Promise.all([
-		User.update({ _id: following.followerId }, { $inc: { followingCount: -1 } }),
-		User.findOne({ _id: following.followerId }).then(({ followingCount }) => FollowingLog.insert({
-			userId: following.followerId,
-			count: followingCount - 1
-		})),
-		User.update({ _id: following.followeeId }, { $inc: { followersCount: -1 } }),
-		User.findOne({ _id: following.followeeId }).then(({ followersCount }) => FollowedLog.insert({
-			userId: following.followeeId,
-			count: followersCount - 1
-		})),
-	]);
-};
+export default ({ $id }) => new Promise((resolve, reject) => {
+	queue.create('http', { type: 'unfollow', id: $id }).save(error => {
+		if (error) {
+			reject(error);
+		} else {
+			resolve();
+		}
+	});
+});
diff --git a/src/remote/activitypub/renderer/undo.ts b/src/remote/activitypub/renderer/undo.ts
new file mode 100644
index 000000000..f38e224b6
--- /dev/null
+++ b/src/remote/activitypub/renderer/undo.ts
@@ -0,0 +1,4 @@
+export default object => ({
+	type: 'Undo',
+	object
+});
diff --git a/src/server/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts
index 5deddc919..bf21bf0cb 100644
--- a/src/server/api/endpoints/following/delete.ts
+++ b/src/server/api/endpoints/following/delete.ts
@@ -2,9 +2,9 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import User, { pack as packUser } from '../../../../models/user';
+import User from '../../../../models/user';
 import Following from '../../../../models/following';
-import event from '../../../../publishers/stream';
+import queue from '../../../../queue';
 
 /**
  * Unfollow a user
@@ -49,28 +49,15 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		return rej('already not following');
 	}
 
-	// Delete following
-	await Following.findOneAndDelete({
-		_id: exist._id
-	});
-
-	// Send response
-	res();
-
-	// Decrement following count
-	User.update({ _id: follower._id }, {
-		$inc: {
-			followingCount: -1
+	queue.create('http', {
+		type: 'unfollow',
+		id: exist._id
+	}).save(error => {
+		if (error) {
+			return rej('unfollow failed');
 		}
-	});
 
-	// Decrement followers count
-	User.update({ _id: followee._id }, {
-		$inc: {
-			followersCount: -1
-		}
+		// Send response
+		res();
 	});
-
-	// Publish follow event
-	event(follower._id, 'unfollow', await packUser(followee, follower));
 });

From 99ef1512c5ef462663563d674f533524e701266a Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Tue, 3 Apr 2018 17:21:59 +0900
Subject: [PATCH 1054/1250] Add a missing semicolon

---
 src/remote/activitypub/act/follow.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/remote/activitypub/act/follow.ts b/src/remote/activitypub/act/follow.ts
index ec00b78bf..385fc58ef 100644
--- a/src/remote/activitypub/act/follow.ts
+++ b/src/remote/activitypub/act/follow.ts
@@ -30,7 +30,7 @@ export default async (resolver, actor, activity, distribute) => {
 		const { _id } = await Following.findOne({
 			followerId: actor._id,
 			followeeId: followee._id
-		})
+		});
 
 		return {
 			resolver,

From 4a1c484b7e3800b90f02aaddefc71563d4f63792 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 3 Apr 2018 17:24:28 +0900
Subject: [PATCH 1055/1250] Add createdAt property

---
 src/processor/http/unfollow.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/processor/http/unfollow.ts b/src/processor/http/unfollow.ts
index 4a8f03391..d3d5f2246 100644
--- a/src/processor/http/unfollow.ts
+++ b/src/processor/http/unfollow.ts
@@ -22,6 +22,7 @@ export default async ({ data }) => {
 		// Decrement following count
 		User.update({ _id: following.followerId }, { $inc: { followingCount: -1 } }),
 		promisedFollower.then(({ followingCount }) => FollowingLog.insert({
+			createdAt: new Date(),
 			userId: following.followerId,
 			count: followingCount - 1
 		})),
@@ -29,6 +30,7 @@ export default async ({ data }) => {
 		// Decrement followers count
 		User.update({ _id: following.followeeId }, { $inc: { followersCount: -1 } }),
 		promisedFollowee.then(({ followersCount }) => FollowedLog.insert({
+			createdAt: new Date(),
 			userId: following.followeeId,
 			count: followersCount - 1
 		})),

From 78b6f355ad176ee1d31b8472b681b5ca5a057278 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 3 Apr 2018 17:46:09 +0900
Subject: [PATCH 1056/1250] Implement visibility param

---
 src/server/api/endpoints/posts/create.ts | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts
index ccd617545..03af7ee76 100644
--- a/src/server/api/endpoints/posts/create.ts
+++ b/src/server/api/endpoints/posts/create.ts
@@ -23,6 +23,10 @@ import distribute from '../../../../post/distribute';
  * @return {Promise<any>}
  */
 module.exports = (params, user: ILocalUser, app) => new Promise(async (res, rej) => {
+	// Get 'visibility' parameter
+	const [visibility = 'public', visibilityErr] = $(params.visibility).optional.string().or(['public', 'unlisted', 'private', 'direct']).$;
+	if (visibilityErr) return rej('invalid visibility');
+
 	// Get 'text' parameter
 	const [text, textErr] = $(params.text).optional.string().pipe(isValidText).$;
 	if (textErr) return rej('invalid text');
@@ -280,6 +284,7 @@ module.exports = (params, user: ILocalUser, app) => new Promise(async (res, rej)
 		userId: user._id,
 		appId: app ? app._id : null,
 		viaMobile: viaMobile,
+		visibility,
 		geo
 	}, reply, repost, atMentions);
 

From e8e1b8fc58ab367f848ab86d83f0566023053c32 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 3 Apr 2018 20:09:26 +0900
Subject: [PATCH 1057/1250] Refactor

---
 src/remote/activitypub/act/create.ts     | 3 ++-
 src/remote/activitypub/act/follow.ts     | 3 ++-
 src/remote/activitypub/act/index.ts      | 3 ++-
 src/remote/activitypub/act/undo/index.ts | 3 ++-
 4 files changed, 8 insertions(+), 4 deletions(-)

diff --git a/src/remote/activitypub/act/create.ts b/src/remote/activitypub/act/create.ts
index a6ba9a1d2..fa681982c 100644
--- a/src/remote/activitypub/act/create.ts
+++ b/src/remote/activitypub/act/create.ts
@@ -1,6 +1,7 @@
 import create from '../create';
+import Resolver from '../resolver';
 
-export default (resolver, actor, activity, distribute) => {
+export default (resolver: Resolver, actor, activity, distribute) => {
 	if ('actor' in activity && actor.account.uri !== activity.actor) {
 		throw new Error();
 	}
diff --git a/src/remote/activitypub/act/follow.ts b/src/remote/activitypub/act/follow.ts
index 385fc58ef..23fa41df8 100644
--- a/src/remote/activitypub/act/follow.ts
+++ b/src/remote/activitypub/act/follow.ts
@@ -7,8 +7,9 @@ import queue from '../../../queue';
 import context from '../renderer/context';
 import renderAccept from '../renderer/accept';
 import request from '../../request';
+import Resolver from '../resolver';
 
-export default async (resolver, actor, activity, distribute) => {
+export default async (resolver: Resolver, actor, activity, distribute) => {
 	const prefix = config.url + '/@';
 	const id = activity.object.id || activity.object;
 
diff --git a/src/remote/activitypub/act/index.ts b/src/remote/activitypub/act/index.ts
index 3b7dd5b24..2af190221 100644
--- a/src/remote/activitypub/act/index.ts
+++ b/src/remote/activitypub/act/index.ts
@@ -2,8 +2,9 @@ import create from './create';
 import follow from './follow';
 import undo from './undo';
 import createObject from '../create';
+import Resolver from '../resolver';
 
-export default (resolver, actor, value, distribute?: boolean) => {
+export default (resolver: Resolver, actor, value, distribute?: boolean) => {
 	return resolver.resolve(value).then(resolved => Promise.all(resolved.map(async promisedResult => {
 		const result = await promisedResult;
 		const created = await (await createObject(result.resolver, actor, [result.object], distribute))[0];
diff --git a/src/remote/activitypub/act/undo/index.ts b/src/remote/activitypub/act/undo/index.ts
index c34d56e70..d104eeb80 100644
--- a/src/remote/activitypub/act/undo/index.ts
+++ b/src/remote/activitypub/act/undo/index.ts
@@ -1,7 +1,8 @@
 import act from '../../act';
 import unfollow from './unfollow';
+import Resolver from '../../resolver';
 
-export default async (resolver, actor, activity) => {
+export default async (resolver: Resolver, actor, activity) => {
 	if ('actor' in activity && actor.account.uri !== activity.actor) {
 		throw new Error();
 	}

From c1ca137547225a58fccbcbf653c8012af89affa1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 3 Apr 2018 20:13:04 +0900
Subject: [PATCH 1058/1250] Improve readability

---
 src/processor/http/deliver-post.ts | 25 +++++++++++--------------
 1 file changed, 11 insertions(+), 14 deletions(-)

diff --git a/src/processor/http/deliver-post.ts b/src/processor/http/deliver-post.ts
index 1389aede8..c00ab912c 100644
--- a/src/processor/http/deliver-post.ts
+++ b/src/processor/http/deliver-post.ts
@@ -25,21 +25,18 @@ export default ({ data }) => Post.findOne({ _id: data.id }).then(post => {
 				User.findOne({ _id: post.userId }),
 
 				// Fetch all followers
-				Following.aggregate([
-					{
-						$lookup: {
-							from: 'users',
-							localField: 'followerId',
-							foreignField: '_id',
-							as: 'follower'
-						}
-					},
-					{
-					$match: {
-							followeeId: post.userId
-						}
+				Following.aggregate([{
+					$lookup: {
+						from: 'users',
+						localField: 'followerId',
+						foreignField: '_id',
+						as: 'follower'
 					}
-				], {
+				}, {
+					$match: {
+						followeeId: post.userId
+					}
+				}], {
 					_id: false
 				})
 			]).then(([user, followers]) => Promise.all(followers.map(following => {

From 4c3c87fb99ca1ae673403793294ad4457e80b9c9 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Tue, 3 Apr 2018 20:39:27 +0900
Subject: [PATCH 1059/1250] Remove resolveRemoteUserObjects method of
 remote/activitypub/resolver

The value of the value returned by resolveRemoteUserObjects method of
remote/activitypub/resolver was inconsistent.
---
 src/processor/http/perform-activitypub.ts |  3 ++-
 src/processor/http/process-inbox.ts       |  2 +-
 src/remote/activitypub/act/index.ts       | 20 +++++++-------
 src/remote/activitypub/create.ts          | 16 ++++++++---
 src/remote/activitypub/resolver.ts        | 33 +----------------------
 5 files changed, 28 insertions(+), 46 deletions(-)

diff --git a/src/processor/http/perform-activitypub.ts b/src/processor/http/perform-activitypub.ts
index 4fdbb901d..963e532fe 100644
--- a/src/processor/http/perform-activitypub.ts
+++ b/src/processor/http/perform-activitypub.ts
@@ -3,4 +3,5 @@ import act from '../../remote/activitypub/act';
 import Resolver from '../../remote/activitypub/resolver';
 
 export default ({ data }) => User.findOne({ _id: data.actor })
-	.then(actor => act(new Resolver(), actor, data.outbox));
+	.then(actor => act(new Resolver(), actor, data.outbox))
+	.then(Promise.all);
diff --git a/src/processor/http/process-inbox.ts b/src/processor/http/process-inbox.ts
index e75c0b5c5..8ceb08257 100644
--- a/src/processor/http/process-inbox.ts
+++ b/src/processor/http/process-inbox.ts
@@ -35,5 +35,5 @@ export default async ({ data }) => {
 		throw 'signature verification failed';
 	}
 
-	await act(new Resolver(), user, data.inbox, true);
+	await Promise.all(await act(new Resolver(), user, data.inbox, true));
 };
diff --git a/src/remote/activitypub/act/index.ts b/src/remote/activitypub/act/index.ts
index 2af190221..030f1cf25 100644
--- a/src/remote/activitypub/act/index.ts
+++ b/src/remote/activitypub/act/index.ts
@@ -4,27 +4,29 @@ import undo from './undo';
 import createObject from '../create';
 import Resolver from '../resolver';
 
-export default (resolver: Resolver, actor, value, distribute?: boolean) => {
-	return resolver.resolve(value).then(resolved => Promise.all(resolved.map(async promisedResult => {
-		const result = await promisedResult;
-		const created = await (await createObject(result.resolver, actor, [result.object], distribute))[0];
+export default async (parentResolver: Resolver, actor, value, distribute?: boolean) => {
+	const collection = await parentResolver.resolveCollection(value);
+
+	return collection.object.map(async element => {
+		const { resolver, object } = await collection.resolver.resolveOne(element);
+		const created = await (await createObject(resolver, actor, [object], distribute))[0];
 
 		if (created !== null) {
 			return created;
 		}
 
-		switch (result.object.type) {
+		switch (object.type) {
 		case 'Create':
-			return create(result.resolver, actor, result.object, distribute);
+			return create(resolver, actor, object, distribute);
 
 		case 'Follow':
-			return follow(result.resolver, actor, result.object, distribute);
+			return follow(resolver, actor, object, distribute);
 
 		case 'Undo':
-			return undo(result.resolver, actor, result.object);
+			return undo(resolver, actor, object);
 
 		default:
 			return null;
 		}
-	})));
+	});
 };
diff --git a/src/remote/activitypub/create.ts b/src/remote/activitypub/create.ts
index f70f56b79..8f3e14629 100644
--- a/src/remote/activitypub/create.ts
+++ b/src/remote/activitypub/create.ts
@@ -93,9 +93,19 @@ class Creator {
 	}
 
 	public async create(parentResolver, value): Promise<Array<Promise<IRemoteUserObject>>> {
-		const results = await parentResolver.resolveRemoteUserObjects(value);
+		const collection = await parentResolver.resolveCollection(value);
+
+		return collection.object.map(async element => {
+			if (typeof element === 'string') {
+				const object = RemoteUserObject.findOne({ uri: element });
+
+				if (object !== null) {
+					return object;
+				}
+			}
+
+			const { resolver, object } = await collection.resolver.resolveOne(element);
 
-		return results.map(promisedResult => promisedResult.then(({ resolver, object }) => {
 			switch (object.type) {
 			case 'Image':
 				return this.createImage(object);
@@ -105,7 +115,7 @@ class Creator {
 			}
 
 			return null;
-		}));
+		});
 	}
 }
 
diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts
index b7e431b91..371ccdcc3 100644
--- a/src/remote/activitypub/resolver.ts
+++ b/src/remote/activitypub/resolver.ts
@@ -1,12 +1,5 @@
-import RemoteUserObject from '../../models/remote-user-object';
-import { IObject } from './type';
 const request = require('request-promise-native');
 
-type IResult = {
-  resolver: Resolver;
-  object: IObject;
-};
-
 export default class Resolver {
 	private requesting: Set<string>;
 
@@ -42,7 +35,7 @@ export default class Resolver {
 		return { resolver, object };
 	}
 
-	private async resolveCollection(value) {
+	public async resolveCollection(value) {
 		const resolved = typeof value === 'string' ?
 			await this.resolveUnrequestedOne(value) :
 			{ resolver: this, object: value };
@@ -66,14 +59,6 @@ export default class Resolver {
 		return resolved;
 	}
 
-	public async resolve(value): Promise<Array<Promise<IResult>>> {
-		const { resolver, object } = await this.resolveCollection(value);
-
-		return object
-			.filter(element => !resolver.requesting.has(element))
-			.map(resolver.resolveUnrequestedOne.bind(resolver));
-	}
-
 	public resolveOne(value) {
 		if (this.requesting.has(value)) {
 			throw new Error();
@@ -81,20 +66,4 @@ export default class Resolver {
 
 		return this.resolveUnrequestedOne(value);
 	}
-
-	public async resolveRemoteUserObjects(value) {
-		const { resolver, object } = await this.resolveCollection(value);
-
-		return object.filter(element => !resolver.requesting.has(element)).map(element => {
-			if (typeof element === 'string') {
-				const object = RemoteUserObject.findOne({ uri: element });
-
-				if (object !== null) {
-					return object;
-				}
-			}
-
-			return resolver.resolveUnrequestedOne(element);
-		});
-	}
 }

From 562ae599f6625fc29d819a7fa70e1b1eb200d9e5 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Tue, 3 Apr 2018 22:19:11 +0900
Subject: [PATCH 1060/1250] Wait for promise to resolve in Undo activity
 handler

---
 src/remote/activitypub/act/undo/index.ts | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/remote/activitypub/act/undo/index.ts b/src/remote/activitypub/act/undo/index.ts
index d104eeb80..38d5bfe21 100644
--- a/src/remote/activitypub/act/undo/index.ts
+++ b/src/remote/activitypub/act/undo/index.ts
@@ -9,7 +9,9 @@ export default async (resolver: Resolver, actor, activity) => {
 
 	const results = await act(resolver, actor, activity.object);
 
-	await Promise.all(results.map(async result => {
+	await Promise.all(results.map(async promisedResult => {
+		const result = await promisedResult;
+
 		if (result === null) {
 			return;
 		}

From 889c9afbb5e72810a4947ed4abe4e210c96a7c22 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 3 Apr 2018 23:01:48 +0900
Subject: [PATCH 1061/1250] Refactor

---
 src/remote/activitypub/create.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/remote/activitypub/create.ts b/src/remote/activitypub/create.ts
index 8f3e14629..44fd66509 100644
--- a/src/remote/activitypub/create.ts
+++ b/src/remote/activitypub/create.ts
@@ -36,7 +36,7 @@ class Creator {
 		return createRemoteUserObject('driveFiles.files', _id, object);
 	}
 
-	private async createNote(resolver, object) {
+	private async createNote(resolver: Resolver, object) {
 		if ('attributedTo' in object && this.actor.account.uri !== object.attributedTo) {
 			throw new Error();
 		}
@@ -92,7 +92,7 @@ class Creator {
 		return promisedRemoteUserObject;
 	}
 
-	public async create(parentResolver, value): Promise<Array<Promise<IRemoteUserObject>>> {
+	public async create(parentResolver: Resolver, value): Promise<Array<Promise<IRemoteUserObject>>> {
 		const collection = await parentResolver.resolveCollection(value);
 
 		return collection.object.map(async element => {

From 5f0e25be6603d34ccc7da1db064daaf2b5a4bf16 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 3 Apr 2018 23:14:56 +0900
Subject: [PATCH 1062/1250] Refactor: Better variable name

---
 src/remote/activitypub/create.ts | 26 +++++++++++++-------------
 1 file changed, 13 insertions(+), 13 deletions(-)

diff --git a/src/remote/activitypub/create.ts b/src/remote/activitypub/create.ts
index 44fd66509..3676ba746 100644
--- a/src/remote/activitypub/create.ts
+++ b/src/remote/activitypub/create.ts
@@ -27,44 +27,44 @@ class Creator {
 		this.distribute = distribute;
 	}
 
-	private async createImage(object) {
-		if ('attributedTo' in object && this.actor.account.uri !== object.attributedTo) {
+	private async createImage(image) {
+		if ('attributedTo' in image && this.actor.account.uri !== image.attributedTo) {
 			throw new Error();
 		}
 
-		const { _id } = await uploadFromUrl(object.url, this.actor);
-		return createRemoteUserObject('driveFiles.files', _id, object);
+		const { _id } = await uploadFromUrl(image.url, this.actor);
+		return createRemoteUserObject('driveFiles.files', _id, image);
 	}
 
-	private async createNote(resolver: Resolver, object) {
-		if ('attributedTo' in object && this.actor.account.uri !== object.attributedTo) {
+	private async createNote(resolver: Resolver, note) {
+		if ('attributedTo' in note && this.actor.account.uri !== note.attributedTo) {
 			throw new Error();
 		}
 
-		const mediaIds = 'attachment' in object &&
-			(await Promise.all(await this.create(resolver, object.attachment)))
+		const mediaIds = 'attachment' in note &&
+			(await Promise.all(await this.create(resolver, note.attachment)))
 				.filter(media => media !== null && media.object.$ref === 'driveFiles.files')
 				.map(({ object }) => object.$id);
 
-		const { window } = new JSDOM(object.content);
+		const { window } = new JSDOM(note.content);
 
 		const inserted = await createPost({
 			channelId: undefined,
 			index: undefined,
-			createdAt: new Date(object.published),
+			createdAt: new Date(note.published),
 			mediaIds,
 			replyId: undefined,
 			repostId: undefined,
 			poll: undefined,
 			text: window.document.body.textContent,
-			textHtml: object.content && createDOMPurify(window).sanitize(object.content),
+			textHtml: note.content && createDOMPurify(window).sanitize(note.content),
 			userId: this.actor._id,
 			appId: null,
 			viaMobile: false,
 			geo: undefined
 		}, null, null, []);
 
-		const promisedRemoteUserObject = createRemoteUserObject('posts', inserted._id, object);
+		const promisedRemoteUserObject = createRemoteUserObject('posts', inserted._id, note);
 		const promises = [];
 
 		if (this.distribute) {
@@ -72,7 +72,7 @@ class Creator {
 		}
 
 		// Register to search database
-		if (object.content && config.elasticsearch.enable) {
+		if (note.content && config.elasticsearch.enable) {
 			const es = require('../../db/elasticsearch');
 
 			promises.push(new Promise((resolve, reject) => {

From 13f75b6f7db8eadff2dbbccdcbfe12e5a516ea9f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 3 Apr 2018 23:57:41 +0900
Subject: [PATCH 1063/1250] Refactor: Specify return type

---
 src/processor/http/process-inbox.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/processor/http/process-inbox.ts b/src/processor/http/process-inbox.ts
index 8ceb08257..f102f8d6b 100644
--- a/src/processor/http/process-inbox.ts
+++ b/src/processor/http/process-inbox.ts
@@ -5,7 +5,7 @@ import act from '../../remote/activitypub/act';
 import resolvePerson from '../../remote/activitypub/resolve-person';
 import Resolver from '../../remote/activitypub/resolver';
 
-export default async ({ data }) => {
+export default async ({ data }): Promise<void> => {
 	const keyIdLower = data.signature.keyId.toLowerCase();
 	let user;
 

From 5c2978d1f6bfaec587092c742689ae6ce6bafa1b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 3 Apr 2018 23:59:03 +0900
Subject: [PATCH 1064/1250] Refactor

---
 src/remote/activitypub/act/undo/index.ts | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/src/remote/activitypub/act/undo/index.ts b/src/remote/activitypub/act/undo/index.ts
index 38d5bfe21..11c7ec0c8 100644
--- a/src/remote/activitypub/act/undo/index.ts
+++ b/src/remote/activitypub/act/undo/index.ts
@@ -2,7 +2,7 @@ import act from '../../act';
 import unfollow from './unfollow';
 import Resolver from '../../resolver';
 
-export default async (resolver: Resolver, actor, activity) => {
+export default async (resolver: Resolver, actor, activity): Promise<void> => {
 	if ('actor' in activity && actor.account.uri !== activity.actor) {
 		throw new Error();
 	}
@@ -21,6 +21,4 @@ export default async (resolver: Resolver, actor, activity) => {
 			await unfollow(result.object);
 		}
 	}));
-
-	return null;
 };

From 6cdce579fa41b6e8b5dd6824cda73d7c82ee71ed Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Tue, 3 Apr 2018 23:45:13 +0900
Subject: [PATCH 1065/1250] Enforce URI uniquness

---
 src/drive/add-file.ts            | 15 +++++--
 src/drive/upload-from-url.ts     |  4 +-
 src/models/drive-file.ts         | 16 ++++---
 src/models/post.ts               |  3 ++
 src/models/remote-user-object.ts | 15 -------
 src/remote/activitypub/create.ts | 77 +++++++++++++++++++++++---------
 6 files changed, 82 insertions(+), 48 deletions(-)
 delete mode 100644 src/models/remote-user-object.ts

diff --git a/src/drive/add-file.ts b/src/drive/add-file.ts
index f48fada7e..24eb5208d 100644
--- a/src/drive/add-file.ts
+++ b/src/drive/add-file.ts
@@ -10,7 +10,7 @@ import * as debug from 'debug';
 import fileType = require('file-type');
 import prominence = require('prominence');
 
-import DriveFile, { getGridFSBucket } from '../models/drive-file';
+import DriveFile, { IMetadata, getGridFSBucket } from '../models/drive-file';
 import DriveFolder from '../models/drive-folder';
 import { pack } from '../models/drive-file';
 import event, { publishDriveStream } from '../publishers/stream';
@@ -45,7 +45,8 @@ const addFile = async (
 	name: string = null,
 	comment: string = null,
 	folderId: mongodb.ObjectID = null,
-	force: boolean = false
+	force: boolean = false,
+	uri: string = null
 ) => {
 	log(`registering ${name} (user: ${getAcct(user)}, path: ${path})`);
 
@@ -224,12 +225,18 @@ const addFile = async (
 		properties['avgColor'] = averageColor;
 	}
 
-	return addToGridFS(detectedName, readable, mime, {
+	const metadata = {
 		userId: user._id,
 		folderId: folder !== null ? folder._id : null,
 		comment: comment,
 		properties: properties
-	});
+	} as IMetadata;
+
+	if (uri !== null) {
+		metadata.uri = uri;
+	}
+
+	return addToGridFS(detectedName, readable, mime, metadata);
 };
 
 /**
diff --git a/src/drive/upload-from-url.ts b/src/drive/upload-from-url.ts
index 7ff16e9e4..f96af0f26 100644
--- a/src/drive/upload-from-url.ts
+++ b/src/drive/upload-from-url.ts
@@ -8,7 +8,7 @@ import * as request from 'request';
 
 const log = debug('misskey:common:drive:upload_from_url');
 
-export default async (url, user, folderId = null): Promise<IDriveFile> => {
+export default async (url, user, folderId = null, uri = null): Promise<IDriveFile> => {
 	let name = URL.parse(url).pathname.split('/').pop();
 	if (!validateFileName(name)) {
 		name = null;
@@ -35,7 +35,7 @@ export default async (url, user, folderId = null): Promise<IDriveFile> => {
 			.on('error', rej);
 	});
 
-	const driveFile = await create(user, path, name, null, folderId);
+	const driveFile = await create(user, path, name, null, folderId, false, uri);
 
 	// clean-up
 	fs.unlink(path, (e) => {
diff --git a/src/models/drive-file.ts b/src/models/drive-file.ts
index fba1aebda..c86570f0f 100644
--- a/src/models/drive-file.ts
+++ b/src/models/drive-file.ts
@@ -6,6 +6,8 @@ import monkDb, { nativeDbConn } from '../db/mongodb';
 
 const DriveFile = monkDb.get<IDriveFile>('driveFiles.files');
 
+DriveFile.createIndex('metadata.uri', { sparse: true, unique: true });
+
 export default DriveFile;
 
 const getGridFSBucket = async (): Promise<mongodb.GridFSBucket> => {
@@ -18,17 +20,21 @@ const getGridFSBucket = async (): Promise<mongodb.GridFSBucket> => {
 
 export { getGridFSBucket };
 
+export type IMetadata = {
+	properties: any;
+	userId: mongodb.ObjectID;
+	folderId: mongodb.ObjectID;
+	comment: string;
+	uri: string;
+};
+
 export type IDriveFile = {
 	_id: mongodb.ObjectID;
 	uploadDate: Date;
 	md5: string;
 	filename: string;
 	contentType: string;
-	metadata: {
-		properties: any;
-		userId: mongodb.ObjectID;
-		folderId: mongodb.ObjectID;
-	}
+	metadata: IMetadata;
 };
 
 export function validateFileName(name: string): boolean {
diff --git a/src/models/post.ts b/src/models/post.ts
index 798c18e4b..2f2b51b94 100644
--- a/src/models/post.ts
+++ b/src/models/post.ts
@@ -11,6 +11,8 @@ import { pack as packFile } from './drive-file';
 
 const Post = db.get<IPost>('posts');
 
+Post.createIndex('uri', { sparse: true, unique: true });
+
 export default Post;
 
 export function isValidText(text: string): boolean {
@@ -49,6 +51,7 @@ export type IPost = {
 		heading: number;
 		speed: number;
 	};
+	uri: string;
 };
 
 /**
diff --git a/src/models/remote-user-object.ts b/src/models/remote-user-object.ts
deleted file mode 100644
index fb5b337c9..000000000
--- a/src/models/remote-user-object.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import * as mongodb from 'mongodb';
-import db from '../db/mongodb';
-
-const RemoteUserObject = db.get<IRemoteUserObject>('remoteUserObjects');
-
-export default RemoteUserObject;
-
-export type IRemoteUserObject = {
-	_id: mongodb.ObjectID;
-	uri: string;
-	object: {
-		$ref: string;
-		$id: mongodb.ObjectID;
-	}
-};
diff --git a/src/remote/activitypub/create.ts b/src/remote/activitypub/create.ts
index 3676ba746..46e4c9988 100644
--- a/src/remote/activitypub/create.ts
+++ b/src/remote/activitypub/create.ts
@@ -1,6 +1,8 @@
 import { JSDOM } from 'jsdom';
+import { ObjectID } from 'mongodb';
 import config from '../../config';
-import RemoteUserObject, { IRemoteUserObject } from '../../models/remote-user-object';
+import DriveFile from '../../models/drive-file';
+import Post from '../../models/post';
 import { IRemoteUser } from '../../models/user';
 import uploadFromUrl from '../../drive/upload-from-url';
 import createPost from '../../post/create';
@@ -8,15 +10,13 @@ import distributePost from '../../post/distribute';
 import Resolver from './resolver';
 const createDOMPurify = require('dompurify');
 
-function createRemoteUserObject($ref, $id, { id }) {
-	const object = { $ref, $id };
-
-	if (!id) {
-		return { object };
-	}
-
-	return RemoteUserObject.insert({ uri: id, object });
-}
+type IResult = {
+	resolver: Resolver;
+	object: {
+		$ref: string;
+		$id: ObjectID;
+	};
+};
 
 class Creator {
 	private actor: IRemoteUser;
@@ -27,17 +27,23 @@ class Creator {
 		this.distribute = distribute;
 	}
 
-	private async createImage(image) {
+	private async createImage(resolver: Resolver, image) {
 		if ('attributedTo' in image && this.actor.account.uri !== image.attributedTo) {
 			throw new Error();
 		}
 
-		const { _id } = await uploadFromUrl(image.url, this.actor);
-		return createRemoteUserObject('driveFiles.files', _id, image);
+		const { _id } = await uploadFromUrl(image.url, this.actor, image.id || null);
+		return {
+			resolver,
+			object: { $ref: 'driveFiles.files', $id: _id }
+		};
 	}
 
 	private async createNote(resolver: Resolver, note) {
-		if ('attributedTo' in note && this.actor.account.uri !== note.attributedTo) {
+		if (
+			('attributedTo' in note && this.actor.account.uri !== note.attributedTo) ||
+			typeof note.id !== 'string'
+		) {
 			throw new Error();
 		}
 
@@ -61,10 +67,10 @@ class Creator {
 			userId: this.actor._id,
 			appId: null,
 			viaMobile: false,
-			geo: undefined
+			geo: undefined,
+			uri: note.id
 		}, null, null, []);
 
-		const promisedRemoteUserObject = createRemoteUserObject('posts', inserted._id, note);
 		const promises = [];
 
 		if (this.distribute) {
@@ -89,18 +95,45 @@ class Creator {
 
 		await Promise.all(promises);
 
-		return promisedRemoteUserObject;
+		return {
+			resolver,
+			object: { $ref: 'posts', id: inserted._id }
+		};
 	}
 
-	public async create(parentResolver: Resolver, value): Promise<Array<Promise<IRemoteUserObject>>> {
+	public async create(parentResolver: Resolver, value): Promise<Array<Promise<IResult>>> {
 		const collection = await parentResolver.resolveCollection(value);
 
 		return collection.object.map(async element => {
 			if (typeof element === 'string') {
-				const object = RemoteUserObject.findOne({ uri: element });
+				try {
+					await Promise.all([
+						DriveFile.findOne({ 'metadata.uri': element }).then(file => {
+							if (file === null) {
+								return;
+							}
 
-				if (object !== null) {
-					return object;
+							throw {
+								$ref: 'driveFile.files',
+								$id: file._id
+							};
+						}, () => {}),
+						Post.findOne({ uri: element }).then(post => {
+							if (post === null) {
+								return;
+							}
+
+							throw {
+								$ref: 'posts',
+								$id: post._id
+							};
+						}, () => {})
+					]);
+				} catch (object) {
+					return {
+						resolver: collection.resolver,
+						object
+					};
 				}
 			}
 
@@ -108,7 +141,7 @@ class Creator {
 
 			switch (object.type) {
 			case 'Image':
-				return this.createImage(object);
+				return this.createImage(resolver, object);
 
 			case 'Note':
 				return this.createNote(resolver, object);

From 7ca7c5e6be1a2d69c05331d634e83d63a1768800 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Wed, 4 Apr 2018 00:14:18 +0900
Subject: [PATCH 1066/1250] Always deduplicate Activity Streams objects by id

---
 src/remote/activitypub/create.ts | 56 ++++++++++++++++----------------
 1 file changed, 28 insertions(+), 28 deletions(-)

diff --git a/src/remote/activitypub/create.ts b/src/remote/activitypub/create.ts
index 46e4c9988..97c72860f 100644
--- a/src/remote/activitypub/create.ts
+++ b/src/remote/activitypub/create.ts
@@ -105,36 +105,36 @@ class Creator {
 		const collection = await parentResolver.resolveCollection(value);
 
 		return collection.object.map(async element => {
-			if (typeof element === 'string') {
-				try {
-					await Promise.all([
-						DriveFile.findOne({ 'metadata.uri': element }).then(file => {
-							if (file === null) {
-								return;
-							}
+			const uri = element.id || element;
 
-							throw {
-								$ref: 'driveFile.files',
-								$id: file._id
-							};
-						}, () => {}),
-						Post.findOne({ uri: element }).then(post => {
-							if (post === null) {
-								return;
-							}
+			try {
+				await Promise.all([
+					DriveFile.findOne({ 'metadata.uri': uri }).then(file => {
+						if (file === null) {
+							return;
+						}
 
-							throw {
-								$ref: 'posts',
-								$id: post._id
-							};
-						}, () => {})
-					]);
-				} catch (object) {
-					return {
-						resolver: collection.resolver,
-						object
-					};
-				}
+						throw {
+							$ref: 'driveFile.files',
+							$id: file._id
+						};
+					}, () => {}),
+					Post.findOne({ uri }).then(post => {
+						if (post === null) {
+							return;
+						}
+
+						throw {
+							$ref: 'posts',
+							$id: post._id
+						};
+					}, () => {})
+				]);
+			} catch (object) {
+				return {
+					resolver: collection.resolver,
+					object
+				};
 			}
 
 			const { resolver, object } = await collection.resolver.resolveOne(element);

From 6d07ce46dc69a6a88e40277cc7617096bce4a40e Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Tue, 3 Apr 2018 17:50:52 +0900
Subject: [PATCH 1067/1250] Implement Delete activity

---
 src/processor/db/delete-post-dependents.ts | 22 ++++++++++++++++++++
 src/processor/db/index.ts                  |  7 +++++++
 src/processor/index.ts                     | 23 +++++++++++++--------
 src/remote/activitypub/act/delete/index.ts | 24 ++++++++++++++++++++++
 src/remote/activitypub/act/delete/post.ts  | 10 +++++++++
 src/remote/activitypub/act/index.ts        |  4 ++++
 6 files changed, 81 insertions(+), 9 deletions(-)
 create mode 100644 src/processor/db/delete-post-dependents.ts
 create mode 100644 src/processor/db/index.ts
 create mode 100644 src/remote/activitypub/act/delete/index.ts
 create mode 100644 src/remote/activitypub/act/delete/post.ts

diff --git a/src/processor/db/delete-post-dependents.ts b/src/processor/db/delete-post-dependents.ts
new file mode 100644
index 000000000..879c41ec9
--- /dev/null
+++ b/src/processor/db/delete-post-dependents.ts
@@ -0,0 +1,22 @@
+import Favorite from '../../models/favorite';
+import Notification from '../../models/notification';
+import PollVote from '../../models/poll-vote';
+import PostReaction from '../../models/post-reaction';
+import PostWatching from '../../models/post-watching';
+import Post from '../../models/post';
+
+export default async ({ data }) => Promise.all([
+	Favorite.remove({ postId: data._id }),
+	Notification.remove({ postId: data._id }),
+	PollVote.remove({ postId: data._id }),
+	PostReaction.remove({ postId: data._id }),
+	PostWatching.remove({ postId: data._id }),
+	Post.find({ repostId: data._id }).then(reposts => Promise.all([
+		Notification.remove({
+			postId: {
+				$in: reposts.map(({ _id }) => _id)
+			}
+		}),
+		Post.remove({ repostId: data._id })
+	]))
+]);
diff --git a/src/processor/db/index.ts b/src/processor/db/index.ts
new file mode 100644
index 000000000..75838c099
--- /dev/null
+++ b/src/processor/db/index.ts
@@ -0,0 +1,7 @@
+import deletePostDependents from './delete-post-dependents';
+
+const handlers = {
+  deletePostDependents
+};
+
+export default (job, done) => handlers[job.data.type](job).then(() => done(), done);
diff --git a/src/processor/index.ts b/src/processor/index.ts
index cd271d372..172048dda 100644
--- a/src/processor/index.ts
+++ b/src/processor/index.ts
@@ -1,13 +1,18 @@
 import queue from '../queue';
+import db from './db';
 import http from './http';
 
-/*
-	256 is the default concurrency limit of Mozilla Firefox and Google
-	Chromium.
+export default () => {
+	queue.process('db', db);
 
-	a8af215e691f3a2205a3758d2d96e9d328e100ff - chromium/src.git - Git at Google
-	https://chromium.googlesource.com/chromium/src.git/+/a8af215e691f3a2205a3758d2d96e9d328e100ff
-	Network.http.max-connections - MozillaZine Knowledge Base
-	http://kb.mozillazine.org/Network.http.max-connections
-*/
-export default () => queue.process('http', 256, http);
+	/*
+		256 is the default concurrency limit of Mozilla Firefox and Google
+		Chromium.
+
+		a8af215e691f3a2205a3758d2d96e9d328e100ff - chromium/src.git - Git at Google
+		https://chromium.googlesource.com/chromium/src.git/+/a8af215e691f3a2205a3758d2d96e9d328e100ff
+		Network.http.max-connections - MozillaZine Knowledge Base
+		http://kb.mozillazine.org/Network.http.max-connections
+	*/
+	queue.process('http', 256, http);
+};
diff --git a/src/remote/activitypub/act/delete/index.ts b/src/remote/activitypub/act/delete/index.ts
new file mode 100644
index 000000000..eabf9a043
--- /dev/null
+++ b/src/remote/activitypub/act/delete/index.ts
@@ -0,0 +1,24 @@
+import create from '../../create';
+import deletePost from './post';
+
+export default async (resolver, actor, activity) => {
+	if ('actor' in activity && actor.account.uri !== activity.actor) {
+		throw new Error();
+	}
+
+	const results = await create(resolver, actor, activity.object);
+
+	await Promise.all(results.map(async promisedResult => {
+		const result = await promisedResult;
+		if (result === null) {
+			return;
+		}
+
+		switch (result.object.$ref) {
+		case 'posts':
+			await deletePost(result.object);
+		}
+	}));
+
+	return null;
+};
diff --git a/src/remote/activitypub/act/delete/post.ts b/src/remote/activitypub/act/delete/post.ts
new file mode 100644
index 000000000..1b748afe8
--- /dev/null
+++ b/src/remote/activitypub/act/delete/post.ts
@@ -0,0 +1,10 @@
+import Post from '../../../../models/post';
+import queue from '../../../../queue';
+
+export default ({ $id }) => Promise.all([
+	Post.findOneAndDelete({ _id: $id }),
+	new Promise((resolve, reject) => queue.create('db', {
+		type: 'deletePostDependents',
+		id: $id
+	}).delay(65536).save(error => error ? reject(error) : resolve()))
+]);
diff --git a/src/remote/activitypub/act/index.ts b/src/remote/activitypub/act/index.ts
index 030f1cf25..d282e1288 100644
--- a/src/remote/activitypub/act/index.ts
+++ b/src/remote/activitypub/act/index.ts
@@ -1,4 +1,5 @@
 import create from './create';
+import performDeleteActivity from './delete';
 import follow from './follow';
 import undo from './undo';
 import createObject from '../create';
@@ -19,6 +20,9 @@ export default async (parentResolver: Resolver, actor, value, distribute?: boole
 		case 'Create':
 			return create(resolver, actor, object, distribute);
 
+		case 'Delete':
+			return performDeleteActivity(resolver, actor, object);
+
 		case 'Follow':
 			return follow(resolver, actor, object, distribute);
 

From 5a11625e59a30e9e34a72791f77b258d64683a5a Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 3 Apr 2018 20:06:17 +0000
Subject: [PATCH 1068/1250] fix(package): update @types/node to version 9.6.2

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 365cf88c5..24952a3a2 100644
--- a/package.json
+++ b/package.json
@@ -65,7 +65,7 @@
 		"@types/morgan": "1.7.35",
 		"@types/ms": "0.7.30",
 		"@types/multer": "1.3.6",
-		"@types/node": "9.6.1",
+		"@types/node": "9.6.2",
 		"@types/nopt": "3.0.29",
 		"@types/proxy-addr": "2.0.0",
 		"@types/pug": "2.0.4",

From 0bccb816202b46ba3cc16a0284a325780731c6bc Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 3 Apr 2018 21:17:01 +0000
Subject: [PATCH 1069/1250] fix(package): update @types/inquirer to version
 0.0.41

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 365cf88c5..fd1ca16cf 100644
--- a/package.json
+++ b/package.json
@@ -52,7 +52,7 @@
 		"@types/gulp-replace": "0.0.31",
 		"@types/gulp-uglify": "3.0.5",
 		"@types/gulp-util": "3.0.34",
-		"@types/inquirer": "0.0.40",
+		"@types/inquirer": "0.0.41",
 		"@types/is-root": "1.0.0",
 		"@types/is-url": "1.2.28",
 		"@types/js-yaml": "3.11.1",

From b926a2c523302926c54e7df893b45bcea4e1e4fb Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 4 Apr 2018 08:43:28 +0000
Subject: [PATCH 1070/1250] fix(package): update element-ui to version 2.3.3

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 38d96117c..88637ee6b 100644
--- a/package.json
+++ b/package.json
@@ -106,7 +106,7 @@
 		"diskusage": "0.2.4",
 		"dompurify": "^1.0.3",
 		"elasticsearch": "14.2.2",
-		"element-ui": "2.3.2",
+		"element-ui": "2.3.3",
 		"emojilib": "2.2.12",
 		"escape-regexp": "0.0.1",
 		"eslint": "4.19.1",

From a3f8a48ddfe7091d6e32a06a9391027a91fbc468 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 4 Apr 2018 19:29:06 +0900
Subject: [PATCH 1071/1250] [wip] Implement like activity

---
 src/remote/activitypub/act/like.ts | 63 ++++++++++++++++++++++++++++++
 1 file changed, 63 insertions(+)
 create mode 100644 src/remote/activitypub/act/like.ts

diff --git a/src/remote/activitypub/act/like.ts b/src/remote/activitypub/act/like.ts
new file mode 100644
index 000000000..ea5324201
--- /dev/null
+++ b/src/remote/activitypub/act/like.ts
@@ -0,0 +1,63 @@
+import { MongoError } from 'mongodb';
+import Reaction, { IPostReaction } from '../../../models/post-reaction';
+import Post from '../../../models/post';
+import queue from '../../../queue';
+
+export default async (resolver, actor, activity, distribute) => {
+	const id = activity.object.id || activity.object;
+
+	// Transform:
+	// https://misskey.ex/@syuilo/xxxx to
+	// xxxx
+	const postId = id.split('/').pop();
+
+	const post = await Post.findOne({ _id: postId });
+	if (post === null) {
+		throw new Error();
+	}
+
+	if (!distribute) {
+		const { _id } = await Reaction.findOne({
+			userId: actor._id,
+			postId: post._id
+		});
+
+		return {
+			resolver,
+			object: { $ref: 'postPeactions', $id: _id }
+		};
+	}
+
+	const promisedReaction = Reaction.insert({
+		createdAt: new Date(),
+		userId: actor._id,
+		postId: post._id,
+		reaction: 'pudding'
+	}).then(reaction => new Promise<IPostReaction>((resolve, reject) => {
+		queue.create('http', {
+			type: 'reaction',
+			reactionId: reaction._id
+		}).save(error => {
+			if (error) {
+				reject(error);
+			} else {
+				resolve(reaction);
+			}
+		});
+	}), async error => {
+		// duplicate key error
+		if (error instanceof MongoError && error.code === 11000) {
+			return Reaction.findOne({
+				userId: actor._id,
+				postId: post._id
+			});
+		}
+
+		throw error;
+	});
+
+	return promisedReaction.then(({ _id }) => ({
+		resolver,
+		object: { $ref: 'postPeactions', $id: _id }
+	}));
+};

From cc45e0c3d4b90c6f6f6dd890b02b37e2d586ad1c Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Wed, 4 Apr 2018 19:51:41 +0900
Subject: [PATCH 1072/1250] Allow to undo Create activity

---
 .../activitypub/act/{delete/index.ts => delete.ts}  |  9 +++------
 src/remote/activitypub/act/delete/post.ts           | 10 ----------
 src/remote/activitypub/act/undo/index.ts            |  5 ++++-
 src/remote/activitypub/delete/index.ts              | 10 ++++++++++
 src/remote/activitypub/delete/post.ts               | 13 +++++++++++++
 5 files changed, 30 insertions(+), 17 deletions(-)
 rename src/remote/activitypub/act/{delete/index.ts => delete.ts} (70%)
 delete mode 100644 src/remote/activitypub/act/delete/post.ts
 create mode 100644 src/remote/activitypub/delete/index.ts
 create mode 100644 src/remote/activitypub/delete/post.ts

diff --git a/src/remote/activitypub/act/delete/index.ts b/src/remote/activitypub/act/delete.ts
similarity index 70%
rename from src/remote/activitypub/act/delete/index.ts
rename to src/remote/activitypub/act/delete.ts
index eabf9a043..f9eb4dd08 100644
--- a/src/remote/activitypub/act/delete/index.ts
+++ b/src/remote/activitypub/act/delete.ts
@@ -1,5 +1,5 @@
-import create from '../../create';
-import deletePost from './post';
+import create from '../create';
+import deleteObject from '../delete';
 
 export default async (resolver, actor, activity) => {
 	if ('actor' in activity && actor.account.uri !== activity.actor) {
@@ -14,10 +14,7 @@ export default async (resolver, actor, activity) => {
 			return;
 		}
 
-		switch (result.object.$ref) {
-		case 'posts':
-			await deletePost(result.object);
-		}
+		await deleteObject(result);
 	}));
 
 	return null;
diff --git a/src/remote/activitypub/act/delete/post.ts b/src/remote/activitypub/act/delete/post.ts
deleted file mode 100644
index 1b748afe8..000000000
--- a/src/remote/activitypub/act/delete/post.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import Post from '../../../../models/post';
-import queue from '../../../../queue';
-
-export default ({ $id }) => Promise.all([
-	Post.findOneAndDelete({ _id: $id }),
-	new Promise((resolve, reject) => queue.create('db', {
-		type: 'deletePostDependents',
-		id: $id
-	}).delay(65536).save(error => error ? reject(error) : resolve()))
-]);
diff --git a/src/remote/activitypub/act/undo/index.ts b/src/remote/activitypub/act/undo/index.ts
index 11c7ec0c8..aa60d3a4f 100644
--- a/src/remote/activitypub/act/undo/index.ts
+++ b/src/remote/activitypub/act/undo/index.ts
@@ -1,4 +1,5 @@
 import act from '../../act';
+import deleteObject from '../../delete';
 import unfollow from './unfollow';
 import Resolver from '../../resolver';
 
@@ -12,7 +13,7 @@ export default async (resolver: Resolver, actor, activity): Promise<void> => {
 	await Promise.all(results.map(async promisedResult => {
 		const result = await promisedResult;
 
-		if (result === null) {
+		if (result === null || await deleteObject(result) !== null) {
 			return;
 		}
 
@@ -21,4 +22,6 @@ export default async (resolver: Resolver, actor, activity): Promise<void> => {
 			await unfollow(result.object);
 		}
 	}));
+
+	return null;
 };
diff --git a/src/remote/activitypub/delete/index.ts b/src/remote/activitypub/delete/index.ts
new file mode 100644
index 000000000..bc9104284
--- /dev/null
+++ b/src/remote/activitypub/delete/index.ts
@@ -0,0 +1,10 @@
+import deletePost from './post';
+
+export default async ({ object }) => {
+	switch (object.$ref) {
+	case 'posts':
+		return deletePost(object);
+	}
+
+	return null;
+};
diff --git a/src/remote/activitypub/delete/post.ts b/src/remote/activitypub/delete/post.ts
new file mode 100644
index 000000000..f6c816647
--- /dev/null
+++ b/src/remote/activitypub/delete/post.ts
@@ -0,0 +1,13 @@
+import Post from '../../../models/post';
+import queue from '../../../queue';
+
+export default async ({ $id }) => {
+	const promisedDeletion = Post.findOneAndDelete({ _id: $id });
+
+	await new Promise((resolve, reject) => queue.create('db', {
+		type: 'deletePostDependents',
+		id: $id
+	}).delay(65536).save(error => error ? reject(error) : resolve()));
+
+	return promisedDeletion;
+};

From 2c7e09fc7230c853d9b2709548e00c7e62f87d8a Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Wed, 4 Apr 2018 20:29:26 +0900
Subject: [PATCH 1073/1250] Extract http request from post delivery job

---
 src/post/distribute.ts             |  96 +++++++++++++++++++++++++--
 src/processor/http/deliver-post.ts | 100 ++++-------------------------
 2 files changed, 104 insertions(+), 92 deletions(-)

diff --git a/src/post/distribute.ts b/src/post/distribute.ts
index 49c6eb22d..ad699d6b8 100644
--- a/src/post/distribute.ts
+++ b/src/post/distribute.ts
@@ -1,8 +1,11 @@
+import Channel from '../models/channel';
+import ChannelWatching from '../models/channel-watching';
+import Following from '../models/following';
 import Mute from '../models/mute';
 import Post, { pack } from '../models/post';
 import Watching from '../models/post-watching';
-import User from '../models/user';
-import stream from '../publishers/stream';
+import User, { isLocalUser } from '../models/user';
+import stream, { publishChannelStream } from '../publishers/stream';
 import notify from '../publishers/notify';
 import pushSw from '../publishers/push-sw';
 import queue from '../queue';
@@ -21,10 +24,6 @@ export default async (user, mentions, post) => {
 				latestPost: post._id
 			}
 		}),
-		new Promise((resolve, reject) => queue.create('http', {
-			type: 'deliverPost',
-			id: post._id,
-		}).save(error => error ? reject(error) : resolve())),
 	] as Array<Promise<any>>;
 
 	function addMention(promisedMentionee, reason) {
@@ -50,6 +49,91 @@ export default async (user, mentions, post) => {
 		}));
 	}
 
+	// タイムラインへの投稿
+	if (!post.channelId) {
+		promises.push(
+			// Publish event to myself's stream
+			promisedPostObj.then(postObj => {
+				stream(post.userId, 'post', postObj);
+			}),
+
+			Promise.all([
+				User.findOne({ _id: post.userId }),
+
+				// Fetch all followers
+				Following.aggregate([{
+					$lookup: {
+						from: 'users',
+						localField: 'followerId',
+						foreignField: '_id',
+						as: 'follower'
+					}
+				}, {
+					$match: {
+						followeeId: post.userId
+					}
+				}], {
+					_id: false
+				})
+			]).then(([user, followers]) => Promise.all(followers.map(following => {
+				if (isLocalUser(following.follower)) {
+					// Publish event to followers stream
+					return promisedPostObj.then(postObj => {
+						stream(following.followerId, 'post', postObj);
+					});
+				}
+
+				return new Promise((resolve, reject) => {
+					queue.create('http', {
+						type: 'deliverPost',
+						fromId: user._id,
+						toId: following.followerId,
+						postId: post._id
+					}).save(error => {
+						if (error) {
+							reject(error);
+						} else {
+							resolve();
+						}
+					});
+				});
+			})))
+		);
+	}
+
+	// チャンネルへの投稿
+	if (post.channelId) {
+		promises.push(
+			// Increment channel index(posts count)
+			Channel.update({ _id: post.channelId }, {
+				$inc: {
+					index: 1
+				}
+			}),
+
+			// Publish event to channel
+			promisedPostObj.then(postObj => {
+				publishChannelStream(post.channelId, 'post', postObj);
+			}),
+
+			Promise.all([
+				promisedPostObj,
+
+				// Get channel watchers
+				ChannelWatching.find({
+					channelId: post.channelId,
+					// 削除されたドキュメントは除く
+					deletedAt: { $exists: false }
+				})
+			]).then(([postObj, watches]) => {
+				// チャンネルの視聴者(のタイムライン)に配信
+				watches.forEach(w => {
+					stream(w.userId, 'post', postObj);
+				});
+			})
+		);
+	}
+
 	// If has in reply to post
 	if (post.replyId) {
 		promises.push(
diff --git a/src/processor/http/deliver-post.ts b/src/processor/http/deliver-post.ts
index c00ab912c..48ad4f95a 100644
--- a/src/processor/http/deliver-post.ts
+++ b/src/processor/http/deliver-post.ts
@@ -1,93 +1,21 @@
-import Channel from '../../models/channel';
-import Following from '../../models/following';
-import ChannelWatching from '../../models/channel-watching';
-import Post, { pack } from '../../models/post';
-import User, { isLocalUser } from '../../models/user';
-import stream, { publishChannelStream } from '../../publishers/stream';
+import Post from '../../models/post';
+import User, { IRemoteUser } from '../../models/user';
 import context from '../../remote/activitypub/renderer/context';
 import renderCreate from '../../remote/activitypub/renderer/create';
 import renderNote from '../../remote/activitypub/renderer/note';
 import request from '../../remote/request';
 
-export default ({ data }) => Post.findOne({ _id: data.id }).then(post => {
-	const promisedPostObj = pack(post);
-	const promises = [];
+export default async ({ data }) => {
+	const promisedTo = User.findOne({ _id: data.toId }) as Promise<IRemoteUser>;
+	const [from, post] = await Promise.all([
+		User.findOne({ _id: data.fromId }),
+		Post.findOne({ _id: data.postId })
+	]);
+	const note = await renderNote(from, post);
+	const to = await promisedTo;
+	const create = renderCreate(note);
 
-	// タイムラインへの投稿
-	if (!post.channelId) {
-		promises.push(
-			// Publish event to myself's stream
-			promisedPostObj.then(postObj => {
-				stream(post.userId, 'post', postObj);
-			}),
+	create['@context'] = context;
 
-			Promise.all([
-				User.findOne({ _id: post.userId }),
-
-				// Fetch all followers
-				Following.aggregate([{
-					$lookup: {
-						from: 'users',
-						localField: 'followerId',
-						foreignField: '_id',
-						as: 'follower'
-					}
-				}, {
-					$match: {
-						followeeId: post.userId
-					}
-				}], {
-					_id: false
-				})
-			]).then(([user, followers]) => Promise.all(followers.map(following => {
-				if (isLocalUser(following.follower)) {
-					// Publish event to followers stream
-					return promisedPostObj.then(postObj => {
-						stream(following.followerId, 'post', postObj);
-					});
-				}
-
-				return renderNote(user, post).then(note => {
-					const create = renderCreate(note);
-					create['@context'] = context;
-					return request(user, following.follower[0].account.inbox, create);
-				});
-			})))
-		);
-	}
-
-	// チャンネルへの投稿
-	if (post.channelId) {
-		promises.push(
-			// Increment channel index(posts count)
-			Channel.update({ _id: post.channelId }, {
-				$inc: {
-					index: 1
-				}
-			}),
-
-			// Publish event to channel
-			promisedPostObj.then(postObj => {
-				publishChannelStream(post.channelId, 'post', postObj);
-			}),
-
-			Promise.all([
-				promisedPostObj,
-
-				// Get channel watchers
-				ChannelWatching.find({
-					channelId: post.channelId,
-					// 削除されたドキュメントは除く
-					deletedAt: { $exists: false }
-				})
-			]).then(([postObj, watches]) => {
-				// チャンネルの視聴者(のタイムライン)に配信
-				watches.forEach(w => {
-					stream(w.userId, 'post', postObj);
-				});
-			})
-		);
-	}
-
-	return Promise.all(promises);
-});
+	return request(from, to.account.inbox, create);
+};

From d73810cb72e47e090ec0fe24f5f3c608571ef411 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Wed, 4 Apr 2018 21:56:04 +0900
Subject: [PATCH 1074/1250] Make HTTP request first in follow processor

---
 src/processor/http/follow.ts | 118 +++++++++++++++++------------------
 1 file changed, 59 insertions(+), 59 deletions(-)

diff --git a/src/processor/http/follow.ts b/src/processor/http/follow.ts
index 8bf890efb..ed36fa18d 100644
--- a/src/processor/http/follow.ts
+++ b/src/processor/http/follow.ts
@@ -1,4 +1,4 @@
-import User, { isLocalUser, pack as packUser } from '../../models/user';
+import User, { isLocalUser, isRemoteUser, pack as packUser } from '../../models/user';
 import Following from '../../models/following';
 import FollowingLog from '../../models/following-log';
 import FollowedLog from '../../models/followed-log';
@@ -7,63 +7,63 @@ import notify from '../../publishers/notify';
 import context from '../../remote/activitypub/renderer/context';
 import render from '../../remote/activitypub/renderer/follow';
 import request from '../../remote/request';
+import Logger from '../../utils/logger';
 
-export default ({ data }) => Following.findOne({ _id: data.following }).then(({ followerId, followeeId }) => {
-	const promisedFollower = User.findOne({ _id: followerId });
-	const promisedFollowee = User.findOne({ _id: followeeId });
-
-	return Promise.all([
-		// Increment following count
-		User.update(followerId, {
-			$inc: {
-				followingCount: 1
-			}
-		}),
-
-		promisedFollower.then(({ followingCount }) => FollowingLog.insert({
-			createdAt: data.following.createdAt,
-			userId: followerId,
-			count: followingCount + 1
-		})),
-
-		// Increment followers count
-		User.update({ _id: followeeId }, {
-			$inc: {
-				followersCount: 1
-			}
-		}),
-
-		promisedFollowee.then(({ followersCount }) => FollowedLog.insert({
-			createdAt: data.following.createdAt,
-			userId: followerId,
-			count: followersCount + 1
-		})),
-
-		// Notify
-		promisedFollowee.then(followee => followee.host === null ?
-			notify(followeeId, followerId, 'follow') : null),
-
-		// Publish follow event
-		Promise.all([promisedFollower, promisedFollowee]).then(([follower, followee]) => {
-			let followerEvent;
-			let followeeEvent;
-
-			if (isLocalUser(follower)) {
-				followerEvent = packUser(followee, follower)
-					.then(packed => event(follower._id, 'follow', packed));
-			}
-
-			if (isLocalUser(followee)) {
-				followeeEvent = packUser(follower, followee)
-					.then(packed => event(followee._id, 'followed', packed));
-			} else if (isLocalUser(follower)) {
-				const rendered = render(follower, followee);
-				rendered['@context'] = context;
-
-				followeeEvent = request(follower, followee.account.inbox, rendered);
-			}
-
-			return Promise.all([followerEvent, followeeEvent]);
-		})
+export default async ({ data }) => {
+	const { followerId, followeeId } = await Following.findOne({ _id: data.following });
+	const [follower, followee] = await Promise.all([
+		User.findOne({ _id: followerId }),
+		User.findOne({ _id: followeeId })
 	]);
-});
+
+	if (isLocalUser(follower) && isRemoteUser(followee)) {
+		const rendered = render(follower, followee);
+		rendered['@context'] = context;
+
+		await request(follower, followee.account.inbox, rendered);
+	}
+
+	try {
+		await Promise.all([
+			// Increment following count
+			User.update(followerId, {
+				$inc: {
+					followingCount: 1
+				}
+			}),
+
+			FollowingLog.insert({
+				createdAt: data.following.createdAt,
+				userId: followerId,
+				count: follower.followingCount + 1
+			}),
+
+			// Increment followers count
+			User.update({ _id: followeeId }, {
+				$inc: {
+					followersCount: 1
+				}
+			}),
+
+			FollowedLog.insert({
+				createdAt: data.following.createdAt,
+				userId: followerId,
+				count: followee.followersCount + 1
+			}),
+
+			// Publish follow event
+			isLocalUser(follower) && packUser(followee, follower)
+				.then(packed => event(follower._id, 'follow', packed)),
+
+			isLocalUser(followee) && Promise.all([
+				packUser(follower, followee)
+					.then(packed => event(followee._id, 'followed', packed)),
+
+				// Notify
+				isLocalUser(followee) && notify(followeeId, followerId, 'follow')
+			])
+		]);
+	} catch (error) {
+		Logger.error(error.toString());
+	}
+};

From 6b10477b00b841cd17e82d16154548b2cb0c0431 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Wed, 4 Apr 2018 22:05:12 +0900
Subject: [PATCH 1075/1250] Make HTTP request first in unfollow job

---
 src/processor/http/unfollow.ts | 81 ++++++++++++++++++----------------
 1 file changed, 44 insertions(+), 37 deletions(-)

diff --git a/src/processor/http/unfollow.ts b/src/processor/http/unfollow.ts
index d3d5f2246..fbfd7b342 100644
--- a/src/processor/http/unfollow.ts
+++ b/src/processor/http/unfollow.ts
@@ -1,56 +1,63 @@
 import FollowedLog from '../../models/followed-log';
 import Following from '../../models/following';
 import FollowingLog from '../../models/following-log';
-import User, { isRemoteUser, pack as packUser } from '../../models/user';
+import User, { isLocalUser, isRemoteUser, pack as packUser } from '../../models/user';
 import stream from '../../publishers/stream';
 import renderFollow from '../../remote/activitypub/renderer/follow';
 import renderUndo from '../../remote/activitypub/renderer/undo';
 import context from '../../remote/activitypub/renderer/context';
 import request from '../../remote/request';
+import Logger from '../../utils/logger';
 
 export default async ({ data }) => {
-	// Delete following
-	const following = await Following.findOneAndDelete({ _id: data.id });
+	const following = await Following.findOne({ _id: data.id });
 	if (following === null) {
 		return;
 	}
 
-	const promisedFollower = User.findOne({ _id: following.followerId });
-	const promisedFollowee = User.findOne({ _id: following.followeeId });
+	const [follower, followee] = await Promise.all([
+		User.findOne({ _id: following.followerId }),
+		User.findOne({ _id: following.followeeId })
+	]);
 
-	await Promise.all([
-		// Decrement following count
-		User.update({ _id: following.followerId }, { $inc: { followingCount: -1 } }),
-		promisedFollower.then(({ followingCount }) => FollowingLog.insert({
-			createdAt: new Date(),
-			userId: following.followerId,
-			count: followingCount - 1
-		})),
+	if (isLocalUser(follower) && isRemoteUser(followee)) {
+		const undo = renderUndo(renderFollow(follower, followee));
+		undo['@context'] = context;
 
-		// Decrement followers count
-		User.update({ _id: following.followeeId }, { $inc: { followersCount: -1 } }),
-		promisedFollowee.then(({ followersCount }) => FollowedLog.insert({
-			createdAt: new Date(),
-			userId: following.followeeId,
-			count: followersCount - 1
-		})),
+		await request(follower, followee.account.inbox, undo);
+	}
+
+	try {
+		await Promise.all([
+			// Delete following
+			Following.findOneAndDelete({ _id: data.id }),
+
+			// Decrement following count
+			User.update({ _id: follower._id }, { $inc: { followingCount: -1 } }),
+			FollowingLog.insert({
+				createdAt: new Date(),
+				userId: follower._id,
+				count: follower.followingCount - 1
+			}),
+
+			// Decrement followers count
+			User.update({ _id: followee._id }, { $inc: { followersCount: -1 } }),
+			FollowedLog.insert({
+				createdAt: new Date(),
+				userId: followee._id,
+				count: followee.followersCount - 1
+			})
+		]);
+
+		if (isLocalUser(follower)) {
+			return;
+		}
+
+		const promisedPackedUser = packUser(followee, follower);
 
 		// Publish follow event
-		Promise.all([promisedFollower, promisedFollowee]).then(async ([follower, followee]) => {
-			if (isRemoteUser(follower)) {
-				return;
-			}
-
-			const promisedPackedUser = packUser(followee, follower);
-
-			if (isRemoteUser(followee)) {
-				const undo = renderUndo(renderFollow(follower, followee));
-				undo['@context'] = context;
-
-				await request(follower, followee.account.inbox, undo);
-			}
-
-			stream(follower._id, 'unfollow', promisedPackedUser);
-		})
-	]);
+		stream(follower._id, 'unfollow', promisedPackedUser);
+	} catch (error) {
+		Logger.error(error.toString());
+	}
 };

From 6cafb15636d5175af989f0749d675ad80e12c345 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Wed, 4 Apr 2018 22:45:55 +0900
Subject: [PATCH 1076/1250] Retry HTTP requests

---
 src/following/distribute.ts                   | 42 +++++++++++++++++++
 src/index.ts                                  |  2 +-
 src/post/distribute.ts                        |  4 +-
 src/processor/http/perform-activitypub.ts     |  7 ----
 src/processor/index.ts                        | 18 --------
 src/queue.ts                                  | 10 -----
 src/queue/index.ts                            | 38 +++++++++++++++++
 .../processors}/db/delete-post-dependents.ts  | 12 +++---
 .../processors}/db/index.ts                   |  0
 .../processors}/http/deliver-post.ts          | 12 +++---
 .../processors}/http/follow.ts                | 20 ++++-----
 .../processors}/http/index.ts                 |  0
 .../processors/http/perform-activitypub.ts    |  7 ++++
 .../processors}/http/process-inbox.ts         | 10 ++---
 .../processors}/http/report-github-failure.ts |  4 +-
 .../processors}/http/unfollow.ts              | 20 ++++-----
 src/remote/activitypub/act/follow.ts          |  4 +-
 src/remote/activitypub/act/undo/unfollow.ts   |  4 +-
 src/remote/activitypub/delete/post.ts         |  4 +-
 src/remote/activitypub/resolve-person.ts      |  4 +-
 src/server/activitypub/inbox.ts               |  4 +-
 src/server/api/endpoints/following/create.ts  |  4 +-
 src/server/api/endpoints/following/delete.ts  |  4 +-
 src/server/api/service/github.ts              |  4 +-
 24 files changed, 145 insertions(+), 93 deletions(-)
 create mode 100644 src/following/distribute.ts
 delete mode 100644 src/processor/http/perform-activitypub.ts
 delete mode 100644 src/processor/index.ts
 delete mode 100644 src/queue.ts
 create mode 100644 src/queue/index.ts
 rename src/{processor => queue/processors}/db/delete-post-dependents.ts (59%)
 rename src/{processor => queue/processors}/db/index.ts (100%)
 rename src/{processor => queue/processors}/http/deliver-post.ts (55%)
 rename src/{processor => queue/processors}/http/follow.ts (74%)
 rename src/{processor => queue/processors}/http/index.ts (100%)
 create mode 100644 src/queue/processors/http/perform-activitypub.ts
 rename src/{processor => queue/processors}/http/process-inbox.ts (76%)
 rename src/{processor => queue/processors}/http/report-github-failure.ts (85%)
 rename src/{processor => queue/processors}/http/unfollow.ts (71%)

diff --git a/src/following/distribute.ts b/src/following/distribute.ts
new file mode 100644
index 000000000..10ff98881
--- /dev/null
+++ b/src/following/distribute.ts
@@ -0,0 +1,42 @@
+import User, { pack as packUser } from '../models/user';
+import FollowingLog from '../models/following-log';
+import FollowedLog from '../models/followed-log';
+import event from '../publishers/stream';
+import notify from '../publishers/notify';
+
+export default async (follower, followee) => Promise.all([
+	// Increment following count
+	User.update(follower._id, {
+		$inc: {
+			followingCount: 1
+		}
+	}),
+
+	FollowingLog.insert({
+		createdAt: new Date(),
+		userId: followee._id,
+		count: follower.followingCount + 1
+	}),
+
+	// Increment followers count
+	User.update({ _id: followee._id }, {
+		$inc: {
+			followersCount: 1
+		}
+	}),
+
+	FollowedLog.insert({
+		createdAt: new Date(),
+		userId: follower._id,
+		count: followee.followersCount + 1
+	}),
+
+	followee.host === null && Promise.all([
+		// Notify
+		notify(followee.id, follower.id, 'follow'),
+
+		// Publish follow event
+		packUser(follower, followee)
+			.then(packed => event(followee._id, 'followed', packed))
+	])
+]);
diff --git a/src/index.ts b/src/index.ts
index 29c4f3431..21fb2f553 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -99,7 +99,7 @@ async function workerMain(opt) {
 
 	if (!opt['only-server']) {
 		// start processor
-		require('./processor').default();
+		require('./queue').process();
 	}
 
 	// Send a 'ready' message to parent process
diff --git a/src/post/distribute.ts b/src/post/distribute.ts
index ad699d6b8..f748a620c 100644
--- a/src/post/distribute.ts
+++ b/src/post/distribute.ts
@@ -8,7 +8,7 @@ import User, { isLocalUser } from '../models/user';
 import stream, { publishChannelStream } from '../publishers/stream';
 import notify from '../publishers/notify';
 import pushSw from '../publishers/push-sw';
-import queue from '../queue';
+import { createHttp } from '../queue';
 import watch from './watch';
 
 export default async (user, mentions, post) => {
@@ -84,7 +84,7 @@ export default async (user, mentions, post) => {
 				}
 
 				return new Promise((resolve, reject) => {
-					queue.create('http', {
+					createHttp({
 						type: 'deliverPost',
 						fromId: user._id,
 						toId: following.followerId,
diff --git a/src/processor/http/perform-activitypub.ts b/src/processor/http/perform-activitypub.ts
deleted file mode 100644
index 963e532fe..000000000
--- a/src/processor/http/perform-activitypub.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import User from '../../models/user';
-import act from '../../remote/activitypub/act';
-import Resolver from '../../remote/activitypub/resolver';
-
-export default ({ data }) => User.findOne({ _id: data.actor })
-	.then(actor => act(new Resolver(), actor, data.outbox))
-	.then(Promise.all);
diff --git a/src/processor/index.ts b/src/processor/index.ts
deleted file mode 100644
index 172048dda..000000000
--- a/src/processor/index.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import queue from '../queue';
-import db from './db';
-import http from './http';
-
-export default () => {
-	queue.process('db', db);
-
-	/*
-		256 is the default concurrency limit of Mozilla Firefox and Google
-		Chromium.
-
-		a8af215e691f3a2205a3758d2d96e9d328e100ff - chromium/src.git - Git at Google
-		https://chromium.googlesource.com/chromium/src.git/+/a8af215e691f3a2205a3758d2d96e9d328e100ff
-		Network.http.max-connections - MozillaZine Knowledge Base
-		http://kb.mozillazine.org/Network.http.max-connections
-	*/
-	queue.process('http', 256, http);
-};
diff --git a/src/queue.ts b/src/queue.ts
deleted file mode 100644
index 08ea13c2a..000000000
--- a/src/queue.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { createQueue } from 'kue';
-import config from './config';
-
-export default createQueue({
-	redis: {
-		port: config.redis.port,
-		host: config.redis.host,
-		auth: config.redis.pass
-	}
-});
diff --git a/src/queue/index.ts b/src/queue/index.ts
new file mode 100644
index 000000000..f90754a56
--- /dev/null
+++ b/src/queue/index.ts
@@ -0,0 +1,38 @@
+import { createQueue } from 'kue';
+import config from '../config';
+import db from './processors/db';
+import http from './processors/http';
+
+const queue = createQueue({
+	redis: {
+		port: config.redis.port,
+		host: config.redis.host,
+		auth: config.redis.pass
+	}
+});
+
+export function createHttp(data) {
+	return queue
+		.create('http', data)
+		.attempts(16)
+		.backoff({ delay: 16384, type: 'exponential' });
+}
+
+export function createDb(data) {
+	return queue.create('db', data);
+}
+
+export function process() {
+	queue.process('db', db);
+
+	/*
+		256 is the default concurrency limit of Mozilla Firefox and Google
+		Chromium.
+
+		a8af215e691f3a2205a3758d2d96e9d328e100ff - chromium/src.git - Git at Google
+		https://chromium.googlesource.com/chromium/src.git/+/a8af215e691f3a2205a3758d2d96e9d328e100ff
+		Network.http.max-connections - MozillaZine Knowledge Base
+		http://kb.mozillazine.org/Network.http.max-connections
+	*/
+	queue.process('http', 256, http);
+}
diff --git a/src/processor/db/delete-post-dependents.ts b/src/queue/processors/db/delete-post-dependents.ts
similarity index 59%
rename from src/processor/db/delete-post-dependents.ts
rename to src/queue/processors/db/delete-post-dependents.ts
index 879c41ec9..6de21eb05 100644
--- a/src/processor/db/delete-post-dependents.ts
+++ b/src/queue/processors/db/delete-post-dependents.ts
@@ -1,9 +1,9 @@
-import Favorite from '../../models/favorite';
-import Notification from '../../models/notification';
-import PollVote from '../../models/poll-vote';
-import PostReaction from '../../models/post-reaction';
-import PostWatching from '../../models/post-watching';
-import Post from '../../models/post';
+import Favorite from '../../../models/favorite';
+import Notification from '../../../models/notification';
+import PollVote from '../../../models/poll-vote';
+import PostReaction from '../../../models/post-reaction';
+import PostWatching from '../../../models/post-watching';
+import Post from '../../../models/post';
 
 export default async ({ data }) => Promise.all([
 	Favorite.remove({ postId: data._id }),
diff --git a/src/processor/db/index.ts b/src/queue/processors/db/index.ts
similarity index 100%
rename from src/processor/db/index.ts
rename to src/queue/processors/db/index.ts
diff --git a/src/processor/http/deliver-post.ts b/src/queue/processors/http/deliver-post.ts
similarity index 55%
rename from src/processor/http/deliver-post.ts
rename to src/queue/processors/http/deliver-post.ts
index 48ad4f95a..e743fc5f6 100644
--- a/src/processor/http/deliver-post.ts
+++ b/src/queue/processors/http/deliver-post.ts
@@ -1,9 +1,9 @@
-import Post from '../../models/post';
-import User, { IRemoteUser } from '../../models/user';
-import context from '../../remote/activitypub/renderer/context';
-import renderCreate from '../../remote/activitypub/renderer/create';
-import renderNote from '../../remote/activitypub/renderer/note';
-import request from '../../remote/request';
+import Post from '../../../models/post';
+import User, { IRemoteUser } from '../../../models/user';
+import context from '../../../remote/activitypub/renderer/context';
+import renderCreate from '../../../remote/activitypub/renderer/create';
+import renderNote from '../../../remote/activitypub/renderer/note';
+import request from '../../../remote/request';
 
 export default async ({ data }) => {
 	const promisedTo = User.findOne({ _id: data.toId }) as Promise<IRemoteUser>;
diff --git a/src/processor/http/follow.ts b/src/queue/processors/http/follow.ts
similarity index 74%
rename from src/processor/http/follow.ts
rename to src/queue/processors/http/follow.ts
index ed36fa18d..4cb72828e 100644
--- a/src/processor/http/follow.ts
+++ b/src/queue/processors/http/follow.ts
@@ -1,13 +1,13 @@
-import User, { isLocalUser, isRemoteUser, pack as packUser } from '../../models/user';
-import Following from '../../models/following';
-import FollowingLog from '../../models/following-log';
-import FollowedLog from '../../models/followed-log';
-import event from '../../publishers/stream';
-import notify from '../../publishers/notify';
-import context from '../../remote/activitypub/renderer/context';
-import render from '../../remote/activitypub/renderer/follow';
-import request from '../../remote/request';
-import Logger from '../../utils/logger';
+import User, { isLocalUser, isRemoteUser, pack as packUser } from '../../../models/user';
+import Following from '../../../models/following';
+import FollowingLog from '../../../models/following-log';
+import FollowedLog from '../../../models/followed-log';
+import event from '../../../publishers/stream';
+import notify from '../../../publishers/notify';
+import context from '../../../remote/activitypub/renderer/context';
+import render from '../../../remote/activitypub/renderer/follow';
+import request from '../../../remote/request';
+import Logger from '../../../utils/logger';
 
 export default async ({ data }) => {
 	const { followerId, followeeId } = await Following.findOne({ _id: data.following });
diff --git a/src/processor/http/index.ts b/src/queue/processors/http/index.ts
similarity index 100%
rename from src/processor/http/index.ts
rename to src/queue/processors/http/index.ts
diff --git a/src/queue/processors/http/perform-activitypub.ts b/src/queue/processors/http/perform-activitypub.ts
new file mode 100644
index 000000000..7b84400d5
--- /dev/null
+++ b/src/queue/processors/http/perform-activitypub.ts
@@ -0,0 +1,7 @@
+import User from '../../../models/user';
+import act from '../../../remote/activitypub/act';
+import Resolver from '../../../remote/activitypub/resolver';
+
+export default ({ data }) => User.findOne({ _id: data.actor })
+	.then(actor => act(new Resolver(), actor, data.outbox))
+	.then(Promise.all);
diff --git a/src/processor/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts
similarity index 76%
rename from src/processor/http/process-inbox.ts
rename to src/queue/processors/http/process-inbox.ts
index f102f8d6b..de1dbd2f9 100644
--- a/src/processor/http/process-inbox.ts
+++ b/src/queue/processors/http/process-inbox.ts
@@ -1,9 +1,9 @@
 import { verifySignature } from 'http-signature';
-import parseAcct from '../../acct/parse';
-import User, { IRemoteUser } from '../../models/user';
-import act from '../../remote/activitypub/act';
-import resolvePerson from '../../remote/activitypub/resolve-person';
-import Resolver from '../../remote/activitypub/resolver';
+import parseAcct from '../../../acct/parse';
+import User, { IRemoteUser } from '../../../models/user';
+import act from '../../../remote/activitypub/act';
+import resolvePerson from '../../../remote/activitypub/resolve-person';
+import Resolver from '../../../remote/activitypub/resolver';
 
 export default async ({ data }): Promise<void> => {
 	const keyIdLower = data.signature.keyId.toLowerCase();
diff --git a/src/processor/http/report-github-failure.ts b/src/queue/processors/http/report-github-failure.ts
similarity index 85%
rename from src/processor/http/report-github-failure.ts
rename to src/queue/processors/http/report-github-failure.ts
index 4f6f5ccee..21683ba3c 100644
--- a/src/processor/http/report-github-failure.ts
+++ b/src/queue/processors/http/report-github-failure.ts
@@ -1,6 +1,6 @@
 import * as request from 'request-promise-native';
-import User from '../../models/user';
-const createPost = require('../../server/api/endpoints/posts/create');
+import User from '../../../models/user';
+const createPost = require('../../../server/api/endpoints/posts/create');
 
 export default async ({ data }) => {
 	const asyncBot = User.findOne({ _id: data.userId });
diff --git a/src/processor/http/unfollow.ts b/src/queue/processors/http/unfollow.ts
similarity index 71%
rename from src/processor/http/unfollow.ts
rename to src/queue/processors/http/unfollow.ts
index fbfd7b342..801a3612a 100644
--- a/src/processor/http/unfollow.ts
+++ b/src/queue/processors/http/unfollow.ts
@@ -1,13 +1,13 @@
-import FollowedLog from '../../models/followed-log';
-import Following from '../../models/following';
-import FollowingLog from '../../models/following-log';
-import User, { isLocalUser, isRemoteUser, pack as packUser } from '../../models/user';
-import stream from '../../publishers/stream';
-import renderFollow from '../../remote/activitypub/renderer/follow';
-import renderUndo from '../../remote/activitypub/renderer/undo';
-import context from '../../remote/activitypub/renderer/context';
-import request from '../../remote/request';
-import Logger from '../../utils/logger';
+import FollowedLog from '../../../models/followed-log';
+import Following from '../../../models/following';
+import FollowingLog from '../../../models/following-log';
+import User, { isLocalUser, isRemoteUser, pack as packUser } from '../../../models/user';
+import stream from '../../../publishers/stream';
+import renderFollow from '../../../remote/activitypub/renderer/follow';
+import renderUndo from '../../../remote/activitypub/renderer/undo';
+import context from '../../../remote/activitypub/renderer/context';
+import request from '../../../remote/request';
+import Logger from '../../../utils/logger';
 
 export default async ({ data }) => {
 	const following = await Following.findOne({ _id: data.id });
diff --git a/src/remote/activitypub/act/follow.ts b/src/remote/activitypub/act/follow.ts
index 23fa41df8..222a257e1 100644
--- a/src/remote/activitypub/act/follow.ts
+++ b/src/remote/activitypub/act/follow.ts
@@ -3,7 +3,7 @@ import parseAcct from '../../../acct/parse';
 import Following, { IFollowing } from '../../../models/following';
 import User from '../../../models/user';
 import config from '../../../config';
-import queue from '../../../queue';
+import { createHttp } from '../../../queue';
 import context from '../renderer/context';
 import renderAccept from '../renderer/accept';
 import request from '../../request';
@@ -44,7 +44,7 @@ export default async (resolver: Resolver, actor, activity, distribute) => {
 		followerId: actor._id,
 		followeeId: followee._id
 	}).then(following => new Promise((resolve, reject) => {
-		queue.create('http', {
+		createHttp({
 			type: 'follow',
 			following: following._id
 		}).save(error => {
diff --git a/src/remote/activitypub/act/undo/unfollow.ts b/src/remote/activitypub/act/undo/unfollow.ts
index c17e06e8a..4f15d9a3e 100644
--- a/src/remote/activitypub/act/undo/unfollow.ts
+++ b/src/remote/activitypub/act/undo/unfollow.ts
@@ -1,7 +1,7 @@
-import queue from '../../../../queue';
+import { createHttp } from '../../../../queue';
 
 export default ({ $id }) => new Promise((resolve, reject) => {
-	queue.create('http', { type: 'unfollow', id: $id }).save(error => {
+	createHttp({ type: 'unfollow', id: $id }).save(error => {
 		if (error) {
 			reject(error);
 		} else {
diff --git a/src/remote/activitypub/delete/post.ts b/src/remote/activitypub/delete/post.ts
index f6c816647..59ae8c2b9 100644
--- a/src/remote/activitypub/delete/post.ts
+++ b/src/remote/activitypub/delete/post.ts
@@ -1,10 +1,10 @@
 import Post from '../../../models/post';
-import queue from '../../../queue';
+import { createDb } from '../../../queue';
 
 export default async ({ $id }) => {
 	const promisedDeletion = Post.findOneAndDelete({ _id: $id });
 
-	await new Promise((resolve, reject) => queue.create('db', {
+	await new Promise((resolve, reject) => createDb({
 		type: 'deletePostDependents',
 		id: $id
 	}).delay(65536).save(error => error ? reject(error) : resolve()));
diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts
index 59be65908..2cf3ad32d 100644
--- a/src/remote/activitypub/resolve-person.ts
+++ b/src/remote/activitypub/resolve-person.ts
@@ -1,7 +1,7 @@
 import { JSDOM } from 'jsdom';
 import { toUnicode } from 'punycode';
 import User, { validateUsername, isValidName, isValidDescription } from '../../models/user';
-import queue from '../../queue';
+import { createHttp } from '../../queue';
 import webFinger from '../webfinger';
 import create from './create';
 import Resolver from './resolver';
@@ -69,7 +69,7 @@ export default async (value, verifier?: string) => {
 		},
 	});
 
-	queue.create('http', {
+	createHttp({
 		type: 'performActivityPub',
 		actor: user._id,
 		outbox
diff --git a/src/server/activitypub/inbox.ts b/src/server/activitypub/inbox.ts
index 5de843385..0907823b2 100644
--- a/src/server/activitypub/inbox.ts
+++ b/src/server/activitypub/inbox.ts
@@ -1,7 +1,7 @@
 import * as bodyParser from 'body-parser';
 import * as express from 'express';
 import { parseRequest } from 'http-signature';
-import queue from '../../queue';
+import { createHttp } from '../../queue';
 
 const app = express();
 
@@ -22,7 +22,7 @@ app.post('/@:user/inbox', bodyParser.json({
 		return res.sendStatus(401);
 	}
 
-	queue.create('http', {
+	createHttp({
 		type: 'processInbox',
 		inbox: req.body,
 		signature,
diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts
index e56859521..9ccbe2017 100644
--- a/src/server/api/endpoints/following/create.ts
+++ b/src/server/api/endpoints/following/create.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import User from '../../../../models/user';
 import Following from '../../../../models/following';
-import queue from '../../../../queue';
+import { createHttp } from '../../../../queue';
 
 /**
  * Follow a user
@@ -56,7 +56,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		followeeId: followee._id
 	});
 
-	queue.create('http', { type: 'follow', following: _id }).save();
+	createHttp({ type: 'follow', following: _id }).save();
 
 	// Send response
 	res();
diff --git a/src/server/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts
index bf21bf0cb..0684b8750 100644
--- a/src/server/api/endpoints/following/delete.ts
+++ b/src/server/api/endpoints/following/delete.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import User from '../../../../models/user';
 import Following from '../../../../models/following';
-import queue from '../../../../queue';
+import { createHttp } from '../../../../queue';
 
 /**
  * Unfollow a user
@@ -49,7 +49,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		return rej('already not following');
 	}
 
-	queue.create('http', {
+	createHttp({
 		type: 'unfollow',
 		id: exist._id
 	}).save(error => {
diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts
index 4fd59c2a9..5fc4a92f5 100644
--- a/src/server/api/service/github.ts
+++ b/src/server/api/service/github.ts
@@ -3,7 +3,7 @@ import * as express from 'express';
 //const crypto = require('crypto');
 import User from '../../../models/user';
 import config from '../../../config';
-import queue from '../../../queue';
+import { createHttp } from '../../../queue';
 
 module.exports = async (app: express.Application) => {
 	if (config.github_bot == null) return;
@@ -42,7 +42,7 @@ module.exports = async (app: express.Application) => {
 				const commit = event.commit;
 				const parent = commit.parents[0];
 
-				queue.create('http', {
+				createHttp({
 					type: 'gitHubFailureReport',
 					userId: bot._id,
 					parentUrl: parent.url,

From bddcd13f58877c7dc626166124d9c47cd5bc524b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 4 Apr 2018 23:12:35 +0900
Subject: [PATCH 1077/1250] wip

---
 src/post/create.ts                            | 107 +++++++++++-
 src/processor/http/deliver-post.ts            |  93 -----------
 src/processor/http/process-inbox.ts           |  39 -----
 src/queue.ts                                  |  10 --
 src/queue/index.ts                            |  37 ++++
 .../processors}/db/delete-post-dependents.ts  |   0
 .../processors}/db/index.ts                   |   0
 src/queue/processors/http/deliver.ts          |  17 ++
 .../processors}/http/follow.ts                |   0
 .../processors}/http/index.ts                 |   0
 .../processors}/http/perform-activitypub.ts   |   0
 src/queue/processors/http/process-inbox.ts    |  55 ++++++
 .../processors}/http/report-github-failure.ts |   0
 .../processors}/http/unfollow.ts              |   0
 src/{processor => queue/processors}/index.ts  |   0
 src/remote/activitypub/act/create.ts          |  92 +++++++++-
 src/remote/activitypub/act/index.ts           |  44 +++--
 src/remote/activitypub/create.ts              | 158 ------------------
 src/remote/activitypub/resolver.ts            |  77 ++++-----
 src/server/activitypub/inbox.ts               |   2 +-
 20 files changed, 354 insertions(+), 377 deletions(-)
 delete mode 100644 src/processor/http/deliver-post.ts
 delete mode 100644 src/processor/http/process-inbox.ts
 delete mode 100644 src/queue.ts
 create mode 100644 src/queue/index.ts
 rename src/{processor => queue/processors}/db/delete-post-dependents.ts (100%)
 rename src/{processor => queue/processors}/db/index.ts (100%)
 create mode 100644 src/queue/processors/http/deliver.ts
 rename src/{processor => queue/processors}/http/follow.ts (100%)
 rename src/{processor => queue/processors}/http/index.ts (100%)
 rename src/{processor => queue/processors}/http/perform-activitypub.ts (100%)
 create mode 100644 src/queue/processors/http/process-inbox.ts
 rename src/{processor => queue/processors}/http/report-github-failure.ts (100%)
 rename src/{processor => queue/processors}/http/unfollow.ts (100%)
 rename src/{processor => queue/processors}/index.ts (100%)
 delete mode 100644 src/remote/activitypub/create.ts

diff --git a/src/post/create.ts b/src/post/create.ts
index ecea37382..f78bbe752 100644
--- a/src/post/create.ts
+++ b/src/post/create.ts
@@ -1,8 +1,14 @@
 import parseAcct from '../acct/parse';
-import Post from '../models/post';
-import User from '../models/user';
+import Post, { pack } from '../models/post';
+import User, { isLocalUser, isRemoteUser, IUser } from '../models/user';
+import stream from '../publishers/stream';
+import Following from '../models/following';
+import { createHttp } from '../queue';
+import renderNote from '../remote/activitypub/renderer/note';
+import renderCreate from '../remote/activitypub/renderer/create';
+import context from '../remote/activitypub/renderer/context';
 
-export default async (post, reply, repost, atMentions) => {
+export default async (user: IUser, post, reply, repost, atMentions) => {
 	post.mentions = [];
 
 	function addMention(mentionee) {
@@ -46,5 +52,98 @@ export default async (post, reply, repost, atMentions) => {
 		addMention(_id);
 	}));
 
-	return Post.insert(post);
+	const inserted = await Post.insert(post);
+
+	User.update({ _id: user._id }, {
+		// Increment my posts count
+		$inc: {
+			postsCount: 1
+		},
+
+		$set: {
+			latestPost: post._id
+		}
+	});
+
+	const postObj = await pack(inserted);
+
+	// タイムラインへの投稿
+	if (!post.channelId) {
+		// Publish event to myself's stream
+		stream(post.userId, 'post', postObj);
+
+		// Fetch all followers
+		const followers = await Following.aggregate([{
+			$lookup: {
+				from: 'users',
+				localField: 'followerId',
+				foreignField: '_id',
+				as: 'follower'
+			}
+		}, {
+			$match: {
+				followeeId: post.userId
+			}
+		}], {
+			_id: false
+		});
+
+		const note = await renderNote(user, post);
+		const content = renderCreate(note);
+		content['@context'] = context;
+
+		Promise.all(followers.map(({ follower }) => {
+			if (isLocalUser(follower)) {
+				// Publish event to followers stream
+				stream(follower._id, 'post', postObj);
+			} else {
+				// フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信
+				if (isLocalUser(user)) {
+					createHttp({
+						type: 'deliver',
+						user,
+						content,
+						to: follower.account.inbox
+					}).save();
+				}
+			}
+		}));
+	}
+
+	// チャンネルへの投稿
+	/* TODO
+	if (post.channelId) {
+		promises.push(
+			// Increment channel index(posts count)
+			Channel.update({ _id: post.channelId }, {
+				$inc: {
+					index: 1
+				}
+			}),
+
+			// Publish event to channel
+			promisedPostObj.then(postObj => {
+				publishChannelStream(post.channelId, 'post', postObj);
+			}),
+
+			Promise.all([
+				promisedPostObj,
+
+				// Get channel watchers
+				ChannelWatching.find({
+					channelId: post.channelId,
+					// 削除されたドキュメントは除く
+					deletedAt: { $exists: false }
+				})
+			]).then(([postObj, watches]) => {
+				// チャンネルの視聴者(のタイムライン)に配信
+				watches.forEach(w => {
+					stream(w.userId, 'post', postObj);
+				});
+			})
+		);
+	}*/
+
+	return Promise.all(promises);
+
 };
diff --git a/src/processor/http/deliver-post.ts b/src/processor/http/deliver-post.ts
deleted file mode 100644
index c00ab912c..000000000
--- a/src/processor/http/deliver-post.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-import Channel from '../../models/channel';
-import Following from '../../models/following';
-import ChannelWatching from '../../models/channel-watching';
-import Post, { pack } from '../../models/post';
-import User, { isLocalUser } from '../../models/user';
-import stream, { publishChannelStream } from '../../publishers/stream';
-import context from '../../remote/activitypub/renderer/context';
-import renderCreate from '../../remote/activitypub/renderer/create';
-import renderNote from '../../remote/activitypub/renderer/note';
-import request from '../../remote/request';
-
-export default ({ data }) => Post.findOne({ _id: data.id }).then(post => {
-	const promisedPostObj = pack(post);
-	const promises = [];
-
-	// タイムラインへの投稿
-	if (!post.channelId) {
-		promises.push(
-			// Publish event to myself's stream
-			promisedPostObj.then(postObj => {
-				stream(post.userId, 'post', postObj);
-			}),
-
-			Promise.all([
-				User.findOne({ _id: post.userId }),
-
-				// Fetch all followers
-				Following.aggregate([{
-					$lookup: {
-						from: 'users',
-						localField: 'followerId',
-						foreignField: '_id',
-						as: 'follower'
-					}
-				}, {
-					$match: {
-						followeeId: post.userId
-					}
-				}], {
-					_id: false
-				})
-			]).then(([user, followers]) => Promise.all(followers.map(following => {
-				if (isLocalUser(following.follower)) {
-					// Publish event to followers stream
-					return promisedPostObj.then(postObj => {
-						stream(following.followerId, 'post', postObj);
-					});
-				}
-
-				return renderNote(user, post).then(note => {
-					const create = renderCreate(note);
-					create['@context'] = context;
-					return request(user, following.follower[0].account.inbox, create);
-				});
-			})))
-		);
-	}
-
-	// チャンネルへの投稿
-	if (post.channelId) {
-		promises.push(
-			// Increment channel index(posts count)
-			Channel.update({ _id: post.channelId }, {
-				$inc: {
-					index: 1
-				}
-			}),
-
-			// Publish event to channel
-			promisedPostObj.then(postObj => {
-				publishChannelStream(post.channelId, 'post', postObj);
-			}),
-
-			Promise.all([
-				promisedPostObj,
-
-				// Get channel watchers
-				ChannelWatching.find({
-					channelId: post.channelId,
-					// 削除されたドキュメントは除く
-					deletedAt: { $exists: false }
-				})
-			]).then(([postObj, watches]) => {
-				// チャンネルの視聴者(のタイムライン)に配信
-				watches.forEach(w => {
-					stream(w.userId, 'post', postObj);
-				});
-			})
-		);
-	}
-
-	return Promise.all(promises);
-});
diff --git a/src/processor/http/process-inbox.ts b/src/processor/http/process-inbox.ts
deleted file mode 100644
index f102f8d6b..000000000
--- a/src/processor/http/process-inbox.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { verifySignature } from 'http-signature';
-import parseAcct from '../../acct/parse';
-import User, { IRemoteUser } from '../../models/user';
-import act from '../../remote/activitypub/act';
-import resolvePerson from '../../remote/activitypub/resolve-person';
-import Resolver from '../../remote/activitypub/resolver';
-
-export default async ({ data }): Promise<void> => {
-	const keyIdLower = data.signature.keyId.toLowerCase();
-	let user;
-
-	if (keyIdLower.startsWith('acct:')) {
-		const { username, host } = parseAcct(keyIdLower.slice('acct:'.length));
-		if (host === null) {
-			throw 'request was made by local user';
-		}
-
-		user = await User.findOne({ usernameLower: username, hostLower: host }) as IRemoteUser;
-	} else {
-		user = await User.findOne({
-			host: { $ne: null },
-			'account.publicKey.id': data.signature.keyId
-		}) as IRemoteUser;
-
-		if (user === null) {
-			user = await resolvePerson(data.signature.keyId);
-		}
-	}
-
-	if (user === null) {
-		throw 'failed to resolve user';
-	}
-
-	if (!verifySignature(data.signature, user.account.publicKey.publicKeyPem)) {
-		throw 'signature verification failed';
-	}
-
-	await Promise.all(await act(new Resolver(), user, data.inbox, true));
-};
diff --git a/src/queue.ts b/src/queue.ts
deleted file mode 100644
index 08ea13c2a..000000000
--- a/src/queue.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { createQueue } from 'kue';
-import config from './config';
-
-export default createQueue({
-	redis: {
-		port: config.redis.port,
-		host: config.redis.host,
-		auth: config.redis.pass
-	}
-});
diff --git a/src/queue/index.ts b/src/queue/index.ts
new file mode 100644
index 000000000..c8c436b18
--- /dev/null
+++ b/src/queue/index.ts
@@ -0,0 +1,37 @@
+import { createQueue } from 'kue';
+import config from '../config';
+import db from './processors/db';
+import http from './processors/http';
+
+const queue = createQueue({
+	redis: {
+		port: config.redis.port,
+		host: config.redis.host,
+		auth: config.redis.pass
+	}
+});
+
+export function createHttp(data) {
+	return queue
+		.create('http', data)
+		.attempts(16)
+		.backoff({ delay: 16384, type: 'exponential' });
+}
+
+export function createDb(data) {
+	return queue.create('db', data);
+}
+
+export function process() {
+	queue.process('db', db);
+
+	/*
+		256 is the default concurrency limit of Mozilla Firefox and Google
+		Chromium.
+		a8af215e691f3a2205a3758d2d96e9d328e100ff - chromium/src.git - Git at Google
+		https://chromium.googlesource.com/chromium/src.git/+/a8af215e691f3a2205a3758d2d96e9d328e100ff
+		Network.http.max-connections - MozillaZine Knowledge Base
+		http://kb.mozillazine.org/Network.http.max-connections
+	*/
+	queue.process('http', 256, http);
+}
diff --git a/src/processor/db/delete-post-dependents.ts b/src/queue/processors/db/delete-post-dependents.ts
similarity index 100%
rename from src/processor/db/delete-post-dependents.ts
rename to src/queue/processors/db/delete-post-dependents.ts
diff --git a/src/processor/db/index.ts b/src/queue/processors/db/index.ts
similarity index 100%
rename from src/processor/db/index.ts
rename to src/queue/processors/db/index.ts
diff --git a/src/queue/processors/http/deliver.ts b/src/queue/processors/http/deliver.ts
new file mode 100644
index 000000000..8cd9eb624
--- /dev/null
+++ b/src/queue/processors/http/deliver.ts
@@ -0,0 +1,17 @@
+import * as kue from 'kue';
+
+import Channel from '../../models/channel';
+import Following from '../../models/following';
+import ChannelWatching from '../../models/channel-watching';
+import Post, { pack } from '../../models/post';
+import User, { isLocalUser } from '../../models/user';
+import stream, { publishChannelStream } from '../../publishers/stream';
+import context from '../../remote/activitypub/renderer/context';
+import renderCreate from '../../remote/activitypub/renderer/create';
+import renderNote from '../../remote/activitypub/renderer/note';
+import request from '../../remote/request';
+
+export default async (job: kue.Job, done): Promise<void> => {
+
+	request(user, following.follower[0].account.inbox, create);
+}
diff --git a/src/processor/http/follow.ts b/src/queue/processors/http/follow.ts
similarity index 100%
rename from src/processor/http/follow.ts
rename to src/queue/processors/http/follow.ts
diff --git a/src/processor/http/index.ts b/src/queue/processors/http/index.ts
similarity index 100%
rename from src/processor/http/index.ts
rename to src/queue/processors/http/index.ts
diff --git a/src/processor/http/perform-activitypub.ts b/src/queue/processors/http/perform-activitypub.ts
similarity index 100%
rename from src/processor/http/perform-activitypub.ts
rename to src/queue/processors/http/perform-activitypub.ts
diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts
new file mode 100644
index 000000000..fff1fbf66
--- /dev/null
+++ b/src/queue/processors/http/process-inbox.ts
@@ -0,0 +1,55 @@
+import * as kue from 'kue';
+
+import { verifySignature } from 'http-signature';
+import parseAcct from '../../acct/parse';
+import User, { IRemoteUser } from '../../models/user';
+import act from '../../remote/activitypub/act';
+import resolvePerson from '../../remote/activitypub/resolve-person';
+import Resolver from '../../remote/activitypub/resolver';
+
+// ユーザーのinboxにアクティビティが届いた時の処理
+export default async (job: kue.Job, done): Promise<void> => {
+	const signature = job.data.signature;
+	const activity = job.data.activity;
+
+	const keyIdLower = signature.keyId.toLowerCase();
+	let user;
+
+	if (keyIdLower.startsWith('acct:')) {
+		const { username, host } = parseAcct(keyIdLower.slice('acct:'.length));
+		if (host === null) {
+			console.warn(`request was made by local user: @${username}`);
+			done();
+		}
+
+		user = await User.findOne({ usernameLower: username, hostLower: host }) as IRemoteUser;
+	} else {
+		user = await User.findOne({
+			host: { $ne: null },
+			'account.publicKey.id': signature.keyId
+		}) as IRemoteUser;
+
+		// アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する
+		if (user === null) {
+			user = await resolvePerson(signature.keyId);
+		}
+	}
+
+	if (user === null) {
+		done(new Error('failed to resolve user'));
+		return;
+	}
+
+	if (!verifySignature(signature, user.account.publicKey.publicKeyPem)) {
+		done(new Error('signature verification failed'));
+		return;
+	}
+
+	// アクティビティを処理
+	try {
+		await act(new Resolver(), user, activity);
+		done();
+	} catch (e) {
+		done(e);
+	}
+};
diff --git a/src/processor/http/report-github-failure.ts b/src/queue/processors/http/report-github-failure.ts
similarity index 100%
rename from src/processor/http/report-github-failure.ts
rename to src/queue/processors/http/report-github-failure.ts
diff --git a/src/processor/http/unfollow.ts b/src/queue/processors/http/unfollow.ts
similarity index 100%
rename from src/processor/http/unfollow.ts
rename to src/queue/processors/http/unfollow.ts
diff --git a/src/processor/index.ts b/src/queue/processors/index.ts
similarity index 100%
rename from src/processor/index.ts
rename to src/queue/processors/index.ts
diff --git a/src/remote/activitypub/act/create.ts b/src/remote/activitypub/act/create.ts
index fa681982c..c1a30ce7d 100644
--- a/src/remote/activitypub/act/create.ts
+++ b/src/remote/activitypub/act/create.ts
@@ -1,10 +1,92 @@
-import create from '../create';
-import Resolver from '../resolver';
+import { JSDOM } from 'jsdom';
+const createDOMPurify = require('dompurify');
 
-export default (resolver: Resolver, actor, activity, distribute) => {
+import Resolver from '../resolver';
+import DriveFile from '../../../models/drive-file';
+import Post from '../../../models/post';
+import uploadFromUrl from '../../../drive/upload-from-url';
+import createPost from '../../../post/create';
+
+export default async (resolver: Resolver, actor, activity): Promise<void> => {
 	if ('actor' in activity && actor.account.uri !== activity.actor) {
-		throw new Error();
+		throw new Error('invalid actor');
 	}
 
-	return create(resolver, actor, activity.object, distribute);
+	const uri = activity.id || activity;
+
+	try {
+		await Promise.all([
+			DriveFile.findOne({ 'metadata.uri': uri }).then(file => {
+				if (file !== null) {
+					throw new Error();
+				}
+			}, () => {}),
+			Post.findOne({ uri }).then(post => {
+				if (post !== null) {
+					throw new Error();
+				}
+			}, () => {})
+		]);
+	} catch (object) {
+		throw new Error(`already registered: ${uri}`);
+	}
+
+	const object = await resolver.resolve(activity);
+
+	switch (object.type) {
+	case 'Image':
+		createImage(resolver, object);
+		break;
+
+	case 'Note':
+		createNote(resolver, object);
+		break;
+	}
+
+	///
+
+	async function createImage(resolver: Resolver, image) {
+		if ('attributedTo' in image && actor.account.uri !== image.attributedTo) {
+			throw new Error('invalid image');
+		}
+
+		return await uploadFromUrl(image.url, actor);
+	}
+
+	async function createNote(resolver: Resolver, note) {
+		if (
+			('attributedTo' in note && actor.account.uri !== note.attributedTo) ||
+			typeof note.id !== 'string'
+		) {
+			throw new Error('invalid note');
+		}
+
+		const mediaIds = [];
+
+		if ('attachment' in note) {
+			note.attachment.forEach(async media => {
+				const created = await createImage(resolver, media);
+				mediaIds.push(created._id);
+			});
+		}
+
+		const { window } = new JSDOM(note.content);
+
+		await createPost(actor, {
+			channelId: undefined,
+			index: undefined,
+			createdAt: new Date(note.published),
+			mediaIds,
+			replyId: undefined,
+			repostId: undefined,
+			poll: undefined,
+			text: window.document.body.textContent,
+			textHtml: note.content && createDOMPurify(window).sanitize(note.content),
+			userId: actor._id,
+			appId: null,
+			viaMobile: false,
+			geo: undefined,
+			uri: note.id
+		}, null, null, []);
+	}
 };
diff --git a/src/remote/activitypub/act/index.ts b/src/remote/activitypub/act/index.ts
index d282e1288..d78335f16 100644
--- a/src/remote/activitypub/act/index.ts
+++ b/src/remote/activitypub/act/index.ts
@@ -2,35 +2,29 @@ import create from './create';
 import performDeleteActivity from './delete';
 import follow from './follow';
 import undo from './undo';
-import createObject from '../create';
 import Resolver from '../resolver';
+import { IObject } from '../type';
 
-export default async (parentResolver: Resolver, actor, value, distribute?: boolean) => {
-	const collection = await parentResolver.resolveCollection(value);
+export default async (parentResolver: Resolver, actor, activity: IObject): Promise<void> => {
+	switch (activity.type) {
+	case 'Create':
+		await create(parentResolver, actor, activity);
+		break;
 
-	return collection.object.map(async element => {
-		const { resolver, object } = await collection.resolver.resolveOne(element);
-		const created = await (await createObject(resolver, actor, [object], distribute))[0];
+	case 'Delete':
+		await performDeleteActivity(parentResolver, actor, activity);
+		break;
 
-		if (created !== null) {
-			return created;
-		}
+	case 'Follow':
+		await follow(parentResolver, actor, activity);
+		break;
 
-		switch (object.type) {
-		case 'Create':
-			return create(resolver, actor, object, distribute);
+	case 'Undo':
+		await undo(parentResolver, actor, activity);
+		break;
 
-		case 'Delete':
-			return performDeleteActivity(resolver, actor, object);
-
-		case 'Follow':
-			return follow(resolver, actor, object, distribute);
-
-		case 'Undo':
-			return undo(resolver, actor, object);
-
-		default:
-			return null;
-		}
-	});
+	default:
+		console.warn(`unknown activity type: ${activity.type}`);
+		return null;
+	}
 };
diff --git a/src/remote/activitypub/create.ts b/src/remote/activitypub/create.ts
deleted file mode 100644
index 97c72860f..000000000
--- a/src/remote/activitypub/create.ts
+++ /dev/null
@@ -1,158 +0,0 @@
-import { JSDOM } from 'jsdom';
-import { ObjectID } from 'mongodb';
-import config from '../../config';
-import DriveFile from '../../models/drive-file';
-import Post from '../../models/post';
-import { IRemoteUser } from '../../models/user';
-import uploadFromUrl from '../../drive/upload-from-url';
-import createPost from '../../post/create';
-import distributePost from '../../post/distribute';
-import Resolver from './resolver';
-const createDOMPurify = require('dompurify');
-
-type IResult = {
-	resolver: Resolver;
-	object: {
-		$ref: string;
-		$id: ObjectID;
-	};
-};
-
-class Creator {
-	private actor: IRemoteUser;
-	private distribute: boolean;
-
-	constructor(actor, distribute) {
-		this.actor = actor;
-		this.distribute = distribute;
-	}
-
-	private async createImage(resolver: Resolver, image) {
-		if ('attributedTo' in image && this.actor.account.uri !== image.attributedTo) {
-			throw new Error();
-		}
-
-		const { _id } = await uploadFromUrl(image.url, this.actor, image.id || null);
-		return {
-			resolver,
-			object: { $ref: 'driveFiles.files', $id: _id }
-		};
-	}
-
-	private async createNote(resolver: Resolver, note) {
-		if (
-			('attributedTo' in note && this.actor.account.uri !== note.attributedTo) ||
-			typeof note.id !== 'string'
-		) {
-			throw new Error();
-		}
-
-		const mediaIds = 'attachment' in note &&
-			(await Promise.all(await this.create(resolver, note.attachment)))
-				.filter(media => media !== null && media.object.$ref === 'driveFiles.files')
-				.map(({ object }) => object.$id);
-
-		const { window } = new JSDOM(note.content);
-
-		const inserted = await createPost({
-			channelId: undefined,
-			index: undefined,
-			createdAt: new Date(note.published),
-			mediaIds,
-			replyId: undefined,
-			repostId: undefined,
-			poll: undefined,
-			text: window.document.body.textContent,
-			textHtml: note.content && createDOMPurify(window).sanitize(note.content),
-			userId: this.actor._id,
-			appId: null,
-			viaMobile: false,
-			geo: undefined,
-			uri: note.id
-		}, null, null, []);
-
-		const promises = [];
-
-		if (this.distribute) {
-			promises.push(distributePost(this.actor, inserted.mentions, inserted));
-		}
-
-		// Register to search database
-		if (note.content && config.elasticsearch.enable) {
-			const es = require('../../db/elasticsearch');
-
-			promises.push(new Promise((resolve, reject) => {
-				es.index({
-					index: 'misskey',
-					type: 'post',
-					id: inserted._id.toString(),
-					body: {
-						text: window.document.body.textContent
-					}
-				}, resolve);
-			}));
-		}
-
-		await Promise.all(promises);
-
-		return {
-			resolver,
-			object: { $ref: 'posts', id: inserted._id }
-		};
-	}
-
-	public async create(parentResolver: Resolver, value): Promise<Array<Promise<IResult>>> {
-		const collection = await parentResolver.resolveCollection(value);
-
-		return collection.object.map(async element => {
-			const uri = element.id || element;
-
-			try {
-				await Promise.all([
-					DriveFile.findOne({ 'metadata.uri': uri }).then(file => {
-						if (file === null) {
-							return;
-						}
-
-						throw {
-							$ref: 'driveFile.files',
-							$id: file._id
-						};
-					}, () => {}),
-					Post.findOne({ uri }).then(post => {
-						if (post === null) {
-							return;
-						}
-
-						throw {
-							$ref: 'posts',
-							$id: post._id
-						};
-					}, () => {})
-				]);
-			} catch (object) {
-				return {
-					resolver: collection.resolver,
-					object
-				};
-			}
-
-			const { resolver, object } = await collection.resolver.resolveOne(element);
-
-			switch (object.type) {
-			case 'Image':
-				return this.createImage(resolver, object);
-
-			case 'Note':
-				return this.createNote(resolver, object);
-			}
-
-			return null;
-		});
-	}
-}
-
-export default (resolver: Resolver, actor, value, distribute?: boolean) => {
-	const creator = new Creator(actor, distribute);
-	return creator.create(resolver, value);
-};
diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts
index 371ccdcc3..de0bba268 100644
--- a/src/remote/activitypub/resolver.ts
+++ b/src/remote/activitypub/resolver.ts
@@ -1,20 +1,45 @@
+import { IObject } from "./type";
+
 const request = require('request-promise-native');
 
 export default class Resolver {
-	private requesting: Set<string>;
+	private history: Set<string>;
 
-	constructor(iterable?: Iterable<string>) {
-		this.requesting = new Set(iterable);
+	constructor() {
+		this.history = new Set();
 	}
 
-	private async resolveUnrequestedOne(value) {
-		if (typeof value !== 'string') {
-			return { resolver: this, object: value };
+	public async resolveCollection(value) {
+		const collection = typeof value === 'string'
+			? await this.resolve(value)
+			: value;
+
+		switch (collection.type) {
+		case 'Collection':
+			collection.objects = collection.object.items;
+			break;
+
+		case 'OrderedCollection':
+			collection.objects = collection.object.orderedItems;
+			break;
+
+		default:
+			throw new Error(`unknown collection type: ${collection.type}`);
 		}
 
-		const resolver = new Resolver(this.requesting);
+		return collection;
+	}
 
-		resolver.requesting.add(value);
+	public async resolve(value): Promise<IObject> {
+		if (typeof value !== 'string') {
+			return value;
+		}
+
+		if (this.history.has(value)) {
+			throw new Error('cannot resolve already resolved one');
+		}
+
+		this.history.add(value);
 
 		const object = await request({
 			url: value,
@@ -29,41 +54,9 @@ export default class Resolver {
 				!object['@context'].includes('https://www.w3.org/ns/activitystreams') :
 				object['@context'] !== 'https://www.w3.org/ns/activitystreams'
 		)) {
-			throw new Error();
+			throw new Error('invalid response');
 		}
 
-		return { resolver, object };
-	}
-
-	public async resolveCollection(value) {
-		const resolved = typeof value === 'string' ?
-			await this.resolveUnrequestedOne(value) :
-			{ resolver: this, object: value };
-
-		switch (resolved.object.type) {
-		case 'Collection':
-			resolved.object = resolved.object.items;
-			break;
-
-		case 'OrderedCollection':
-			resolved.object = resolved.object.orderedItems;
-			break;
-
-		default:
-			if (!Array.isArray(value)) {
-				resolved.object = [resolved.object];
-			}
-			break;
-		}
-
-		return resolved;
-	}
-
-	public resolveOne(value) {
-		if (this.requesting.has(value)) {
-			throw new Error();
-		}
-
-		return this.resolveUnrequestedOne(value);
+		return object;
 	}
 }
diff --git a/src/server/activitypub/inbox.ts b/src/server/activitypub/inbox.ts
index 5de843385..847dc19af 100644
--- a/src/server/activitypub/inbox.ts
+++ b/src/server/activitypub/inbox.ts
@@ -24,7 +24,7 @@ app.post('/@:user/inbox', bodyParser.json({
 
 	queue.create('http', {
 		type: 'processInbox',
-		inbox: req.body,
+		activity: req.body,
 		signature,
 	}).save();
 

From 07884a383ef80ec4a961a8573b1fb3dce853fcc9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 4 Apr 2018 23:22:48 +0900
Subject: [PATCH 1078/1250] wip

---
 src/queue/processors/http/deliver.ts | 16 +++-------------
 1 file changed, 3 insertions(+), 13 deletions(-)

diff --git a/src/queue/processors/http/deliver.ts b/src/queue/processors/http/deliver.ts
index 8cd9eb624..1700063a5 100644
--- a/src/queue/processors/http/deliver.ts
+++ b/src/queue/processors/http/deliver.ts
@@ -1,17 +1,7 @@
 import * as kue from 'kue';
 
-import Channel from '../../models/channel';
-import Following from '../../models/following';
-import ChannelWatching from '../../models/channel-watching';
-import Post, { pack } from '../../models/post';
-import User, { isLocalUser } from '../../models/user';
-import stream, { publishChannelStream } from '../../publishers/stream';
-import context from '../../remote/activitypub/renderer/context';
-import renderCreate from '../../remote/activitypub/renderer/create';
-import renderNote from '../../remote/activitypub/renderer/note';
-import request from '../../remote/request';
+import request from '../../../remote/request';
 
 export default async (job: kue.Job, done): Promise<void> => {
-
-	request(user, following.follower[0].account.inbox, create);
-}
+	await request(job.data.user, job.data.to, job.data.content);
+};

From de40762ee855498c6d60d6c4c2b50609bb6edbdb Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 4 Apr 2018 23:59:38 +0900
Subject: [PATCH 1079/1250] wip

---
 src/{ => api}/drive/add-file.ts        |  0
 src/{ => api}/drive/upload-from-url.ts |  0
 src/api/following/create.ts            | 82 ++++++++++++++++++++++
 src/{ => api}/post/create.ts           |  0
 src/{ => api}/post/distribute.ts       |  0
 src/{ => api}/post/watch.ts            |  0
 src/queue/processors/http/unfollow.ts  | 97 ++++++++++++++------------
 src/remote/activitypub/act/create.ts   |  8 ++-
 src/remote/activitypub/act/follow.ts   | 59 +---------------
 9 files changed, 142 insertions(+), 104 deletions(-)
 rename src/{ => api}/drive/add-file.ts (100%)
 rename src/{ => api}/drive/upload-from-url.ts (100%)
 create mode 100644 src/api/following/create.ts
 rename src/{ => api}/post/create.ts (100%)
 rename src/{ => api}/post/distribute.ts (100%)
 rename src/{ => api}/post/watch.ts (100%)

diff --git a/src/drive/add-file.ts b/src/api/drive/add-file.ts
similarity index 100%
rename from src/drive/add-file.ts
rename to src/api/drive/add-file.ts
diff --git a/src/drive/upload-from-url.ts b/src/api/drive/upload-from-url.ts
similarity index 100%
rename from src/drive/upload-from-url.ts
rename to src/api/drive/upload-from-url.ts
diff --git a/src/api/following/create.ts b/src/api/following/create.ts
new file mode 100644
index 000000000..353a6c892
--- /dev/null
+++ b/src/api/following/create.ts
@@ -0,0 +1,82 @@
+import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user';
+import Following from '../../models/following';
+import FollowingLog from '../../models/following-log';
+import FollowedLog from '../../models/followed-log';
+import event from '../../publishers/stream';
+import notify from '../../publishers/notify';
+import context from '../../remote/activitypub/renderer/context';
+import renderFollow from '../../remote/activitypub/renderer/follow';
+import renderAccept from '../../remote/activitypub/renderer/accept';
+import { createHttp } from '../../queue';
+
+export default async function(follower: IUser, followee: IUser, activity?) {
+	const following = await Following.insert({
+		createdAt: new Date(),
+		followerId: follower._id,
+		followeeId: followee._id
+	});
+
+	//#region Increment following count
+	User.update({ _id: follower._id }, {
+		$inc: {
+			followingCount: 1
+		}
+	});
+
+	FollowingLog.insert({
+		createdAt: following.createdAt,
+		userId: follower._id,
+		count: follower.followingCount + 1
+	});
+	//#endregion
+
+	//#region Increment followers count
+	User.update({ _id: followee._id }, {
+		$inc: {
+			followersCount: 1
+		}
+	});
+	FollowedLog.insert({
+		createdAt: following.createdAt,
+		userId: followee._id,
+		count: followee.followersCount + 1
+	});
+	//#endregion
+
+	// Publish follow event
+	if (isLocalUser(follower)) {
+		packUser(followee, follower).then(packed => event(follower._id, 'follow', packed));
+	}
+
+	// Publish followed event
+	if (isLocalUser(followee)) {
+		packUser(follower, followee).then(packed => event(followee._id, 'followed', packed)),
+
+		// 通知を作成
+		notify(followee._id, follower._id, 'follow');
+	}
+
+	if (isLocalUser(follower) && isRemoteUser(followee)) {
+		const content = renderFollow(follower, followee);
+		content['@context'] = context;
+
+		createHttp({
+			type: 'deliver',
+			user: follower,
+			content,
+			to: followee.account.inbox
+		}).save();
+	}
+
+	if (isRemoteUser(follower) && isLocalUser(followee)) {
+		const content = renderAccept(activity);
+		content['@context'] = context;
+
+		createHttp({
+			type: 'deliver',
+			user: followee,
+			content,
+			to: follower.account.inbox
+		}).save();
+	}
+}
diff --git a/src/post/create.ts b/src/api/post/create.ts
similarity index 100%
rename from src/post/create.ts
rename to src/api/post/create.ts
diff --git a/src/post/distribute.ts b/src/api/post/distribute.ts
similarity index 100%
rename from src/post/distribute.ts
rename to src/api/post/distribute.ts
diff --git a/src/post/watch.ts b/src/api/post/watch.ts
similarity index 100%
rename from src/post/watch.ts
rename to src/api/post/watch.ts
diff --git a/src/queue/processors/http/unfollow.ts b/src/queue/processors/http/unfollow.ts
index d3d5f2246..801a3612a 100644
--- a/src/queue/processors/http/unfollow.ts
+++ b/src/queue/processors/http/unfollow.ts
@@ -1,56 +1,63 @@
-import FollowedLog from '../../models/followed-log';
-import Following from '../../models/following';
-import FollowingLog from '../../models/following-log';
-import User, { isRemoteUser, pack as packUser } from '../../models/user';
-import stream from '../../publishers/stream';
-import renderFollow from '../../remote/activitypub/renderer/follow';
-import renderUndo from '../../remote/activitypub/renderer/undo';
-import context from '../../remote/activitypub/renderer/context';
-import request from '../../remote/request';
+import FollowedLog from '../../../models/followed-log';
+import Following from '../../../models/following';
+import FollowingLog from '../../../models/following-log';
+import User, { isLocalUser, isRemoteUser, pack as packUser } from '../../../models/user';
+import stream from '../../../publishers/stream';
+import renderFollow from '../../../remote/activitypub/renderer/follow';
+import renderUndo from '../../../remote/activitypub/renderer/undo';
+import context from '../../../remote/activitypub/renderer/context';
+import request from '../../../remote/request';
+import Logger from '../../../utils/logger';
 
 export default async ({ data }) => {
-	// Delete following
-	const following = await Following.findOneAndDelete({ _id: data.id });
+	const following = await Following.findOne({ _id: data.id });
 	if (following === null) {
 		return;
 	}
 
-	const promisedFollower = User.findOne({ _id: following.followerId });
-	const promisedFollowee = User.findOne({ _id: following.followeeId });
+	const [follower, followee] = await Promise.all([
+		User.findOne({ _id: following.followerId }),
+		User.findOne({ _id: following.followeeId })
+	]);
 
-	await Promise.all([
-		// Decrement following count
-		User.update({ _id: following.followerId }, { $inc: { followingCount: -1 } }),
-		promisedFollower.then(({ followingCount }) => FollowingLog.insert({
-			createdAt: new Date(),
-			userId: following.followerId,
-			count: followingCount - 1
-		})),
+	if (isLocalUser(follower) && isRemoteUser(followee)) {
+		const undo = renderUndo(renderFollow(follower, followee));
+		undo['@context'] = context;
 
-		// Decrement followers count
-		User.update({ _id: following.followeeId }, { $inc: { followersCount: -1 } }),
-		promisedFollowee.then(({ followersCount }) => FollowedLog.insert({
-			createdAt: new Date(),
-			userId: following.followeeId,
-			count: followersCount - 1
-		})),
+		await request(follower, followee.account.inbox, undo);
+	}
+
+	try {
+		await Promise.all([
+			// Delete following
+			Following.findOneAndDelete({ _id: data.id }),
+
+			// Decrement following count
+			User.update({ _id: follower._id }, { $inc: { followingCount: -1 } }),
+			FollowingLog.insert({
+				createdAt: new Date(),
+				userId: follower._id,
+				count: follower.followingCount - 1
+			}),
+
+			// Decrement followers count
+			User.update({ _id: followee._id }, { $inc: { followersCount: -1 } }),
+			FollowedLog.insert({
+				createdAt: new Date(),
+				userId: followee._id,
+				count: followee.followersCount - 1
+			})
+		]);
+
+		if (isLocalUser(follower)) {
+			return;
+		}
+
+		const promisedPackedUser = packUser(followee, follower);
 
 		// Publish follow event
-		Promise.all([promisedFollower, promisedFollowee]).then(async ([follower, followee]) => {
-			if (isRemoteUser(follower)) {
-				return;
-			}
-
-			const promisedPackedUser = packUser(followee, follower);
-
-			if (isRemoteUser(followee)) {
-				const undo = renderUndo(renderFollow(follower, followee));
-				undo['@context'] = context;
-
-				await request(follower, followee.account.inbox, undo);
-			}
-
-			stream(follower._id, 'unfollow', promisedPackedUser);
-		})
-	]);
+		stream(follower._id, 'unfollow', promisedPackedUser);
+	} catch (error) {
+		Logger.error(error.toString());
+	}
 };
diff --git a/src/remote/activitypub/act/create.ts b/src/remote/activitypub/act/create.ts
index c1a30ce7d..7ee9f8dfb 100644
--- a/src/remote/activitypub/act/create.ts
+++ b/src/remote/activitypub/act/create.ts
@@ -4,10 +4,10 @@ const createDOMPurify = require('dompurify');
 import Resolver from '../resolver';
 import DriveFile from '../../../models/drive-file';
 import Post from '../../../models/post';
-import uploadFromUrl from '../../../drive/upload-from-url';
-import createPost from '../../../post/create';
+import uploadFromUrl from '../../../api/drive/upload-from-url';
+import createPost from '../../../api/post/create';
 
-export default async (resolver: Resolver, actor, activity): Promise<void> => {
+export default async (actor, activity): Promise<void> => {
 	if ('actor' in activity && actor.account.uri !== activity.actor) {
 		throw new Error('invalid actor');
 	}
@@ -31,6 +31,8 @@ export default async (resolver: Resolver, actor, activity): Promise<void> => {
 		throw new Error(`already registered: ${uri}`);
 	}
 
+	const resolver = new Resolver();
+
 	const object = await resolver.resolve(activity);
 
 	switch (object.type) {
diff --git a/src/remote/activitypub/act/follow.ts b/src/remote/activitypub/act/follow.ts
index 23fa41df8..dc173a0ac 100644
--- a/src/remote/activitypub/act/follow.ts
+++ b/src/remote/activitypub/act/follow.ts
@@ -1,15 +1,9 @@
-import { MongoError } from 'mongodb';
 import parseAcct from '../../../acct/parse';
-import Following, { IFollowing } from '../../../models/following';
 import User from '../../../models/user';
 import config from '../../../config';
-import queue from '../../../queue';
-import context from '../renderer/context';
-import renderAccept from '../renderer/accept';
-import request from '../../request';
-import Resolver from '../resolver';
+import follow from '../../../api/following/create';
 
-export default async (resolver: Resolver, actor, activity, distribute) => {
+export default async (actor, activity): Promise<void> => {
 	const prefix = config.url + '/@';
 	const id = activity.object.id || activity.object;
 
@@ -27,52 +21,5 @@ export default async (resolver: Resolver, actor, activity, distribute) => {
 		throw new Error();
 	}
 
-	if (!distribute) {
-		const { _id } = await Following.findOne({
-			followerId: actor._id,
-			followeeId: followee._id
-		});
-
-		return {
-			resolver,
-			object: { $ref: 'following', $id: _id }
-		};
-	}
-
-	const promisedFollowing = Following.insert({
-		createdAt: new Date(),
-		followerId: actor._id,
-		followeeId: followee._id
-	}).then(following => new Promise((resolve, reject) => {
-		queue.create('http', {
-			type: 'follow',
-			following: following._id
-		}).save(error => {
-			if (error) {
-				reject(error);
-			} else {
-				resolve(following);
-			}
-		});
-	}) as Promise<IFollowing>, async error => {
-		// duplicate key error
-		if (error instanceof MongoError && error.code === 11000) {
-			return Following.findOne({
-				followerId: actor._id,
-				followeeId: followee._id
-			});
-		}
-
-		throw error;
-	});
-
-	const accept = renderAccept(activity);
-	accept['@context'] = context;
-
-	await request(followee, actor.account.inbox, accept);
-
-	return promisedFollowing.then(({ _id }) => ({
-		resolver,
-		object: { $ref: 'following', $id: _id }
-	}));
+	await follow(actor, followee, activity);
 };

From 8ff4b0011448e9e4ad0e444f0fb6fa2cd965d51d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 5 Apr 2018 00:01:01 +0900
Subject: [PATCH 1080/1250] wip

---
 src/queue/processors/http/follow.ts | 69 -----------------------------
 1 file changed, 69 deletions(-)
 delete mode 100644 src/queue/processors/http/follow.ts

diff --git a/src/queue/processors/http/follow.ts b/src/queue/processors/http/follow.ts
deleted file mode 100644
index 8bf890efb..000000000
--- a/src/queue/processors/http/follow.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import User, { isLocalUser, pack as packUser } from '../../models/user';
-import Following from '../../models/following';
-import FollowingLog from '../../models/following-log';
-import FollowedLog from '../../models/followed-log';
-import event from '../../publishers/stream';
-import notify from '../../publishers/notify';
-import context from '../../remote/activitypub/renderer/context';
-import render from '../../remote/activitypub/renderer/follow';
-import request from '../../remote/request';
-
-export default ({ data }) => Following.findOne({ _id: data.following }).then(({ followerId, followeeId }) => {
-	const promisedFollower = User.findOne({ _id: followerId });
-	const promisedFollowee = User.findOne({ _id: followeeId });
-
-	return Promise.all([
-		// Increment following count
-		User.update(followerId, {
-			$inc: {
-				followingCount: 1
-			}
-		}),
-
-		promisedFollower.then(({ followingCount }) => FollowingLog.insert({
-			createdAt: data.following.createdAt,
-			userId: followerId,
-			count: followingCount + 1
-		})),
-
-		// Increment followers count
-		User.update({ _id: followeeId }, {
-			$inc: {
-				followersCount: 1
-			}
-		}),
-
-		promisedFollowee.then(({ followersCount }) => FollowedLog.insert({
-			createdAt: data.following.createdAt,
-			userId: followerId,
-			count: followersCount + 1
-		})),
-
-		// Notify
-		promisedFollowee.then(followee => followee.host === null ?
-			notify(followeeId, followerId, 'follow') : null),
-
-		// Publish follow event
-		Promise.all([promisedFollower, promisedFollowee]).then(([follower, followee]) => {
-			let followerEvent;
-			let followeeEvent;
-
-			if (isLocalUser(follower)) {
-				followerEvent = packUser(followee, follower)
-					.then(packed => event(follower._id, 'follow', packed));
-			}
-
-			if (isLocalUser(followee)) {
-				followeeEvent = packUser(follower, followee)
-					.then(packed => event(followee._id, 'followed', packed));
-			} else if (isLocalUser(follower)) {
-				const rendered = render(follower, followee);
-				rendered['@context'] = context;
-
-				followeeEvent = request(follower, followee.account.inbox, rendered);
-			}
-
-			return Promise.all([followerEvent, followeeEvent]);
-		})
-	]);
-});

From 2f08b56498b4d05f0a070313ca8ff47920e702ed Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 5 Apr 2018 00:40:34 +0900
Subject: [PATCH 1081/1250] wip

---
 src/api/post/create.ts     | 290 ++++++++++++++++++++++++++++++-------
 src/api/post/distribute.ts | 190 ------------------------
 2 files changed, 235 insertions(+), 245 deletions(-)
 delete mode 100644 src/api/post/distribute.ts

diff --git a/src/api/post/create.ts b/src/api/post/create.ts
index f78bbe752..af94c6d81 100644
--- a/src/api/post/create.ts
+++ b/src/api/post/create.ts
@@ -1,76 +1,90 @@
-import parseAcct from '../acct/parse';
-import Post, { pack } from '../models/post';
-import User, { isLocalUser, isRemoteUser, IUser } from '../models/user';
-import stream from '../publishers/stream';
-import Following from '../models/following';
-import { createHttp } from '../queue';
-import renderNote from '../remote/activitypub/renderer/note';
-import renderCreate from '../remote/activitypub/renderer/create';
-import context from '../remote/activitypub/renderer/context';
+import parseAcct from '../../acct/parse';
+import Post, { pack, IPost } from '../../models/post';
+import User, { isLocalUser, isRemoteUser, IUser } from '../../models/user';
+import stream from '../../publishers/stream';
+import Following from '../../models/following';
+import { createHttp } from '../../queue';
+import renderNote from '../../remote/activitypub/renderer/note';
+import renderCreate from '../../remote/activitypub/renderer/create';
+import context from '../../remote/activitypub/renderer/context';
+import { IDriveFile } from '../../models/drive-file';
+import notify from '../../publishers/notify';
+import PostWatching from '../../models/post-watching';
+import watch from './watch';
+import Mute from '../../models/mute';
+import pushSw from '../../publishers/push-sw';
+import event from '../../publishers/stream';
+import parse from '../../text/parse';
 
-export default async (user: IUser, post, reply, repost, atMentions) => {
-	post.mentions = [];
+export default async (user: IUser, content: {
+	createdAt: Date;
+	text: string;
+	reply: IPost;
+	repost: IPost;
+	media: IDriveFile[];
+	geo: any;
+	viaMobile: boolean;
+	tags: string[];
+}) => new Promise(async (res, rej) => {
+	const tags = content.tags || [];
 
-	function addMention(mentionee) {
-		// Reject if already added
-		if (post.mentions.some(x => x.equals(mentionee))) return;
+	let tokens = null;
 
-		// Add mention
-		post.mentions.push(mentionee);
+	if (content.text) {
+		// Analyze
+		tokens = parse(content.text);
+
+		// Extract hashtags
+		const hashtags = tokens
+			.filter(t => t.type == 'hashtag')
+			.map(t => t.hashtag);
+
+		hashtags.forEach(tag => {
+			if (tags.indexOf(tag) == -1) {
+				tags.push(tag);
+			}
+		});
 	}
 
-	if (reply) {
-		// Add mention
-		addMention(reply.userId);
-		post.replyId = reply._id;
-		post._reply = { userId: reply.userId };
-	} else {
-		post.replyId = null;
-		post._reply = null;
-	}
+	// 投稿を作成
+	const post = await Post.insert({
+		createdAt: content.createdAt,
+		mediaIds: content.media ? content.media.map(file => file._id) : [],
+		replyId: content.reply ? content.reply._id : null,
+		repostId: content.repost ? content.repost._id : null,
+		text: content.text,
+		tags,
+		userId: user._id,
+		viaMobile: content.viaMobile,
+		geo: content.geo || null,
 
-	if (repost) {
-		if (post.text) {
-			// Add mention
-			addMention(repost.userId);
-		}
+		// 以下非正規化データ
+		_reply: content.reply ? { userId: content.reply.userId } : null,
+		_repost: content.repost ? { userId: content.repost.userId } : null,
+	});
 
-		post.repostId = repost._id;
-		post._repost = { userId: repost.userId };
-	} else {
-		post.repostId = null;
-		post._repost = null;
-	}
-
-	await Promise.all(atMentions.map(async mention => {
-		// Fetch mentioned user
-		// SELECT _id
-		const { _id } = await User
-			.findOne(parseAcct(mention), { _id: true });
-
-		// Add mention
-		addMention(_id);
-	}));
-
-	const inserted = await Post.insert(post);
+	res(post);
 
 	User.update({ _id: user._id }, {
-		// Increment my posts count
+		// Increment posts count
 		$inc: {
 			postsCount: 1
 		},
-
+		// Update latest post
 		$set: {
-			latestPost: post._id
+			latestPost: post
 		}
 	});
 
-	const postObj = await pack(inserted);
+	// Serialize
+	const postObj = await pack(post);
 
 	// タイムラインへの投稿
 	if (!post.channelId) {
 		// Publish event to myself's stream
-		stream(post.userId, 'post', postObj);
+		if (isLocalUser(user)) {
+			stream(post.userId, 'post', postObj);
+		}
 
 		// Fetch all followers
 		const followers = await Following.aggregate([{
@@ -144,6 +158,172 @@ export default async (user: IUser, post, reply, repost, atMentions) => {
 		);
 	}*/
 
-	return Promise.all(promises);
+	const mentions = [];
 
-};
+	async function addMention(mentionee, reason) {
+		// Reject if already added
+		if (mentions.some(x => x.equals(mentionee))) return;
+
+		// Add mention
+		mentions.push(mentionee);
+
+		// Publish event
+		if (!user._id.equals(mentionee)) {
+			const mentioneeMutes = await Mute.find({
+				muter_id: mentionee,
+				deleted_at: { $exists: false }
+			});
+			const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString());
+			if (mentioneesMutedUserIds.indexOf(user._id.toString()) == -1) {
+				event(mentionee, reason, postObj);
+				pushSw(mentionee, reason, postObj);
+			}
+		}
+	}
+
+	// If has in reply to post
+	if (content.reply) {
+		// Increment replies count
+		Post.update({ _id: content.reply._id }, {
+			$inc: {
+				repliesCount: 1
+			}
+		});
+
+		// (自分自身へのリプライでない限りは)通知を作成
+		notify(content.reply.userId, user._id, 'reply', {
+			postId: post._id
+		});
+
+		// Fetch watchers
+		PostWatching.find({
+			postId: content.reply._id,
+			userId: { $ne: user._id },
+			// 削除されたドキュメントは除く
+			deletedAt: { $exists: false }
+		}, {
+			fields: {
+				userId: true
+			}
+		}).then(watchers => {
+			watchers.forEach(watcher => {
+				notify(watcher.userId, user._id, 'reply', {
+					postId: post._id
+				});
+			});
+		});
+
+		// この投稿をWatchする
+		if (isLocalUser(user) && user.account.settings.autoWatch !== false) {
+			watch(user._id, content.reply);
+		}
+
+		// Add mention
+		addMention(content.reply.userId, 'reply');
+	}
+
+	// If it is repost
+	if (content.repost) {
+		// Notify
+		const type = content.text ? 'quote' : 'repost';
+		notify(content.repost.userId, user._id, type, {
+			post_id: post._id
+		});
+
+		// Fetch watchers
+		PostWatching.find({
+			postId: content.repost._id,
+			userId: { $ne: user._id },
+			// 削除されたドキュメントは除く
+			deletedAt: { $exists: false }
+		}, {
+			fields: {
+				userId: true
+			}
+		}).then(watchers => {
+			watchers.forEach(watcher => {
+				notify(watcher.userId, user._id, type, {
+					postId: post._id
+				});
+			});
+		});
+
+		// この投稿をWatchする
+		if (isLocalUser(user) && user.account.settings.autoWatch !== false) {
+			watch(user._id, content.repost);
+		}
+
+		// If it is quote repost
+		if (content.text) {
+			// Add mention
+			addMention(content.repost.userId, 'quote');
+		} else {
+			// Publish event
+			if (!user._id.equals(content.repost.userId)) {
+				event(content.repost.userId, 'repost', postObj);
+			}
+		}
+
+		// 今までで同じ投稿をRepostしているか
+		const existRepost = await Post.findOne({
+			userId: user._id,
+			repostId: content.repost._id,
+			_id: {
+				$ne: post._id
+			}
+		});
+
+		if (!existRepost) {
+			// Update repostee status
+			Post.update({ _id: content.repost._id }, {
+				$inc: {
+					repostCount: 1
+				}
+			});
+		}
+	}
+
+	// If has text content
+	if (content.text) {
+		// Extract an '@' mentions
+		const atMentions = tokens
+			.filter(t => t.type == 'mention')
+			.map(m => m.username)
+			// Drop dupulicates
+			.filter((v, i, s) => s.indexOf(v) == i);
+
+		// Resolve all mentions
+		await Promise.all(atMentions.map(async mention => {
+			// Fetch mentioned user
+			// SELECT _id
+			const mentionee = await User
+				.findOne({
+					usernameLower: mention.toLowerCase()
+				}, { _id: true });
+
+			// When mentioned user not found
+			if (mentionee == null) return;
+
+			// 既に言及されたユーザーに対する返信や引用repostの場合も無視
+			if (content.reply && content.reply.userId.equals(mentionee._id)) return;
+			if (content.repost && content.repost.userId.equals(mentionee._id)) return;
+
+			// Add mention
+			addMention(mentionee._id, 'mention');
+
+			// Create notification
+			notify(mentionee._id, user._id, 'mention', {
+				post_id: post._id
+			});
+		}));
+	}
+
+	// Append mentions data
+	if (mentions.length > 0) {
+		Post.update({ _id: post._id }, {
+			$set: {
+				mentions
+			}
+		});
+	}
+});
diff --git a/src/api/post/distribute.ts b/src/api/post/distribute.ts
deleted file mode 100644
index 49c6eb22d..000000000
--- a/src/api/post/distribute.ts
+++ /dev/null
@@ -1,190 +0,0 @@
-import Mute from '../models/mute';
-import Post, { pack } from '../models/post';
-import Watching from '../models/post-watching';
-import User from '../models/user';
-import stream from '../publishers/stream';
-import notify from '../publishers/notify';
-import pushSw from '../publishers/push-sw';
-import queue from '../queue';
-import watch from './watch';
-
-export default async (user, mentions, post) => {
-	const promisedPostObj = pack(post);
-	const promises = [
-		User.update({ _id: user._id }, {
-			// Increment my posts count
-			$inc: {
-				postsCount: 1
-			},
-
-			$set: {
-				latestPost: post._id
-			}
-		}),
-		new Promise((resolve, reject) => queue.create('http', {
-			type: 'deliverPost',
-			id: post._id,
-		}).save(error => error ? reject(error) : resolve())),
-	] as Array<Promise<any>>;
-
-	function addMention(promisedMentionee, reason) {
-		// Publish event
-		promises.push(promisedMentionee.then(mentionee => {
-			if (user._id.equals(mentionee)) {
-				return Promise.resolve();
-			}
-
-			return Promise.all([
-				promisedPostObj,
-				Mute.find({
-					muterId: mentionee,
-					deletedAt: { $exists: false }
-				})
-			]).then(([postObj, mentioneeMutes]) => {
-				const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString());
-				if (mentioneesMutedUserIds.indexOf(user._id.toString()) == -1) {
-					stream(mentionee, reason, postObj);
-					pushSw(mentionee, reason, postObj);
-				}
-			});
-		}));
-	}
-
-	// If has in reply to post
-	if (post.replyId) {
-		promises.push(
-			// Increment replies count
-			Post.update({ _id: post.replyId }, {
-				$inc: {
-					repliesCount: 1
-				}
-			}),
-
-			// 自分自身へのリプライでない限りは通知を作成
-			promisedPostObj.then(({ reply }) => {
-				return notify(reply.userId, user._id, 'reply', {
-					postId: post._id
-				});
-			}),
-
-			// Fetch watchers
-			Watching
-				.find({
-					postId: post.replyId,
-					userId: { $ne: user._id },
-					// 削除されたドキュメントは除く
-					deletedAt: { $exists: false }
-				}, {
-					fields: {
-						userId: true
-					}
-				})
-				.then(watchers => {
-					watchers.forEach(watcher => {
-						notify(watcher.userId, user._id, 'reply', {
-							postId: post._id
-						});
-					});
-				})
-		);
-
-		// Add mention
-		addMention(promisedPostObj.then(({ reply }) => reply.userId), 'reply');
-
-		// この投稿をWatchする
-		if (user.account.settings.autoWatch !== false) {
-			promises.push(promisedPostObj.then(({ reply }) => {
-				return watch(user._id, reply);
-			}));
-		}
-	}
-
-	// If it is repost
-	if (post.repostId) {
-		const type = post.text ? 'quote' : 'repost';
-
-		promises.push(
-			promisedPostObj.then(({ repost }) => Promise.all([
-				// Notify
-				notify(repost.userId, user._id, type, {
-					postId: post._id
-				}),
-
-				// この投稿をWatchする
-				// TODO: ユーザーが「Repostしたときに自動でWatchする」設定を
-				//       オフにしていた場合はしない
-				watch(user._id, repost)
-			])),
-
-			// Fetch watchers
-			Watching
-				.find({
-					postId: post.repostId,
-					userId: { $ne: user._id },
-					// 削除されたドキュメントは除く
-					deletedAt: { $exists: false }
-				}, {
-					fields: {
-						userId: true
-					}
-				})
-				.then(watchers => {
-					watchers.forEach(watcher => {
-						notify(watcher.userId, user._id, type, {
-							postId: post._id
-						});
-					});
-				})
-		);
-
-		// If it is quote repost
-		if (post.text) {
-			// Add mention
-			addMention(promisedPostObj.then(({ repost }) => repost.userId), 'quote');
-		} else {
-			promises.push(promisedPostObj.then(postObj => {
-				// Publish event
-				if (!user._id.equals(postObj.repost.userId)) {
-					stream(postObj.repost.userId, 'repost', postObj);
-				}
-			}));
-		}
-
-		// 今までで同じ投稿をRepostしているか
-		const existRepost = await Post.findOne({
-			userId: user._id,
-			repostId: post.repostId,
-			_id: {
-				$ne: post._id
-			}
-		});
-
-		if (!existRepost) {
-			// Update repostee status
-			promises.push(Post.update({ _id: post.repostId }, {
-				$inc: {
-					repostCount: 1
-				}
-			}));
-		}
-	}
-
-	// Resolve all mentions
-	await promisedPostObj.then(({ reply, repost }) => Promise.all(mentions.map(async mention => {
-		// 既に言及されたユーザーに対する返信や引用repostの場合も無視
-		if (reply && reply.userId.equals(mention)) return;
-		if (repost && repost.userId.equals(mention)) return;
-
-		// Add mention
-		addMention(mention, 'mention');
-
-		// Create notification
-		await notify(mention, user._id, 'mention', {
-			postId: post._id
-		});
-	})));
-
-	await Promise.all(promises);
-
-	return promisedPostObj;
-};

From e850c99418cfc3f4beb7b23b7b36ca7fd1d1038f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 5 Apr 2018 00:50:57 +0900
Subject: [PATCH 1082/1250] wip

---
 src/api/post/create.ts                   | 15 ++++-
 src/remote/activitypub/act/create.ts     | 18 ++----
 src/server/api/endpoints/posts/create.ts | 82 ++++--------------------
 3 files changed, 31 insertions(+), 84 deletions(-)

diff --git a/src/api/post/create.ts b/src/api/post/create.ts
index af94c6d81..8256cbc35 100644
--- a/src/api/post/create.ts
+++ b/src/api/post/create.ts
@@ -15,6 +15,8 @@ import Mute from '../../models/mute';
 import pushSw from '../../publishers/push-sw';
 import event from '../../publishers/stream';
 import parse from '../../text/parse';
+import html from '../../text/html';
+import { IApp } from '../../models/app';
 
 export default async (user: IUser, content: {
 	createdAt: Date;
@@ -23,9 +25,14 @@ export default async (user: IUser, content: {
 	repost: IPost;
 	media: IDriveFile[];
 	geo: any;
+	poll: any;
 	viaMobile: boolean;
 	tags: string[];
-}) => new Promise(async (res, rej) => {
+	cw: string;
+	visibility: string;
+	uri?: string;
+	app?: IApp;
+}) => new Promise<IPost>(async (res, rej) => {
 	const tags = content.tags || [];
 
 	let tokens = null;
@@ -53,10 +60,16 @@ export default async (user: IUser, content: {
 		replyId: content.reply ? content.reply._id : null,
 		repostId: content.repost ? content.repost._id : null,
 		text: content.text,
+		textHtml: tokens === null ? null : html(tokens),
+		poll: content.poll,
+		cw: content.cw,
 		tags,
 		userId: user._id,
 		viaMobile: content.viaMobile,
 		geo: content.geo || null,
+		uri: content.uri,
+		appId: content.app ? content.app._id : null,
+		visibility: content.visibility,
 
 		// 以下非正規化データ
 		_reply: content.reply ? { userId: content.reply.userId } : null,
diff --git a/src/remote/activitypub/act/create.ts b/src/remote/activitypub/act/create.ts
index 7ee9f8dfb..957900900 100644
--- a/src/remote/activitypub/act/create.ts
+++ b/src/remote/activitypub/act/create.ts
@@ -63,32 +63,26 @@ export default async (actor, activity): Promise<void> => {
 			throw new Error('invalid note');
 		}
 
-		const mediaIds = [];
+		const media = [];
 
 		if ('attachment' in note) {
 			note.attachment.forEach(async media => {
 				const created = await createImage(resolver, media);
-				mediaIds.push(created._id);
+				media.push(created);
 			});
 		}
 
 		const { window } = new JSDOM(note.content);
 
 		await createPost(actor, {
-			channelId: undefined,
-			index: undefined,
 			createdAt: new Date(note.published),
-			mediaIds,
-			replyId: undefined,
-			repostId: undefined,
-			poll: undefined,
+			media,
+			reply: undefined,
+			repost: undefined,
 			text: window.document.body.textContent,
-			textHtml: note.content && createDOMPurify(window).sanitize(note.content),
-			userId: actor._id,
-			appId: null,
 			viaMobile: false,
 			geo: undefined,
 			uri: note.id
-		}, null, null, []);
+		});
 	}
 };
diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts
index 03af7ee76..d241c8c38 100644
--- a/src/server/api/endpoints/posts/create.ts
+++ b/src/server/api/endpoints/posts/create.ts
@@ -3,16 +3,12 @@
  */
 import $ from 'cafy';
 import deepEqual = require('deep-equal');
-import renderAcct from '../../../../acct/render';
-import config from '../../../../config';
-import html from '../../../../text/html';
-import parse from '../../../../text/parse';
-import Post, { IPost, isValidText, isValidCw } from '../../../../models/post';
+import Post, { IPost, isValidText, isValidCw, pack } from '../../../../models/post';
 import { ILocalUser } from '../../../../models/user';
 import Channel, { IChannel } from '../../../../models/channel';
 import DriveFile from '../../../../models/drive-file';
-import create from '../../../../post/create';
-import distribute from '../../../../post/distribute';
+import create from '../../../../api/post/create';
+import { IApp } from '../../../../models/app';
 
 /**
  * Create a post
@@ -22,7 +18,7 @@ import distribute from '../../../../post/distribute';
  * @param {any} app
  * @return {Promise<any>}
  */
-module.exports = (params, user: ILocalUser, app) => new Promise(async (res, rej) => {
+module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res, rej) => {
 	// Get 'visibility' parameter
 	const [visibility = 'public', visibilityErr] = $(params.visibility).optional.string().or(['public', 'unlisted', 'private', 'direct']).$;
 	if (visibilityErr) return rej('invalid visibility');
@@ -230,82 +226,26 @@ module.exports = (params, user: ILocalUser, app) => new Promise(async (res, rej)
 		}
 	}
 
-	let tokens = null;
-	if (text) {
-		// Analyze
-		tokens = parse(text);
-
-		// Extract hashtags
-		const hashtags = tokens
-			.filter(t => t.type == 'hashtag')
-			.map(t => t.hashtag);
-
-		hashtags.forEach(tag => {
-			if (tags.indexOf(tag) == -1) {
-				tags.push(tag);
-			}
-		});
-	}
-
-	let atMentions = [];
-
-	// If has text content
-	if (text) {
-		/*
-				// Extract a hashtags
-				const hashtags = tokens
-					.filter(t => t.type == 'hashtag')
-					.map(t => t.hashtag)
-					// Drop dupulicates
-					.filter((v, i, s) => s.indexOf(v) == i);
-
-				// ハッシュタグをデータベースに登録
-				registerHashtags(user, hashtags);
-		*/
-		// Extract an '@' mentions
-		atMentions = tokens
-			.filter(t => t.type == 'mention')
-			.map(renderAcct)
-			// Drop dupulicates
-			.filter((v, i, s) => s.indexOf(v) == i);
-	}
-
 	// 投稿を作成
-	const post = await create({
+	const post = await create(user, {
 		createdAt: new Date(),
-		channelId: channel ? channel._id : undefined,
-		index: channel ? channel.index + 1 : undefined,
-		mediaIds: files ? files.map(file => file._id) : [],
+		media: files,
 		poll: poll,
 		text: text,
-		textHtml: tokens === null ? null : html(tokens),
+		reply,
+		repost,
 		cw: cw,
 		tags: tags,
-		userId: user._id,
-		appId: app ? app._id : null,
+		app: app,
 		viaMobile: viaMobile,
 		visibility,
 		geo
-	}, reply, repost, atMentions);
+	});
 
-	const postObj = await distribute(user, post.mentions, post);
+	const postObj = await pack(post, user);
 
 	// Reponse
 	res({
 		createdPost: postObj
 	});
-
-	// Register to search database
-	if (post.text && config.elasticsearch.enable) {
-		const es = require('../../../db/elasticsearch');
-
-		es.index({
-			index: 'misskey',
-			type: 'post',
-			id: post._id.toString(),
-			body: {
-				text: post.text
-			}
-		});
-	}
 });

From f0a518ff65a7d3d64b950cce13b26b8986e2785a Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Thu, 5 Apr 2018 01:04:44 +0900
Subject: [PATCH 1083/1250] Let unhandled rejection handler handle rejections
 in jobs

---
 .../processors/db/delete-post-dependents.ts   |  4 +-
 src/queue/processors/db/index.ts              |  2 +-
 src/queue/processors/http/deliver-post.ts     | 28 ++++---
 src/queue/processors/http/follow.ts           | 79 +++++++++----------
 src/queue/processors/http/index.ts            |  2 +-
 .../processors/http/perform-activitypub.ts    |  5 +-
 src/queue/processors/http/process-inbox.ts    | 55 +++++++------
 .../processors/http/report-github-failure.ts  | 39 +++++----
 src/queue/processors/http/unfollow.ts         | 31 +++++---
 9 files changed, 134 insertions(+), 111 deletions(-)

diff --git a/src/queue/processors/db/delete-post-dependents.ts b/src/queue/processors/db/delete-post-dependents.ts
index 6de21eb05..fb6617e95 100644
--- a/src/queue/processors/db/delete-post-dependents.ts
+++ b/src/queue/processors/db/delete-post-dependents.ts
@@ -5,7 +5,7 @@ import PostReaction from '../../../models/post-reaction';
 import PostWatching from '../../../models/post-watching';
 import Post from '../../../models/post';
 
-export default async ({ data }) => Promise.all([
+export default ({ data }, done) => Promise.all([
 	Favorite.remove({ postId: data._id }),
 	Notification.remove({ postId: data._id }),
 	PollVote.remove({ postId: data._id }),
@@ -19,4 +19,4 @@ export default async ({ data }) => Promise.all([
 		}),
 		Post.remove({ repostId: data._id })
 	]))
-]);
+]).then(() => done(), done);
diff --git a/src/queue/processors/db/index.ts b/src/queue/processors/db/index.ts
index 75838c099..468ec442a 100644
--- a/src/queue/processors/db/index.ts
+++ b/src/queue/processors/db/index.ts
@@ -4,4 +4,4 @@ const handlers = {
   deletePostDependents
 };
 
-export default (job, done) => handlers[job.data.type](job).then(() => done(), done);
+export default (job, done) => handlers[job.data.type](job, done);
diff --git a/src/queue/processors/http/deliver-post.ts b/src/queue/processors/http/deliver-post.ts
index e743fc5f6..8107c8bf7 100644
--- a/src/queue/processors/http/deliver-post.ts
+++ b/src/queue/processors/http/deliver-post.ts
@@ -5,17 +5,23 @@ import renderCreate from '../../../remote/activitypub/renderer/create';
 import renderNote from '../../../remote/activitypub/renderer/note';
 import request from '../../../remote/request';
 
-export default async ({ data }) => {
-	const promisedTo = User.findOne({ _id: data.toId }) as Promise<IRemoteUser>;
-	const [from, post] = await Promise.all([
-		User.findOne({ _id: data.fromId }),
-		Post.findOne({ _id: data.postId })
-	]);
-	const note = await renderNote(from, post);
-	const to = await promisedTo;
-	const create = renderCreate(note);
+export default async ({ data }, done) => {
+	try {
+		const promisedTo = User.findOne({ _id: data.toId }) as Promise<IRemoteUser>;
+		const [from, post] = await Promise.all([
+			User.findOne({ _id: data.fromId }),
+			Post.findOne({ _id: data.postId })
+		]);
+		const note = await renderNote(from, post);
+		const to = await promisedTo;
+		const create = renderCreate(note);
 
-	create['@context'] = context;
+		create['@context'] = context;
 
-	return request(from, to.account.inbox, create);
+		await request(from, to.account.inbox, create);
+	} catch (error) {
+		done(error);
+	}
+
+	done();
 };
diff --git a/src/queue/processors/http/follow.ts b/src/queue/processors/http/follow.ts
index 4cb72828e..ba1cc3118 100644
--- a/src/queue/processors/http/follow.ts
+++ b/src/queue/processors/http/follow.ts
@@ -7,10 +7,8 @@ import notify from '../../../publishers/notify';
 import context from '../../../remote/activitypub/renderer/context';
 import render from '../../../remote/activitypub/renderer/follow';
 import request from '../../../remote/request';
-import Logger from '../../../utils/logger';
 
-export default async ({ data }) => {
-	const { followerId, followeeId } = await Following.findOne({ _id: data.following });
+export default ({ data }, done) => Following.findOne({ _id: data.following }).then(async ({ followerId, followeeId }) => {
 	const [follower, followee] = await Promise.all([
 		User.findOne({ _id: followerId }),
 		User.findOne({ _id: followeeId })
@@ -23,47 +21,46 @@ export default async ({ data }) => {
 		await request(follower, followee.account.inbox, rendered);
 	}
 
-	try {
-		await Promise.all([
-			// Increment following count
-			User.update(followerId, {
-				$inc: {
-					followingCount: 1
-				}
-			}),
+	return [follower, followee];
+}).then(([follower, followee]) => Promise.all([
+	// Increment following count
+	User.update(follower._id, {
+		$inc: {
+			followingCount: 1
+		}
+	}),
 
-			FollowingLog.insert({
-				createdAt: data.following.createdAt,
-				userId: followerId,
-				count: follower.followingCount + 1
-			}),
+	FollowingLog.insert({
+		createdAt: data.following.createdAt,
+		userId: follower._id,
+		count: follower.followingCount + 1
+	}),
 
-			// Increment followers count
-			User.update({ _id: followeeId }, {
-				$inc: {
-					followersCount: 1
-				}
-			}),
+	// Increment followers count
+	User.update({ _id: followee._id }, {
+		$inc: {
+			followersCount: 1
+		}
+	}),
 
-			FollowedLog.insert({
-				createdAt: data.following.createdAt,
-				userId: followerId,
-				count: followee.followersCount + 1
-			}),
+	FollowedLog.insert({
+		createdAt: data.following.createdAt,
+		userId: follower._id,
+		count: followee.followersCount + 1
+	}),
 
-			// Publish follow event
-			isLocalUser(follower) && packUser(followee, follower)
-				.then(packed => event(follower._id, 'follow', packed)),
+	// Publish follow event
+	isLocalUser(follower) && packUser(followee, follower)
+		.then(packed => event(follower._id, 'follow', packed)),
 
-			isLocalUser(followee) && Promise.all([
-				packUser(follower, followee)
-					.then(packed => event(followee._id, 'followed', packed)),
+	isLocalUser(followee) && Promise.all([
+		packUser(follower, followee)
+			.then(packed => event(followee._id, 'followed', packed)),
 
-				// Notify
-				isLocalUser(followee) && notify(followeeId, followerId, 'follow')
-			])
-		]);
-	} catch (error) {
-		Logger.error(error.toString());
-	}
-};
+		// Notify
+		isLocalUser(followee) && notify(followee._id, follower._id, 'follow')
+	])
+]).then(() => done(), error => {
+	done();
+	throw error;
+}), done);
diff --git a/src/queue/processors/http/index.ts b/src/queue/processors/http/index.ts
index 8f9aa717c..0ea79305c 100644
--- a/src/queue/processors/http/index.ts
+++ b/src/queue/processors/http/index.ts
@@ -14,4 +14,4 @@ const handlers = {
   unfollow
 };
 
-export default (job, done) => handlers[job.data.type](job).then(() => done(), done);
+export default (job, done) => handlers[job.data.type](job, done);
diff --git a/src/queue/processors/http/perform-activitypub.ts b/src/queue/processors/http/perform-activitypub.ts
index 7b84400d5..ae70c0f0b 100644
--- a/src/queue/processors/http/perform-activitypub.ts
+++ b/src/queue/processors/http/perform-activitypub.ts
@@ -2,6 +2,7 @@ import User from '../../../models/user';
 import act from '../../../remote/activitypub/act';
 import Resolver from '../../../remote/activitypub/resolver';
 
-export default ({ data }) => User.findOne({ _id: data.actor })
+export default ({ data }, done) => User.findOne({ _id: data.actor })
 	.then(actor => act(new Resolver(), actor, data.outbox))
-	.then(Promise.all);
+	.then(Promise.all)
+	.then(() => done(), done);
diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts
index de1dbd2f9..88fbb9737 100644
--- a/src/queue/processors/http/process-inbox.ts
+++ b/src/queue/processors/http/process-inbox.ts
@@ -5,35 +5,40 @@ import act from '../../../remote/activitypub/act';
 import resolvePerson from '../../../remote/activitypub/resolve-person';
 import Resolver from '../../../remote/activitypub/resolver';
 
-export default async ({ data }): Promise<void> => {
-	const keyIdLower = data.signature.keyId.toLowerCase();
-	let user;
+export default async ({ data }, done) => {
+	try {
+		const keyIdLower = data.signature.keyId.toLowerCase();
+		let user;
 
-	if (keyIdLower.startsWith('acct:')) {
-		const { username, host } = parseAcct(keyIdLower.slice('acct:'.length));
-		if (host === null) {
-			throw 'request was made by local user';
+		if (keyIdLower.startsWith('acct:')) {
+			const { username, host } = parseAcct(keyIdLower.slice('acct:'.length));
+			if (host === null) {
+				done();
+				return;
+			}
+
+			user = await User.findOne({ usernameLower: username, hostLower: host }) as IRemoteUser;
+		} else {
+			user = await User.findOne({
+				host: { $ne: null },
+				'account.publicKey.id': data.signature.keyId
+			}) as IRemoteUser;
+
+			if (user === null) {
+				user = await resolvePerson(data.signature.keyId);
+			}
 		}
 
-		user = await User.findOne({ usernameLower: username, hostLower: host }) as IRemoteUser;
-	} else {
-		user = await User.findOne({
-			host: { $ne: null },
-			'account.publicKey.id': data.signature.keyId
-		}) as IRemoteUser;
-
-		if (user === null) {
-			user = await resolvePerson(data.signature.keyId);
+		if (user === null || !verifySignature(data.signature, user.account.publicKey.publicKeyPem)) {
+			done();
+			return;
 		}
+
+		await Promise.all(await act(new Resolver(), user, data.inbox, true));
+	} catch (error) {
+		done(error);
+		return;
 	}
 
-	if (user === null) {
-		throw 'failed to resolve user';
-	}
-
-	if (!verifySignature(data.signature, user.account.publicKey.publicKeyPem)) {
-		throw 'signature verification failed';
-	}
-
-	await Promise.all(await act(new Resolver(), user, data.inbox, true));
+	done();
 };
diff --git a/src/queue/processors/http/report-github-failure.ts b/src/queue/processors/http/report-github-failure.ts
index 21683ba3c..af9659bda 100644
--- a/src/queue/processors/http/report-github-failure.ts
+++ b/src/queue/processors/http/report-github-failure.ts
@@ -2,23 +2,30 @@ import * as request from 'request-promise-native';
 import User from '../../../models/user';
 const createPost = require('../../../server/api/endpoints/posts/create');
 
-export default async ({ data }) => {
-	const asyncBot = User.findOne({ _id: data.userId });
+export default async ({ data }, done) => {
+	try {
+		const asyncBot = User.findOne({ _id: data.userId });
 
-	// Fetch parent status
-	const parentStatuses = await request({
-		url: `${data.parentUrl}/statuses`,
-		headers: {
-			'User-Agent': 'misskey'
-		},
-		json: true
-	});
+		// Fetch parent status
+		const parentStatuses = await request({
+			url: `${data.parentUrl}/statuses`,
+			headers: {
+				'User-Agent': 'misskey'
+			},
+			json: true
+		});
 
-	const parentState = parentStatuses[0].state;
-	const stillFailed = parentState == 'failure' || parentState == 'error';
-	const text = stillFailed ?
-		`**⚠️BUILD STILL FAILED⚠️**: ?[${data.message}](${data.htmlUrl})` :
-		`**🚨BUILD FAILED🚨**: →→→?[${data.message}](${data.htmlUrl})←←←`;
+		const parentState = parentStatuses[0].state;
+		const stillFailed = parentState == 'failure' || parentState == 'error';
+		const text = stillFailed ?
+			`**⚠️BUILD STILL FAILED⚠️**: ?[${data.message}](${data.htmlUrl})` :
+			`**🚨BUILD FAILED🚨**: →→→?[${data.message}](${data.htmlUrl})←←←`;
 
-	createPost({ text }, await asyncBot);
+		createPost({ text }, await asyncBot);
+	} catch (error) {
+		done(error);
+		return;
+	}
+
+	done();
 };
diff --git a/src/queue/processors/http/unfollow.ts b/src/queue/processors/http/unfollow.ts
index 801a3612a..dc50e946c 100644
--- a/src/queue/processors/http/unfollow.ts
+++ b/src/queue/processors/http/unfollow.ts
@@ -7,24 +7,31 @@ import renderFollow from '../../../remote/activitypub/renderer/follow';
 import renderUndo from '../../../remote/activitypub/renderer/undo';
 import context from '../../../remote/activitypub/renderer/context';
 import request from '../../../remote/request';
-import Logger from '../../../utils/logger';
 
-export default async ({ data }) => {
+export default async ({ data }, done) => {
 	const following = await Following.findOne({ _id: data.id });
 	if (following === null) {
+		done();
 		return;
 	}
 
-	const [follower, followee] = await Promise.all([
-		User.findOne({ _id: following.followerId }),
-		User.findOne({ _id: following.followeeId })
-	]);
+	let follower, followee;
 
-	if (isLocalUser(follower) && isRemoteUser(followee)) {
-		const undo = renderUndo(renderFollow(follower, followee));
-		undo['@context'] = context;
+	try {
+		[follower, followee] = await Promise.all([
+			User.findOne({ _id: following.followerId }),
+			User.findOne({ _id: following.followeeId })
+		]);
 
-		await request(follower, followee.account.inbox, undo);
+		if (isLocalUser(follower) && isRemoteUser(followee)) {
+			const undo = renderUndo(renderFollow(follower, followee));
+			undo['@context'] = context;
+
+			await request(follower, followee.account.inbox, undo);
+		}
+	} catch (error) {
+		done(error);
+		return;
 	}
 
 	try {
@@ -57,7 +64,7 @@ export default async ({ data }) => {
 
 		// Publish follow event
 		stream(follower._id, 'unfollow', promisedPackedUser);
-	} catch (error) {
-		Logger.error(error.toString());
+	} finally {
+		done();
 	}
 };

From 6bb258f6f0def6419c7b5802f2b5075270146211 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 5 Apr 2018 01:07:07 +0900
Subject: [PATCH 1084/1250] wip

---
 src/queue/processors/http/index.ts            | 10 +---
 .../processors/http/perform-activitypub.ts    |  7 ---
 src/remote/activitypub/resolve-person.ts      | 58 +++++--------------
 3 files changed, 15 insertions(+), 60 deletions(-)
 delete mode 100644 src/queue/processors/http/perform-activitypub.ts

diff --git a/src/queue/processors/http/index.ts b/src/queue/processors/http/index.ts
index 8f9aa717c..06c6b1d1a 100644
--- a/src/queue/processors/http/index.ts
+++ b/src/queue/processors/http/index.ts
@@ -1,17 +1,11 @@
-import deliverPost from './deliver-post';
-import follow from './follow';
-import performActivityPub from './perform-activitypub';
+import deliver from './deliver';
 import processInbox from './process-inbox';
 import reportGitHubFailure from './report-github-failure';
-import unfollow from './unfollow';
 
 const handlers = {
-  deliverPost,
-  follow,
-  performActivityPub,
+  deliver,
   processInbox,
   reportGitHubFailure,
-  unfollow
 };
 
 export default (job, done) => handlers[job.data.type](job).then(() => done(), done);
diff --git a/src/queue/processors/http/perform-activitypub.ts b/src/queue/processors/http/perform-activitypub.ts
deleted file mode 100644
index 963e532fe..000000000
--- a/src/queue/processors/http/perform-activitypub.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import User from '../../models/user';
-import act from '../../remote/activitypub/act';
-import Resolver from '../../remote/activitypub/resolver';
-
-export default ({ data }) => User.findOne({ _id: data.actor })
-	.then(actor => act(new Resolver(), actor, data.outbox))
-	.then(Promise.all);
diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts
index 59be65908..77d08398b 100644
--- a/src/remote/activitypub/resolve-person.ts
+++ b/src/remote/activitypub/resolve-person.ts
@@ -1,17 +1,15 @@
 import { JSDOM } from 'jsdom';
 import { toUnicode } from 'punycode';
 import User, { validateUsername, isValidName, isValidDescription } from '../../models/user';
-import queue from '../../queue';
 import webFinger from '../webfinger';
 import create from './create';
 import Resolver from './resolver';
-
-async function isCollection(collection) {
-	return ['Collection', 'OrderedCollection'].includes(collection.type);
-}
+import uploadFromUrl from '../../api/drive/upload-from-url';
 
 export default async (value, verifier?: string) => {
-	const { resolver, object } = await new Resolver().resolveOne(value);
+	const resolver = new Resolver();
+
+	const object = await resolver.resolve(value) as any;
 
 	if (
 		object === null ||
@@ -21,24 +19,10 @@ export default async (value, verifier?: string) => {
 		!isValidName(object.name) ||
 		!isValidDescription(object.summary)
 	) {
-		throw new Error();
+		throw new Error('invalid person');
 	}
 
-	const [followers, following, outbox, finger] = await Promise.all([
-		resolver.resolveOne(object.followers).then(
-			resolved => isCollection(resolved.object) ? resolved.object : null,
-			() => null
-		),
-		resolver.resolveOne(object.following).then(
-			resolved => isCollection(resolved.object) ? resolved.object : null,
-			() => null
-		),
-		resolver.resolveOne(object.outbox).then(
-			resolved => isCollection(resolved.object) ? resolved.object : null,
-			() => null
-		),
-		webFinger(object.id, verifier),
-	]);
+	const finger = await webFinger(object.id, verifier);
 
 	const host = toUnicode(finger.subject.replace(/^.*?@/, ''));
 	const hostLower = host.replace(/[A-Z]+/, matched => matched.toLowerCase());
@@ -50,10 +34,10 @@ export default async (value, verifier?: string) => {
 		bannerId: null,
 		createdAt: Date.parse(object.published),
 		description: summaryDOM.textContent,
-		followersCount: followers ? followers.totalItem || 0 : 0,
-		followingCount: following ? following.totalItem || 0 : 0,
+		followersCount: 0,
+		followingCount: 0,
 		name: object.name,
-		postsCount: outbox ? outbox.totalItem || 0 : 0,
+		postsCount: 0,
 		driveCapacity: 1024 * 1024 * 8, // 8MiB
 		username: object.preferredUsername,
 		usernameLower: object.preferredUsername.toLowerCase(),
@@ -69,33 +53,17 @@ export default async (value, verifier?: string) => {
 		},
 	});
 
-	queue.create('http', {
-		type: 'performActivityPub',
-		actor: user._id,
-		outbox
-	}).save();
-
 	const [avatarId, bannerId] = await Promise.all([
 		object.icon,
 		object.image
-	].map(async value => {
-		if (value === undefined) {
+	].map(async url => {
+		if (url === undefined) {
 			return null;
 		}
 
-		try {
-			const created = await create(resolver, user, value);
+		const img = await uploadFromUrl(url, user);
 
-			await Promise.all(created.map(asyncCreated => asyncCreated.then(created => {
-				if (created !== null && created.object.$ref === 'driveFiles.files') {
-					throw created.object.$id;
-				}
-			}, () => {})));
-
-			return null;
-		} catch (id) {
-			return id;
-		}
+		return img._id;
 	}));
 
 	User.update({ _id: user._id }, { $set: { avatarId, bannerId } });

From 4460515693804e167d2111ff73cda292df1a2e80 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Thu, 5 Apr 2018 01:07:55 +0900
Subject: [PATCH 1085/1250] Do not declare two variables in a statement

---
 src/queue/processors/http/unfollow.ts | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/queue/processors/http/unfollow.ts b/src/queue/processors/http/unfollow.ts
index dc50e946c..d62eb280d 100644
--- a/src/queue/processors/http/unfollow.ts
+++ b/src/queue/processors/http/unfollow.ts
@@ -15,7 +15,8 @@ export default async ({ data }, done) => {
 		return;
 	}
 
-	let follower, followee;
+	let follower;
+	let followee;
 
 	try {
 		[follower, followee] = await Promise.all([

From cb8a504362c7b557f669b849a7e24c1b6616a72a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 5 Apr 2018 01:22:41 +0900
Subject: [PATCH 1086/1250] wip

---
 src/api/following/delete.ts                 | 69 +++++++++++++++++++++
 src/queue/processors/http/unfollow.ts       | 63 -------------------
 src/remote/activitypub/act/create.ts        |  1 -
 src/remote/activitypub/act/index.ts         | 11 ++--
 src/remote/activitypub/act/undo.ts          | 15 +++++
 src/remote/activitypub/act/undo/index.ts    | 27 --------
 src/remote/activitypub/act/undo/unfollow.ts | 11 ----
 src/remote/activitypub/act/unfollow.ts      | 25 ++++++++
 8 files changed, 114 insertions(+), 108 deletions(-)
 create mode 100644 src/api/following/delete.ts
 delete mode 100644 src/queue/processors/http/unfollow.ts
 create mode 100644 src/remote/activitypub/act/undo.ts
 delete mode 100644 src/remote/activitypub/act/undo/index.ts
 delete mode 100644 src/remote/activitypub/act/undo/unfollow.ts
 create mode 100644 src/remote/activitypub/act/unfollow.ts

diff --git a/src/api/following/delete.ts b/src/api/following/delete.ts
new file mode 100644
index 000000000..4cdff7ce1
--- /dev/null
+++ b/src/api/following/delete.ts
@@ -0,0 +1,69 @@
+import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user';
+import Following from '../../models/following';
+import FollowingLog from '../../models/following-log';
+import FollowedLog from '../../models/followed-log';
+import event from '../../publishers/stream';
+import context from '../../remote/activitypub/renderer/context';
+import renderFollow from '../../remote/activitypub/renderer/follow';
+import renderUndo from '../../remote/activitypub/renderer/undo';
+import { createHttp } from '../../queue';
+
+export default async function(follower: IUser, followee: IUser, activity?) {
+	const following = await Following.findOne({
+		followerId: follower._id,
+		followeeId: followee._id
+	});
+
+	if (following == null) {
+		console.warn('フォロー解除がリクエストされましたがフォローしていませんでした');
+		return;
+	}
+
+	Following.remove({
+		_id: following._id
+	});
+
+	//#region Decrement following count
+	User.update({ _id: follower._id }, {
+		$inc: {
+			followingCount: -1
+		}
+	});
+
+	FollowingLog.insert({
+		createdAt: following.createdAt,
+		userId: follower._id,
+		count: follower.followingCount - 1
+	});
+	//#endregion
+
+	//#region Decrement followers count
+	User.update({ _id: followee._id }, {
+		$inc: {
+			followersCount: -1
+		}
+	});
+	FollowedLog.insert({
+		createdAt: following.createdAt,
+		userId: followee._id,
+		count: followee.followersCount - 1
+	});
+	//#endregion
+
+	// Publish unfollow event
+	if (isLocalUser(follower)) {
+		packUser(followee, follower).then(packed => event(follower._id, 'unfollow', packed));
+	}
+
+	if (isLocalUser(follower) && isRemoteUser(followee)) {
+		const content = renderUndo(renderFollow(follower, followee));
+		content['@context'] = context;
+
+		createHttp({
+			type: 'deliver',
+			user: follower,
+			content,
+			to: followee.account.inbox
+		}).save();
+	}
+}
diff --git a/src/queue/processors/http/unfollow.ts b/src/queue/processors/http/unfollow.ts
deleted file mode 100644
index 801a3612a..000000000
--- a/src/queue/processors/http/unfollow.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import FollowedLog from '../../../models/followed-log';
-import Following from '../../../models/following';
-import FollowingLog from '../../../models/following-log';
-import User, { isLocalUser, isRemoteUser, pack as packUser } from '../../../models/user';
-import stream from '../../../publishers/stream';
-import renderFollow from '../../../remote/activitypub/renderer/follow';
-import renderUndo from '../../../remote/activitypub/renderer/undo';
-import context from '../../../remote/activitypub/renderer/context';
-import request from '../../../remote/request';
-import Logger from '../../../utils/logger';
-
-export default async ({ data }) => {
-	const following = await Following.findOne({ _id: data.id });
-	if (following === null) {
-		return;
-	}
-
-	const [follower, followee] = await Promise.all([
-		User.findOne({ _id: following.followerId }),
-		User.findOne({ _id: following.followeeId })
-	]);
-
-	if (isLocalUser(follower) && isRemoteUser(followee)) {
-		const undo = renderUndo(renderFollow(follower, followee));
-		undo['@context'] = context;
-
-		await request(follower, followee.account.inbox, undo);
-	}
-
-	try {
-		await Promise.all([
-			// Delete following
-			Following.findOneAndDelete({ _id: data.id }),
-
-			// Decrement following count
-			User.update({ _id: follower._id }, { $inc: { followingCount: -1 } }),
-			FollowingLog.insert({
-				createdAt: new Date(),
-				userId: follower._id,
-				count: follower.followingCount - 1
-			}),
-
-			// Decrement followers count
-			User.update({ _id: followee._id }, { $inc: { followersCount: -1 } }),
-			FollowedLog.insert({
-				createdAt: new Date(),
-				userId: followee._id,
-				count: followee.followersCount - 1
-			})
-		]);
-
-		if (isLocalUser(follower)) {
-			return;
-		}
-
-		const promisedPackedUser = packUser(followee, follower);
-
-		// Publish follow event
-		stream(follower._id, 'unfollow', promisedPackedUser);
-	} catch (error) {
-		Logger.error(error.toString());
-	}
-};
diff --git a/src/remote/activitypub/act/create.ts b/src/remote/activitypub/act/create.ts
index 957900900..c486571fc 100644
--- a/src/remote/activitypub/act/create.ts
+++ b/src/remote/activitypub/act/create.ts
@@ -1,5 +1,4 @@
 import { JSDOM } from 'jsdom';
-const createDOMPurify = require('dompurify');
 
 import Resolver from '../resolver';
 import DriveFile from '../../../models/drive-file';
diff --git a/src/remote/activitypub/act/index.ts b/src/remote/activitypub/act/index.ts
index d78335f16..f22500ace 100644
--- a/src/remote/activitypub/act/index.ts
+++ b/src/remote/activitypub/act/index.ts
@@ -2,25 +2,24 @@ import create from './create';
 import performDeleteActivity from './delete';
 import follow from './follow';
 import undo from './undo';
-import Resolver from '../resolver';
 import { IObject } from '../type';
 
-export default async (parentResolver: Resolver, actor, activity: IObject): Promise<void> => {
+export default async (actor, activity: IObject): Promise<void> => {
 	switch (activity.type) {
 	case 'Create':
-		await create(parentResolver, actor, activity);
+		await create(actor, activity);
 		break;
 
 	case 'Delete':
-		await performDeleteActivity(parentResolver, actor, activity);
+		await performDeleteActivity(actor, activity);
 		break;
 
 	case 'Follow':
-		await follow(parentResolver, actor, activity);
+		await follow(actor, activity);
 		break;
 
 	case 'Undo':
-		await undo(parentResolver, actor, activity);
+		await undo(actor, activity);
 		break;
 
 	default:
diff --git a/src/remote/activitypub/act/undo.ts b/src/remote/activitypub/act/undo.ts
new file mode 100644
index 000000000..b3b83777d
--- /dev/null
+++ b/src/remote/activitypub/act/undo.ts
@@ -0,0 +1,15 @@
+import unfollow from './unfollow';
+
+export default async (actor, activity): Promise<void> => {
+	if ('actor' in activity && actor.account.uri !== activity.actor) {
+		throw new Error('invalid actor');
+	}
+
+	switch (activity.object.type) {
+		case 'Follow':
+			unfollow(activity.object);
+			break;
+	}
+
+	return null;
+};
diff --git a/src/remote/activitypub/act/undo/index.ts b/src/remote/activitypub/act/undo/index.ts
deleted file mode 100644
index aa60d3a4f..000000000
--- a/src/remote/activitypub/act/undo/index.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import act from '../../act';
-import deleteObject from '../../delete';
-import unfollow from './unfollow';
-import Resolver from '../../resolver';
-
-export default async (resolver: Resolver, actor, activity): Promise<void> => {
-	if ('actor' in activity && actor.account.uri !== activity.actor) {
-		throw new Error();
-	}
-
-	const results = await act(resolver, actor, activity.object);
-
-	await Promise.all(results.map(async promisedResult => {
-		const result = await promisedResult;
-
-		if (result === null || await deleteObject(result) !== null) {
-			return;
-		}
-
-		switch (result.object.$ref) {
-		case 'following':
-			await unfollow(result.object);
-		}
-	}));
-
-	return null;
-};
diff --git a/src/remote/activitypub/act/undo/unfollow.ts b/src/remote/activitypub/act/undo/unfollow.ts
deleted file mode 100644
index c17e06e8a..000000000
--- a/src/remote/activitypub/act/undo/unfollow.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import queue from '../../../../queue';
-
-export default ({ $id }) => new Promise((resolve, reject) => {
-	queue.create('http', { type: 'unfollow', id: $id }).save(error => {
-		if (error) {
-			reject(error);
-		} else {
-			resolve();
-		}
-	});
-});
diff --git a/src/remote/activitypub/act/unfollow.ts b/src/remote/activitypub/act/unfollow.ts
new file mode 100644
index 000000000..e3c9e1c1c
--- /dev/null
+++ b/src/remote/activitypub/act/unfollow.ts
@@ -0,0 +1,25 @@
+import parseAcct from '../../../acct/parse';
+import User from '../../../models/user';
+import config from '../../../config';
+import unfollow from '../../../api/following/delete';
+
+export default async (actor, activity): Promise<void> => {
+	const prefix = config.url + '/@';
+	const id = activity.object.id || activity.object;
+
+	if (!id.startsWith(prefix)) {
+		return null;
+	}
+
+	const { username, host } = parseAcct(id.slice(prefix.length));
+	if (host !== null) {
+		throw new Error();
+	}
+
+	const followee = await User.findOne({ username, host });
+	if (followee === null) {
+		throw new Error();
+	}
+
+	await unfollow(actor, followee, activity);
+};

From de79ac7aced889ed56e2ecbf183e837de90c4f37 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 5 Apr 2018 01:24:01 +0900
Subject: [PATCH 1087/1250] wip

---
 src/queue/processors/http/process-inbox.ts | 12 ++++++------
 src/queue/processors/index.ts              | 18 ------------------
 2 files changed, 6 insertions(+), 24 deletions(-)
 delete mode 100644 src/queue/processors/index.ts

diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts
index fff1fbf66..82585d3a6 100644
--- a/src/queue/processors/http/process-inbox.ts
+++ b/src/queue/processors/http/process-inbox.ts
@@ -1,11 +1,11 @@
 import * as kue from 'kue';
 
 import { verifySignature } from 'http-signature';
-import parseAcct from '../../acct/parse';
-import User, { IRemoteUser } from '../../models/user';
-import act from '../../remote/activitypub/act';
-import resolvePerson from '../../remote/activitypub/resolve-person';
-import Resolver from '../../remote/activitypub/resolver';
+import parseAcct from '../../../acct/parse';
+import User, { IRemoteUser } from '../../../models/user';
+import act from '../../../remote/activitypub/act';
+import resolvePerson from '../../../remote/activitypub/resolve-person';
+import Resolver from '../../../remote/activitypub/resolver';
 
 // ユーザーのinboxにアクティビティが届いた時の処理
 export default async (job: kue.Job, done): Promise<void> => {
@@ -47,7 +47,7 @@ export default async (job: kue.Job, done): Promise<void> => {
 
 	// アクティビティを処理
 	try {
-		await act(new Resolver(), user, activity);
+		await act(user, activity);
 		done();
 	} catch (e) {
 		done(e);
diff --git a/src/queue/processors/index.ts b/src/queue/processors/index.ts
deleted file mode 100644
index 172048dda..000000000
--- a/src/queue/processors/index.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import queue from '../queue';
-import db from './db';
-import http from './http';
-
-export default () => {
-	queue.process('db', db);
-
-	/*
-		256 is the default concurrency limit of Mozilla Firefox and Google
-		Chromium.
-
-		a8af215e691f3a2205a3758d2d96e9d328e100ff - chromium/src.git - Git at Google
-		https://chromium.googlesource.com/chromium/src.git/+/a8af215e691f3a2205a3758d2d96e9d328e100ff
-		Network.http.max-connections - MozillaZine Knowledge Base
-		http://kb.mozillazine.org/Network.http.max-connections
-	*/
-	queue.process('http', 256, http);
-};

From 5218c75f8088f5429fe553ba8b89597fb8b8a8d2 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Thu, 5 Apr 2018 01:24:39 +0900
Subject: [PATCH 1088/1250] Implement Mention object

---
 src/post/create.ts                         | 14 ++------------
 src/queue/processors/http/process-inbox.ts |  2 +-
 src/remote/activitypub/create.ts           | 12 +++++++++++-
 src/remote/activitypub/resolve-person.ts   |  5 ++---
 src/remote/resolve-user.ts                 |  3 ++-
 src/server/api/endpoints/posts/create.ts   | 10 +++++++---
 6 files changed, 25 insertions(+), 21 deletions(-)

diff --git a/src/post/create.ts b/src/post/create.ts
index ecea37382..4ad1503e0 100644
--- a/src/post/create.ts
+++ b/src/post/create.ts
@@ -1,8 +1,6 @@
-import parseAcct from '../acct/parse';
 import Post from '../models/post';
-import User from '../models/user';
 
-export default async (post, reply, repost, atMentions) => {
+export default async (post, reply, repost, mentions) => {
 	post.mentions = [];
 
 	function addMention(mentionee) {
@@ -36,15 +34,7 @@ export default async (post, reply, repost, atMentions) => {
 		post._repost = null;
 	}
 
-	await Promise.all(atMentions.map(async mention => {
-		// Fetch mentioned user
-		// SELECT _id
-		const { _id } = await User
-			.findOne(parseAcct(mention), { _id: true });
-
-		// Add mention
-		addMention(_id);
-	}));
+	await Promise.all(mentions.map(({ _id }) => addMention(_id)));
 
 	return Post.insert(post);
 };
diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts
index 88fbb9737..7eeaa19f8 100644
--- a/src/queue/processors/http/process-inbox.ts
+++ b/src/queue/processors/http/process-inbox.ts
@@ -25,7 +25,7 @@ export default async ({ data }, done) => {
 			}) as IRemoteUser;
 
 			if (user === null) {
-				user = await resolvePerson(data.signature.keyId);
+				user = await resolvePerson(new Resolver(), data.signature.keyId);
 			}
 		}
 
diff --git a/src/remote/activitypub/create.ts b/src/remote/activitypub/create.ts
index 97c72860f..710d56fd3 100644
--- a/src/remote/activitypub/create.ts
+++ b/src/remote/activitypub/create.ts
@@ -7,6 +7,7 @@ import { IRemoteUser } from '../../models/user';
 import uploadFromUrl from '../../drive/upload-from-url';
 import createPost from '../../post/create';
 import distributePost from '../../post/distribute';
+import resolvePerson from './resolve-person';
 import Resolver from './resolver';
 const createDOMPurify = require('dompurify');
 
@@ -53,6 +54,15 @@ class Creator {
 				.map(({ object }) => object.$id);
 
 		const { window } = new JSDOM(note.content);
+		const mentions = [];
+
+		for (const { href, type } of note.tags) {
+			switch (type) {
+			case 'Mention':
+				mentions.push(resolvePerson(resolver, href));
+				break;
+			}
+		}
 
 		const inserted = await createPost({
 			channelId: undefined,
@@ -69,7 +79,7 @@ class Creator {
 			viaMobile: false,
 			geo: undefined,
 			uri: note.id
-		}, null, null, []);
+		}, null, null, await Promise.all(mentions));
 
 		const promises = [];
 
diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts
index 2cf3ad32d..7ed01e322 100644
--- a/src/remote/activitypub/resolve-person.ts
+++ b/src/remote/activitypub/resolve-person.ts
@@ -4,14 +4,13 @@ import User, { validateUsername, isValidName, isValidDescription } from '../../m
 import { createHttp } from '../../queue';
 import webFinger from '../webfinger';
 import create from './create';
-import Resolver from './resolver';
 
 async function isCollection(collection) {
 	return ['Collection', 'OrderedCollection'].includes(collection.type);
 }
 
-export default async (value, verifier?: string) => {
-	const { resolver, object } = await new Resolver().resolveOne(value);
+export default async (parentResolver, value, verifier?: string) => {
+	const { resolver, object } = parentResolver.resolveOne(value);
 
 	if (
 		object === null ||
diff --git a/src/remote/resolve-user.ts b/src/remote/resolve-user.ts
index 48219e8cb..097ed6673 100644
--- a/src/remote/resolve-user.ts
+++ b/src/remote/resolve-user.ts
@@ -1,6 +1,7 @@
 import { toUnicode, toASCII } from 'punycode';
 import User from '../models/user';
 import resolvePerson from './activitypub/resolve-person';
+import Resolver from './activitypub/resolver';
 import webFinger from './webfinger';
 
 export default async (username, host, option) => {
@@ -19,7 +20,7 @@ export default async (username, host, option) => {
 			throw new Error();
 		}
 
-		user = await resolvePerson(self.href, acctLower);
+		user = await resolvePerson(new Resolver(), self.href, acctLower);
 	}
 
 	return user;
diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts
index 03af7ee76..47897626f 100644
--- a/src/server/api/endpoints/posts/create.ts
+++ b/src/server/api/endpoints/posts/create.ts
@@ -3,12 +3,13 @@
  */
 import $ from 'cafy';
 import deepEqual = require('deep-equal');
+import parseAcct from '../../../../acct/parse';
 import renderAcct from '../../../../acct/render';
 import config from '../../../../config';
 import html from '../../../../text/html';
 import parse from '../../../../text/parse';
 import Post, { IPost, isValidText, isValidCw } from '../../../../models/post';
-import { ILocalUser } from '../../../../models/user';
+import User, { ILocalUser } from '../../../../models/user';
 import Channel, { IChannel } from '../../../../models/channel';
 import DriveFile from '../../../../models/drive-file';
 import create from '../../../../post/create';
@@ -267,7 +268,10 @@ module.exports = (params, user: ILocalUser, app) => new Promise(async (res, rej)
 			.filter(t => t.type == 'mention')
 			.map(renderAcct)
 			// Drop dupulicates
-			.filter((v, i, s) => s.indexOf(v) == i);
+			.filter((v, i, s) => s.indexOf(v) == i)
+			// Fetch mentioned user
+			// SELECT _id
+			.map(mention => User.findOne(parseAcct(mention), { _id: true }));
 	}
 
 	// 投稿を作成
@@ -286,7 +290,7 @@ module.exports = (params, user: ILocalUser, app) => new Promise(async (res, rej)
 		viaMobile: viaMobile,
 		visibility,
 		geo
-	}, reply, repost, atMentions);
+	}, reply, repost, await Promise.all(atMentions));
 
 	const postObj = await distribute(user, post.mentions, post);
 

From c5882f9966968cae2dd780042ac9db46debfb6e4 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Thu, 5 Apr 2018 01:29:56 +0900
Subject: [PATCH 1089/1250] Implement Hashtag object

---
 src/remote/activitypub/create.ts | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

diff --git a/src/remote/activitypub/create.ts b/src/remote/activitypub/create.ts
index 710d56fd3..31e9dba86 100644
--- a/src/remote/activitypub/create.ts
+++ b/src/remote/activitypub/create.ts
@@ -55,9 +55,16 @@ class Creator {
 
 		const { window } = new JSDOM(note.content);
 		const mentions = [];
+		const tags = [];
 
-		for (const { href, type } of note.tags) {
+		for (const { href, name, type } of note.tags) {
 			switch (type) {
+			case 'Hashtag':
+				if (name.startsWith('#')) {
+					tags.push(name.slice(1));
+				}
+				break;
+
 			case 'Mention':
 				mentions.push(resolvePerson(resolver, href));
 				break;
@@ -78,7 +85,8 @@ class Creator {
 			appId: null,
 			viaMobile: false,
 			geo: undefined,
-			uri: note.id
+			uri: note.id,
+			tags
 		}, null, null, await Promise.all(mentions));
 
 		const promises = [];

From 1c7e35c001a1dc4e79bdf5a3e3697a8a59a1ca4e Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Thu, 5 Apr 2018 01:57:18 +0900
Subject: [PATCH 1090/1250] Handle inReplyTo property

---
 src/remote/activitypub/create.ts | 25 +++++++++++++++++--------
 1 file changed, 17 insertions(+), 8 deletions(-)

diff --git a/src/remote/activitypub/create.ts b/src/remote/activitypub/create.ts
index 31e9dba86..3bc0c66f3 100644
--- a/src/remote/activitypub/create.ts
+++ b/src/remote/activitypub/create.ts
@@ -48,11 +48,6 @@ class Creator {
 			throw new Error();
 		}
 
-		const mediaIds = 'attachment' in note &&
-			(await Promise.all(await this.create(resolver, note.attachment)))
-				.filter(media => media !== null && media.object.$ref === 'driveFiles.files')
-				.map(({ object }) => object.$id);
-
 		const { window } = new JSDOM(note.content);
 		const mentions = [];
 		const tags = [];
@@ -71,13 +66,27 @@ class Creator {
 			}
 		}
 
+		const [mediaIds, reply] = await Promise.all([
+			'attachment' in note && this.create(resolver, note.attachment)
+				.then(collection => Promise.all(collection))
+				.then(collection => collection
+					.filter(media => media !== null && media.object.$ref === 'driveFiles.files')
+					.map(({ object }: IResult) => object.$id)),
+
+			'inReplyTo' in note && this.create(resolver, note.inReplyTo)
+				.then(collection => Promise.all(collection.map(promise => promise.then(result => {
+					if (result !== null && result.object.$ref === 'posts') {
+						throw result.object;
+					}
+				}, () => { }))))
+				.then(() => null, ({ $id }) => Post.findOne({ _id: $id }))
+		]);
+
 		const inserted = await createPost({
 			channelId: undefined,
 			index: undefined,
 			createdAt: new Date(note.published),
 			mediaIds,
-			replyId: undefined,
-			repostId: undefined,
 			poll: undefined,
 			text: window.document.body.textContent,
 			textHtml: note.content && createDOMPurify(window).sanitize(note.content),
@@ -87,7 +96,7 @@ class Creator {
 			geo: undefined,
 			uri: note.id,
 			tags
-		}, null, null, await Promise.all(mentions));
+		}, reply, null, await Promise.all(mentions));
 
 		const promises = [];
 

From 074ea114abc4da9271c2fe5e8fbb10d171201214 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 5 Apr 2018 03:21:11 +0900
Subject: [PATCH 1091/1250] wip

---
 src/api/drive/add-file.ts                     | 12 +++----
 src/api/drive/upload-from-url.ts              |  2 +-
 src/api/post/create.ts                        | 13 ++++----
 src/api/post/watch.ts                         |  2 +-
 .../processors/db/delete-post-dependents.ts   | 12 +++----
 src/queue/processors/http/process-inbox.ts    |  1 -
 src/remote/activitypub/act/create.ts          | 25 ++++++++++-----
 src/remote/activitypub/act/delete.ts          | 31 ++++++++++++-------
 src/remote/activitypub/act/undo.ts            |  2 +-
 src/remote/activitypub/delete/index.ts        | 10 ------
 src/remote/activitypub/delete/post.ts         | 13 --------
 src/remote/activitypub/resolve-person.ts      |  1 -
 src/server/activitypub/inbox.ts               |  4 +--
 13 files changed, 60 insertions(+), 68 deletions(-)
 delete mode 100644 src/remote/activitypub/delete/index.ts
 delete mode 100644 src/remote/activitypub/delete/post.ts

diff --git a/src/api/drive/add-file.ts b/src/api/drive/add-file.ts
index 24eb5208d..64a2f1834 100644
--- a/src/api/drive/add-file.ts
+++ b/src/api/drive/add-file.ts
@@ -10,12 +10,12 @@ import * as debug from 'debug';
 import fileType = require('file-type');
 import prominence = require('prominence');
 
-import DriveFile, { IMetadata, getGridFSBucket } from '../models/drive-file';
-import DriveFolder from '../models/drive-folder';
-import { pack } from '../models/drive-file';
-import event, { publishDriveStream } from '../publishers/stream';
-import getAcct from '../acct/render';
-import config from '../config';
+import DriveFile, { IMetadata, getGridFSBucket } from '../../models/drive-file';
+import DriveFolder from '../../models/drive-folder';
+import { pack } from '../../models/drive-file';
+import event, { publishDriveStream } from '../../publishers/stream';
+import getAcct from '../../acct/render';
+import config from '../../config';
 
 const gm = _gm.subClass({
 	imageMagick: true
diff --git a/src/api/drive/upload-from-url.ts b/src/api/drive/upload-from-url.ts
index f96af0f26..26c890d15 100644
--- a/src/api/drive/upload-from-url.ts
+++ b/src/api/drive/upload-from-url.ts
@@ -1,5 +1,5 @@
 import * as URL from 'url';
-import { IDriveFile, validateFileName } from '../models/drive-file';
+import { IDriveFile, validateFileName } from '../../models/drive-file';
 import create from './add-file';
 import * as debug from 'debug';
 import * as tmp from 'tmp';
diff --git a/src/api/post/create.ts b/src/api/post/create.ts
index 8256cbc35..36819ec2b 100644
--- a/src/api/post/create.ts
+++ b/src/api/post/create.ts
@@ -1,6 +1,5 @@
-import parseAcct from '../../acct/parse';
 import Post, { pack, IPost } from '../../models/post';
-import User, { isLocalUser, isRemoteUser, IUser } from '../../models/user';
+import User, { isLocalUser, IUser } from '../../models/user';
 import stream from '../../publishers/stream';
 import Following from '../../models/following';
 import { createHttp } from '../../queue';
@@ -25,14 +24,16 @@ export default async (user: IUser, content: {
 	repost: IPost;
 	media: IDriveFile[];
 	geo: any;
-	poll: any;
+	poll?: any;
 	viaMobile: boolean;
-	tags: string[];
-	cw: string;
-	visibility: string;
+	tags?: string[];
+	cw?: string;
+	visibility?: string;
 	uri?: string;
 	app?: IApp;
 }) => new Promise<IPost>(async (res, rej) => {
+	if (content.visibility == null) content.visibility = 'public';
+
 	const tags = content.tags || [];
 
 	let tokens = null;
diff --git a/src/api/post/watch.ts b/src/api/post/watch.ts
index 61ea44443..bbd9976f4 100644
--- a/src/api/post/watch.ts
+++ b/src/api/post/watch.ts
@@ -1,5 +1,5 @@
 import * as mongodb from 'mongodb';
-import Watching from '../models/post-watching';
+import Watching from '../../models/post-watching';
 
 export default async (me: mongodb.ObjectID, post: object) => {
 	// 自分の投稿はwatchできない
diff --git a/src/queue/processors/db/delete-post-dependents.ts b/src/queue/processors/db/delete-post-dependents.ts
index 879c41ec9..6de21eb05 100644
--- a/src/queue/processors/db/delete-post-dependents.ts
+++ b/src/queue/processors/db/delete-post-dependents.ts
@@ -1,9 +1,9 @@
-import Favorite from '../../models/favorite';
-import Notification from '../../models/notification';
-import PollVote from '../../models/poll-vote';
-import PostReaction from '../../models/post-reaction';
-import PostWatching from '../../models/post-watching';
-import Post from '../../models/post';
+import Favorite from '../../../models/favorite';
+import Notification from '../../../models/notification';
+import PollVote from '../../../models/poll-vote';
+import PostReaction from '../../../models/post-reaction';
+import PostWatching from '../../../models/post-watching';
+import Post from '../../../models/post';
 
 export default async ({ data }) => Promise.all([
 	Favorite.remove({ postId: data._id }),
diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts
index 82585d3a6..c3074429f 100644
--- a/src/queue/processors/http/process-inbox.ts
+++ b/src/queue/processors/http/process-inbox.ts
@@ -5,7 +5,6 @@ import parseAcct from '../../../acct/parse';
 import User, { IRemoteUser } from '../../../models/user';
 import act from '../../../remote/activitypub/act';
 import resolvePerson from '../../../remote/activitypub/resolve-person';
-import Resolver from '../../../remote/activitypub/resolver';
 
 // ユーザーのinboxにアクティビティが届いた時の処理
 export default async (job: kue.Job, done): Promise<void> => {
diff --git a/src/remote/activitypub/act/create.ts b/src/remote/activitypub/act/create.ts
index c486571fc..f97832a98 100644
--- a/src/remote/activitypub/act/create.ts
+++ b/src/remote/activitypub/act/create.ts
@@ -36,17 +36,17 @@ export default async (actor, activity): Promise<void> => {
 
 	switch (object.type) {
 	case 'Image':
-		createImage(resolver, object);
+		createImage(object);
 		break;
 
 	case 'Note':
-		createNote(resolver, object);
+		createNote(object);
 		break;
 	}
 
 	///
 
-	async function createImage(resolver: Resolver, image) {
+	async function createImage(image) {
 		if ('attributedTo' in image && actor.account.uri !== image.attributedTo) {
 			throw new Error('invalid image');
 		}
@@ -54,7 +54,7 @@ export default async (actor, activity): Promise<void> => {
 		return await uploadFromUrl(image.url, actor);
 	}
 
-	async function createNote(resolver: Resolver, note) {
+	async function createNote(note) {
 		if (
 			('attributedTo' in note && actor.account.uri !== note.attributedTo) ||
 			typeof note.id !== 'string'
@@ -63,20 +63,29 @@ export default async (actor, activity): Promise<void> => {
 		}
 
 		const media = [];
-
 		if ('attachment' in note) {
 			note.attachment.forEach(async media => {
-				const created = await createImage(resolver, media);
+				const created = await createImage(media);
 				media.push(created);
 			});
 		}
 
+		let reply = null;
+		if ('inReplyTo' in note) {
+			const inReplyToPost = await Post.findOne({ uri: note.id || note });
+			if (inReplyToPost) {
+				reply = inReplyToPost;
+			} else {
+				reply = await createNote(await resolver.resolve(note));
+			}
+		}
+
 		const { window } = new JSDOM(note.content);
 
-		await createPost(actor, {
+		return await createPost(actor, {
 			createdAt: new Date(note.published),
 			media,
-			reply: undefined,
+			reply,
 			repost: undefined,
 			text: window.document.body.textContent,
 			viaMobile: false,
diff --git a/src/remote/activitypub/act/delete.ts b/src/remote/activitypub/act/delete.ts
index f9eb4dd08..334ca47ed 100644
--- a/src/remote/activitypub/act/delete.ts
+++ b/src/remote/activitypub/act/delete.ts
@@ -1,21 +1,28 @@
-import create from '../create';
-import deleteObject from '../delete';
+import Resolver from '../resolver';
+import Post from '../../../models/post';
+import { createDb } from '../../../queue';
 
-export default async (resolver, actor, activity) => {
+export default async (actor, activity): Promise<void> => {
 	if ('actor' in activity && actor.account.uri !== activity.actor) {
 		throw new Error();
 	}
 
-	const results = await create(resolver, actor, activity.object);
+	const resolver = new Resolver();
 
-	await Promise.all(results.map(async promisedResult => {
-		const result = await promisedResult;
-		if (result === null) {
-			return;
-		}
+	const object = await resolver.resolve(activity);
 
-		await deleteObject(result);
-	}));
+	switch (object.type) {
+	case 'Note':
+		deleteNote(object);
+		break;
+	}
 
-	return null;
+	async function deleteNote(note) {
+		const post = await Post.findOneAndDelete({ uri: note.id });
+
+		createDb({
+			type: 'deletePostDependents',
+			id: post._id
+		}).delay(65536).save();
+	}
 };
diff --git a/src/remote/activitypub/act/undo.ts b/src/remote/activitypub/act/undo.ts
index b3b83777d..9d9f6b035 100644
--- a/src/remote/activitypub/act/undo.ts
+++ b/src/remote/activitypub/act/undo.ts
@@ -7,7 +7,7 @@ export default async (actor, activity): Promise<void> => {
 
 	switch (activity.object.type) {
 		case 'Follow':
-			unfollow(activity.object);
+			unfollow(actor, activity.object);
 			break;
 	}
 
diff --git a/src/remote/activitypub/delete/index.ts b/src/remote/activitypub/delete/index.ts
deleted file mode 100644
index bc9104284..000000000
--- a/src/remote/activitypub/delete/index.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import deletePost from './post';
-
-export default async ({ object }) => {
-	switch (object.$ref) {
-	case 'posts':
-		return deletePost(object);
-	}
-
-	return null;
-};
diff --git a/src/remote/activitypub/delete/post.ts b/src/remote/activitypub/delete/post.ts
deleted file mode 100644
index f6c816647..000000000
--- a/src/remote/activitypub/delete/post.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import Post from '../../../models/post';
-import queue from '../../../queue';
-
-export default async ({ $id }) => {
-	const promisedDeletion = Post.findOneAndDelete({ _id: $id });
-
-	await new Promise((resolve, reject) => queue.create('db', {
-		type: 'deletePostDependents',
-		id: $id
-	}).delay(65536).save(error => error ? reject(error) : resolve()));
-
-	return promisedDeletion;
-};
diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts
index 77d08398b..28162497f 100644
--- a/src/remote/activitypub/resolve-person.ts
+++ b/src/remote/activitypub/resolve-person.ts
@@ -2,7 +2,6 @@ import { JSDOM } from 'jsdom';
 import { toUnicode } from 'punycode';
 import User, { validateUsername, isValidName, isValidDescription } from '../../models/user';
 import webFinger from '../webfinger';
-import create from './create';
 import Resolver from './resolver';
 import uploadFromUrl from '../../api/drive/upload-from-url';
 
diff --git a/src/server/activitypub/inbox.ts b/src/server/activitypub/inbox.ts
index 847dc19af..b0015409a 100644
--- a/src/server/activitypub/inbox.ts
+++ b/src/server/activitypub/inbox.ts
@@ -1,7 +1,7 @@
 import * as bodyParser from 'body-parser';
 import * as express from 'express';
 import { parseRequest } from 'http-signature';
-import queue from '../../queue';
+import { createHttp } from '../../queue';
 
 const app = express();
 
@@ -22,7 +22,7 @@ app.post('/@:user/inbox', bodyParser.json({
 		return res.sendStatus(401);
 	}
 
-	queue.create('http', {
+	createHttp({
 		type: 'processInbox',
 		activity: req.body,
 		signature,

From 2dad0c8d6b719437a9786f954aa39e7d7c73b53c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 5 Apr 2018 15:50:52 +0900
Subject: [PATCH 1092/1250] wip

---
 src/api/post/create.ts | 11 +++++++----
 src/client/app/init.ts |  6 +-----
 2 files changed, 8 insertions(+), 9 deletions(-)

diff --git a/src/api/post/create.ts b/src/api/post/create.ts
index 36819ec2b..549511753 100644
--- a/src/api/post/create.ts
+++ b/src/api/post/create.ts
@@ -54,8 +54,7 @@ export default async (user: IUser, content: {
 		});
 	}
 
-	// 投稿を作成
-	const post = await Post.insert({
+	const data: any = {
 		createdAt: content.createdAt,
 		mediaIds: content.media ? content.media.map(file => file._id) : [],
 		replyId: content.reply ? content.reply._id : null,
@@ -68,14 +67,18 @@ export default async (user: IUser, content: {
 		userId: user._id,
 		viaMobile: content.viaMobile,
 		geo: content.geo || null,
-		uri: content.uri,
 		appId: content.app ? content.app._id : null,
 		visibility: content.visibility,
 
 		// 以下非正規化データ
 		_reply: content.reply ? { userId: content.reply.userId } : null,
 		_repost: content.repost ? { userId: content.repost.userId } : null,
-	});
+	};
+
+	if (content.uri != null) data.uri = content.uri;
+
+	// 投稿を作成
+	const post = await Post.insert(data);
 
 	res(post);
 
diff --git a/src/client/app/init.ts b/src/client/app/init.ts
index 3e5c38961..2fb8f15cf 100644
--- a/src/client/app/init.ts
+++ b/src/client/app/init.ts
@@ -14,7 +14,7 @@ import ElementLocaleJa from 'element-ui/lib/locale/lang/ja';
 import App from './app.vue';
 import checkForUpdate from './common/scripts/check-for-update';
 import MiOS, { API } from './common/mios';
-import { version, codename, hostname, lang } from './config';
+import { version, codename, lang } from './config';
 
 let elementLocale;
 switch (lang) {
@@ -60,10 +60,6 @@ console.info(
 window.clearTimeout((window as any).mkBootTimer);
 delete (window as any).mkBootTimer;
 
-if (hostname != 'localhost') {
-	document.domain = hostname;
-}
-
 //#region Set lang attr
 const html = document.documentElement;
 html.setAttribute('lang', lang);

From cf3216223de747400faa8b9ab2cf0d9196ac9c1e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 5 Apr 2018 15:54:12 +0900
Subject: [PATCH 1093/1250] wip

---
 src/server/webfinger.ts | 21 ++++++++++++---------
 1 file changed, 12 insertions(+), 9 deletions(-)

diff --git a/src/server/webfinger.ts b/src/server/webfinger.ts
index 20057da31..fd7ebc3fb 100644
--- a/src/server/webfinger.ts
+++ b/src/server/webfinger.ts
@@ -1,11 +1,12 @@
+import * as express from 'express';
+
 import config from '../config';
 import parseAcct from '../acct/parse';
 import User from '../models/user';
-const express = require('express');
 
 const app = express();
 
-app.get('/.well-known/webfinger', async (req, res) => {
+app.get('/.well-known/webfinger', async (req: express.Request, res: express.Response) => {
 	if (typeof req.query.resource !== 'string') {
 		return res.sendStatus(400);
 	}
@@ -34,13 +35,15 @@ app.get('/.well-known/webfinger', async (req, res) => {
 
 	return res.json({
 		subject: `acct:${user.username}@${config.host}`,
-		links: [
-			{
-				rel: 'self',
-				type: 'application/activity+json',
-				href: `${config.url}/@${user.username}`
-			}
-		]
+		links: [{
+			rel: 'self',
+			type: 'application/activity+json',
+			href: `${config.url}/@${user.username}`
+		}, {
+			rel: 'http://webfinger.net/rel/profile-page',
+			type: 'text/html',
+			href: `${config.url}/@${user.username}`
+		}]
 	});
 });
 

From f8bfec29633e1f99bdf336cfcfa894c49a8cd355 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 5 Apr 2018 17:52:46 +0900
Subject: [PATCH 1094/1250] wip

---
 src/server/activitypub/inbox.ts     | 4 +---
 src/server/activitypub/outbox.ts    | 3 +--
 src/server/activitypub/post.ts      | 3 +--
 src/server/activitypub/publickey.ts | 3 +--
 src/server/activitypub/user.ts      | 3 +--
 5 files changed, 5 insertions(+), 11 deletions(-)

diff --git a/src/server/activitypub/inbox.ts b/src/server/activitypub/inbox.ts
index b0015409a..1b6cc0c00 100644
--- a/src/server/activitypub/inbox.ts
+++ b/src/server/activitypub/inbox.ts
@@ -3,9 +3,7 @@ import * as express from 'express';
 import { parseRequest } from 'http-signature';
 import { createHttp } from '../../queue';
 
-const app = express();
-
-app.disable('x-powered-by');
+const app = express.Router();
 
 app.post('/@:user/inbox', bodyParser.json({
 	type() {
diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts
index 9ecb0c071..976908d1f 100644
--- a/src/server/activitypub/outbox.ts
+++ b/src/server/activitypub/outbox.ts
@@ -6,8 +6,7 @@ import config from '../../config';
 import Post from '../../models/post';
 import withUser from './with-user';
 
-const app = express();
-app.disable('x-powered-by');
+const app = express.Router();
 
 app.get('/@:user/outbox', withUser(username => {
 	return `${config.url}/@${username}/inbox`;
diff --git a/src/server/activitypub/post.ts b/src/server/activitypub/post.ts
index 91d91aeb9..355c60356 100644
--- a/src/server/activitypub/post.ts
+++ b/src/server/activitypub/post.ts
@@ -5,8 +5,7 @@ import parseAcct from '../../acct/parse';
 import Post from '../../models/post';
 import User from '../../models/user';
 
-const app = express();
-app.disable('x-powered-by');
+const app = express.Router();
 
 app.get('/@:user/:post', async (req, res, next) => {
 	const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']);
diff --git a/src/server/activitypub/publickey.ts b/src/server/activitypub/publickey.ts
index c564c437e..b48504927 100644
--- a/src/server/activitypub/publickey.ts
+++ b/src/server/activitypub/publickey.ts
@@ -4,8 +4,7 @@ import render from '../../remote/activitypub/renderer/key';
 import config from '../../config';
 import withUser from './with-user';
 
-const app = express();
-app.disable('x-powered-by');
+const app = express.Router();
 
 app.get('/@:user/publickey', withUser(username => {
 	return `${config.url}/@${username}/publickey`;
diff --git a/src/server/activitypub/user.ts b/src/server/activitypub/user.ts
index baf2dc9a0..f05497451 100644
--- a/src/server/activitypub/user.ts
+++ b/src/server/activitypub/user.ts
@@ -11,8 +11,7 @@ const respond = withUser(username => `${config.url}/@${username}`, (user, req, r
 	res.json(rendered);
 });
 
-const app = express();
-app.disable('x-powered-by');
+const app = express.Router();
 
 app.get('/@:user', (req, res, next) => {
 	const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']);

From 9c9b09c5702c4b13afd1ae2a38a6aff67d31c4d1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 5 Apr 2018 18:08:51 +0900
Subject: [PATCH 1095/1250] wip

---
 src/api/drive/upload-from-url.ts         |  8 +++++++-
 src/index.ts                             |  4 ++++
 src/remote/activitypub/resolve-person.ts | 10 +++++-----
 src/remote/activitypub/resolver.ts       |  8 ++++++--
 4 files changed, 22 insertions(+), 8 deletions(-)

diff --git a/src/api/drive/upload-from-url.ts b/src/api/drive/upload-from-url.ts
index 26c890d15..676586cd1 100644
--- a/src/api/drive/upload-from-url.ts
+++ b/src/api/drive/upload-from-url.ts
@@ -6,14 +6,18 @@ import * as tmp from 'tmp';
 import * as fs from 'fs';
 import * as request from 'request';
 
-const log = debug('misskey:common:drive:upload_from_url');
+const log = debug('misskey:drive:upload-from-url');
 
 export default async (url, user, folderId = null, uri = null): Promise<IDriveFile> => {
+	log(`REQUESTED: ${url}`);
+
 	let name = URL.parse(url).pathname.split('/').pop();
 	if (!validateFileName(name)) {
 		name = null;
 	}
 
+	log(`name: ${name}`);
+
 	// Create temp file
 	const path = await new Promise((res: (string) => void, rej) => {
 		tmp.file((e, path) => {
@@ -37,6 +41,8 @@ export default async (url, user, folderId = null, uri = null): Promise<IDriveFil
 
 	const driveFile = await create(user, path, name, null, folderId, false, uri);
 
+	log(`created: ${driveFile._id}`);
+
 	// clean-up
 	fs.unlink(path, (e) => {
 		if (e) log(e.stack);
diff --git a/src/index.ts b/src/index.ts
index 29c4f3431..e35c917a4 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -30,6 +30,10 @@ const ev = new Xev();
 
 process.title = 'Misskey';
 
+if (process.env.NODE_ENV != 'production') {
+	process.env.DEBUG = 'misskey:*';
+}
+
 // https://github.com/Automattic/kue/issues/822
 require('events').EventEmitter.prototype._maxListeners = 256;
 
diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts
index 28162497f..c288a2f00 100644
--- a/src/remote/activitypub/resolve-person.ts
+++ b/src/remote/activitypub/resolve-person.ts
@@ -31,7 +31,7 @@ export default async (value, verifier?: string) => {
 	const user = await User.insert({
 		avatarId: null,
 		bannerId: null,
-		createdAt: Date.parse(object.published),
+		createdAt: Date.parse(object.published) || null,
 		description: summaryDOM.textContent,
 		followersCount: 0,
 		followingCount: 0,
@@ -55,14 +55,14 @@ export default async (value, verifier?: string) => {
 	const [avatarId, bannerId] = await Promise.all([
 		object.icon,
 		object.image
-	].map(async url => {
-		if (url === undefined) {
+	].map(async img => {
+		if (img === undefined) {
 			return null;
 		}
 
-		const img = await uploadFromUrl(url, user);
+		const file = await uploadFromUrl(img.url, user);
 
-		return img._id;
+		return file._id;
 	}));
 
 	User.update({ _id: user._id }, { $set: { avatarId, bannerId } });
diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts
index de0bba268..09a6e7005 100644
--- a/src/remote/activitypub/resolver.ts
+++ b/src/remote/activitypub/resolver.ts
@@ -1,6 +1,8 @@
-import { IObject } from "./type";
+import * as request from 'request-promise-native';
+import * as debug from 'debug';
+import { IObject } from './type';
 
-const request = require('request-promise-native');
+const log = debug('misskey:activitypub:resolver');
 
 export default class Resolver {
 	private history: Set<string>;
@@ -57,6 +59,8 @@ export default class Resolver {
 			throw new Error('invalid response');
 		}
 
+		log(`resolved: ${JSON.stringify(object)}`);
+
 		return object;
 	}
 }

From e20e321a82c54263cbb18e6e0e2e9b429e5c2176 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 5 Apr 2018 18:43:06 +0900
Subject: [PATCH 1096/1250] wip

---
 src/api/post/create.ts                           | 15 ++++++++-------
 src/index.ts                                     |  2 +-
 src/queue/index.ts                               |  8 +++++++-
 src/queue/processors/http/deliver.ts             |  9 ++++++++-
 src/queue/processors/http/index.ts               | 16 ++++++++++++----
 .../processors/http/report-github-failure.ts     |  6 +++---
 src/remote/activitypub/resolver.ts               |  2 +-
 src/remote/request.ts                            |  8 ++++++++
 src/server/api/endpoints/following/create.ts     | 11 ++---------
 9 files changed, 50 insertions(+), 27 deletions(-)

diff --git a/src/api/post/create.ts b/src/api/post/create.ts
index 549511753..dbeb87ae8 100644
--- a/src/api/post/create.ts
+++ b/src/api/post/create.ts
@@ -18,20 +18,21 @@ import html from '../../text/html';
 import { IApp } from '../../models/app';
 
 export default async (user: IUser, content: {
-	createdAt: Date;
-	text: string;
-	reply: IPost;
-	repost: IPost;
-	media: IDriveFile[];
-	geo: any;
+	createdAt?: Date;
+	text?: string;
+	reply?: IPost;
+	repost?: IPost;
+	media?: IDriveFile[];
+	geo?: any;
 	poll?: any;
-	viaMobile: boolean;
+	viaMobile?: boolean;
 	tags?: string[];
 	cw?: string;
 	visibility?: string;
 	uri?: string;
 	app?: IApp;
 }) => new Promise<IPost>(async (res, rej) => {
+	if (content.createdAt == null) content.createdAt = new Date();
 	if (content.visibility == null) content.visibility = 'public';
 
 	const tags = content.tags || [];
diff --git a/src/index.ts b/src/index.ts
index e35c917a4..f45bcaa6a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -103,7 +103,7 @@ async function workerMain(opt) {
 
 	if (!opt['only-server']) {
 		// start processor
-		require('./processor').default();
+		require('./queue').default();
 	}
 
 	// Send a 'ready' message to parent process
diff --git a/src/queue/index.ts b/src/queue/index.ts
index c8c436b18..86600dc26 100644
--- a/src/queue/index.ts
+++ b/src/queue/index.ts
@@ -1,8 +1,12 @@
 import { createQueue } from 'kue';
+import * as debug from 'debug';
+
 import config from '../config';
 import db from './processors/db';
 import http from './processors/http';
 
+const log = debug('misskey:queue');
+
 const queue = createQueue({
 	redis: {
 		port: config.redis.port,
@@ -12,6 +16,8 @@ const queue = createQueue({
 });
 
 export function createHttp(data) {
+	log(`HTTP job created: ${JSON.stringify(data)}`);
+
 	return queue
 		.create('http', data)
 		.attempts(16)
@@ -22,7 +28,7 @@ export function createDb(data) {
 	return queue.create('db', data);
 }
 
-export function process() {
+export default function() {
 	queue.process('db', db);
 
 	/*
diff --git a/src/queue/processors/http/deliver.ts b/src/queue/processors/http/deliver.ts
index 1700063a5..da7e8bc36 100644
--- a/src/queue/processors/http/deliver.ts
+++ b/src/queue/processors/http/deliver.ts
@@ -3,5 +3,12 @@ import * as kue from 'kue';
 import request from '../../../remote/request';
 
 export default async (job: kue.Job, done): Promise<void> => {
-	await request(job.data.user, job.data.to, job.data.content);
+	try {
+		await request(job.data.user, job.data.to, job.data.content);
+		done();
+	} catch (e) {
+		console.warn(`deliver failed: ${e}`);
+
+		done(e);
+	}
 };
diff --git a/src/queue/processors/http/index.ts b/src/queue/processors/http/index.ts
index 06c6b1d1a..3d7d941b1 100644
--- a/src/queue/processors/http/index.ts
+++ b/src/queue/processors/http/index.ts
@@ -3,9 +3,17 @@ import processInbox from './process-inbox';
 import reportGitHubFailure from './report-github-failure';
 
 const handlers = {
-  deliver,
-  processInbox,
-  reportGitHubFailure,
+	deliver,
+	processInbox,
+	reportGitHubFailure
 };
 
-export default (job, done) => handlers[job.data.type](job).then(() => done(), done);
+export default (job, done) => {
+	const handler = handlers[job.data.type];
+
+	if (handler) {
+		handler(job).then(() => done(), done);
+	} else {
+		console.warn(`Unknown job: ${job.data.type}`);
+	}
+};
diff --git a/src/queue/processors/http/report-github-failure.ts b/src/queue/processors/http/report-github-failure.ts
index 4f6f5ccee..e747d062d 100644
--- a/src/queue/processors/http/report-github-failure.ts
+++ b/src/queue/processors/http/report-github-failure.ts
@@ -1,6 +1,6 @@
 import * as request from 'request-promise-native';
-import User from '../../models/user';
-const createPost = require('../../server/api/endpoints/posts/create');
+import User from '../../../models/user';
+import createPost from '../../../api/post/create';
 
 export default async ({ data }) => {
 	const asyncBot = User.findOne({ _id: data.userId });
@@ -20,5 +20,5 @@ export default async ({ data }) => {
 		`**⚠️BUILD STILL FAILED⚠️**: ?[${data.message}](${data.htmlUrl})` :
 		`**🚨BUILD FAILED🚨**: →→→?[${data.message}](${data.htmlUrl})←←←`;
 
-	createPost({ text }, await asyncBot);
+	createPost(await asyncBot, { text });
 };
diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts
index 09a6e7005..38639c681 100644
--- a/src/remote/activitypub/resolver.ts
+++ b/src/remote/activitypub/resolver.ts
@@ -59,7 +59,7 @@ export default class Resolver {
 			throw new Error('invalid response');
 		}
 
-		log(`resolved: ${JSON.stringify(object)}`);
+		log(`resolved: ${JSON.stringify(object, null, 2)}`);
 
 		return object;
 	}
diff --git a/src/remote/request.ts b/src/remote/request.ts
index 72262cbf6..a375aebfb 100644
--- a/src/remote/request.ts
+++ b/src/remote/request.ts
@@ -1,9 +1,15 @@
 import { request } from 'https';
 import { sign } from 'http-signature';
 import { URL } from 'url';
+import * as debug from 'debug';
+
 import config from '../config';
 
+const log = debug('misskey:activitypub:deliver');
+
 export default ({ account, username }, url, object) => new Promise((resolve, reject) => {
+	log(`--> ${url}`);
+
 	const { protocol, hostname, port, pathname, search } = new URL(url);
 
 	const req = request({
@@ -14,6 +20,8 @@ export default ({ account, username }, url, object) => new Promise((resolve, rej
 		path: pathname + search,
 	}, res => {
 		res.on('end', () => {
+			log(`${url} --> ${res.statusCode}`);
+
 			if (res.statusCode >= 200 && res.statusCode < 300) {
 				resolve();
 			} else {
diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts
index e56859521..fae686ce5 100644
--- a/src/server/api/endpoints/following/create.ts
+++ b/src/server/api/endpoints/following/create.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import User from '../../../../models/user';
 import Following from '../../../../models/following';
-import queue from '../../../../queue';
+import create from '../../../../api/following/create';
 
 /**
  * Follow a user
@@ -50,15 +50,8 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}
 
 	// Create following
-	const { _id } = await Following.insert({
-		createdAt: new Date(),
-		followerId: follower._id,
-		followeeId: followee._id
-	});
-
-	queue.create('http', { type: 'follow', following: _id }).save();
+	create(follower, followee);
 
 	// Send response
 	res();
-
 });

From 00dd472ebe8cf6c153a0f1e3617fdcad0950dbed Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 5 Apr 2018 18:50:52 +0900
Subject: [PATCH 1097/1250] wip

---
 src/remote/activitypub/resolve-person.ts | 2 +-
 src/remote/resolve-user.ts               | 2 +-
 src/server/api/endpoints/users/show.ts   | 3 ++-
 3 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts
index c288a2f00..b979bb1cd 100644
--- a/src/remote/activitypub/resolve-person.ts
+++ b/src/remote/activitypub/resolve-person.ts
@@ -15,7 +15,7 @@ export default async (value, verifier?: string) => {
 		object.type !== 'Person' ||
 		typeof object.preferredUsername !== 'string' ||
 		!validateUsername(object.preferredUsername) ||
-		!isValidName(object.name) ||
+		(object.name != '' && !isValidName(object.name)) ||
 		!isValidDescription(object.summary)
 	) {
 		throw new Error('invalid person');
diff --git a/src/remote/resolve-user.ts b/src/remote/resolve-user.ts
index 48219e8cb..9e1ae5195 100644
--- a/src/remote/resolve-user.ts
+++ b/src/remote/resolve-user.ts
@@ -16,7 +16,7 @@ export default async (username, host, option) => {
 		const finger = await webFinger(acctLower, acctLower);
 		const self = finger.links.find(link => link.rel && link.rel.toLowerCase() === 'self');
 		if (!self) {
-			throw new Error();
+			throw new Error('self link not found');
 		}
 
 		user = await resolvePerson(self.href, acctLower);
diff --git a/src/server/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts
index 2b0279937..d272ce463 100644
--- a/src/server/api/endpoints/users/show.ts
+++ b/src/server/api/endpoints/users/show.ts
@@ -37,7 +37,8 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	if (typeof host === 'string') {
 		try {
 			user = await resolveRemoteUser(username, host, cursorOption);
-		} catch (exception) {
+		} catch (e) {
+			console.warn(`failed to resolve remote user: ${e}`);
 			return rej('failed to resolve remote user');
 		}
 	} else {

From 2468b34d93c15e25782f477783c9141ff34418cb Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 5 Apr 2018 19:19:00 +0900
Subject: [PATCH 1098/1250] wip

---
 src/remote/activitypub/act/create.ts    | 46 ++++++++++++++-----------
 src/remote/activitypub/act/index.ts     |  4 +++
 src/remote/activitypub/renderer/note.ts | 13 ++++---
 3 files changed, 38 insertions(+), 25 deletions(-)

diff --git a/src/remote/activitypub/act/create.ts b/src/remote/activitypub/act/create.ts
index f97832a98..80afb61bd 100644
--- a/src/remote/activitypub/act/create.ts
+++ b/src/remote/activitypub/act/create.ts
@@ -1,11 +1,13 @@
 import { JSDOM } from 'jsdom';
+import * as debug from 'debug';
 
 import Resolver from '../resolver';
-import DriveFile from '../../../models/drive-file';
 import Post from '../../../models/post';
 import uploadFromUrl from '../../../api/drive/upload-from-url';
 import createPost from '../../../api/post/create';
 
+const log = debug('misskey:activitypub');
+
 export default async (actor, activity): Promise<void> => {
 	if ('actor' in activity && actor.account.uri !== activity.actor) {
 		throw new Error('invalid actor');
@@ -13,26 +15,20 @@ export default async (actor, activity): Promise<void> => {
 
 	const uri = activity.id || activity;
 
-	try {
-		await Promise.all([
-			DriveFile.findOne({ 'metadata.uri': uri }).then(file => {
-				if (file !== null) {
-					throw new Error();
-				}
-			}, () => {}),
-			Post.findOne({ uri }).then(post => {
-				if (post !== null) {
-					throw new Error();
-				}
-			}, () => {})
-		]);
-	} catch (object) {
-		throw new Error(`already registered: ${uri}`);
-	}
+	log(`Create: ${uri}`);
+
+	// TODO: 同じURIをもつものが既に登録されていないかチェック
 
 	const resolver = new Resolver();
 
-	const object = await resolver.resolve(activity);
+	let object;
+
+	try {
+		object = await resolver.resolve(activity.object);
+	} catch (e) {
+		log(`Resolve failed: ${e}`);
+		throw e;
+	}
 
 	switch (object.type) {
 	case 'Image':
@@ -42,15 +38,22 @@ export default async (actor, activity): Promise<void> => {
 	case 'Note':
 		createNote(object);
 		break;
+
+	default:
+		console.warn(`Unknown type: ${object.type}`);
+		break;
 	}
 
 	///
 
 	async function createImage(image) {
 		if ('attributedTo' in image && actor.account.uri !== image.attributedTo) {
+			log(`invalid image: ${JSON.stringify(image, null, 2)}`);
 			throw new Error('invalid image');
 		}
 
+		log(`Creating the Image: ${uri}`);
+
 		return await uploadFromUrl(image.url, actor);
 	}
 
@@ -59,11 +62,14 @@ export default async (actor, activity): Promise<void> => {
 			('attributedTo' in note && actor.account.uri !== note.attributedTo) ||
 			typeof note.id !== 'string'
 		) {
+			log(`invalid note: ${JSON.stringify(note, null, 2)}`);
 			throw new Error('invalid note');
 		}
 
+		log(`Creating the Note: ${uri}`);
+
 		const media = [];
-		if ('attachment' in note) {
+		if ('attachment' in note && note.attachment != null) {
 			note.attachment.forEach(async media => {
 				const created = await createImage(media);
 				media.push(created);
@@ -71,7 +77,7 @@ export default async (actor, activity): Promise<void> => {
 		}
 
 		let reply = null;
-		if ('inReplyTo' in note) {
+		if ('inReplyTo' in note && note.inReplyTo != null) {
 			const inReplyToPost = await Post.findOne({ uri: note.id || note });
 			if (inReplyToPost) {
 				reply = inReplyToPost;
diff --git a/src/remote/activitypub/act/index.ts b/src/remote/activitypub/act/index.ts
index f22500ace..584022709 100644
--- a/src/remote/activitypub/act/index.ts
+++ b/src/remote/activitypub/act/index.ts
@@ -18,6 +18,10 @@ export default async (actor, activity: IObject): Promise<void> => {
 		await follow(actor, activity);
 		break;
 
+	case 'Accept':
+		// noop
+		break;
+
 	case 'Undo':
 		await undo(actor, activity);
 		break;
diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts
index 43531b121..e45b10215 100644
--- a/src/remote/activitypub/renderer/note.ts
+++ b/src/remote/activitypub/renderer/note.ts
@@ -2,11 +2,14 @@ import renderDocument from './document';
 import renderHashtag from './hashtag';
 import config from '../../../config';
 import DriveFile from '../../../models/drive-file';
-import Post from '../../../models/post';
-import User from '../../../models/user';
+import Post, { IPost } from '../../../models/post';
+import User, { IUser } from '../../../models/user';
+
+export default async (user: IUser, post: IPost) => {
+	const promisedFiles = post.mediaIds
+		? DriveFile.find({ _id: { $in: post.mediaIds } })
+		: Promise.resolve([]);
 
-export default async (user, post) => {
-	const promisedFiles = DriveFile.find({ _id: { $in: post.mediaIds } });
 	let inReplyTo;
 
 	if (post.replyId) {
@@ -39,6 +42,6 @@ export default async (user, post) => {
 		cc: `${attributedTo}/followers`,
 		inReplyTo,
 		attachment: (await promisedFiles).map(renderDocument),
-		tag: post.tags.map(renderHashtag)
+		tag: (post.tags || []).map(renderHashtag)
 	};
 };

From 504d845a016b61127017926d2575ac50a4dfd66c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 5 Apr 2018 19:23:42 +0900
Subject: [PATCH 1099/1250] wip

---
 src/remote/activitypub/act/create.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/remote/activitypub/act/create.ts b/src/remote/activitypub/act/create.ts
index 80afb61bd..7d5a9d427 100644
--- a/src/remote/activitypub/act/create.ts
+++ b/src/remote/activitypub/act/create.ts
@@ -78,7 +78,7 @@ export default async (actor, activity): Promise<void> => {
 
 		let reply = null;
 		if ('inReplyTo' in note && note.inReplyTo != null) {
-			const inReplyToPost = await Post.findOne({ uri: note.id || note });
+			const inReplyToPost = await Post.findOne({ uri: note.inReplyTo.id || note.inReplyTo });
 			if (inReplyToPost) {
 				reply = inReplyToPost;
 			} else {

From 3cae375cf4cf53720c66f702d02fa10ec84a25ef Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 5 Apr 2018 11:24:06 +0000
Subject: [PATCH 1100/1250] fix(package): update webpack to version 4.5.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 38d96117c..45ceaaf88 100644
--- a/package.json
+++ b/package.json
@@ -209,7 +209,7 @@
 		"vuedraggable": "2.16.0",
 		"web-push": "3.3.0",
 		"webfinger.js": "2.6.6",
-		"webpack": "4.4.1",
+		"webpack": "4.5.0",
 		"webpack-cli": "2.0.13",
 		"webpack-replace-loader": "1.3.0",
 		"websocket": "1.0.25",

From 3483700d03c26494e22d234a5821d2f4cebecccc Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 5 Apr 2018 12:13:49 +0000
Subject: [PATCH 1101/1250] fix(package): update object-assign-deep to version
 0.4.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 38d96117c..4df14a8d4 100644
--- a/package.json
+++ b/package.json
@@ -158,7 +158,7 @@
 		"node-sass-json-importer": "3.1.6",
 		"nopt": "4.0.1",
 		"nprogress": "0.2.0",
-		"object-assign-deep": "0.3.1",
+		"object-assign-deep": "0.4.0",
 		"on-build-webpack": "0.1.0",
 		"os-utils": "0.0.14",
 		"progress-bar-webpack-plugin": "1.11.0",

From b7d72b6bb425ac62db1243d100e366c276e56772 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Thu, 5 Apr 2018 21:16:14 +0900
Subject: [PATCH 1102/1250] Add a missing await expression

---
 src/remote/activitypub/resolve-person.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts
index 7ed01e322..a7c0020dd 100644
--- a/src/remote/activitypub/resolve-person.ts
+++ b/src/remote/activitypub/resolve-person.ts
@@ -10,7 +10,7 @@ async function isCollection(collection) {
 }
 
 export default async (parentResolver, value, verifier?: string) => {
-	const { resolver, object } = parentResolver.resolveOne(value);
+	const { resolver, object } = await parentResolver.resolveOne(value);
 
 	if (
 		object === null ||

From 207332d71f161e25bc9b1217fde282cb20e2857e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 5 Apr 2018 22:49:41 +0900
Subject: [PATCH 1103/1250] wip

---
 src/remote/activitypub/act/create.ts     | 106 ++++++++++++-----------
 src/remote/activitypub/act/index.ts      |   3 +-
 src/remote/activitypub/resolve-person.ts |   2 +-
 src/remote/activitypub/resolver.ts       |   4 +
 4 files changed, 62 insertions(+), 53 deletions(-)

diff --git a/src/remote/activitypub/act/create.ts b/src/remote/activitypub/act/create.ts
index 7d5a9d427..9669348d5 100644
--- a/src/remote/activitypub/act/create.ts
+++ b/src/remote/activitypub/act/create.ts
@@ -5,10 +5,12 @@ import Resolver from '../resolver';
 import Post from '../../../models/post';
 import uploadFromUrl from '../../../api/drive/upload-from-url';
 import createPost from '../../../api/post/create';
+import { IRemoteUser, isRemoteUser } from '../../../models/user';
+import resolvePerson from '../resolve-person';
 
 const log = debug('misskey:activitypub');
 
-export default async (actor, activity): Promise<void> => {
+export default async (actor: IRemoteUser, activity): Promise<void> => {
 	if ('actor' in activity && actor.account.uri !== activity.actor) {
 		throw new Error('invalid actor');
 	}
@@ -32,71 +34,73 @@ export default async (actor, activity): Promise<void> => {
 
 	switch (object.type) {
 	case 'Image':
-		createImage(object);
+		createImage(resolver, actor, object);
 		break;
 
 	case 'Note':
-		createNote(object);
+		createNote(resolver, actor, object);
 		break;
 
 	default:
 		console.warn(`Unknown type: ${object.type}`);
 		break;
 	}
+};
 
-	///
-
-	async function createImage(image) {
-		if ('attributedTo' in image && actor.account.uri !== image.attributedTo) {
-			log(`invalid image: ${JSON.stringify(image, null, 2)}`);
-			throw new Error('invalid image');
-		}
-
-		log(`Creating the Image: ${uri}`);
-
-		return await uploadFromUrl(image.url, actor);
+async function createImage(resolver: Resolver, actor: IRemoteUser, image) {
+	if ('attributedTo' in image && actor.account.uri !== image.attributedTo) {
+		log(`invalid image: ${JSON.stringify(image, null, 2)}`);
+		throw new Error('invalid image');
 	}
 
-	async function createNote(note) {
-		if (
-			('attributedTo' in note && actor.account.uri !== note.attributedTo) ||
-			typeof note.id !== 'string'
-		) {
-			log(`invalid note: ${JSON.stringify(note, null, 2)}`);
-			throw new Error('invalid note');
-		}
+	log(`Creating the Image: ${image.id}`);
 
-		log(`Creating the Note: ${uri}`);
+	return await uploadFromUrl(image.url, actor);
+}
 
-		const media = [];
-		if ('attachment' in note && note.attachment != null) {
-			note.attachment.forEach(async media => {
-				const created = await createImage(media);
-				media.push(created);
-			});
-		}
+async function createNote(resolver: Resolver, actor: IRemoteUser, note) {
+	if (
+		('attributedTo' in note && actor.account.uri !== note.attributedTo) ||
+		typeof note.id !== 'string'
+	) {
+		log(`invalid note: ${JSON.stringify(note, null, 2)}`);
+		throw new Error('invalid note');
+	}
 
-		let reply = null;
-		if ('inReplyTo' in note && note.inReplyTo != null) {
-			const inReplyToPost = await Post.findOne({ uri: note.inReplyTo.id || note.inReplyTo });
-			if (inReplyToPost) {
-				reply = inReplyToPost;
-			} else {
-				reply = await createNote(await resolver.resolve(note));
-			}
-		}
+	log(`Creating the Note: ${note.id}`);
 
-		const { window } = new JSDOM(note.content);
-
-		return await createPost(actor, {
-			createdAt: new Date(note.published),
-			media,
-			reply,
-			repost: undefined,
-			text: window.document.body.textContent,
-			viaMobile: false,
-			geo: undefined,
-			uri: note.id
+	const media = [];
+	if ('attachment' in note && note.attachment != null) {
+		note.attachment.forEach(async media => {
+			const created = await createImage(resolver, note.actor, media);
+			media.push(created);
 		});
 	}
-};
+
+	let reply = null;
+	if ('inReplyTo' in note && note.inReplyTo != null) {
+		const inReplyToPost = await Post.findOne({ uri: note.inReplyTo.id || note.inReplyTo });
+		if (inReplyToPost) {
+			reply = inReplyToPost;
+		} else {
+			const inReplyTo = await resolver.resolve(note.inReplyTo) as any;
+			const actor = await resolvePerson(inReplyTo.attributedTo);
+			if (isRemoteUser(actor)) {
+				reply = await createNote(resolver, actor, inReplyTo);
+			}
+		}
+	}
+
+	const { window } = new JSDOM(note.content);
+
+	return await createPost(actor, {
+		createdAt: new Date(note.published),
+		media,
+		reply,
+		repost: undefined,
+		text: window.document.body.textContent,
+		viaMobile: false,
+		geo: undefined,
+		uri: note.id
+	});
+}
diff --git a/src/remote/activitypub/act/index.ts b/src/remote/activitypub/act/index.ts
index 584022709..f58505b0a 100644
--- a/src/remote/activitypub/act/index.ts
+++ b/src/remote/activitypub/act/index.ts
@@ -3,8 +3,9 @@ import performDeleteActivity from './delete';
 import follow from './follow';
 import undo from './undo';
 import { IObject } from '../type';
+import { IUser } from '../../../models/user';
 
-export default async (actor, activity: IObject): Promise<void> => {
+export default async (actor: IUser, activity: IObject): Promise<void> => {
 	switch (activity.type) {
 	case 'Create':
 		await create(actor, activity);
diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts
index b979bb1cd..2bf7a1354 100644
--- a/src/remote/activitypub/resolve-person.ts
+++ b/src/remote/activitypub/resolve-person.ts
@@ -11,7 +11,7 @@ export default async (value, verifier?: string) => {
 	const object = await resolver.resolve(value) as any;
 
 	if (
-		object === null ||
+		object == null ||
 		object.type !== 'Person' ||
 		typeof object.preferredUsername !== 'string' ||
 		!validateUsername(object.preferredUsername) ||
diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts
index 38639c681..4a97e2ef6 100644
--- a/src/remote/activitypub/resolver.ts
+++ b/src/remote/activitypub/resolver.ts
@@ -33,6 +33,10 @@ export default class Resolver {
 	}
 
 	public async resolve(value): Promise<IObject> {
+		if (value == null) {
+			throw new Error('resolvee is null (or undefined)');
+		}
+
 		if (typeof value !== 'string') {
 			return value;
 		}

From 71699ab4fa5b8478387d401163690cb8e42a4cf7 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 5 Apr 2018 14:10:22 +0000
Subject: [PATCH 1104/1250] fix(package): update webpack-cli to version 2.0.14

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 38d96117c..b721682d4 100644
--- a/package.json
+++ b/package.json
@@ -210,7 +210,7 @@
 		"web-push": "3.3.0",
 		"webfinger.js": "2.6.6",
 		"webpack": "4.4.1",
-		"webpack-cli": "2.0.13",
+		"webpack-cli": "2.0.14",
 		"webpack-replace-loader": "1.3.0",
 		"websocket": "1.0.25",
 		"ws": "5.1.1",

From 925815d67ef5c13f825d8271e23d315998e03f4b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 5 Apr 2018 23:19:47 +0900
Subject: [PATCH 1105/1250] wip

---
 src/api/post/create.ts               | 40 +++++++++++++++-------------
 src/remote/activitypub/act/create.ts |  6 ++---
 2 files changed, 24 insertions(+), 22 deletions(-)

diff --git a/src/api/post/create.ts b/src/api/post/create.ts
index dbeb87ae8..7b7fceda2 100644
--- a/src/api/post/create.ts
+++ b/src/api/post/create.ts
@@ -31,7 +31,7 @@ export default async (user: IUser, content: {
 	visibility?: string;
 	uri?: string;
 	app?: IApp;
-}) => new Promise<IPost>(async (res, rej) => {
+}, silent = false) => new Promise<IPost>(async (res, rej) => {
 	if (content.createdAt == null) content.createdAt = new Date();
 	if (content.visibility == null) content.visibility = 'public';
 
@@ -120,26 +120,28 @@ export default async (user: IUser, content: {
 			_id: false
 		});
 
-		const note = await renderNote(user, post);
-		const content = renderCreate(note);
-		content['@context'] = context;
+		if (!silent) {
+			const note = await renderNote(user, post);
+			const content = renderCreate(note);
+			content['@context'] = context;
 
-		Promise.all(followers.map(({ follower }) => {
-			if (isLocalUser(follower)) {
-				// Publish event to followers stream
-				stream(follower._id, 'post', postObj);
-			} else {
-				// フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信
-				if (isLocalUser(user)) {
-					createHttp({
-						type: 'deliver',
-						user,
-						content,
-						to: follower.account.inbox
-					}).save();
+			Promise.all(followers.map(({ follower }) => {
+				if (isLocalUser(follower)) {
+					// Publish event to followers stream
+					stream(follower._id, 'post', postObj);
+				} else {
+					// フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信
+					if (isLocalUser(user)) {
+						createHttp({
+							type: 'deliver',
+							user,
+							content,
+							to: follower.account.inbox
+						}).save();
+					}
 				}
-			}
-		}));
+			}));
+		}
 	}
 
 	// チャンネルへの投稿
diff --git a/src/remote/activitypub/act/create.ts b/src/remote/activitypub/act/create.ts
index 9669348d5..fe58f58f8 100644
--- a/src/remote/activitypub/act/create.ts
+++ b/src/remote/activitypub/act/create.ts
@@ -58,7 +58,7 @@ async function createImage(resolver: Resolver, actor: IRemoteUser, image) {
 	return await uploadFromUrl(image.url, actor);
 }
 
-async function createNote(resolver: Resolver, actor: IRemoteUser, note) {
+async function createNote(resolver: Resolver, actor: IRemoteUser, note, silent = false) {
 	if (
 		('attributedTo' in note && actor.account.uri !== note.attributedTo) ||
 		typeof note.id !== 'string'
@@ -86,7 +86,7 @@ async function createNote(resolver: Resolver, actor: IRemoteUser, note) {
 			const inReplyTo = await resolver.resolve(note.inReplyTo) as any;
 			const actor = await resolvePerson(inReplyTo.attributedTo);
 			if (isRemoteUser(actor)) {
-				reply = await createNote(resolver, actor, inReplyTo);
+				reply = await createNote(resolver, actor, inReplyTo, true);
 			}
 		}
 	}
@@ -102,5 +102,5 @@ async function createNote(resolver: Resolver, actor: IRemoteUser, note) {
 		viaMobile: false,
 		geo: undefined,
 		uri: note.id
-	});
+	}, silent);
 }

From 9f02eeac93e82908fdc333d580a2314df0f556b9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 5 Apr 2018 23:24:51 +0900
Subject: [PATCH 1106/1250] wip

---
 src/api/following/create.ts | 16 +++-------------
 src/api/following/delete.ts |  9 ++-------
 src/api/post/create.ts      |  9 ++-------
 src/queue/index.ts          |  9 +++++++++
 4 files changed, 16 insertions(+), 27 deletions(-)

diff --git a/src/api/following/create.ts b/src/api/following/create.ts
index 353a6c892..d919f4487 100644
--- a/src/api/following/create.ts
+++ b/src/api/following/create.ts
@@ -7,7 +7,7 @@ import notify from '../../publishers/notify';
 import context from '../../remote/activitypub/renderer/context';
 import renderFollow from '../../remote/activitypub/renderer/follow';
 import renderAccept from '../../remote/activitypub/renderer/accept';
-import { createHttp } from '../../queue';
+import { deliver } from '../../queue';
 
 export default async function(follower: IUser, followee: IUser, activity?) {
 	const following = await Following.insert({
@@ -60,23 +60,13 @@ export default async function(follower: IUser, followee: IUser, activity?) {
 		const content = renderFollow(follower, followee);
 		content['@context'] = context;
 
-		createHttp({
-			type: 'deliver',
-			user: follower,
-			content,
-			to: followee.account.inbox
-		}).save();
+		deliver(follower, content, followee.account.inbox).save();
 	}
 
 	if (isRemoteUser(follower) && isLocalUser(followee)) {
 		const content = renderAccept(activity);
 		content['@context'] = context;
 
-		createHttp({
-			type: 'deliver',
-			user: followee,
-			content,
-			to: follower.account.inbox
-		}).save();
+		deliver(followee, content, follower.account.inbox).save();
 	}
 }
diff --git a/src/api/following/delete.ts b/src/api/following/delete.ts
index 4cdff7ce1..364a4803b 100644
--- a/src/api/following/delete.ts
+++ b/src/api/following/delete.ts
@@ -6,7 +6,7 @@ import event from '../../publishers/stream';
 import context from '../../remote/activitypub/renderer/context';
 import renderFollow from '../../remote/activitypub/renderer/follow';
 import renderUndo from '../../remote/activitypub/renderer/undo';
-import { createHttp } from '../../queue';
+import { deliver } from '../../queue';
 
 export default async function(follower: IUser, followee: IUser, activity?) {
 	const following = await Following.findOne({
@@ -59,11 +59,6 @@ export default async function(follower: IUser, followee: IUser, activity?) {
 		const content = renderUndo(renderFollow(follower, followee));
 		content['@context'] = context;
 
-		createHttp({
-			type: 'deliver',
-			user: follower,
-			content,
-			to: followee.account.inbox
-		}).save();
+		deliver(follower, content, followee.account.inbox).save();
 	}
 }
diff --git a/src/api/post/create.ts b/src/api/post/create.ts
index 7b7fceda2..9723dbe45 100644
--- a/src/api/post/create.ts
+++ b/src/api/post/create.ts
@@ -2,7 +2,7 @@ import Post, { pack, IPost } from '../../models/post';
 import User, { isLocalUser, IUser } from '../../models/user';
 import stream from '../../publishers/stream';
 import Following from '../../models/following';
-import { createHttp } from '../../queue';
+import { deliver } from '../../queue';
 import renderNote from '../../remote/activitypub/renderer/note';
 import renderCreate from '../../remote/activitypub/renderer/create';
 import context from '../../remote/activitypub/renderer/context';
@@ -132,12 +132,7 @@ export default async (user: IUser, content: {
 				} else {
 					// フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信
 					if (isLocalUser(user)) {
-						createHttp({
-							type: 'deliver',
-							user,
-							content,
-							to: follower.account.inbox
-						}).save();
+						deliver(user, content, follower.account.inbox).save();
 					}
 				}
 			}));
diff --git a/src/queue/index.ts b/src/queue/index.ts
index 86600dc26..689985e0e 100644
--- a/src/queue/index.ts
+++ b/src/queue/index.ts
@@ -28,6 +28,15 @@ export function createDb(data) {
 	return queue.create('db', data);
 }
 
+export function deliver(user, content, to) {
+	return createHttp({
+		type: 'deliver',
+		user,
+		content,
+		to
+	});
+}
+
 export default function() {
 	queue.process('db', db);
 

From 99b14f01b3a05e899174b23e0b433cffedd81fca Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Fri, 6 Apr 2018 01:36:34 +0900
Subject: [PATCH 1107/1250] Allow name property of user to be null

---
 src/client/app/ch/tags/channel.tag            | 10 ++++++--
 .../common/scripts/compose-notification.ts    | 13 ++++++-----
 .../common/views/components/autocomplete.vue  |  4 +++-
 .../app/common/views/components/messaging.vue |  6 +++--
 .../views/components/welcome-timeline.vue     |  4 +++-
 .../views/components/followers-window.vue     | 11 +++++++--
 .../views/components/following-window.vue     | 11 +++++++--
 .../views/components/friends-maker.vue        |  4 +++-
 .../components/messaging-room-window.vue      |  6 ++++-
 .../views/components/notifications.vue        | 16 +++++++------
 .../views/components/post-detail.sub.vue      |  6 ++++-
 .../desktop/views/components/post-detail.vue  | 11 +++++++--
 .../desktop/views/components/post-preview.vue |  6 ++++-
 .../views/components/posts.post.sub.vue       |  6 ++++-
 .../desktop/views/components/posts.post.vue   |  6 ++++-
 .../views/components/settings.mute.vue        |  6 +++--
 .../views/components/settings.profile.vue     |  4 ++--
 .../desktop/views/components/ui.header.vue    |  9 +++++++-
 .../views/components/users-list.item.vue      |  6 ++++-
 .../desktop/views/pages/messaging-room.vue    |  3 ++-
 .../pages/user/user.followers-you-know.vue    |  6 +++--
 .../desktop/views/pages/user/user.header.vue  |  6 ++++-
 .../app/desktop/views/pages/user/user.vue     |  3 ++-
 .../views/widgets/channel.channel.post.vue    |  6 ++++-
 .../app/desktop/views/widgets/profile.vue     |  9 +++++++-
 .../app/desktop/views/widgets/users.vue       |  4 +++-
 .../views/components/notification-preview.vue | 23 +++++++++++++------
 .../mobile/views/components/notification.vue  | 15 ++++++++----
 .../app/mobile/views/components/post-card.vue |  6 ++++-
 .../views/components/post-detail.sub.vue      |  6 ++++-
 .../mobile/views/components/post-detail.vue   | 11 +++++++--
 .../mobile/views/components/post-preview.vue  |  6 ++++-
 .../app/mobile/views/components/post.sub.vue  |  6 ++++-
 .../app/mobile/views/components/post.vue      | 11 +++++++--
 .../app/mobile/views/components/ui.header.vue |  8 ++++++-
 .../app/mobile/views/components/ui.nav.vue    |  8 ++++++-
 .../app/mobile/views/components/user-card.vue |  6 ++++-
 .../mobile/views/components/user-preview.vue  |  6 ++++-
 .../app/mobile/views/pages/followers.vue      | 10 ++++++--
 .../app/mobile/views/pages/following.vue      | 10 ++++++--
 .../app/mobile/views/pages/messaging-room.vue | 10 ++++++--
 .../mobile/views/pages/profile-setting.vue    |  4 ++--
 .../app/mobile/views/pages/settings.vue       |  8 ++++++-
 src/client/app/mobile/views/pages/user.vue    | 11 +++++----
 .../pages/user/home.followers-you-know.vue    |  8 ++++++-
 .../app/mobile/views/widgets/profile.vue      | 10 +++++++-
 src/models/user.ts                            |  6 ++---
 src/othello/ai/back.ts                        | 17 +++++++-------
 src/renderers/get-notification-summary.ts     | 15 ++++++------
 src/renderers/get-user-summary.ts             |  3 ++-
 src/server/api/bot/core.ts                    |  7 +++---
 src/server/api/bot/interfaces/line.ts         |  5 ++--
 52 files changed, 311 insertions(+), 107 deletions(-)

diff --git a/src/client/app/ch/tags/channel.tag b/src/client/app/ch/tags/channel.tag
index 1ebc3cceb..4856728de 100644
--- a/src/client/app/ch/tags/channel.tag
+++ b/src/client/app/ch/tags/channel.tag
@@ -165,7 +165,7 @@
 <mk-channel-post>
 	<header>
 		<a class="index" @click="reply">{ post.index }:</a>
-		<a class="name" href={ _URL_ + '/@' + acct }><b>{ post.user.name }</b></a>
+		<a class="name" href={ _URL_ + '/@' + acct }><b>{ getUserName(post.user) }</b></a>
 		<mk-time time={ post.createdAt }/>
 		<mk-time time={ post.createdAt } mode="detail"/>
 		<span>ID:<i>{ acct }</i></span>
@@ -230,10 +230,12 @@
 	</style>
 	<script lang="typescript">
 		import getAcct from '../../../../acct/render';
+		import getUserName from '../../../../renderers/get-user-name';
 
 		this.post = this.opts.post;
 		this.form = this.opts.form;
 		this.acct = getAcct(this.post.user);
+		this.name = getUserName(this.post.user);
 
 		this.reply = () => {
 			this.form.update({
@@ -244,7 +246,7 @@
 </mk-channel-post>
 
 <mk-channel-form>
-	<p v-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> ({ getUserName(reply.user) }): <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>
@@ -286,6 +288,8 @@
 
 	</style>
 	<script lang="typescript">
+		import getUserName from '../../../../renderers/get-user-name';
+
 		this.mixin('api');
 
 		this.channel = this.opts.channel;
@@ -373,6 +377,8 @@
 				}
 			});
 		};
+
+		this.getUserName = getUserName;
 	</script>
 </mk-channel-form>
 
diff --git a/src/client/app/common/scripts/compose-notification.ts b/src/client/app/common/scripts/compose-notification.ts
index ebc15952f..e99d50296 100644
--- a/src/client/app/common/scripts/compose-notification.ts
+++ b/src/client/app/common/scripts/compose-notification.ts
@@ -1,5 +1,6 @@
 import getPostSummary from '../../../../renderers/get-post-summary';
 import getReactionEmoji from '../../../../renderers/get-reaction-emoji';
+import getUserName from '../../../../renderers/get-user-name';
 
 type Notification = {
 	title: string;
@@ -21,35 +22,35 @@ export default function(type, data): Notification {
 
 		case 'mention':
 			return {
-				title: `${data.user.name}さんから:`,
+				title: `${getUserName(data.user)}さんから:`,
 				body: getPostSummary(data),
 				icon: data.user.avatarUrl + '?thumbnail&size=64'
 			};
 
 		case 'reply':
 			return {
-				title: `${data.user.name}さんから返信:`,
+				title: `${getUserName(data.user)}さんから返信:`,
 				body: getPostSummary(data),
 				icon: data.user.avatarUrl + '?thumbnail&size=64'
 			};
 
 		case 'quote':
 			return {
-				title: `${data.user.name}さんが引用:`,
+				title: `${getUserName(data.user)}さんが引用:`,
 				body: getPostSummary(data),
 				icon: data.user.avatarUrl + '?thumbnail&size=64'
 			};
 
 		case 'reaction':
 			return {
-				title: `${data.user.name}: ${getReactionEmoji(data.reaction)}:`,
+				title: `${getUserName(data.user)}: ${getReactionEmoji(data.reaction)}:`,
 				body: getPostSummary(data.post),
 				icon: data.user.avatarUrl + '?thumbnail&size=64'
 			};
 
 		case 'unread_messaging_message':
 			return {
-				title: `${data.user.name}さんからメッセージ:`,
+				title: `${getUserName(data.user)}さんからメッセージ:`,
 				body: data.text, // TODO: getMessagingMessageSummary(data),
 				icon: data.user.avatarUrl + '?thumbnail&size=64'
 			};
@@ -57,7 +58,7 @@ export default function(type, data): Notification {
 		case 'othello_invited':
 			return {
 				title: '対局への招待があります',
-				body: `${data.parent.name}さんから`,
+				body: `${getUserName(data.parent)}さんから`,
 				icon: data.parent.avatarUrl + '?thumbnail&size=64'
 			};
 
diff --git a/src/client/app/common/views/components/autocomplete.vue b/src/client/app/common/views/components/autocomplete.vue
index 38eaf8650..8837fde6b 100644
--- a/src/client/app/common/views/components/autocomplete.vue
+++ b/src/client/app/common/views/components/autocomplete.vue
@@ -3,7 +3,7 @@
 	<ol class="users" ref="suggests" v-if="users.length > 0">
 		<li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1">
 			<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=32`" alt=""/>
-			<span class="name">{{ user.name }}</span>
+			<span class="name">{{ getUserName(user) }}</span>
 			<span class="username">@{{ getAcct(user) }}</span>
 		</li>
 	</ol>
@@ -22,6 +22,7 @@ import Vue from 'vue';
 import * as emojilib from 'emojilib';
 import contains from '../../../common/scripts/contains';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 const lib = Object.entries(emojilib.lib).filter((x: any) => {
 	return x[1].category != 'flags';
@@ -107,6 +108,7 @@ export default Vue.extend({
 	},
 	methods: {
 		getAcct,
+		getUserName,
 		exec() {
 			this.select = -1;
 			if (this.$refs.suggests) {
diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue
index 4ab3e46e8..9b1449daa 100644
--- a/src/client/app/common/views/components/messaging.vue
+++ b/src/client/app/common/views/components/messaging.vue
@@ -14,7 +14,7 @@
 					tabindex="-1"
 				>
 					<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=32`" alt=""/>
-					<span class="name">{{ user.name }}</span>
+					<span class="name">{{ getUserName(user) }}</span>
 					<span class="username">@{{ getAcct(user) }}</span>
 				</li>
 			</ol>
@@ -33,7 +33,7 @@
 				<div>
 					<img class="avatar" :src="`${isMe(message) ? message.recipient.avatarUrl : message.user.avatarUrl}?thumbnail&size=64`" alt=""/>
 					<header>
-						<span class="name">{{ isMe(message) ? message.recipient.name : message.user.name }}</span>
+						<span class="name">{{ getUserName(isMe(message) ? message.recipient : message.user) }}</span>
 						<span class="username">@{{ getAcct(isMe(message) ? message.recipient : message.user) }}</span>
 						<mk-time :time="message.createdAt"/>
 					</header>
@@ -52,6 +52,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: {
@@ -94,6 +95,7 @@ export default Vue.extend({
 	},
 	methods: {
 		getAcct,
+		getUserName,
 		isMe(message) {
 			return message.userId == (this as any).os.i.id;
 		},
diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue
index ef0c7beaa..bfb4b2bbe 100644
--- a/src/client/app/common/views/components/welcome-timeline.vue
+++ b/src/client/app/common/views/components/welcome-timeline.vue
@@ -6,7 +6,7 @@
 		</router-link>
 		<div class="body">
 			<header>
-				<router-link class="name" :to="`/@${getAcct(post.user)}`" v-user-preview="post.user.id">{{ post.user.name }}</router-link>
+				<router-link class="name" :to="`/@${getAcct(post.user)}`" v-user-preview="post.user.id">{{ getUserName(post.user) }}</router-link>
 				<span class="username">@{{ getAcct(post.user) }}</span>
 				<div class="info">
 					<router-link class="created-at" :to="`/@${getAcct(post.user)}/${post.id}`">
@@ -25,6 +25,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -38,6 +39,7 @@ export default Vue.extend({
 	},
 	methods: {
 		getAcct,
+		getUserName,
 		fetch(cb?) {
 			this.fetching = true;
 			(this as any).api('posts', {
diff --git a/src/client/app/desktop/views/components/followers-window.vue b/src/client/app/desktop/views/components/followers-window.vue
index 623971fa3..d37ca745a 100644
--- a/src/client/app/desktop/views/components/followers-window.vue
+++ b/src/client/app/desktop/views/components/followers-window.vue
@@ -1,7 +1,7 @@
 <template>
 <mk-window width="400px" height="550px" @closed="$destroy">
 	<span slot="header" :class="$style.header">
-		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ user.name }}のフォロワー
+		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ name }}のフォロワー
 	</span>
 	<mk-followers :user="user"/>
 </mk-window>
@@ -9,8 +9,15 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import getUserName from '../../../../../renderers/get-user-name';
+
 export default Vue.extend({
-	props: ['user']
+	props: ['user'],
+	computed {
+		name() {
+			return getUserName(this.user);
+		}
+	}
 });
 </script>
 
diff --git a/src/client/app/desktop/views/components/following-window.vue b/src/client/app/desktop/views/components/following-window.vue
index 612847b38..cbd8ec5f9 100644
--- a/src/client/app/desktop/views/components/following-window.vue
+++ b/src/client/app/desktop/views/components/following-window.vue
@@ -1,7 +1,7 @@
 <template>
 <mk-window width="400px" height="550px" @closed="$destroy">
 	<span slot="header" :class="$style.header">
-		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ user.name }}のフォロー
+		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ name }}のフォロー
 	</span>
 	<mk-following :user="user"/>
 </mk-window>
@@ -9,8 +9,15 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import getUserName from '../../../../../renderers/get-user-name';
+
 export default Vue.extend({
-	props: ['user']
+	props: ['user'],
+	computed: {
+		name() {
+			return getUserName(this.user);
+		}
+	}
 });
 </script>
 
diff --git a/src/client/app/desktop/views/components/friends-maker.vue b/src/client/app/desktop/views/components/friends-maker.vue
index 351e9e1c5..acc4542d9 100644
--- a/src/client/app/desktop/views/components/friends-maker.vue
+++ b/src/client/app/desktop/views/components/friends-maker.vue
@@ -7,7 +7,7 @@
 				<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="user.id"/>
 			</router-link>
 			<div class="body">
-				<router-link class="name" :to="`/@${getAcct(user)}`" v-user-preview="user.id">{{ user.name }}</router-link>
+				<router-link class="name" :to="`/@${getAcct(user)}`" v-user-preview="user.id">{{ getUserName(user) }}</router-link>
 				<p class="username">@{{ getAcct(user) }}</p>
 			</div>
 			<mk-follow-button :user="user"/>
@@ -23,6 +23,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -38,6 +39,7 @@ export default Vue.extend({
 	},
 	methods: {
 		getAcct,
+		getUserName,
 		fetch() {
 			this.fetching = true;
 			this.users = [];
diff --git a/src/client/app/desktop/views/components/messaging-room-window.vue b/src/client/app/desktop/views/components/messaging-room-window.vue
index f29f9b74e..7f8c35c2f 100644
--- a/src/client/app/desktop/views/components/messaging-room-window.vue
+++ b/src/client/app/desktop/views/components/messaging-room-window.vue
@@ -1,6 +1,6 @@
 <template>
 <mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="$destroy">
-	<span slot="header" :class="$style.header">%fa:comments%メッセージ: {{ user.name }}</span>
+	<span slot="header" :class="$style.header">%fa:comments%メッセージ: {{ name }}</span>
 	<mk-messaging-room :user="user" :class="$style.content"/>
 </mk-window>
 </template>
@@ -9,10 +9,14 @@
 import Vue from 'vue';
 import { url } from '../../../config';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['user'],
 	computed: {
+		name(): string {
+			return getUserName(this.user);
+		},
 		popout(): string {
 			return `${url}/i/messaging/${getAcct(this.user)}`;
 		}
diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
index c5ab284df..d8b8eab21 100644
--- a/src/client/app/desktop/views/components/notifications.vue
+++ b/src/client/app/desktop/views/components/notifications.vue
@@ -11,7 +11,7 @@
 					<div class="text">
 						<p>
 							<mk-reaction-icon :reaction="notification.reaction"/>
-							<router-link :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ notification.user.name }}</router-link>
+							<router-link :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ getUserName(notification.user) }}</router-link>
 						</p>
 						<router-link class="post-ref" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">
 							%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%
@@ -24,7 +24,7 @@
 					</router-link>
 					<div class="text">
 						<p>%fa:retweet%
-							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ notification.post.user.name }}</router-link>
+							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ getUserName(notification.post.user) }}</router-link>
 						</p>
 						<router-link class="post-ref" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">
 							%fa:quote-left%{{ getPostSummary(notification.post.repost) }}%fa:quote-right%
@@ -37,7 +37,7 @@
 					</router-link>
 					<div class="text">
 						<p>%fa:quote-left%
-							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ notification.post.user.name }}</router-link>
+							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ getUserName(notification.post.user) }}</router-link>
 						</p>
 						<router-link class="post-preview" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link>
 					</div>
@@ -48,7 +48,7 @@
 					</router-link>
 					<div class="text">
 						<p>%fa:user-plus%
-							<router-link :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ notification.user.name }}</router-link>
+							<router-link :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ getUserName(notification.user) }}</router-link>
 						</p>
 					</div>
 				</template>
@@ -58,7 +58,7 @@
 					</router-link>
 					<div class="text">
 						<p>%fa:reply%
-							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ notification.post.user.name }}</router-link>
+							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ getUserName(notification.post.user) }}</router-link>
 						</p>
 						<router-link class="post-preview" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link>
 					</div>
@@ -69,7 +69,7 @@
 					</router-link>
 					<div class="text">
 						<p>%fa:at%
-							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ notification.post.user.name }}</router-link>
+							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ getUserName(notification.post.user) }}</router-link>
 						</p>
 						<a class="post-preview" :href="`/@${getAcct(notification.post.user)}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</a>
 					</div>
@@ -79,7 +79,7 @@
 						<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
-						<p>%fa:chart-pie%<a :href="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ notification.user.name }}</a></p>
+						<p>%fa:chart-pie%<a :href="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ getUserName(notification.user) }}</a></p>
 						<router-link class="post-ref" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">
 							%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%
 						</router-link>
@@ -104,6 +104,7 @@
 import Vue from 'vue';
 import getAcct from '../../../../../acct/render';
 import getPostSummary from '../../../../../renderers/get-post-summary';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -154,6 +155,7 @@ export default Vue.extend({
 	},
 	methods: {
 		getAcct,
+		getUserName,
 		fetchMoreNotifications() {
 			this.fetchingMoreNotifications = true;
 
diff --git a/src/client/app/desktop/views/components/post-detail.sub.vue b/src/client/app/desktop/views/components/post-detail.sub.vue
index 59bc9ce0c..496003eb8 100644
--- a/src/client/app/desktop/views/components/post-detail.sub.vue
+++ b/src/client/app/desktop/views/components/post-detail.sub.vue
@@ -6,7 +6,7 @@
 	<div class="main">
 		<header>
 			<div class="left">
-				<router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ post.user.name }}</router-link>
+				<router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ getUserName(post.user) }}</router-link>
 				<span class="username">@{{ acct }}</span>
 			</div>
 			<div class="right">
@@ -29,6 +29,7 @@
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['post'],
@@ -36,6 +37,9 @@ export default Vue.extend({
 		acct() {
 			return getAcct(this.post.user);
 		},
+		name() {
+			return getUserName(this.post.user);
+		},
 		title(): string {
 			return dateStringify(this.post.createdAt);
 		}
diff --git a/src/client/app/desktop/views/components/post-detail.vue b/src/client/app/desktop/views/components/post-detail.vue
index 8000ce2e6..1a3c0d1b6 100644
--- a/src/client/app/desktop/views/components/post-detail.vue
+++ b/src/client/app/desktop/views/components/post-detail.vue
@@ -22,7 +22,7 @@
 				<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
 			</router-link>
 			%fa:retweet%
-			<router-link class="name" :href="`/@${acct}`">{{ post.user.name }}</router-link>
+			<router-link class="name" :href="`/@${acct}`">{{ getUserName(post.user) }}</router-link>
 			がRepost
 		</p>
 	</div>
@@ -31,7 +31,7 @@
 			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
 		</router-link>
 		<header>
-			<router-link class="name" :to="`/@${pAcct}`" v-user-preview="p.user.id">{{ p.user.name }}</router-link>
+			<router-link class="name" :to="`/@${pAcct}`" v-user-preview="p.user.id">{{ getUserName(p.user) }}</router-link>
 			<span class="username">@{{ pAcct }}</span>
 			<router-link class="time" :to="`/@${pAcct}/${p.id}`">
 				<mk-time :time="p.createdAt"/>
@@ -79,6 +79,7 @@
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 import parse from '../../../../../text/parse';
 
 import MkPostFormWindow from './post-form-window.vue';
@@ -133,9 +134,15 @@ export default Vue.extend({
 		acct(): string {
 			return getAcct(this.post.user);
 		},
+		name(): string {
+			return getUserName(this.post.user);
+		},
 		pAcct(): string {
 			return getAcct(this.p.user);
 		},
+		pName(): string {
+			return getUserName(this.p.user);
+		},
 		urls(): string[] {
 			if (this.p.text) {
 				const ast = parse(this.p.text);
diff --git a/src/client/app/desktop/views/components/post-preview.vue b/src/client/app/desktop/views/components/post-preview.vue
index 7129f67b3..99d9442d9 100644
--- a/src/client/app/desktop/views/components/post-preview.vue
+++ b/src/client/app/desktop/views/components/post-preview.vue
@@ -5,7 +5,7 @@
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ post.user.name }}</router-link>
+			<router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ name }}</router-link>
 			<span class="username">@{{ acct }}</span>
 			<router-link class="time" :to="`/@${acct}/${post.id}`">
 				<mk-time :time="post.createdAt"/>
@@ -22,6 +22,7 @@
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['post'],
@@ -29,6 +30,9 @@ export default Vue.extend({
 		acct() {
 			return getAcct(this.post.user);
 		},
+		name() {
+			return getUserName(this.post.user);
+		},
 		title(): string {
 			return dateStringify(this.post.createdAt);
 		}
diff --git a/src/client/app/desktop/views/components/posts.post.sub.vue b/src/client/app/desktop/views/components/posts.post.sub.vue
index dffecb89c..a9cd0a927 100644
--- a/src/client/app/desktop/views/components/posts.post.sub.vue
+++ b/src/client/app/desktop/views/components/posts.post.sub.vue
@@ -5,7 +5,7 @@
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ post.user.name }}</router-link>
+			<router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ name }}</router-link>
 			<span class="username">@{{ acct }}</span>
 			<router-link class="created-at" :to="`/@${acct}/${post.id}`">
 				<mk-time :time="post.createdAt"/>
@@ -22,6 +22,7 @@
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['post'],
@@ -29,6 +30,9 @@ export default Vue.extend({
 		acct() {
 			return getAcct(this.post.user);
 		},
+		name(): string {
+			return getUserName(this.post.user);
+		},
 		title(): string {
 			return dateStringify(this.post.createdAt);
 		}
diff --git a/src/client/app/desktop/views/components/posts.post.vue b/src/client/app/desktop/views/components/posts.post.vue
index 9a13dd687..17fe33042 100644
--- a/src/client/app/desktop/views/components/posts.post.vue
+++ b/src/client/app/desktop/views/components/posts.post.vue
@@ -10,7 +10,7 @@
 			</router-link>
 			%fa:retweet%
 			<span>{{ '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('{')) }}</span>
-			<a class="name" :href="`/@${acct}`" v-user-preview="post.userId">{{ post.user.name }}</a>
+			<a class="name" :href="`/@${acct}`" v-user-preview="post.userId">{{ getUserName(post.user) }}</a>
 			<span>{{ '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1) }}</span>
 		</p>
 		<mk-time :time="post.createdAt"/>
@@ -86,6 +86,7 @@
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 import parse from '../../../../../text/parse';
 
 import MkPostFormWindow from './post-form-window.vue';
@@ -124,6 +125,9 @@ export default Vue.extend({
 		acct(): string {
 			return getAcct(this.p.user);
 		},
+		name(): string {
+			return getUserName(this.p.user);
+		},
 		isRepost(): boolean {
 			return (this.post.repost &&
 				this.post.text == null &&
diff --git a/src/client/app/desktop/views/components/settings.mute.vue b/src/client/app/desktop/views/components/settings.mute.vue
index c87f973fa..6bdc76653 100644
--- a/src/client/app/desktop/views/components/settings.mute.vue
+++ b/src/client/app/desktop/views/components/settings.mute.vue
@@ -5,7 +5,7 @@
 	</div>
 	<div class="users" v-if="users.length != 0">
 		<div v-for="user in users" :key="user.id">
-			<p><b>{{ user.name }}</b> @{{ getAcct(user) }}</p>
+			<p><b>{{ getUserName(user) }}</b> @{{ getAcct(user) }}</p>
 		</div>
 	</div>
 </div>
@@ -14,6 +14,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -23,7 +24,8 @@ export default Vue.extend({
 		};
 	},
 	methods: {
-		getAcct
+		getAcct,
+		getUserName
 	},
 	mounted() {
 		(this as any).api('mute/list').then(x => {
diff --git a/src/client/app/desktop/views/components/settings.profile.vue b/src/client/app/desktop/views/components/settings.profile.vue
index ba86286f8..28be48e0a 100644
--- a/src/client/app/desktop/views/components/settings.profile.vue
+++ b/src/client/app/desktop/views/components/settings.profile.vue
@@ -42,7 +42,7 @@ export default Vue.extend({
 		};
 	},
 	created() {
-		this.name = (this as any).os.i.name;
+		this.name = (this as any).os.i.name || '';
 		this.location = (this as any).os.i.account.profile.location;
 		this.description = (this as any).os.i.description;
 		this.birthday = (this as any).os.i.account.profile.birthday;
@@ -53,7 +53,7 @@ export default Vue.extend({
 		},
 		save() {
 			(this as any).api('i/update', {
-				name: this.name,
+				name: this.name || null,
 				location: this.location || null,
 				description: this.description || null,
 				birthday: this.birthday || null
diff --git a/src/client/app/desktop/views/components/ui.header.vue b/src/client/app/desktop/views/components/ui.header.vue
index 448d04d26..7d93847fa 100644
--- a/src/client/app/desktop/views/components/ui.header.vue
+++ b/src/client/app/desktop/views/components/ui.header.vue
@@ -4,7 +4,7 @@
 	<div class="main" ref="main">
 		<div class="backdrop"></div>
 		<div class="main">
-			<p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ os.i.name }}</b>さん</p>
+			<p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ name }}</b>さん</p>
 			<div class="container" ref="mainContainer">
 				<div class="left">
 					<x-nav/>
@@ -33,7 +33,14 @@ import XNotifications from './ui.header.notifications.vue';
 import XPost from './ui.header.post.vue';
 import XClock from './ui.header.clock.vue';
 
+import getUserName from '../../../../../renderers/get-user-name';
+
 export default Vue.extend({
+	computed: {
+		name() {
+			return getUserName(this.os.i);
+		}
+	},
 	components: {
 		XNav,
 		XSearch,
diff --git a/src/client/app/desktop/views/components/users-list.item.vue b/src/client/app/desktop/views/components/users-list.item.vue
index 2d7d4dc72..c7a132ecf 100644
--- a/src/client/app/desktop/views/components/users-list.item.vue
+++ b/src/client/app/desktop/views/components/users-list.item.vue
@@ -5,7 +5,7 @@
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`" v-user-preview="user.id">{{ user.name }}</router-link>
+			<router-link class="name" :to="`/@${acct}`" v-user-preview="user.id">{{ name }}</router-link>
 			<span class="username">@{{ acct }}</span>
 		</header>
 		<div class="body">
@@ -20,12 +20,16 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['user'],
 	computed: {
 		acct() {
 			return getAcct(this.user);
+		},
+		name() {
+			return getUserName(this.user);
 		}
 	}
 });
diff --git a/src/client/app/desktop/views/pages/messaging-room.vue b/src/client/app/desktop/views/pages/messaging-room.vue
index 1e61f3ce1..1cc8d8a77 100644
--- a/src/client/app/desktop/views/pages/messaging-room.vue
+++ b/src/client/app/desktop/views/pages/messaging-room.vue
@@ -8,6 +8,7 @@
 import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
 import parseAcct from '../../../../../acct/parse';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -34,7 +35,7 @@ export default Vue.extend({
 				this.user = user;
 				this.fetching = false;
 
-				document.title = 'メッセージ: ' + this.user.name;
+				document.title = 'メッセージ: ' + getUserName(this.user);
 
 				Progress.done();
 			});
diff --git a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
index 7497acd0e..16625b689 100644
--- a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
+++ b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
@@ -4,7 +4,7 @@
 	<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="`/@${getAcct(user)}`" :key="user.id">
-		<img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="user.name" v-user-preview="user.id"/>
+		<img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="getUserName(user)" v-user-preview="user.id"/>
 	</router-link>
 	</div>
 	<p class="empty" v-if="!fetching && users.length == 0">%i18n:desktop.tags.mk-user.followers-you-know.no-users%</p>
@@ -14,6 +14,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../../acct/render';
+import getUserName from '../../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['user'],
@@ -24,7 +25,8 @@ export default Vue.extend({
 		};
 	},
 	method() {
-		getAcct
+		getAcct,
+		getUserName
 	},
 	mounted() {
 		(this as any).api('users/followers', {
diff --git a/src/client/app/desktop/views/pages/user/user.header.vue b/src/client/app/desktop/views/pages/user/user.header.vue
index d30f423d5..5c6746d5d 100644
--- a/src/client/app/desktop/views/pages/user/user.header.vue
+++ b/src/client/app/desktop/views/pages/user/user.header.vue
@@ -7,7 +7,7 @@
 	<div class="container">
 		<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=150`" alt="avatar"/>
 		<div class="title">
-			<p class="name">{{ user.name }}</p>
+			<p class="name">{{ name }}</p>
 			<p class="username">@{{ acct }}</p>
 			<p class="location" v-if="user.host === null && user.account.profile.location">%fa:map-marker%{{ user.account.profile.location }}</p>
 		</div>
@@ -23,12 +23,16 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../../acct/render';
+import getUserName from '../../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['user'],
 	computed: {
 		acct() {
 			return getAcct(this.user);
+		},
+		name() {
+			return getUserName(this.user);
 		}
 	},
 	mounted() {
diff --git a/src/client/app/desktop/views/pages/user/user.vue b/src/client/app/desktop/views/pages/user/user.vue
index 02ddc2421..d07b462b5 100644
--- a/src/client/app/desktop/views/pages/user/user.vue
+++ b/src/client/app/desktop/views/pages/user/user.vue
@@ -10,6 +10,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import parseAcct from '../../../../../../acct/parse';
+import getUserName from '../../../../../../renderers/get-user-name';
 import Progress from '../../../../common/scripts/loading';
 import XHeader from './user.header.vue';
 import XHome from './user.home.vue';
@@ -44,7 +45,7 @@ export default Vue.extend({
 				this.user = user;
 				this.fetching = false;
 				Progress.done();
-				document.title = user.name + ' | Misskey';
+				document.title = getUserName(user) + ' | Misskey';
 			});
 		}
 	}
diff --git a/src/client/app/desktop/views/widgets/channel.channel.post.vue b/src/client/app/desktop/views/widgets/channel.channel.post.vue
index e10e9c4f7..fa6d8c34a 100644
--- a/src/client/app/desktop/views/widgets/channel.channel.post.vue
+++ b/src/client/app/desktop/views/widgets/channel.channel.post.vue
@@ -2,7 +2,7 @@
 <div class="post">
 	<header>
 		<a class="index" @click="reply">{{ post.index }}:</a>
-		<router-link class="name" :to="`/@${acct}`" v-user-preview="post.user.id"><b>{{ post.user.name }}</b></router-link>
+		<router-link class="name" :to="`/@${acct}`" v-user-preview="post.user.id"><b>{{ name }}</b></router-link>
 		<span>ID:<i>{{ acct }}</i></span>
 	</header>
 	<div>
@@ -20,12 +20,16 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['post'],
 	computed: {
 		acct() {
 			return getAcct(this.post.user);
+		},
+		name() {
+			return getUserName(this.post.user);
 		}
 	},
 	methods: {
diff --git a/src/client/app/desktop/views/widgets/profile.vue b/src/client/app/desktop/views/widgets/profile.vue
index 83cd67b50..98e42222e 100644
--- a/src/client/app/desktop/views/widgets/profile.vue
+++ b/src/client/app/desktop/views/widgets/profile.vue
@@ -15,19 +15,26 @@
 		title="クリックでアバター編集"
 		v-user-preview="os.i.id"
 	/>
-	<router-link class="name" :to="`/@${os.i.username}`">{{ os.i.name }}</router-link>
+	<router-link class="name" :to="`/@${os.i.username}`">{{ name }}</router-link>
 	<p class="username">@{{ os.i.username }}</p>
 </div>
 </template>
 
 <script lang="ts">
 import define from '../../../common/define-widget';
+import getUserName from '../../../../../renderers/get-user-name';
+
 export default define({
 	name: 'profile',
 	props: () => ({
 		design: 0
 	})
 }).extend({
+	computed: {
+		name() {
+			return getUserName(this.os.i);
+		}
+	},
 	methods: {
 		func() {
 			if (this.props.design == 2) {
diff --git a/src/client/app/desktop/views/widgets/users.vue b/src/client/app/desktop/views/widgets/users.vue
index 6f6a10157..a5dabb68f 100644
--- a/src/client/app/desktop/views/widgets/users.vue
+++ b/src/client/app/desktop/views/widgets/users.vue
@@ -11,7 +11,7 @@
 				<img class="avatar" :src="`${_user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="_user.id"/>
 			</router-link>
 			<div class="body">
-				<router-link class="name" :to="`/@${getAcct(_user)}`" v-user-preview="_user.id">{{ _user.name }}</router-link>
+				<router-link class="name" :to="`/@${getAcct(_user)}`" v-user-preview="_user.id">{{ getUserName(_user) }}</router-link>
 				<p class="username">@{{ getAcct(_user) }}</p>
 			</div>
 			<mk-follow-button :user="_user"/>
@@ -24,6 +24,7 @@
 <script lang="ts">
 import define from '../../../common/define-widget';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 const limit = 3;
 
@@ -45,6 +46,7 @@ export default define({
 	},
 	methods: {
 		getAcct,
+		getUserName,
 		func() {
 			this.props.compact = !this.props.compact;
 		},
diff --git a/src/client/app/mobile/views/components/notification-preview.vue b/src/client/app/mobile/views/components/notification-preview.vue
index 77abd3c0e..0492c5d86 100644
--- a/src/client/app/mobile/views/components/notification-preview.vue
+++ b/src/client/app/mobile/views/components/notification-preview.vue
@@ -3,7 +3,7 @@
 	<template v-if="notification.type == 'reaction'">
 		<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p><mk-reaction-icon :reaction="notification.reaction"/>{{ notification.user.name }}</p>
+			<p><mk-reaction-icon :reaction="notification.reaction"/>{{ name }}</p>
 			<p class="post-ref">%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%</p>
 		</div>
 	</template>
@@ -11,7 +11,7 @@
 	<template v-if="notification.type == 'repost'">
 		<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:retweet%{{ notification.post.user.name }}</p>
+			<p>%fa:retweet%{{ posterName }}</p>
 			<p class="post-ref">%fa:quote-left%{{ getPostSummary(notification.post.repost) }}%fa:quote-right%</p>
 		</div>
 	</template>
@@ -19,7 +19,7 @@
 	<template v-if="notification.type == 'quote'">
 		<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:quote-left%{{ notification.post.user.name }}</p>
+			<p>%fa:quote-left%{{ posterName }}</p>
 			<p class="post-preview">{{ getPostSummary(notification.post) }}</p>
 		</div>
 	</template>
@@ -27,14 +27,14 @@
 	<template v-if="notification.type == 'follow'">
 		<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:user-plus%{{ notification.user.name }}</p>
+			<p>%fa:user-plus%{{ name }}</p>
 		</div>
 	</template>
 
 	<template v-if="notification.type == 'reply'">
 		<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:reply%{{ notification.post.user.name }}</p>
+			<p>%fa:reply%{{ posterName }}</p>
 			<p class="post-preview">{{ getPostSummary(notification.post) }}</p>
 		</div>
 	</template>
@@ -42,7 +42,7 @@
 	<template v-if="notification.type == 'mention'">
 		<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:at%{{ notification.post.user.name }}</p>
+			<p>%fa:at%{{ posterName }}</p>
 			<p class="post-preview">{{ getPostSummary(notification.post) }}</p>
 		</div>
 	</template>
@@ -50,7 +50,7 @@
 	<template v-if="notification.type == 'poll_vote'">
 		<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:chart-pie%{{ notification.user.name }}</p>
+			<p>%fa:chart-pie%{{ name }}</p>
 			<p class="post-ref">%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%</p>
 		</div>
 	</template>
@@ -60,9 +60,18 @@
 <script lang="ts">
 import Vue from 'vue';
 import getPostSummary from '../../../../../renderers/get-post-summary';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['notification'],
+	computed: {
+		name() {
+			return getUserName(this.notification.user);
+		},
+		posterName() {
+			return getUserName(this.notification.post.user);
+		}
+	},
 	data() {
 		return {
 			getPostSummary
diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue
index 189d7195f..62a0e6425 100644
--- a/src/client/app/mobile/views/components/notification.vue
+++ b/src/client/app/mobile/views/components/notification.vue
@@ -8,7 +8,7 @@
 		<div class="text">
 			<p>
 				<mk-reaction-icon :reaction="notification.reaction"/>
-				<router-link :to="`/@${acct}`">{{ notification.user.name }}</router-link>
+				<router-link :to="`/@${acct}`">{{ getUserName(notification.user) }}</router-link>
 			</p>
 			<router-link class="post-ref" :to="`/@${acct}/${notification.post.id}`">
 				%fa:quote-left%{{ getPostSummary(notification.post) }}
@@ -25,7 +25,7 @@
 		<div class="text">
 			<p>
 				%fa:retweet%
-				<router-link :to="`/@${acct}`">{{ notification.post.user.name }}</router-link>
+				<router-link :to="`/@${acct}`">{{ getUserName(notification.post.user) }}</router-link>
 			</p>
 			<router-link class="post-ref" :to="`/@${acct}/${notification.post.id}`">
 				%fa:quote-left%{{ getPostSummary(notification.post.repost) }}%fa:quote-right%
@@ -45,7 +45,7 @@
 		<div class="text">
 			<p>
 				%fa:user-plus%
-				<router-link :to="`/@${acct}`">{{ notification.user.name }}</router-link>
+				<router-link :to="`/@${acct}`">{{ getUserName(notification.user) }}</router-link>
 			</p>
 		</div>
 	</div>
@@ -66,7 +66,7 @@
 		<div class="text">
 			<p>
 				%fa:chart-pie%
-				<router-link :to="`/@${acct}`">{{ notification.user.name }}</router-link>
+				<router-link :to="`/@${acct}`">{{ getUserName(notification.user) }}</router-link>
 			</p>
 			<router-link class="post-ref" :to="`/@${acct}/${notification.post.id}`">
 				%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%
@@ -80,12 +80,19 @@
 import Vue from 'vue';
 import getPostSummary from '../../../../../renderers/get-post-summary';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['notification'],
 	computed: {
 		acct() {
 			return getAcct(this.notification.user);
+		},
+		name() {
+			return getUserName(this.notification.user);
+		},
+		posterName() {
+ 			return getUserName(this.notification.post.user);
 		}
 	},
 	data() {
diff --git a/src/client/app/mobile/views/components/post-card.vue b/src/client/app/mobile/views/components/post-card.vue
index 38d4522d2..68a42ef24 100644
--- a/src/client/app/mobile/views/components/post-card.vue
+++ b/src/client/app/mobile/views/components/post-card.vue
@@ -2,7 +2,7 @@
 <div class="mk-post-card">
 	<a :href="`/@${acct}/${post.id}`">
 		<header>
-			<img :src="`${acct}?thumbnail&size=64`" alt="avatar"/><h3>{{ post.user.name }}</h3>
+			<img :src="`${acct}?thumbnail&size=64`" alt="avatar"/><h3>{{ name }}</h3>
 		</header>
 		<div>
 			{{ text }}
@@ -16,6 +16,7 @@
 import Vue from 'vue';
 import summary from '../../../../../renderers/get-post-summary';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['post'],
@@ -23,6 +24,9 @@ export default Vue.extend({
 		acct() {
 			return getAcct(this.post.user);
 		},
+		name() {
+			return getUserName(this.post.user);
+		},
 		text(): string {
 			return summary(this.post);
 		}
diff --git a/src/client/app/mobile/views/components/post-detail.sub.vue b/src/client/app/mobile/views/components/post-detail.sub.vue
index 7ff9f1aab..98d6a14ca 100644
--- a/src/client/app/mobile/views/components/post-detail.sub.vue
+++ b/src/client/app/mobile/views/components/post-detail.sub.vue
@@ -5,7 +5,7 @@
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`">{{ post.user.name }}</router-link>
+			<router-link class="name" :to="`/@${acct}`">{{ getUserName(post.user) }}</router-link>
 			<span class="username">@{{ acct }}</span>
 			<router-link class="time" :to="`/@${acct}/${post.id}`">
 				<mk-time :time="post.createdAt"/>
@@ -21,12 +21,16 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['post'],
 	computed: {
 		acct() {
 			return getAcct(this.post.user);
+		},
+		name() {
+			return getUserName(this.post.user);
 		}
 	}
 });
diff --git a/src/client/app/mobile/views/components/post-detail.vue b/src/client/app/mobile/views/components/post-detail.vue
index ed394acd3..0226ce081 100644
--- a/src/client/app/mobile/views/components/post-detail.vue
+++ b/src/client/app/mobile/views/components/post-detail.vue
@@ -22,7 +22,7 @@
 			</router-link>
 			%fa:retweet%
 			<router-link class="name" :to="`/@${acct}`">
-				{{ post.user.name }}
+				{{ name }}
 			</router-link>
 			がRepost
 		</p>
@@ -33,7 +33,7 @@
 				<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 			</router-link>
 			<div>
-				<router-link class="name" :to="`/@${pAcct}`">{{ p.user.name }}</router-link>
+				<router-link class="name" :to="`/@${pAcct}`">{{ pName }}</router-link>
 				<span class="username">@{{ pAcct }}</span>
 			</div>
 		</header>
@@ -81,6 +81,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 import parse from '../../../../../text/parse';
 
 import MkPostMenu from '../../../common/views/components/post-menu.vue';
@@ -114,9 +115,15 @@ export default Vue.extend({
 		acct(): string {
 			return getAcct(this.post.user);
 		},
+		name(): string {
+			return getUserName(this.post.user);
+		},
 		pAcct(): string {
 			return getAcct(this.p.user);
 		},
+		pName(): string {
+			return getUserName(this.p.user);
+		},
 		isRepost(): boolean {
 			return (this.post.repost &&
 				this.post.text == null &&
diff --git a/src/client/app/mobile/views/components/post-preview.vue b/src/client/app/mobile/views/components/post-preview.vue
index 81b0c22bf..96bd4d5c1 100644
--- a/src/client/app/mobile/views/components/post-preview.vue
+++ b/src/client/app/mobile/views/components/post-preview.vue
@@ -5,7 +5,7 @@
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`">{{ post.user.name }}</router-link>
+			<router-link class="name" :to="`/@${acct}`">{{ name }}</router-link>
 			<span class="username">@{{ acct }}</span>
 			<router-link class="time" :to="`/@${acct}/${post.id}`">
 				<mk-time :time="post.createdAt"/>
@@ -21,12 +21,16 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['post'],
 	computed: {
 		acct() {
 			return getAcct(this.post.user);
+		},
+		name() {
+			return getUserName(this.post.user);
 		}
 	}
 });
diff --git a/src/client/app/mobile/views/components/post.sub.vue b/src/client/app/mobile/views/components/post.sub.vue
index 85ddb9188..909d5cb59 100644
--- a/src/client/app/mobile/views/components/post.sub.vue
+++ b/src/client/app/mobile/views/components/post.sub.vue
@@ -5,7 +5,7 @@
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`">{{ post.user.name }}</router-link>
+			<router-link class="name" :to="`/@${acct}`">{{ getUserName(post.user) }}</router-link>
 			<span class="username">@{{ acct }}</span>
 			<router-link class="created-at" :to="`/@${acct}/${post.id}`">
 				<mk-time :time="post.createdAt"/>
@@ -21,12 +21,16 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['post'],
 	computed: {
 		acct() {
 			return getAcct(this.post.user);
+		},
+		name() {
+			return getUserName(this.post.user);
 		}
 	}
 });
diff --git a/src/client/app/mobile/views/components/post.vue b/src/client/app/mobile/views/components/post.vue
index 1454bc939..eee1e80fd 100644
--- a/src/client/app/mobile/views/components/post.vue
+++ b/src/client/app/mobile/views/components/post.vue
@@ -10,7 +10,7 @@
 			</router-link>
 			%fa:retweet%
 			<span>{{ '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('{')) }}</span>
-			<router-link class="name" :to="`/@${acct}`">{{ post.user.name }}</router-link>
+			<router-link class="name" :to="`/@${acct}`">{{ name }}</router-link>
 			<span>{{ '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr('%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1) }}</span>
 		</p>
 		<mk-time :time="post.createdAt"/>
@@ -21,7 +21,7 @@
 		</router-link>
 		<div class="main">
 			<header>
-				<router-link class="name" :to="`/@${pAcct}`">{{ p.user.name }}</router-link>
+				<router-link class="name" :to="`/@${pAcct}`">{{ pName }}</router-link>
 				<span class="is-bot" v-if="p.user.host === null && p.user.account.isBot">bot</span>
 				<span class="username">@{{ pAcct }}</span>
 				<div class="info">
@@ -78,6 +78,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 import parse from '../../../../../text/parse';
 
 import MkPostMenu from '../../../common/views/components/post-menu.vue';
@@ -102,9 +103,15 @@ export default Vue.extend({
 		acct(): string {
 			return getAcct(this.post.user);
 		},
+		name(): string {
+			return getUserName(this.post.user);
+		},
 		pAcct(): string {
 			return getAcct(this.p.user);
 		},
+		pName(): string {
+			return getUserName(this.p.user);
+		},
 		isRepost(): boolean {
 			return (this.post.repost &&
 				this.post.text == null &&
diff --git a/src/client/app/mobile/views/components/ui.header.vue b/src/client/app/mobile/views/components/ui.header.vue
index 2bf47a90a..fd4f31fd9 100644
--- a/src/client/app/mobile/views/components/ui.header.vue
+++ b/src/client/app/mobile/views/components/ui.header.vue
@@ -3,7 +3,7 @@
 	<mk-special-message/>
 	<div class="main" ref="main">
 		<div class="backdrop"></div>
-		<p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ os.i.name }}</b>さん</p>
+		<p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ name }}</b>さん</p>
 		<div class="content" ref="mainContainer">
 			<button class="nav" @click="$parent.isDrawerOpening = true">%fa:bars%</button>
 			<template v-if="hasUnreadNotifications || hasUnreadMessagingMessages || hasGameInvitations">%fa:circle%</template>
@@ -19,9 +19,15 @@
 <script lang="ts">
 import Vue from 'vue';
 import * as anime from 'animejs';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['func'],
+	computed: {
+		name() {
+			return getUserName(this.os.i);
+		}
+	},
 	data() {
 		return {
 			hasUnreadNotifications: false,
diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue
index a923774a7..61dd8ca9d 100644
--- a/src/client/app/mobile/views/components/ui.nav.vue
+++ b/src/client/app/mobile/views/components/ui.nav.vue
@@ -11,7 +11,7 @@
 		<div class="body" v-if="isOpen">
 			<router-link class="me" v-if="os.isSignedIn" :to="`/@${os.i.username}`">
 				<img class="avatar" :src="`${os.i.avatarUrl}?thumbnail&size=128`" alt="avatar"/>
-				<p class="name">{{ os.i.name }}</p>
+				<p class="name">{{ name }}</p>
 			</router-link>
 			<div class="links">
 				<ul>
@@ -40,9 +40,15 @@
 <script lang="ts">
 import Vue from 'vue';
 import { docsUrl, chUrl, lang } from '../../../config';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['isOpen'],
+	computed: {
+		name() {
+			return getUserName(this.os.i);
+		}
+	},
 	data() {
 		return {
 			hasUnreadNotifications: false,
diff --git a/src/client/app/mobile/views/components/user-card.vue b/src/client/app/mobile/views/components/user-card.vue
index 46fa3b473..e8698a62f 100644
--- a/src/client/app/mobile/views/components/user-card.vue
+++ b/src/client/app/mobile/views/components/user-card.vue
@@ -5,7 +5,7 @@
 			<img :src="`${user.avatarUrl}?thumbnail&size=200`" alt="avatar"/>
 		</a>
 	</header>
-	<a class="name" :href="`/@${acct}`" target="_blank">{{ user.name }}</a>
+	<a class="name" :href="`/@${acct}`" target="_blank">{{ name }}</a>
 	<p class="username">@{{ acct }}</p>
 	<mk-follow-button :user="user"/>
 </div>
@@ -14,12 +14,16 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['user'],
 	computed: {
 		acct() {
 			return getAcct(this.user);
+		},
+		name() {
+			return getUserName(this.user);
 		}
 	}
 });
diff --git a/src/client/app/mobile/views/components/user-preview.vue b/src/client/app/mobile/views/components/user-preview.vue
index 00f87e554..72a6bcf8a 100644
--- a/src/client/app/mobile/views/components/user-preview.vue
+++ b/src/client/app/mobile/views/components/user-preview.vue
@@ -5,7 +5,7 @@
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`">{{ user.name }}</router-link>
+			<router-link class="name" :to="`/@${acct}`">{{ name }}</router-link>
 			<span class="username">@{{ acct }}</span>
 		</header>
 		<div class="body">
@@ -18,12 +18,16 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['user'],
 	computed: {
 		acct() {
 			return getAcct(this.user);
+		},
+		name() {
+			return getUserName(this.user);
 		}
 	}
 });
diff --git a/src/client/app/mobile/views/pages/followers.vue b/src/client/app/mobile/views/pages/followers.vue
index d2e6a9aea..f4225d556 100644
--- a/src/client/app/mobile/views/pages/followers.vue
+++ b/src/client/app/mobile/views/pages/followers.vue
@@ -2,7 +2,7 @@
 <mk-ui>
 	<template slot="header" v-if="!fetching">
 		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt="">
-		{{ '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name) }}
+		{{ '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', name) }}
 	</template>
 	<mk-users-list
 		v-if="!fetching"
@@ -20,6 +20,7 @@
 import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
 import parseAcct from '../../../../../acct/parse';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -28,6 +29,11 @@ export default Vue.extend({
 			user: null
 		};
 	},
+	computed: {
+		name() {
+			return getUserName(this.user);
+		}
+	},
 	watch: {
 		$route: 'fetch'
 	},
@@ -46,7 +52,7 @@ export default Vue.extend({
 				this.user = user;
 				this.fetching = false;
 
-				document.title = '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name) + ' | Misskey';
+				document.title = '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', this.name) + ' | Misskey';
 			});
 		},
 		onLoaded() {
diff --git a/src/client/app/mobile/views/pages/following.vue b/src/client/app/mobile/views/pages/following.vue
index 3690536cf..cc2442e47 100644
--- a/src/client/app/mobile/views/pages/following.vue
+++ b/src/client/app/mobile/views/pages/following.vue
@@ -2,7 +2,7 @@
 <mk-ui>
 	<template slot="header" v-if="!fetching">
 		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt="">
-		{{ '%i18n:mobile.tags.mk-user-following-page.following-of%'.replace('{}', user.name) }}
+		{{ '%i18n:mobile.tags.mk-user-following-page.following-of%'.replace('{}', name) }}
 	</template>
 	<mk-users-list
 		v-if="!fetching"
@@ -20,6 +20,7 @@
 import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
 import parseAcct from '../../../../../acct/parse';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -28,6 +29,11 @@ export default Vue.extend({
 			user: null
 		};
 	},
+	computed: {
+		name() {
+			return getUserName(this.user);
+		}
+	},
 	watch: {
 		$route: 'fetch'
 	},
@@ -46,7 +52,7 @@ export default Vue.extend({
 				this.user = user;
 				this.fetching = false;
 
-				document.title = '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name) + ' | Misskey';
+				document.title = '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', this.name) + ' | Misskey';
 			});
 		},
 		onLoaded() {
diff --git a/src/client/app/mobile/views/pages/messaging-room.vue b/src/client/app/mobile/views/pages/messaging-room.vue
index 172666eea..ae6662dc0 100644
--- a/src/client/app/mobile/views/pages/messaging-room.vue
+++ b/src/client/app/mobile/views/pages/messaging-room.vue
@@ -1,7 +1,7 @@
 <template>
 <mk-ui>
 	<span slot="header">
-		<template v-if="user">%fa:R comments%{{ user.name }}</template>
+		<template v-if="user">%fa:R comments%{{ name }}</template>
 		<template v-else><mk-ellipsis/></template>
 	</span>
 	<mk-messaging-room v-if="!fetching" :user="user" :is-naked="true"/>
@@ -11,6 +11,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import parseAcct from '../../../../../acct/parse';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -19,6 +20,11 @@ export default Vue.extend({
 			user: null
 		};
 	},
+	computed: {
+		name() {
+			return getUserName(this.user);
+		}
+	},
 	watch: {
 		$route: 'fetch'
 	},
@@ -33,7 +39,7 @@ export default Vue.extend({
 				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%: ${this.name} | Misskey`;
 			});
 		}
 	}
diff --git a/src/client/app/mobile/views/pages/profile-setting.vue b/src/client/app/mobile/views/pages/profile-setting.vue
index 15f9bc9b6..4a560c027 100644
--- a/src/client/app/mobile/views/pages/profile-setting.vue
+++ b/src/client/app/mobile/views/pages/profile-setting.vue
@@ -52,7 +52,7 @@ export default Vue.extend({
 		};
 	},
 	created() {
-		this.name = (this as any).os.i.name;
+		this.name = (this as any).os.i.name || '';
 		this.location = (this as any).os.i.account.profile.location;
 		this.description = (this as any).os.i.description;
 		this.birthday = (this as any).os.i.account.profile.birthday;
@@ -94,7 +94,7 @@ export default Vue.extend({
 			this.saving = true;
 
 			(this as any).api('i/update', {
-				name: this.name,
+				name: this.name || null,
 				location: this.location || null,
 				description: this.description || null,
 				birthday: this.birthday || null
diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue
index a945a21c5..58a9d4e37 100644
--- a/src/client/app/mobile/views/pages/settings.vue
+++ b/src/client/app/mobile/views/pages/settings.vue
@@ -2,7 +2,7 @@
 <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>
+		<p v-html="'%i18n:mobile.tags.mk-settings.signed-in-as%'.replace('{}', '<b>' + name + '</b>')"></p>
 		<ul>
 			<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>
@@ -20,6 +20,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import { version, codename } from '../../../config';
+import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -28,6 +29,11 @@ export default Vue.extend({
 			codename
 		};
 	},
+	computed: {
+		name() {
+			return getUserName(this.os.i);
+		}
+	},
 	mounted() {
 		document.title = 'Misskey | %i18n:mobile.tags.mk-settings-page.settings%';
 		document.documentElement.style.background = '#313a42';
diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue
index be696484b..3b7b4d6c2 100644
--- a/src/client/app/mobile/views/pages/user.vue
+++ b/src/client/app/mobile/views/pages/user.vue
@@ -1,6 +1,6 @@
 <template>
 <mk-ui>
-	<span slot="header" v-if="!fetching">%fa:user% {{ user.name }}</span>
+	<span slot="header" v-if="!fetching">%fa:user% {{ user }}</span>
 	<main v-if="!fetching">
 		<header>
 			<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=1024)` : ''"></div>
@@ -12,7 +12,7 @@
 					<mk-follow-button v-if="os.isSignedIn && os.i.id != user.id" :user="user"/>
 				</div>
 				<div class="title">
-					<h1>{{ user.name }}</h1>
+					<h1>{{ user }}</h1>
 					<span class="username">@{{ acct }}</span>
 					<span class="followed" v-if="user.isFollowed">%i18n:mobile.tags.mk-user.follows-you%</span>
 				</div>
@@ -61,7 +61,7 @@
 import Vue from 'vue';
 import * as age from 's-age';
 import getAcct from '../../../../../acct/render';
-import getAcct from '../../../../../acct/parse';
+import getUserName from '../../../../../renderers/get-user-name';
 import Progress from '../../../common/scripts/loading';
 import XHome from './user/home.vue';
 
@@ -82,6 +82,9 @@ export default Vue.extend({
 		},
 		age(): number {
 			return age(this.user.account.profile.birthday);
+		},
+		name() {
+			return getUserName(this.user);
 		}
 	},
 	watch: {
@@ -102,7 +105,7 @@ export default Vue.extend({
 				this.fetching = false;
 
 				Progress.done();
-				document.title = user.name + ' | Misskey';
+				document.title = this.name + ' | Misskey';
 			});
 		}
 	}
diff --git a/src/client/app/mobile/views/pages/user/home.followers-you-know.vue b/src/client/app/mobile/views/pages/user/home.followers-you-know.vue
index ffdd9f178..1b128e2f2 100644
--- a/src/client/app/mobile/views/pages/user/home.followers-you-know.vue
+++ b/src/client/app/mobile/views/pages/user/home.followers-you-know.vue
@@ -3,7 +3,7 @@
 	<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="`/@${getAcct(user)}`">
-			<img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="user.name"/>
+			<img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="getUserName(user)"/>
 		</a>
 	</div>
 	<p class="empty" v-if="!fetching && users.length == 0">%i18n:mobile.tags.mk-user-overview-followers-you-know.no-users%</p>
@@ -13,6 +13,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../../acct/render';
+import getUserName from '../../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['user'],
@@ -22,6 +23,11 @@ export default Vue.extend({
 			users: []
 		};
 	},
+	computed: {
+		name() {
+			return getUserName(this.user);
+		}
+	},
 	methods: {
 		getAcct
 	},
diff --git a/src/client/app/mobile/views/widgets/profile.vue b/src/client/app/mobile/views/widgets/profile.vue
index f1d283e45..bd257a3ff 100644
--- a/src/client/app/mobile/views/widgets/profile.vue
+++ b/src/client/app/mobile/views/widgets/profile.vue
@@ -8,15 +8,23 @@
 			:src="`${os.i.avatarUrl}?thumbnail&size=96`"
 			alt="avatar"
 		/>
-		<router-link :class="$style.name" :to="`/@${os.i.username}`">{{ os.i.name }}</router-link>
+		<router-link :class="$style.name" :to="`/@${os.i.username}`">{{ name }}</router-link>
 	</mk-widget-container>
 </div>
 </template>
 
 <script lang="ts">
 import define from '../../../common/define-widget';
+import getUserName from '../../../../../renderers/get-user-name';
+
 export default define({
 	name: 'profile'
+}).extend({
+	computed: {
+		name() {
+			return getUserName(this.os.i);
+		}
+	}
 });
 </script>
 
diff --git a/src/models/user.ts b/src/models/user.ts
index f817c33aa..92091c687 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -21,7 +21,7 @@ type IUserBase = {
 	deletedAt: Date;
 	followersCount: number;
 	followingCount: number;
-	name: string;
+	name?: string;
 	postsCount: number;
 	driveCapacity: number;
 	username: string;
@@ -99,8 +99,8 @@ export function validatePassword(password: string): boolean {
 	return typeof password == 'string' && password != '';
 }
 
-export function isValidName(name: string): boolean {
-	return typeof name == 'string' && name.length < 30 && name.trim() != '';
+export function isValidName(name?: string): boolean {
+	return name === null || (typeof name == 'string' && name.length < 30 && name.trim() != '');
 }
 
 export function isValidDescription(description: string): boolean {
diff --git a/src/othello/ai/back.ts b/src/othello/ai/back.ts
index d6704b175..4d06ed956 100644
--- a/src/othello/ai/back.ts
+++ b/src/othello/ai/back.ts
@@ -9,6 +9,7 @@
 import * as request from 'request-promise-native';
 import Othello, { Color } from '../core';
 import conf from '../../config';
+import getUserName from '../../renderers/get-user-name';
 
 let game;
 let form;
@@ -47,8 +48,8 @@ process.on('message', async msg => {
 		const user = game.user1Id == id ? game.user2 : game.user1;
 		const isSettai = form[0].value === 0;
 		const text = isSettai
-			? `?[${user.name}](${conf.url}/@${user.username})さんの接待を始めました!`
-			: `対局を?[${user.name}](${conf.url}/@${user.username})さんと始めました! (強さ${form[0].value})`;
+			? `?[${getUserName(user)}](${conf.url}/@${user.username})さんの接待を始めました!`
+			: `対局を?[${getUserName(user)}](${conf.url}/@${user.username})さんと始めました! (強さ${form[0].value})`;
 
 		const res = await request.post(`${conf.api_url}/posts/create`, {
 			json: { i,
@@ -72,15 +73,15 @@ process.on('message', async msg => {
 		const isSettai = form[0].value === 0;
 		const text = isSettai
 			? msg.body.winnerId === null
-				? `?[${user.name}](${conf.url}/@${user.username})さんに接待で引き分けました...`
+				? `?[${getUserName(user)}](${conf.url}/@${user.username})さんに接待で引き分けました...`
 				: msg.body.winnerId == id
-					? `?[${user.name}](${conf.url}/@${user.username})さんに接待で勝ってしまいました...`
-					: `?[${user.name}](${conf.url}/@${user.username})さんに接待で負けてあげました♪`
+					? `?[${getUserName(user)}](${conf.url}/@${user.username})さんに接待で勝ってしまいました...`
+					: `?[${getUserName(user)}](${conf.url}/@${user.username})さんに接待で負けてあげました♪`
 			: msg.body.winnerId === null
-				? `?[${user.name}](${conf.url}/@${user.username})さんと引き分けました~`
+				? `?[${getUserName(user)}](${conf.url}/@${user.username})さんと引き分けました~`
 				: msg.body.winnerId == id
-					? `?[${user.name}](${conf.url}/@${user.username})さんに勝ちました♪`
-					: `?[${user.name}](${conf.url}/@${user.username})さんに負けました...`;
+					? `?[${getUserName(user)}](${conf.url}/@${user.username})さんに勝ちました♪`
+					: `?[${getUserName(user)}](${conf.url}/@${user.username})さんに負けました...`;
 
 		await request.post(`${conf.api_url}/posts/create`, {
 			json: { i,
diff --git a/src/renderers/get-notification-summary.ts b/src/renderers/get-notification-summary.ts
index 03db722c8..f5e38faf9 100644
--- a/src/renderers/get-notification-summary.ts
+++ b/src/renderers/get-notification-summary.ts
@@ -1,3 +1,4 @@
+import getUserName from '../renderers/get-user-name';
 import getPostSummary from './get-post-summary';
 import getReactionEmoji from './get-reaction-emoji';
 
@@ -8,19 +9,19 @@ import getReactionEmoji from './get-reaction-emoji';
 export default function(notification: any): string {
 	switch (notification.type) {
 		case 'follow':
-			return `${notification.user.name}にフォローされました`;
+			return `${getUserName(notification.user)}にフォローされました`;
 		case 'mention':
-			return `言及されました:\n${notification.user.name}「${getPostSummary(notification.post)}」`;
+			return `言及されました:\n${getUserName(notification.user)}「${getPostSummary(notification.post)}」`;
 		case 'reply':
-			return `返信されました:\n${notification.user.name}「${getPostSummary(notification.post)}」`;
+			return `返信されました:\n${getUserName(notification.user)}「${getPostSummary(notification.post)}」`;
 		case 'repost':
-			return `Repostされました:\n${notification.user.name}「${getPostSummary(notification.post)}」`;
+			return `Repostされました:\n${getUserName(notification.user)}「${getPostSummary(notification.post)}」`;
 		case 'quote':
-			return `引用されました:\n${notification.user.name}「${getPostSummary(notification.post)}」`;
+			return `引用されました:\n${getUserName(notification.user)}「${getPostSummary(notification.post)}」`;
 		case 'reaction':
-			return `リアクションされました:\n${notification.user.name} <${getReactionEmoji(notification.reaction)}>「${getPostSummary(notification.post)}」`;
+			return `リアクションされました:\n${getUserName(notification.user)} <${getReactionEmoji(notification.reaction)}>「${getPostSummary(notification.post)}」`;
 		case 'poll_vote':
-			return `投票されました:\n${notification.user.name}「${getPostSummary(notification.post)}」`;
+			return `投票されました:\n${getUserName(notification.user)}「${getPostSummary(notification.post)}」`;
 		default:
 			return `<不明な通知タイプ: ${notification.type}>`;
 	}
diff --git a/src/renderers/get-user-summary.ts b/src/renderers/get-user-summary.ts
index 208987113..d002933b6 100644
--- a/src/renderers/get-user-summary.ts
+++ b/src/renderers/get-user-summary.ts
@@ -1,12 +1,13 @@
 import { IUser, isLocalUser } from '../models/user';
 import getAcct from '../acct/render';
+import getUserName from './get-user-name';
 
 /**
  * ユーザーを表す文字列を取得します。
  * @param user ユーザー
  */
 export default function(user: IUser): string {
-	let string = `${user.name} (@${getAcct(user)})\n` +
+	let string = `${getUserName(user)} (@${getAcct(user)})\n` +
 		`${user.postsCount}投稿、${user.followingCount}フォロー、${user.followersCount}フォロワー\n`;
 
 	if (isLocalUser(user)) {
diff --git a/src/server/api/bot/core.ts b/src/server/api/bot/core.ts
index 7e80f31e5..a44aa9d7b 100644
--- a/src/server/api/bot/core.ts
+++ b/src/server/api/bot/core.ts
@@ -4,6 +4,7 @@ import * as bcrypt from 'bcryptjs';
 import User, { IUser, init as initUser, ILocalUser } from '../../../models/user';
 
 import getPostSummary from '../../../renderers/get-post-summary';
+import getUserName from '../../../renderers/get-user-name';
 import getUserSummary from '../../../renderers/get-user-summary';
 import parseAcct from '../../../acct/parse';
 import getNotificationSummary from '../../../renderers/get-notification-summary';
@@ -90,7 +91,7 @@ export default class BotCore extends EventEmitter {
 					'タイムラインや通知を見た後、「次」というとさらに遡ることができます。';
 
 			case 'me':
-				return this.user ? `${this.user.name}としてサインインしています。\n\n${getUserSummary(this.user)}` : 'サインインしていません';
+				return this.user ? `${getUserName(this.user)}としてサインインしています。\n\n${getUserSummary(this.user)}` : 'サインインしていません';
 
 			case 'login':
 			case 'signin':
@@ -230,7 +231,7 @@ class SigninContext extends Context {
 			if (same) {
 				this.bot.signin(this.temporaryUser);
 				this.bot.clearContext();
-				return `${this.temporaryUser.name}さん、おかえりなさい!`;
+				return `${getUserName(this.temporaryUser)}さん、おかえりなさい!`;
 			} else {
 				return `パスワードが違います... もう一度教えてください:`;
 			}
@@ -305,7 +306,7 @@ class TlContext extends Context {
 			this.emit('updated');
 
 			const text = tl
-				.map(post => `${post.user.name}\n「${getPostSummary(post)}」`)
+				.map(post => `${getUserName(post.user)}\n「${getPostSummary(post)}」`)
 				.join('\n-----\n');
 
 			return text;
diff --git a/src/server/api/bot/interfaces/line.ts b/src/server/api/bot/interfaces/line.ts
index 7847cbdea..1191aaf39 100644
--- a/src/server/api/bot/interfaces/line.ts
+++ b/src/server/api/bot/interfaces/line.ts
@@ -10,6 +10,7 @@ import prominence = require('prominence');
 import getAcct from '../../../../acct/render';
 import parseAcct from '../../../../acct/parse';
 import getPostSummary from '../../../../renderers/get-post-summary';
+import getUserName from '../../../../renderers/get-user-name';
 
 const redis = prominence(_redis);
 
@@ -131,7 +132,7 @@ class LineBot extends BotCore {
 			template: {
 				type: 'buttons',
 				thumbnailImageUrl: `${user.avatarUrl}?thumbnail&size=1024`,
-				title: `${user.name} (@${acct})`,
+				title: `${getUserName(user)} (@${acct})`,
 				text: user.description || '(no description)',
 				actions: actions
 			}
@@ -146,7 +147,7 @@ class LineBot extends BotCore {
 			limit: 5
 		}, this.user);
 
-		const text = `${tl[0].user.name}さんのタイムラインはこちらです:\n\n` + tl
+		const text = `${getUserName(tl[0].user)}さんのタイムラインはこちらです:\n\n` + tl
 			.map(post => getPostSummary(post))
 			.join('\n-----\n');
 

From 06400ea2f5c28b87fa71a0a69001a8b81d9ca161 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Fri, 6 Apr 2018 01:37:24 +0900
Subject: [PATCH 1108/1250] =?UTF-8?q?Do=20not=20save=20=E5=90=8D=E7=84=A1?=
 =?UTF-8?q?=E3=81=97=20as=20the=20name=20of=20a=20new=20user?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/server/api/private/signup.ts | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts
index 4203ce526..c54d6f1a1 100644
--- a/src/server/api/private/signup.ts
+++ b/src/server/api/private/signup.ts
@@ -47,7 +47,6 @@ export default async (req: express.Request, res: express.Response) => {
 
 	const username = req.body['username'];
 	const password = req.body['password'];
-	const name = '名無し';
 
 	// Validate username
 	if (!validateUsername(username)) {
@@ -113,7 +112,7 @@ export default async (req: express.Request, res: express.Response) => {
 		description: null,
 		followersCount: 0,
 		followingCount: 0,
-		name: name,
+		name: null,
 		postsCount: 0,
 		driveCapacity: 1024 * 1024 * 128, // 128MiB
 		username: username,

From 7961ef33b28db67c4725a1e6534d03dd59d4aa44 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 6 Apr 2018 03:10:25 +0900
Subject: [PATCH 1109/1250] RENAME: api --> services

---
 src/queue/processors/http/report-github-failure.ts | 2 +-
 src/remote/activitypub/act/create.ts               | 4 ++--
 src/remote/activitypub/act/follow.ts               | 2 +-
 src/remote/activitypub/act/unfollow.ts             | 2 +-
 src/remote/activitypub/resolve-person.ts           | 2 +-
 src/server/api/endpoints/following/create.ts       | 2 +-
 src/server/api/endpoints/posts/create.ts           | 2 +-
 src/{api => services}/drive/add-file.ts            | 0
 src/{api => services}/drive/upload-from-url.ts     | 0
 src/{api => services}/following/create.ts          | 0
 src/{api => services}/following/delete.ts          | 0
 src/{api => services}/post/create.ts               | 0
 src/services/post/reaction/create.ts               | 0
 src/{api => services}/post/watch.ts                | 0
 14 files changed, 8 insertions(+), 8 deletions(-)
 rename src/{api => services}/drive/add-file.ts (100%)
 rename src/{api => services}/drive/upload-from-url.ts (100%)
 rename src/{api => services}/following/create.ts (100%)
 rename src/{api => services}/following/delete.ts (100%)
 rename src/{api => services}/post/create.ts (100%)
 create mode 100644 src/services/post/reaction/create.ts
 rename src/{api => services}/post/watch.ts (100%)

diff --git a/src/queue/processors/http/report-github-failure.ts b/src/queue/processors/http/report-github-failure.ts
index e747d062d..1e0b51f89 100644
--- a/src/queue/processors/http/report-github-failure.ts
+++ b/src/queue/processors/http/report-github-failure.ts
@@ -1,6 +1,6 @@
 import * as request from 'request-promise-native';
 import User from '../../../models/user';
-import createPost from '../../../api/post/create';
+import createPost from '../../../services/post/create';
 
 export default async ({ data }) => {
 	const asyncBot = User.findOne({ _id: data.userId });
diff --git a/src/remote/activitypub/act/create.ts b/src/remote/activitypub/act/create.ts
index fe58f58f8..139c98f3b 100644
--- a/src/remote/activitypub/act/create.ts
+++ b/src/remote/activitypub/act/create.ts
@@ -3,8 +3,8 @@ import * as debug from 'debug';
 
 import Resolver from '../resolver';
 import Post from '../../../models/post';
-import uploadFromUrl from '../../../api/drive/upload-from-url';
-import createPost from '../../../api/post/create';
+import uploadFromUrl from '../../../services/drive/upload-from-url';
+import createPost from '../../../services/post/create';
 import { IRemoteUser, isRemoteUser } from '../../../models/user';
 import resolvePerson from '../resolve-person';
 
diff --git a/src/remote/activitypub/act/follow.ts b/src/remote/activitypub/act/follow.ts
index dc173a0ac..4fc423d15 100644
--- a/src/remote/activitypub/act/follow.ts
+++ b/src/remote/activitypub/act/follow.ts
@@ -1,7 +1,7 @@
 import parseAcct from '../../../acct/parse';
 import User from '../../../models/user';
 import config from '../../../config';
-import follow from '../../../api/following/create';
+import follow from '../../../services/following/create';
 
 export default async (actor, activity): Promise<void> => {
 	const prefix = config.url + '/@';
diff --git a/src/remote/activitypub/act/unfollow.ts b/src/remote/activitypub/act/unfollow.ts
index e3c9e1c1c..66c15e9a9 100644
--- a/src/remote/activitypub/act/unfollow.ts
+++ b/src/remote/activitypub/act/unfollow.ts
@@ -1,7 +1,7 @@
 import parseAcct from '../../../acct/parse';
 import User from '../../../models/user';
 import config from '../../../config';
-import unfollow from '../../../api/following/delete';
+import unfollow from '../../../services/following/delete';
 
 export default async (actor, activity): Promise<void> => {
 	const prefix = config.url + '/@';
diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts
index 2bf7a1354..907f19834 100644
--- a/src/remote/activitypub/resolve-person.ts
+++ b/src/remote/activitypub/resolve-person.ts
@@ -3,7 +3,7 @@ import { toUnicode } from 'punycode';
 import User, { validateUsername, isValidName, isValidDescription } from '../../models/user';
 import webFinger from '../webfinger';
 import Resolver from './resolver';
-import uploadFromUrl from '../../api/drive/upload-from-url';
+import uploadFromUrl from '../../services/drive/upload-from-url';
 
 export default async (value, verifier?: string) => {
 	const resolver = new Resolver();
diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts
index fae686ce5..0ccac8d83 100644
--- a/src/server/api/endpoints/following/create.ts
+++ b/src/server/api/endpoints/following/create.ts
@@ -4,7 +4,7 @@
 import $ from 'cafy';
 import User from '../../../../models/user';
 import Following from '../../../../models/following';
-import create from '../../../../api/following/create';
+import create from '../../../../services/following/create';
 
 /**
  * Follow a user
diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts
index d241c8c38..003a892bc 100644
--- a/src/server/api/endpoints/posts/create.ts
+++ b/src/server/api/endpoints/posts/create.ts
@@ -7,7 +7,7 @@ import Post, { IPost, isValidText, isValidCw, pack } from '../../../../models/po
 import { ILocalUser } from '../../../../models/user';
 import Channel, { IChannel } from '../../../../models/channel';
 import DriveFile from '../../../../models/drive-file';
-import create from '../../../../api/post/create';
+import create from '../../../../services/post/create';
 import { IApp } from '../../../../models/app';
 
 /**
diff --git a/src/api/drive/add-file.ts b/src/services/drive/add-file.ts
similarity index 100%
rename from src/api/drive/add-file.ts
rename to src/services/drive/add-file.ts
diff --git a/src/api/drive/upload-from-url.ts b/src/services/drive/upload-from-url.ts
similarity index 100%
rename from src/api/drive/upload-from-url.ts
rename to src/services/drive/upload-from-url.ts
diff --git a/src/api/following/create.ts b/src/services/following/create.ts
similarity index 100%
rename from src/api/following/create.ts
rename to src/services/following/create.ts
diff --git a/src/api/following/delete.ts b/src/services/following/delete.ts
similarity index 100%
rename from src/api/following/delete.ts
rename to src/services/following/delete.ts
diff --git a/src/api/post/create.ts b/src/services/post/create.ts
similarity index 100%
rename from src/api/post/create.ts
rename to src/services/post/create.ts
diff --git a/src/services/post/reaction/create.ts b/src/services/post/reaction/create.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/api/post/watch.ts b/src/services/post/watch.ts
similarity index 100%
rename from src/api/post/watch.ts
rename to src/services/post/watch.ts

From 8ef8383ac3369741b2fc731ddbecc6544bf34cdc Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 6 Apr 2018 03:42:55 +0900
Subject: [PATCH 1110/1250] Fix bugs

---
 src/queue/processors/http/index.ts | 3 ++-
 src/services/post/create.ts        | 8 +++++---
 2 files changed, 7 insertions(+), 4 deletions(-)

diff --git a/src/queue/processors/http/index.ts b/src/queue/processors/http/index.ts
index 3d7d941b1..61d7f9ac9 100644
--- a/src/queue/processors/http/index.ts
+++ b/src/queue/processors/http/index.ts
@@ -12,8 +12,9 @@ export default (job, done) => {
 	const handler = handlers[job.data.type];
 
 	if (handler) {
-		handler(job).then(() => done(), done);
+		handler(job, done);
 	} else {
 		console.warn(`Unknown job: ${job.data.type}`);
+		done();
 	}
 };
diff --git a/src/services/post/create.ts b/src/services/post/create.ts
index 9723dbe45..405e4a2f7 100644
--- a/src/services/post/create.ts
+++ b/src/services/post/create.ts
@@ -98,7 +98,7 @@ export default async (user: IUser, content: {
 	const postObj = await pack(post);
 
 	// タイムラインへの投稿
-	if (!post.channelId) {
+	if (post.channelId == null) {
 		// Publish event to myself's stream
 		if (isLocalUser(user)) {
 			stream(post.userId, 'post', postObj);
@@ -110,7 +110,7 @@ export default async (user: IUser, content: {
 				from: 'users',
 				localField: 'followerId',
 				foreignField: '_id',
-				as: 'follower'
+				as: 'user'
 			}
 		}, {
 			$match: {
@@ -125,7 +125,9 @@ export default async (user: IUser, content: {
 			const content = renderCreate(note);
 			content['@context'] = context;
 
-			Promise.all(followers.map(({ follower }) => {
+			Promise.all(followers.map(follower => {
+				follower = follower.user[0];
+
 				if (isLocalUser(follower)) {
 					// Publish event to followers stream
 					stream(follower._id, 'post', postObj);

From 2ee0e81140ee8989d286c446477fa947371b85aa Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 6 Apr 2018 04:04:50 +0900
Subject: [PATCH 1111/1250] wip

---
 src/models/post.ts          | 14 ++++++
 src/services/post/create.ts | 98 +++++++++++++++++++++----------------
 2 files changed, 69 insertions(+), 43 deletions(-)

diff --git a/src/models/post.ts b/src/models/post.ts
index 2f2b51b94..68a638fa2 100644
--- a/src/models/post.ts
+++ b/src/models/post.ts
@@ -52,6 +52,20 @@ export type IPost = {
 		speed: number;
 	};
 	uri: string;
+
+	_reply?: {
+		userId: mongo.ObjectID;
+	};
+	_repost?: {
+		userId: mongo.ObjectID;
+	};
+	_user: {
+		host: string;
+		hostLower: string;
+		account: {
+			inbox?: string;
+		};
+	};
 };
 
 /**
diff --git a/src/services/post/create.ts b/src/services/post/create.ts
index 405e4a2f7..745683b51 100644
--- a/src/services/post/create.ts
+++ b/src/services/post/create.ts
@@ -1,5 +1,5 @@
 import Post, { pack, IPost } from '../../models/post';
-import User, { isLocalUser, IUser } from '../../models/user';
+import User, { isLocalUser, IUser, isRemoteUser } from '../../models/user';
 import stream from '../../publishers/stream';
 import Following from '../../models/following';
 import { deliver } from '../../queue';
@@ -17,7 +17,7 @@ import parse from '../../text/parse';
 import html from '../../text/html';
 import { IApp } from '../../models/app';
 
-export default async (user: IUser, content: {
+export default async (user: IUser, data: {
 	createdAt?: Date;
 	text?: string;
 	reply?: IPost;
@@ -32,16 +32,16 @@ export default async (user: IUser, content: {
 	uri?: string;
 	app?: IApp;
 }, silent = false) => new Promise<IPost>(async (res, rej) => {
-	if (content.createdAt == null) content.createdAt = new Date();
-	if (content.visibility == null) content.visibility = 'public';
+	if (data.createdAt == null) data.createdAt = new Date();
+	if (data.visibility == null) data.visibility = 'public';
 
-	const tags = content.tags || [];
+	const tags = data.tags || [];
 
 	let tokens = null;
 
-	if (content.text) {
+	if (data.text) {
 		// Analyze
-		tokens = parse(content.text);
+		tokens = parse(data.text);
 
 		// Extract hashtags
 		const hashtags = tokens
@@ -55,31 +55,38 @@ export default async (user: IUser, content: {
 		});
 	}
 
-	const data: any = {
-		createdAt: content.createdAt,
-		mediaIds: content.media ? content.media.map(file => file._id) : [],
-		replyId: content.reply ? content.reply._id : null,
-		repostId: content.repost ? content.repost._id : null,
-		text: content.text,
+	const insert: any = {
+		createdAt: data.createdAt,
+		mediaIds: data.media ? data.media.map(file => file._id) : [],
+		replyId: data.reply ? data.reply._id : null,
+		repostId: data.repost ? data.repost._id : null,
+		text: data.text,
 		textHtml: tokens === null ? null : html(tokens),
-		poll: content.poll,
-		cw: content.cw,
+		poll: data.poll,
+		cw: data.cw,
 		tags,
 		userId: user._id,
-		viaMobile: content.viaMobile,
-		geo: content.geo || null,
-		appId: content.app ? content.app._id : null,
-		visibility: content.visibility,
+		viaMobile: data.viaMobile,
+		geo: data.geo || null,
+		appId: data.app ? data.app._id : null,
+		visibility: data.visibility,
 
 		// 以下非正規化データ
-		_reply: content.reply ? { userId: content.reply.userId } : null,
-		_repost: content.repost ? { userId: content.repost.userId } : null,
+		_reply: data.reply ? { userId: data.reply.userId } : null,
+		_repost: data.repost ? { userId: data.repost.userId } : null,
+		_user: {
+			host: user.host,
+			hostLower: user.hostLower,
+			account: isLocalUser(user) ? {} : {
+				inbox: user.account.inbox
+			}
+		}
 	};
 
-	if (content.uri != null) data.uri = content.uri;
+	if (data.uri != null) insert.uri = data.uri;
 
 	// 投稿を作成
-	const post = await Post.insert(data);
+	const post = await Post.insert(insert);
 
 	res(post);
 
@@ -125,6 +132,11 @@ export default async (user: IUser, content: {
 			const content = renderCreate(note);
 			content['@context'] = context;
 
+			// 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送
+			if (data.reply && isLocalUser(user) && isRemoteUser(data.reply._user)) {
+				deliver(user, content, data.reply._user.account.inbox).save();
+			}
+
 			Promise.all(followers.map(follower => {
 				follower = follower.user[0];
 
@@ -199,22 +211,22 @@ export default async (user: IUser, content: {
 	}
 
 	// If has in reply to post
-	if (content.reply) {
+	if (data.reply) {
 		// Increment replies count
-		Post.update({ _id: content.reply._id }, {
+		Post.update({ _id: data.reply._id }, {
 			$inc: {
 				repliesCount: 1
 			}
 		});
 
 		// (自分自身へのリプライでない限りは)通知を作成
-		notify(content.reply.userId, user._id, 'reply', {
+		notify(data.reply.userId, user._id, 'reply', {
 			postId: post._id
 		});
 
 		// Fetch watchers
 		PostWatching.find({
-			postId: content.reply._id,
+			postId: data.reply._id,
 			userId: { $ne: user._id },
 			// 削除されたドキュメントは除く
 			deletedAt: { $exists: false }
@@ -232,24 +244,24 @@ export default async (user: IUser, content: {
 
 		// この投稿をWatchする
 		if (isLocalUser(user) && user.account.settings.autoWatch !== false) {
-			watch(user._id, content.reply);
+			watch(user._id, data.reply);
 		}
 
 		// Add mention
-		addMention(content.reply.userId, 'reply');
+		addMention(data.reply.userId, 'reply');
 	}
 
 	// If it is repost
-	if (content.repost) {
+	if (data.repost) {
 		// Notify
-		const type = content.text ? 'quote' : 'repost';
-		notify(content.repost.userId, user._id, type, {
+		const type = data.text ? 'quote' : 'repost';
+		notify(data.repost.userId, user._id, type, {
 			post_id: post._id
 		});
 
 		// Fetch watchers
 		PostWatching.find({
-			postId: content.repost._id,
+			postId: data.repost._id,
 			userId: { $ne: user._id },
 			// 削除されたドキュメントは除く
 			deletedAt: { $exists: false }
@@ -267,24 +279,24 @@ export default async (user: IUser, content: {
 
 		// この投稿をWatchする
 		if (isLocalUser(user) && user.account.settings.autoWatch !== false) {
-			watch(user._id, content.repost);
+			watch(user._id, data.repost);
 		}
 
 		// If it is quote repost
-		if (content.text) {
+		if (data.text) {
 			// Add mention
-			addMention(content.repost.userId, 'quote');
+			addMention(data.repost.userId, 'quote');
 		} else {
 			// Publish event
-			if (!user._id.equals(content.repost.userId)) {
-				event(content.repost.userId, 'repost', postObj);
+			if (!user._id.equals(data.repost.userId)) {
+				event(data.repost.userId, 'repost', postObj);
 			}
 		}
 
 		// 今までで同じ投稿をRepostしているか
 		const existRepost = await Post.findOne({
 			userId: user._id,
-			repostId: content.repost._id,
+			repostId: data.repost._id,
 			_id: {
 				$ne: post._id
 			}
@@ -292,7 +304,7 @@ export default async (user: IUser, content: {
 
 		if (!existRepost) {
 			// Update repostee status
-			Post.update({ _id: content.repost._id }, {
+			Post.update({ _id: data.repost._id }, {
 				$inc: {
 					repostCount: 1
 				}
@@ -301,7 +313,7 @@ export default async (user: IUser, content: {
 	}
 
 	// If has text content
-	if (content.text) {
+	if (data.text) {
 		// Extract an '@' mentions
 		const atMentions = tokens
 			.filter(t => t.type == 'mention')
@@ -322,8 +334,8 @@ export default async (user: IUser, content: {
 			if (mentionee == null) return;
 
 			// 既に言及されたユーザーに対する返信や引用repostの場合も無視
-			if (content.reply && content.reply.userId.equals(mentionee._id)) return;
-			if (content.repost && content.repost.userId.equals(mentionee._id)) return;
+			if (data.reply && data.reply.userId.equals(mentionee._id)) return;
+			if (data.repost && data.repost.userId.equals(mentionee._id)) return;
 
 			// Add mention
 			addMention(mentionee._id, 'mention');

From 3fae72400b0f99eb4ba6180bd061aa1bfe6c3611 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Fri, 6 Apr 2018 12:19:44 +0900
Subject: [PATCH 1112/1250] Add missing src/renderers/get-user-name.ts

---
 src/renderers/get-user-name.ts | 5 +++++
 1 file changed, 5 insertions(+)
 create mode 100644 src/renderers/get-user-name.ts

diff --git a/src/renderers/get-user-name.ts b/src/renderers/get-user-name.ts
new file mode 100644
index 000000000..acd5e6626
--- /dev/null
+++ b/src/renderers/get-user-name.ts
@@ -0,0 +1,5 @@
+import { IUser } from '../models/user';
+
+export default function(user: IUser): string {
+	return user.name || '名無し';
+}

From 26b5b5b5202b8ceff811e19344bb35eede276deb Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Fri, 6 Apr 2018 12:20:11 +0900
Subject: [PATCH 1113/1250] Resolve local Person ID

---
 src/remote/activitypub/resolve-person.ts | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)

diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts
index a7c0020dd..84746169f 100644
--- a/src/remote/activitypub/resolve-person.ts
+++ b/src/remote/activitypub/resolve-person.ts
@@ -1,5 +1,7 @@
 import { JSDOM } from 'jsdom';
 import { toUnicode } from 'punycode';
+import parseAcct from '../../acct/parse';
+import config from '../../config';
 import User, { validateUsername, isValidName, isValidDescription } from '../../models/user';
 import { createHttp } from '../../queue';
 import webFinger from '../webfinger';
@@ -10,10 +12,18 @@ async function isCollection(collection) {
 }
 
 export default async (parentResolver, value, verifier?: string) => {
+	const id = value.id || value;
+	const localPrefix = config.url + '/@';
+
+	if (id.startsWith(localPrefix)) {
+		return User.findOne(parseAcct(id.slice(localPrefix)));
+	}
+
 	const { resolver, object } = await parentResolver.resolveOne(value);
 
 	if (
 		object === null ||
+		object.id !== id ||
 		object.type !== 'Person' ||
 		typeof object.preferredUsername !== 'string' ||
 		!validateUsername(object.preferredUsername) ||
@@ -36,7 +46,7 @@ export default async (parentResolver, value, verifier?: string) => {
 			resolved => isCollection(resolved.object) ? resolved.object : null,
 			() => null
 		),
-		webFinger(object.id, verifier),
+		webFinger(id, verifier),
 	]);
 
 	const host = toUnicode(finger.subject.replace(/^.*?@/, ''));
@@ -64,7 +74,7 @@ export default async (parentResolver, value, verifier?: string) => {
 				publicKeyPem: object.publicKey.publicKeyPem
 			},
 			inbox: object.inbox,
-			uri: object.id,
+			uri: id,
 		},
 	});
 

From 0c4c15fc7d588b707b6f981dd544ffac024c6119 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Fri, 6 Apr 2018 12:53:39 +0900
Subject: [PATCH 1114/1250] Resolve local Object ID

---
 src/remote/activitypub/create.ts | 37 ++++++++++++++++++++++++++++++++
 1 file changed, 37 insertions(+)

diff --git a/src/remote/activitypub/create.ts b/src/remote/activitypub/create.ts
index 3bc0c66f3..bbe595a45 100644
--- a/src/remote/activitypub/create.ts
+++ b/src/remote/activitypub/create.ts
@@ -1,8 +1,10 @@
 import { JSDOM } from 'jsdom';
 import { ObjectID } from 'mongodb';
+import parseAcct from '../../acct/parse';
 import config from '../../config';
 import DriveFile from '../../models/drive-file';
 import Post from '../../models/post';
+import User from '../../models/user';
 import { IRemoteUser } from '../../models/user';
 import uploadFromUrl from '../../drive/upload-from-url';
 import createPost from '../../post/create';
@@ -133,6 +135,41 @@ class Creator {
 
 		return collection.object.map(async element => {
 			const uri = element.id || element;
+			const localPrefix = config.url + '/@';
+
+			if (uri.startsWith(localPrefix)) {
+				const [acct, id] = uri.slice(localPrefix).split('/', 2);
+				const user = await User.aggregate([
+					{
+						$match: parseAcct(acct)
+					},
+					{
+						$lookup: {
+							from: 'posts',
+							localField: '_id',
+							foreignField: 'userId',
+							as: 'post'
+						}
+					},
+					{
+						$match: {
+							post: { _id: id }
+						}
+					}
+				]);
+
+				if (user === null || user.posts.length <= 0) {
+					throw new Error();
+				}
+
+				return {
+					resolver: collection.resolver,
+					object: {
+						$ref: 'posts',
+						id
+					}
+				};
+			}
 
 			try {
 				await Promise.all([

From a0ea46a1ae99351bccabba9f03169c3f8ce14c81 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 6 Apr 2018 14:35:17 +0900
Subject: [PATCH 1115/1250] Fix bug

---
 src/queue/processors/http/process-inbox.ts | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts
index c3074429f..4666e7f37 100644
--- a/src/queue/processors/http/process-inbox.ts
+++ b/src/queue/processors/http/process-inbox.ts
@@ -19,6 +19,7 @@ export default async (job: kue.Job, done): Promise<void> => {
 		if (host === null) {
 			console.warn(`request was made by local user: @${username}`);
 			done();
+			return;
 		}
 
 		user = await User.findOne({ usernameLower: username, hostLower: host }) as IRemoteUser;
@@ -40,7 +41,8 @@ export default async (job: kue.Job, done): Promise<void> => {
 	}
 
 	if (!verifySignature(signature, user.account.publicKey.publicKeyPem)) {
-		done(new Error('signature verification failed'));
+		console.warn('signature verification failed');
+		done();
 		return;
 	}
 

From d9ccb0c668899fb003883323bf570ecd348ad01f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 6 Apr 2018 14:39:28 +0900
Subject: [PATCH 1116/1250] Use error instaed of warn

---
 src/queue/processors/http/index.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/queue/processors/http/index.ts b/src/queue/processors/http/index.ts
index 61d7f9ac9..3dc259537 100644
--- a/src/queue/processors/http/index.ts
+++ b/src/queue/processors/http/index.ts
@@ -14,7 +14,7 @@ export default (job, done) => {
 	if (handler) {
 		handler(job, done);
 	} else {
-		console.warn(`Unknown job: ${job.data.type}`);
+		console.error(`Unknown job: ${job.data.type}`);
 		done();
 	}
 };

From 9de64085576916d235edde98df33035703871951 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 6 Apr 2018 14:47:00 +0900
Subject: [PATCH 1117/1250] Better English

---
 src/remote/activitypub/act/create.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/remote/activitypub/act/create.ts b/src/remote/activitypub/act/create.ts
index 139c98f3b..10083995d 100644
--- a/src/remote/activitypub/act/create.ts
+++ b/src/remote/activitypub/act/create.ts
@@ -28,7 +28,7 @@ export default async (actor: IRemoteUser, activity): Promise<void> => {
 	try {
 		object = await resolver.resolve(activity.object);
 	} catch (e) {
-		log(`Resolve failed: ${e}`);
+		log(`Resolution failed: ${e}`);
 		throw e;
 	}
 

From b3f92e760800be761736d5989a2e21fa7658a1c5 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, 6 Apr 2018 14:57:01 +0900
Subject: [PATCH 1118/1250] Remove needless log

---
 src/queue/index.ts | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/src/queue/index.ts b/src/queue/index.ts
index 689985e0e..691223de2 100644
--- a/src/queue/index.ts
+++ b/src/queue/index.ts
@@ -1,12 +1,9 @@
 import { createQueue } from 'kue';
-import * as debug from 'debug';
 
 import config from '../config';
 import db from './processors/db';
 import http from './processors/http';
 
-const log = debug('misskey:queue');
-
 const queue = createQueue({
 	redis: {
 		port: config.redis.port,
@@ -16,8 +13,6 @@ const queue = createQueue({
 });
 
 export function createHttp(data) {
-	log(`HTTP job created: ${JSON.stringify(data)}`);
-
 	return queue
 		.create('http', data)
 		.attempts(16)

From 38dab6704bf336fc358dd9a099cd24ea95324466 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 6 Apr 2018 19:18:38 +0900
Subject: [PATCH 1119/1250] Remove silent flag

---
 src/remote/activitypub/act/create.ts | 6 +++---
 src/services/post/create.ts          | 7 +++++--
 2 files changed, 8 insertions(+), 5 deletions(-)

diff --git a/src/remote/activitypub/act/create.ts b/src/remote/activitypub/act/create.ts
index 10083995d..1b9bad8ff 100644
--- a/src/remote/activitypub/act/create.ts
+++ b/src/remote/activitypub/act/create.ts
@@ -58,7 +58,7 @@ async function createImage(resolver: Resolver, actor: IRemoteUser, image) {
 	return await uploadFromUrl(image.url, actor);
 }
 
-async function createNote(resolver: Resolver, actor: IRemoteUser, note, silent = false) {
+async function createNote(resolver: Resolver, actor: IRemoteUser, note) {
 	if (
 		('attributedTo' in note && actor.account.uri !== note.attributedTo) ||
 		typeof note.id !== 'string'
@@ -86,7 +86,7 @@ async function createNote(resolver: Resolver, actor: IRemoteUser, note, silent =
 			const inReplyTo = await resolver.resolve(note.inReplyTo) as any;
 			const actor = await resolvePerson(inReplyTo.attributedTo);
 			if (isRemoteUser(actor)) {
-				reply = await createNote(resolver, actor, inReplyTo, true);
+				reply = await createNote(resolver, actor, inReplyTo);
 			}
 		}
 	}
@@ -102,5 +102,5 @@ async function createNote(resolver: Resolver, actor: IRemoteUser, note, silent =
 		viaMobile: false,
 		geo: undefined,
 		uri: note.id
-	}, silent);
+	});
 }
diff --git a/src/services/post/create.ts b/src/services/post/create.ts
index 745683b51..0bede2772 100644
--- a/src/services/post/create.ts
+++ b/src/services/post/create.ts
@@ -31,7 +31,7 @@ export default async (user: IUser, data: {
 	visibility?: string;
 	uri?: string;
 	app?: IApp;
-}, silent = false) => new Promise<IPost>(async (res, rej) => {
+}) => new Promise<IPost>(async (res, rej) => {
 	if (data.createdAt == null) data.createdAt = new Date();
 	if (data.visibility == null) data.visibility = 'public';
 
@@ -127,7 +127,10 @@ export default async (user: IUser, data: {
 			_id: false
 		});
 
-		if (!silent) {
+		// この投稿が3分以内に作成されたものであるならストリームに配信
+		const shouldDistribute = new Date().getTime() - post.createdAt.getTime() < 1000 * 60 * 3;
+
+		if (shouldDistribute) {
 			const note = await renderNote(user, post);
 			const content = renderCreate(note);
 			content['@context'] = context;

From aeae5aa9ffa6e827ce31cc1243420de20623e113 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 6 Apr 2018 19:23:13 +0900
Subject: [PATCH 1120/1250] Add todos

---
 src/remote/activitypub/act/create.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/remote/activitypub/act/create.ts b/src/remote/activitypub/act/create.ts
index 1b9bad8ff..d1eeacbc3 100644
--- a/src/remote/activitypub/act/create.ts
+++ b/src/remote/activitypub/act/create.ts
@@ -71,6 +71,8 @@ async function createNote(resolver: Resolver, actor: IRemoteUser, note) {
 
 	const media = [];
 	if ('attachment' in note && note.attachment != null) {
+		// TODO: attachmentは必ずしもImageではない
+		// TODO: ループの中でawaitはすべきでない
 		note.attachment.forEach(async media => {
 			const created = await createImage(resolver, note.actor, media);
 			media.push(created);

From 0c1337905a493ec304b966fdd71bffc9ea6c1b73 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 6 Apr 2018 19:26:17 +0900
Subject: [PATCH 1121/1250] Fix type annotation

---
 src/remote/activitypub/act/index.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/remote/activitypub/act/index.ts b/src/remote/activitypub/act/index.ts
index f58505b0a..c1d64b7c7 100644
--- a/src/remote/activitypub/act/index.ts
+++ b/src/remote/activitypub/act/index.ts
@@ -3,9 +3,9 @@ import performDeleteActivity from './delete';
 import follow from './follow';
 import undo from './undo';
 import { IObject } from '../type';
-import { IUser } from '../../../models/user';
+import { IRemoteUser } from '../../../models/user';
 
-export default async (actor: IUser, activity: IObject): Promise<void> => {
+export default async (actor: IRemoteUser, activity: IObject): Promise<void> => {
 	switch (activity.type) {
 	case 'Create':
 		await create(actor, activity);

From 5974e64f06777054f1f32f2320b179dd98db9a0c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 6 Apr 2018 19:35:23 +0900
Subject: [PATCH 1122/1250] Split code

---
 src/remote/activitypub/act/create.ts          | 108 ------------------
 src/remote/activitypub/act/create/image.ts    |  19 +++
 src/remote/activitypub/act/create/index.ts    |  45 ++++++++
 src/remote/activitypub/act/create/note.ts     |  60 ++++++++++
 .../act/{delete.ts => delete/index.ts}        |  14 +--
 src/remote/activitypub/act/delete/note.ts     |  11 ++
 6 files changed, 137 insertions(+), 120 deletions(-)
 delete mode 100644 src/remote/activitypub/act/create.ts
 create mode 100644 src/remote/activitypub/act/create/image.ts
 create mode 100644 src/remote/activitypub/act/create/index.ts
 create mode 100644 src/remote/activitypub/act/create/note.ts
 rename src/remote/activitypub/act/{delete.ts => delete/index.ts} (50%)
 create mode 100644 src/remote/activitypub/act/delete/note.ts

diff --git a/src/remote/activitypub/act/create.ts b/src/remote/activitypub/act/create.ts
deleted file mode 100644
index d1eeacbc3..000000000
--- a/src/remote/activitypub/act/create.ts
+++ /dev/null
@@ -1,108 +0,0 @@
-import { JSDOM } from 'jsdom';
-import * as debug from 'debug';
-
-import Resolver from '../resolver';
-import Post from '../../../models/post';
-import uploadFromUrl from '../../../services/drive/upload-from-url';
-import createPost from '../../../services/post/create';
-import { IRemoteUser, isRemoteUser } from '../../../models/user';
-import resolvePerson from '../resolve-person';
-
-const log = debug('misskey:activitypub');
-
-export default async (actor: IRemoteUser, activity): Promise<void> => {
-	if ('actor' in activity && actor.account.uri !== activity.actor) {
-		throw new Error('invalid actor');
-	}
-
-	const uri = activity.id || activity;
-
-	log(`Create: ${uri}`);
-
-	// TODO: 同じURIをもつものが既に登録されていないかチェック
-
-	const resolver = new Resolver();
-
-	let object;
-
-	try {
-		object = await resolver.resolve(activity.object);
-	} catch (e) {
-		log(`Resolution failed: ${e}`);
-		throw e;
-	}
-
-	switch (object.type) {
-	case 'Image':
-		createImage(resolver, actor, object);
-		break;
-
-	case 'Note':
-		createNote(resolver, actor, object);
-		break;
-
-	default:
-		console.warn(`Unknown type: ${object.type}`);
-		break;
-	}
-};
-
-async function createImage(resolver: Resolver, actor: IRemoteUser, image) {
-	if ('attributedTo' in image && actor.account.uri !== image.attributedTo) {
-		log(`invalid image: ${JSON.stringify(image, null, 2)}`);
-		throw new Error('invalid image');
-	}
-
-	log(`Creating the Image: ${image.id}`);
-
-	return await uploadFromUrl(image.url, actor);
-}
-
-async function createNote(resolver: Resolver, actor: IRemoteUser, note) {
-	if (
-		('attributedTo' in note && actor.account.uri !== note.attributedTo) ||
-		typeof note.id !== 'string'
-	) {
-		log(`invalid note: ${JSON.stringify(note, null, 2)}`);
-		throw new Error('invalid note');
-	}
-
-	log(`Creating the Note: ${note.id}`);
-
-	const media = [];
-	if ('attachment' in note && note.attachment != null) {
-		// TODO: attachmentは必ずしもImageではない
-		// TODO: ループの中でawaitはすべきでない
-		note.attachment.forEach(async media => {
-			const created = await createImage(resolver, note.actor, media);
-			media.push(created);
-		});
-	}
-
-	let reply = null;
-	if ('inReplyTo' in note && note.inReplyTo != null) {
-		const inReplyToPost = await Post.findOne({ uri: note.inReplyTo.id || note.inReplyTo });
-		if (inReplyToPost) {
-			reply = inReplyToPost;
-		} else {
-			const inReplyTo = await resolver.resolve(note.inReplyTo) as any;
-			const actor = await resolvePerson(inReplyTo.attributedTo);
-			if (isRemoteUser(actor)) {
-				reply = await createNote(resolver, actor, inReplyTo);
-			}
-		}
-	}
-
-	const { window } = new JSDOM(note.content);
-
-	return await createPost(actor, {
-		createdAt: new Date(note.published),
-		media,
-		reply,
-		repost: undefined,
-		text: window.document.body.textContent,
-		viaMobile: false,
-		geo: undefined,
-		uri: note.id
-	});
-}
diff --git a/src/remote/activitypub/act/create/image.ts b/src/remote/activitypub/act/create/image.ts
new file mode 100644
index 000000000..cd9e7b4e0
--- /dev/null
+++ b/src/remote/activitypub/act/create/image.ts
@@ -0,0 +1,19 @@
+import * as debug from 'debug';
+
+import Resolver from '../../resolver';
+import uploadFromUrl from '../../../../services/drive/upload-from-url';
+import { IRemoteUser } from '../../../../models/user';
+import { IDriveFile } from '../../../../models/drive-file';
+
+const log = debug('misskey:activitypub');
+
+export default async function(resolver: Resolver, actor: IRemoteUser, image): Promise<IDriveFile> {
+	if ('attributedTo' in image && actor.account.uri !== image.attributedTo) {
+		log(`invalid image: ${JSON.stringify(image, null, 2)}`);
+		throw new Error('invalid image');
+	}
+
+	log(`Creating the Image: ${image.id}`);
+
+	return await uploadFromUrl(image.url, actor);
+}
diff --git a/src/remote/activitypub/act/create/index.ts b/src/remote/activitypub/act/create/index.ts
new file mode 100644
index 000000000..d210aa4c5
--- /dev/null
+++ b/src/remote/activitypub/act/create/index.ts
@@ -0,0 +1,45 @@
+import * as debug from 'debug';
+
+import Resolver from '../../resolver';
+import { IRemoteUser } from '../../../../models/user';
+import createNote from './note';
+import createImage from './image';
+
+const log = debug('misskey:activitypub');
+
+export default async (actor: IRemoteUser, activity): Promise<void> => {
+	if ('actor' in activity && actor.account.uri !== activity.actor) {
+		throw new Error('invalid actor');
+	}
+
+	const uri = activity.id || activity;
+
+	log(`Create: ${uri}`);
+
+	// TODO: 同じURIをもつものが既に登録されていないかチェック
+
+	const resolver = new Resolver();
+
+	let object;
+
+	try {
+		object = await resolver.resolve(activity.object);
+	} catch (e) {
+		log(`Resolution failed: ${e}`);
+		throw e;
+	}
+
+	switch (object.type) {
+	case 'Image':
+		createImage(resolver, actor, object);
+		break;
+
+	case 'Note':
+		createNote(resolver, actor, object);
+		break;
+
+	default:
+		console.warn(`Unknown type: ${object.type}`);
+		break;
+	}
+};
diff --git a/src/remote/activitypub/act/create/note.ts b/src/remote/activitypub/act/create/note.ts
new file mode 100644
index 000000000..2ccd503ae
--- /dev/null
+++ b/src/remote/activitypub/act/create/note.ts
@@ -0,0 +1,60 @@
+import { JSDOM } from 'jsdom';
+import * as debug from 'debug';
+
+import Resolver from '../../resolver';
+import Post, { IPost } from '../../../../models/post';
+import createPost from '../../../../services/post/create';
+import { IRemoteUser, isRemoteUser } from '../../../../models/user';
+import resolvePerson from '../../resolve-person';
+import createImage from './image';
+
+const log = debug('misskey:activitypub');
+
+export default async function createNote(resolver: Resolver, actor: IRemoteUser, note): Promise<IPost> {
+	if (
+		('attributedTo' in note && actor.account.uri !== note.attributedTo) ||
+		typeof note.id !== 'string'
+	) {
+		log(`invalid note: ${JSON.stringify(note, null, 2)}`);
+		throw new Error('invalid note');
+	}
+
+	log(`Creating the Note: ${note.id}`);
+
+	const media = [];
+	if ('attachment' in note && note.attachment != null) {
+		// TODO: attachmentは必ずしもImageではない
+		// TODO: ループの中でawaitはすべきでない
+		note.attachment.forEach(async media => {
+			const created = await createImage(resolver, note.actor, media);
+			media.push(created);
+		});
+	}
+
+	let reply = null;
+	if ('inReplyTo' in note && note.inReplyTo != null) {
+		const inReplyToPost = await Post.findOne({ uri: note.inReplyTo.id || note.inReplyTo });
+		if (inReplyToPost) {
+			reply = inReplyToPost;
+		} else {
+			const inReplyTo = await resolver.resolve(note.inReplyTo) as any;
+			const actor = await resolvePerson(inReplyTo.attributedTo);
+			if (isRemoteUser(actor)) {
+				reply = await createNote(resolver, actor, inReplyTo);
+			}
+		}
+	}
+
+	const { window } = new JSDOM(note.content);
+
+	return await createPost(actor, {
+		createdAt: new Date(note.published),
+		media,
+		reply,
+		repost: undefined,
+		text: window.document.body.textContent,
+		viaMobile: false,
+		geo: undefined,
+		uri: note.id
+	});
+}
diff --git a/src/remote/activitypub/act/delete.ts b/src/remote/activitypub/act/delete/index.ts
similarity index 50%
rename from src/remote/activitypub/act/delete.ts
rename to src/remote/activitypub/act/delete/index.ts
index 334ca47ed..764814bac 100644
--- a/src/remote/activitypub/act/delete.ts
+++ b/src/remote/activitypub/act/delete/index.ts
@@ -1,6 +1,5 @@
-import Resolver from '../resolver';
-import Post from '../../../models/post';
-import { createDb } from '../../../queue';
+import Resolver from '../../resolver';
+import deleteNote from './note';
 
 export default async (actor, activity): Promise<void> => {
 	if ('actor' in activity && actor.account.uri !== activity.actor) {
@@ -16,13 +15,4 @@ export default async (actor, activity): Promise<void> => {
 		deleteNote(object);
 		break;
 	}
-
-	async function deleteNote(note) {
-		const post = await Post.findOneAndDelete({ uri: note.id });
-
-		createDb({
-			type: 'deletePostDependents',
-			id: post._id
-		}).delay(65536).save();
-	}
 };
diff --git a/src/remote/activitypub/act/delete/note.ts b/src/remote/activitypub/act/delete/note.ts
new file mode 100644
index 000000000..3b821f87c
--- /dev/null
+++ b/src/remote/activitypub/act/delete/note.ts
@@ -0,0 +1,11 @@
+import Post from '../../../../models/post';
+import { createDb } from '../../../../queue';
+
+export default async function(note) {
+	const post = await Post.findOneAndDelete({ uri: note.id });
+
+	createDb({
+		type: 'deletePostDependents',
+		id: post._id
+	}).delay(65536).save();
+}

From 20956d46c01303774e81c5bd9c35c6a099af52a6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 6 Apr 2018 20:45:33 +0900
Subject: [PATCH 1123/1250] Revert "Remove silent flag"

This reverts commit 9c15c94f801de7019f014446aa3b8ea42980a1da.
---
 src/remote/activitypub/act/create/note.ts | 2 +-
 src/services/post/create.ts               | 7 ++-----
 2 files changed, 3 insertions(+), 6 deletions(-)

diff --git a/src/remote/activitypub/act/create/note.ts b/src/remote/activitypub/act/create/note.ts
index 2ccd503ae..d50042e16 100644
--- a/src/remote/activitypub/act/create/note.ts
+++ b/src/remote/activitypub/act/create/note.ts
@@ -10,7 +10,7 @@ import createImage from './image';
 
 const log = debug('misskey:activitypub');
 
-export default async function createNote(resolver: Resolver, actor: IRemoteUser, note): Promise<IPost> {
+export default async function createNote(resolver: Resolver, actor: IRemoteUser, note, silent = false): Promise<IPost> {
 	if (
 		('attributedTo' in note && actor.account.uri !== note.attributedTo) ||
 		typeof note.id !== 'string'
diff --git a/src/services/post/create.ts b/src/services/post/create.ts
index 0bede2772..745683b51 100644
--- a/src/services/post/create.ts
+++ b/src/services/post/create.ts
@@ -31,7 +31,7 @@ export default async (user: IUser, data: {
 	visibility?: string;
 	uri?: string;
 	app?: IApp;
-}) => new Promise<IPost>(async (res, rej) => {
+}, silent = false) => new Promise<IPost>(async (res, rej) => {
 	if (data.createdAt == null) data.createdAt = new Date();
 	if (data.visibility == null) data.visibility = 'public';
 
@@ -127,10 +127,7 @@ export default async (user: IUser, data: {
 			_id: false
 		});
 
-		// この投稿が3分以内に作成されたものであるならストリームに配信
-		const shouldDistribute = new Date().getTime() - post.createdAt.getTime() < 1000 * 60 * 3;
-
-		if (shouldDistribute) {
+		if (!silent) {
 			const note = await renderNote(user, post);
 			const content = renderCreate(note);
 			content['@context'] = context;

From d2abfa5f2ec413ae096b2c74d78532e6c2a1a1e5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 6 Apr 2018 20:52:38 +0900
Subject: [PATCH 1124/1250] Resolve local Person ID

---
 src/remote/activitypub/resolve-person.ts | 13 +++++++++++--
 1 file changed, 11 insertions(+), 2 deletions(-)

diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts
index 907f19834..9cca50e41 100644
--- a/src/remote/activitypub/resolve-person.ts
+++ b/src/remote/activitypub/resolve-person.ts
@@ -1,11 +1,20 @@
 import { JSDOM } from 'jsdom';
 import { toUnicode } from 'punycode';
+import parseAcct from '../../acct/parse';
+import config from '../../config';
 import User, { validateUsername, isValidName, isValidDescription } from '../../models/user';
 import webFinger from '../webfinger';
 import Resolver from './resolver';
 import uploadFromUrl from '../../services/drive/upload-from-url';
 
 export default async (value, verifier?: string) => {
+	const id = value.id || value;
+	const localPrefix = config.url + '/@';
+
+	if (id.startsWith(localPrefix)) {
+		return User.findOne(parseAcct(id.slice(localPrefix)));
+	}
+
 	const resolver = new Resolver();
 
 	const object = await resolver.resolve(value) as any;
@@ -21,7 +30,7 @@ export default async (value, verifier?: string) => {
 		throw new Error('invalid person');
 	}
 
-	const finger = await webFinger(object.id, verifier);
+	const finger = await webFinger(id, verifier);
 
 	const host = toUnicode(finger.subject.replace(/^.*?@/, ''));
 	const hostLower = host.replace(/[A-Z]+/, matched => matched.toLowerCase());
@@ -48,7 +57,7 @@ export default async (value, verifier?: string) => {
 				publicKeyPem: object.publicKey.publicKeyPem
 			},
 			inbox: object.inbox,
-			uri: object.id,
+			uri: id,
 		},
 	});
 

From 24cfde31ab2b576f68f1c1d16278cb4cd13a497d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 6 Apr 2018 22:00:37 +0900
Subject: [PATCH 1125/1250] Add todo

---
 src/remote/activitypub/act/create/note.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/remote/activitypub/act/create/note.ts b/src/remote/activitypub/act/create/note.ts
index d50042e16..8b22f1b47 100644
--- a/src/remote/activitypub/act/create/note.ts
+++ b/src/remote/activitypub/act/create/note.ts
@@ -40,6 +40,7 @@ export default async function createNote(resolver: Resolver, actor: IRemoteUser,
 			const inReplyTo = await resolver.resolve(note.inReplyTo) as any;
 			const actor = await resolvePerson(inReplyTo.attributedTo);
 			if (isRemoteUser(actor)) {
+				// TODO: silentを常にtrueにしてはならない
 				reply = await createNote(resolver, actor, inReplyTo);
 			}
 		}

From 88e11c7b0cb1c54f6f7c84462379a8438835f984 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 6 Apr 2018 22:15:31 +0900
Subject: [PATCH 1126/1250] Fix bug

---
 src/models/user.ts                       | 2 +-
 src/remote/activitypub/resolve-person.ts | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/models/user.ts b/src/models/user.ts
index f817c33aa..7c1ee498d 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -100,7 +100,7 @@ export function validatePassword(password: string): boolean {
 }
 
 export function isValidName(name: string): boolean {
-	return typeof name == 'string' && name.length < 30 && name.trim() != '';
+	return name === null || (typeof name == 'string' && name.length < 30 && name.trim() != '');
 }
 
 export function isValidDescription(description: string): boolean {
diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts
index 9cca50e41..39887ef77 100644
--- a/src/remote/activitypub/resolve-person.ts
+++ b/src/remote/activitypub/resolve-person.ts
@@ -24,7 +24,7 @@ export default async (value, verifier?: string) => {
 		object.type !== 'Person' ||
 		typeof object.preferredUsername !== 'string' ||
 		!validateUsername(object.preferredUsername) ||
-		(object.name != '' && !isValidName(object.name)) ||
+		!isValidName(object.name == '' ? null : object.name) ||
 		!isValidDescription(object.summary)
 	) {
 		throw new Error('invalid person');

From bcad5524964b188e41cd4a15ea4df28e43c625d7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 6 Apr 2018 22:40:06 +0900
Subject: [PATCH 1127/1250] Log

---
 src/queue/processors/http/process-inbox.ts | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts
index 4666e7f37..eb4b62d37 100644
--- a/src/queue/processors/http/process-inbox.ts
+++ b/src/queue/processors/http/process-inbox.ts
@@ -1,4 +1,5 @@
 import * as kue from 'kue';
+import * as debug from 'debug';
 
 import { verifySignature } from 'http-signature';
 import parseAcct from '../../../acct/parse';
@@ -6,11 +7,20 @@ import User, { IRemoteUser } from '../../../models/user';
 import act from '../../../remote/activitypub/act';
 import resolvePerson from '../../../remote/activitypub/resolve-person';
 
+const log = debug('misskey:queue:inbox');
+
 // ユーザーのinboxにアクティビティが届いた時の処理
 export default async (job: kue.Job, done): Promise<void> => {
 	const signature = job.data.signature;
 	const activity = job.data.activity;
 
+	//#region Log
+	const info = Object.assign({}, activity);
+	delete info['@context'];
+	delete info['signature'];
+	log(info);
+	//#endregion
+
 	const keyIdLower = signature.keyId.toLowerCase();
 	let user;
 

From 716f3a450508e2830a3856a860dba07a547b6195 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 6 Apr 2018 23:32:38 +0900
Subject: [PATCH 1128/1250] Add index

---
 src/models/post-watching.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/models/post-watching.ts b/src/models/post-watching.ts
index b4ddcaafa..032b9d10f 100644
--- a/src/models/post-watching.ts
+++ b/src/models/post-watching.ts
@@ -2,6 +2,7 @@ import * as mongo from 'mongodb';
 import db from '../db/mongodb';
 
 const PostWatching = db.get<IPostWatching>('postWatching');
+PostWatching.createIndex(['userId', 'postId'], { unique: true });
 export default PostWatching;
 
 export interface IPostWatching {

From 2057fdde4a33dc9664112571e5f0e779e7fdeaaf Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Apr 2018 00:07:30 +0900
Subject: [PATCH 1129/1250] Add todo

---
 src/remote/activitypub/act/index.ts | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/src/remote/activitypub/act/index.ts b/src/remote/activitypub/act/index.ts
index c1d64b7c7..5be07c478 100644
--- a/src/remote/activitypub/act/index.ts
+++ b/src/remote/activitypub/act/index.ts
@@ -5,7 +5,7 @@ import undo from './undo';
 import { IObject } from '../type';
 import { IRemoteUser } from '../../../models/user';
 
-export default async (actor: IRemoteUser, activity: IObject): Promise<void> => {
+const self = async (actor: IRemoteUser, activity: IObject): Promise<void> => {
 	switch (activity.type) {
 	case 'Create':
 		await create(actor, activity);
@@ -27,8 +27,15 @@ export default async (actor: IRemoteUser, activity: IObject): Promise<void> => {
 		await undo(actor, activity);
 		break;
 
+	case 'Collection':
+	case 'OrderedCollection':
+		// TODO
+		break;
+
 	default:
 		console.warn(`unknown activity type: ${activity.type}`);
 		return null;
 	}
 };
+
+export default self;

From ceaea43b4e482d1f9c3248f4f361c60b78be04ca Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Fri, 6 Apr 2018 19:56:16 +0000
Subject: [PATCH 1130/1250] fix(package): update @types/mongodb to version
 3.0.10

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 38d96117c..2300b43b4 100644
--- a/package.json
+++ b/package.json
@@ -60,7 +60,7 @@
 		"@types/license-checker": "15.0.0",
 		"@types/mkdirp": "0.5.2",
 		"@types/mocha": "5.0.0",
-		"@types/mongodb": "3.0.9",
+		"@types/mongodb": "3.0.10",
 		"@types/monk": "6.0.0",
 		"@types/morgan": "1.7.35",
 		"@types/ms": "0.7.30",

From efb172bce8044fad5cee946071312362b35028bd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Apr 2018 06:13:40 +0900
Subject: [PATCH 1131/1250] Bug fixes and some refactors

---
 src/remote/activitypub/act/create/index.ts |  2 --
 src/remote/activitypub/act/create/note.ts  | 40 ++++++++++++++++------
 src/remote/activitypub/renderer/note.ts    |  4 +--
 3 files changed, 31 insertions(+), 15 deletions(-)

diff --git a/src/remote/activitypub/act/create/index.ts b/src/remote/activitypub/act/create/index.ts
index d210aa4c5..7ab4c2aba 100644
--- a/src/remote/activitypub/act/create/index.ts
+++ b/src/remote/activitypub/act/create/index.ts
@@ -16,8 +16,6 @@ export default async (actor: IRemoteUser, activity): Promise<void> => {
 
 	log(`Create: ${uri}`);
 
-	// TODO: 同じURIをもつものが既に登録されていないかチェック
-
 	const resolver = new Resolver();
 
 	let object;
diff --git a/src/remote/activitypub/act/create/note.ts b/src/remote/activitypub/act/create/note.ts
index 8b22f1b47..253478b6f 100644
--- a/src/remote/activitypub/act/create/note.ts
+++ b/src/remote/activitypub/act/create/note.ts
@@ -4,23 +4,31 @@ import * as debug from 'debug';
 import Resolver from '../../resolver';
 import Post, { IPost } from '../../../../models/post';
 import createPost from '../../../../services/post/create';
-import { IRemoteUser, isRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/user';
 import resolvePerson from '../../resolve-person';
 import createImage from './image';
+import config from '../../../../config';
 
 const log = debug('misskey:activitypub');
 
+/**
+ * 投稿作成アクティビティを捌きます
+ */
 export default async function createNote(resolver: Resolver, actor: IRemoteUser, note, silent = false): Promise<IPost> {
-	if (
-		('attributedTo' in note && actor.account.uri !== note.attributedTo) ||
-		typeof note.id !== 'string'
-	) {
+	if (typeof note.id !== 'string') {
 		log(`invalid note: ${JSON.stringify(note, null, 2)}`);
 		throw new Error('invalid note');
 	}
 
+	// 既に同じURIを持つものが登録されていないかチェックし、登録されていたらそれを返す
+	const exist = await Post.findOne({ uri: note.id });
+	if (exist) {
+		return exist;
+	}
+
 	log(`Creating the Note: ${note.id}`);
 
+	//#region 添付メディア
 	const media = [];
 	if ('attachment' in note && note.attachment != null) {
 		// TODO: attachmentは必ずしもImageではない
@@ -30,21 +38,31 @@ export default async function createNote(resolver: Resolver, actor: IRemoteUser,
 			media.push(created);
 		});
 	}
+	//#endregion
 
+	//#region リプライ
 	let reply = null;
 	if ('inReplyTo' in note && note.inReplyTo != null) {
-		const inReplyToPost = await Post.findOne({ uri: note.inReplyTo.id || note.inReplyTo });
+		// リプライ先の投稿がMisskeyに登録されているか調べる
+		const uri: string = note.inReplyTo.id || note.inReplyTo;
+		const inReplyToPost = uri.startsWith(config.url + '/')
+			? await Post.findOne({ _id: uri.split('/').pop() })
+			: await Post.findOne({ uri });
+
 		if (inReplyToPost) {
 			reply = inReplyToPost;
 		} else {
+			// 無かったらフェッチ
 			const inReplyTo = await resolver.resolve(note.inReplyTo) as any;
-			const actor = await resolvePerson(inReplyTo.attributedTo);
-			if (isRemoteUser(actor)) {
-				// TODO: silentを常にtrueにしてはならない
-				reply = await createNote(resolver, actor, inReplyTo);
-			}
+
+			// リプライ先の投稿の投稿者をフェッチ
+			const actor = await resolvePerson(inReplyTo.attributedTo) as IRemoteUser;
+
+			// TODO: silentを常にtrueにしてはならない
+			reply = await createNote(resolver, actor, inReplyTo);
 		}
 	}
+	//#endregion
 
 	const { window } = new JSDOM(note.content);
 
diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts
index e45b10215..b971a5395 100644
--- a/src/remote/activitypub/renderer/note.ts
+++ b/src/remote/activitypub/renderer/note.ts
@@ -19,11 +19,11 @@ export default async (user: IUser, post: IPost) => {
 
 		if (inReplyToPost !== null) {
 			const inReplyToUser = await User.findOne({
-				_id: post.userId,
+				_id: inReplyToPost.userId,
 			});
 
 			if (inReplyToUser !== null) {
-				inReplyTo = `${config.url}@${inReplyToUser.username}/${inReplyToPost._id}`;
+				inReplyTo = inReplyToPost.uri || `${config.url}/@${inReplyToUser.username}/${inReplyToPost._id}`;
 			}
 		}
 	} else {

From 7b885d8368c5ed0afb0a0faf27841a925cae38d2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Apr 2018 06:22:03 +0900
Subject: [PATCH 1132/1250] Fix bug

---
 src/queue/processors/http/deliver.ts | 13 +++++++++----
 1 file changed, 9 insertions(+), 4 deletions(-)

diff --git a/src/queue/processors/http/deliver.ts b/src/queue/processors/http/deliver.ts
index da7e8bc36..f5d162fd0 100644
--- a/src/queue/processors/http/deliver.ts
+++ b/src/queue/processors/http/deliver.ts
@@ -6,9 +6,14 @@ export default async (job: kue.Job, done): Promise<void> => {
 	try {
 		await request(job.data.user, job.data.to, job.data.content);
 		done();
-	} catch (e) {
-		console.warn(`deliver failed: ${e}`);
-
-		done(e);
+	} catch (res) {
+		if (res.statusCode >= 300 && res.statusCode < 400) {
+			// HTTPステータスコード4xxはクライアントエラーであり、それはつまり
+			// 何回再送しても成功することはないということなのでエラーにはしないでおく
+			done();
+		} else {
+			console.warn(`deliver failed: ${res.statusMessage}`);
+			done(new Error(res.statusMessage));
+		}
 	}
 };

From d5f013193b6238b1e2c8d8943092a11fab87430f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Apr 2018 06:23:38 +0900
Subject: [PATCH 1133/1250] Fix bug

---
 src/queue/processors/http/deliver.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/queue/processors/http/deliver.ts b/src/queue/processors/http/deliver.ts
index f5d162fd0..422e355b5 100644
--- a/src/queue/processors/http/deliver.ts
+++ b/src/queue/processors/http/deliver.ts
@@ -7,7 +7,7 @@ export default async (job: kue.Job, done): Promise<void> => {
 		await request(job.data.user, job.data.to, job.data.content);
 		done();
 	} catch (res) {
-		if (res.statusCode >= 300 && res.statusCode < 400) {
+		if (res.statusCode >= 400 && res.statusCode < 500) {
 			// HTTPステータスコード4xxはクライアントエラーであり、それはつまり
 			// 何回再送しても成功することはないということなのでエラーにはしないでおく
 			done();

From dc328301bd5d91fe572d46291200688f8acac393 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Apr 2018 06:25:58 +0900
Subject: [PATCH 1134/1250] Increase limit to avoid warning

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

diff --git a/src/index.ts b/src/index.ts
index f45bcaa6a..68b289793 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -35,7 +35,7 @@ if (process.env.NODE_ENV != 'production') {
 }
 
 // https://github.com/Automattic/kue/issues/822
-require('events').EventEmitter.prototype._maxListeners = 256;
+require('events').EventEmitter.prototype._maxListeners = 512;
 
 // Start app
 main();

From 42f9621361954d6e093b19d4888aabe8c86aa699 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Fri, 6 Apr 2018 21:27:37 +0000
Subject: [PATCH 1135/1250] fix(package): update vue-loader to version
 15.0.0-rc.1

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 38d96117c..daa146fbe 100644
--- a/package.json
+++ b/package.json
@@ -203,7 +203,7 @@
 		"vue-cropperjs": "2.2.0",
 		"vue-js-modal": "1.3.12",
 		"vue-json-tree-view": "2.1.3",
-		"vue-loader": "14.2.2",
+		"vue-loader": "15.0.0-rc.1",
 		"vue-router": "3.0.1",
 		"vue-template-compiler": "2.5.16",
 		"vuedraggable": "2.16.0",

From 3e9607037163e3f8815fdb577a6cb6093d933624 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Apr 2018 06:44:29 +0900
Subject: [PATCH 1136/1250] Fix bug

---
 src/remote/activitypub/act/delete/index.ts | 23 +++++++++++++++++++---
 src/remote/activitypub/act/delete/note.ts  | 10 ++++++++--
 2 files changed, 28 insertions(+), 5 deletions(-)

diff --git a/src/remote/activitypub/act/delete/index.ts b/src/remote/activitypub/act/delete/index.ts
index 764814bac..42272433d 100644
--- a/src/remote/activitypub/act/delete/index.ts
+++ b/src/remote/activitypub/act/delete/index.ts
@@ -1,18 +1,35 @@
 import Resolver from '../../resolver';
 import deleteNote from './note';
+import Post from '../../../../models/post';
 
+/**
+ * 削除アクティビティを捌きます
+ */
 export default async (actor, activity): Promise<void> => {
 	if ('actor' in activity && actor.account.uri !== activity.actor) {
-		throw new Error();
+		throw new Error('invalid actor');
 	}
 
 	const resolver = new Resolver();
 
-	const object = await resolver.resolve(activity);
+	const object = await resolver.resolve(activity.object);
+
+	const uri = (object as any).id;
 
 	switch (object.type) {
 	case 'Note':
-		deleteNote(object);
+		deleteNote(uri);
+		break;
+
+	case 'Tombstone':
+		const post = await Post.findOne({ uri });
+		if (post != null) {
+			deleteNote(uri);
+		}
+		break;
+
+	default:
+		console.warn(`Unknown type: ${object.type}`);
 		break;
 	}
 };
diff --git a/src/remote/activitypub/act/delete/note.ts b/src/remote/activitypub/act/delete/note.ts
index 3b821f87c..75534250e 100644
--- a/src/remote/activitypub/act/delete/note.ts
+++ b/src/remote/activitypub/act/delete/note.ts
@@ -1,8 +1,14 @@
+import * as debug from 'debug';
+
 import Post from '../../../../models/post';
 import { createDb } from '../../../../queue';
 
-export default async function(note) {
-	const post = await Post.findOneAndDelete({ uri: note.id });
+const log = debug('misskey:activitypub');
+
+export default async function(uri: string) {
+	log(`Deleting the Note: ${uri}`);
+
+	const post = await Post.findOneAndDelete({ uri });
 
 	createDb({
 		type: 'deletePostDependents',

From e1649f21a9e4cbdbb4b8d1c1f63ac4074beb0e35 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Apr 2018 06:51:35 +0900
Subject: [PATCH 1137/1250] Fix bug

---
 src/remote/activitypub/act/delete/index.ts |  4 ++--
 src/remote/activitypub/act/delete/note.ts  | 14 ++++++++++++--
 2 files changed, 14 insertions(+), 4 deletions(-)

diff --git a/src/remote/activitypub/act/delete/index.ts b/src/remote/activitypub/act/delete/index.ts
index 42272433d..8163ffc32 100644
--- a/src/remote/activitypub/act/delete/index.ts
+++ b/src/remote/activitypub/act/delete/index.ts
@@ -18,13 +18,13 @@ export default async (actor, activity): Promise<void> => {
 
 	switch (object.type) {
 	case 'Note':
-		deleteNote(uri);
+		deleteNote(actor, uri);
 		break;
 
 	case 'Tombstone':
 		const post = await Post.findOne({ uri });
 		if (post != null) {
-			deleteNote(uri);
+			deleteNote(actor, uri);
 		}
 		break;
 
diff --git a/src/remote/activitypub/act/delete/note.ts b/src/remote/activitypub/act/delete/note.ts
index 75534250e..5306b705e 100644
--- a/src/remote/activitypub/act/delete/note.ts
+++ b/src/remote/activitypub/act/delete/note.ts
@@ -5,10 +5,20 @@ import { createDb } from '../../../../queue';
 
 const log = debug('misskey:activitypub');
 
-export default async function(uri: string) {
+export default async function(actor, uri: string) {
 	log(`Deleting the Note: ${uri}`);
 
-	const post = await Post.findOneAndDelete({ uri });
+	const post = await Post.findOne({ uri });
+
+	if (post == null) {
+		throw new Error('post not found');
+	}
+
+	if (post.userId !== actor._id) {
+		throw new Error('投稿を削除しようとしているユーザーは投稿の作成者ではありません');
+	}
+
+	Post.remove({ _id: post._id });
 
 	createDb({
 		type: 'deletePostDependents',

From ee85f38eba38598c8a8899f6f358a0365b8369a1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Apr 2018 06:58:39 +0900
Subject: [PATCH 1138/1250] Fix bug

---
 src/server/web/index.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/server/web/index.ts b/src/server/web/index.ts
index 1445d1aef..5b1b6409b 100644
--- a/src/server/web/index.ts
+++ b/src/server/web/index.ts
@@ -11,7 +11,7 @@ import * as bodyParser from 'body-parser';
 import * as favicon from 'serve-favicon';
 import * as compression from 'compression';
 
-const client = `${__dirname}/../../client/`;
+const client = path.resolve(`${__dirname}/../../client/`);
 
 // Create server
 const app = express();

From 3f85ae0fbf6416f7414381e32bc933d8ab3f5ee0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Apr 2018 06:58:53 +0900
Subject: [PATCH 1139/1250] Refactor

---
 src/remote/activitypub/act/delete/index.ts | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/remote/activitypub/act/delete/index.ts b/src/remote/activitypub/act/delete/index.ts
index 8163ffc32..e34577b31 100644
--- a/src/remote/activitypub/act/delete/index.ts
+++ b/src/remote/activitypub/act/delete/index.ts
@@ -1,11 +1,12 @@
 import Resolver from '../../resolver';
 import deleteNote from './note';
 import Post from '../../../../models/post';
+import { IRemoteUser } from '../../../../models/user';
 
 /**
  * 削除アクティビティを捌きます
  */
-export default async (actor, activity): Promise<void> => {
+export default async (actor: IRemoteUser, activity): Promise<void> => {
 	if ('actor' in activity && actor.account.uri !== activity.actor) {
 		throw new Error('invalid actor');
 	}

From 2311f3017e75c5a15c67b484bb5c4ecadc4fb2c5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Apr 2018 06:59:20 +0900
Subject: [PATCH 1140/1250] Fix bug

---
 src/remote/activitypub/act/delete/note.ts | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/remote/activitypub/act/delete/note.ts b/src/remote/activitypub/act/delete/note.ts
index 5306b705e..ff9a8ee5f 100644
--- a/src/remote/activitypub/act/delete/note.ts
+++ b/src/remote/activitypub/act/delete/note.ts
@@ -2,10 +2,11 @@ import * as debug from 'debug';
 
 import Post from '../../../../models/post';
 import { createDb } from '../../../../queue';
+import { IRemoteUser } from '../../../../models/user';
 
 const log = debug('misskey:activitypub');
 
-export default async function(actor, uri: string) {
+export default async function(actor: IRemoteUser, uri: string): Promise<void> {
 	log(`Deleting the Note: ${uri}`);
 
 	const post = await Post.findOne({ uri });
@@ -14,7 +15,7 @@ export default async function(actor, uri: string) {
 		throw new Error('post not found');
 	}
 
-	if (post.userId !== actor._id) {
+	if (!post.userId.equals(actor._id)) {
 		throw new Error('投稿を削除しようとしているユーザーは投稿の作成者ではありません');
 	}
 

From c2cced0a4519ebd5afd1debd5f1ea1217c8fe009 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Apr 2018 07:19:30 +0900
Subject: [PATCH 1141/1250] =?UTF-8?q?=E6=8A=95=E7=A8=BF=E3=81=AB=E9=96=A2?=
 =?UTF-8?q?=E3=81=97=E3=81=A6=E3=81=AF=E8=AB=96=E7=90=86=E5=89=8A=E9=99=A4?=
 =?UTF-8?q?=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

処理をシンプルにするため
---
 src/models/post.ts                            |  1 +
 src/queue/index.ts                            |  7 ------
 .../processors/db/delete-post-dependents.ts   | 22 -------------------
 src/queue/processors/db/index.ts              |  7 ------
 src/remote/activitypub/act/delete/note.ts     | 16 ++++++++------
 5 files changed, 10 insertions(+), 43 deletions(-)
 delete mode 100644 src/queue/processors/db/delete-post-dependents.ts
 delete mode 100644 src/queue/processors/db/index.ts

diff --git a/src/models/post.ts b/src/models/post.ts
index 68a638fa2..ac7890d2e 100644
--- a/src/models/post.ts
+++ b/src/models/post.ts
@@ -27,6 +27,7 @@ export type IPost = {
 	_id: mongo.ObjectID;
 	channelId: mongo.ObjectID;
 	createdAt: Date;
+	deletedAt: Date;
 	mediaIds: mongo.ObjectID[];
 	replyId: mongo.ObjectID;
 	repostId: mongo.ObjectID;
diff --git a/src/queue/index.ts b/src/queue/index.ts
index 691223de2..4aa1dc032 100644
--- a/src/queue/index.ts
+++ b/src/queue/index.ts
@@ -1,7 +1,6 @@
 import { createQueue } from 'kue';
 
 import config from '../config';
-import db from './processors/db';
 import http from './processors/http';
 
 const queue = createQueue({
@@ -19,10 +18,6 @@ export function createHttp(data) {
 		.backoff({ delay: 16384, type: 'exponential' });
 }
 
-export function createDb(data) {
-	return queue.create('db', data);
-}
-
 export function deliver(user, content, to) {
 	return createHttp({
 		type: 'deliver',
@@ -33,8 +28,6 @@ export function deliver(user, content, to) {
 }
 
 export default function() {
-	queue.process('db', db);
-
 	/*
 		256 is the default concurrency limit of Mozilla Firefox and Google
 		Chromium.
diff --git a/src/queue/processors/db/delete-post-dependents.ts b/src/queue/processors/db/delete-post-dependents.ts
deleted file mode 100644
index 6de21eb05..000000000
--- a/src/queue/processors/db/delete-post-dependents.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import Favorite from '../../../models/favorite';
-import Notification from '../../../models/notification';
-import PollVote from '../../../models/poll-vote';
-import PostReaction from '../../../models/post-reaction';
-import PostWatching from '../../../models/post-watching';
-import Post from '../../../models/post';
-
-export default async ({ data }) => Promise.all([
-	Favorite.remove({ postId: data._id }),
-	Notification.remove({ postId: data._id }),
-	PollVote.remove({ postId: data._id }),
-	PostReaction.remove({ postId: data._id }),
-	PostWatching.remove({ postId: data._id }),
-	Post.find({ repostId: data._id }).then(reposts => Promise.all([
-		Notification.remove({
-			postId: {
-				$in: reposts.map(({ _id }) => _id)
-			}
-		}),
-		Post.remove({ repostId: data._id })
-	]))
-]);
diff --git a/src/queue/processors/db/index.ts b/src/queue/processors/db/index.ts
deleted file mode 100644
index 75838c099..000000000
--- a/src/queue/processors/db/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import deletePostDependents from './delete-post-dependents';
-
-const handlers = {
-  deletePostDependents
-};
-
-export default (job, done) => handlers[job.data.type](job).then(() => done(), done);
diff --git a/src/remote/activitypub/act/delete/note.ts b/src/remote/activitypub/act/delete/note.ts
index ff9a8ee5f..8e9447b48 100644
--- a/src/remote/activitypub/act/delete/note.ts
+++ b/src/remote/activitypub/act/delete/note.ts
@@ -1,7 +1,6 @@
 import * as debug from 'debug';
 
 import Post from '../../../../models/post';
-import { createDb } from '../../../../queue';
 import { IRemoteUser } from '../../../../models/user';
 
 const log = debug('misskey:activitypub');
@@ -19,10 +18,13 @@ export default async function(actor: IRemoteUser, uri: string): Promise<void> {
 		throw new Error('投稿を削除しようとしているユーザーは投稿の作成者ではありません');
 	}
 
-	Post.remove({ _id: post._id });
-
-	createDb({
-		type: 'deletePostDependents',
-		id: post._id
-	}).delay(65536).save();
+	Post.update({ _id: post._id }, {
+		$set: {
+			deletedAt: new Date(),
+			text: null,
+			textHtml: null,
+			mediaIds: [],
+			poll: null
+		}
+	});
 }

From 32516b1d8e2db3948d67fb452a01888da0e81804 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Apr 2018 07:27:18 +0900
Subject: [PATCH 1142/1250] Visibility support

---
 src/remote/activitypub/act/create/note.ts | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/remote/activitypub/act/create/note.ts b/src/remote/activitypub/act/create/note.ts
index 253478b6f..88e3a875a 100644
--- a/src/remote/activitypub/act/create/note.ts
+++ b/src/remote/activitypub/act/create/note.ts
@@ -28,6 +28,11 @@ export default async function createNote(resolver: Resolver, actor: IRemoteUser,
 
 	log(`Creating the Note: ${note.id}`);
 
+	//#region Visibility
+	let visibility = 'public';
+	if (note.cc.length == 0) visibility = 'private';
+	//#endergion
+
 	//#region 添付メディア
 	const media = [];
 	if ('attachment' in note && note.attachment != null) {
@@ -74,6 +79,7 @@ export default async function createNote(resolver: Resolver, actor: IRemoteUser,
 		text: window.document.body.textContent,
 		viaMobile: false,
 		geo: undefined,
+		visibility,
 		uri: note.id
 	});
 }

From ad4378bda88750a05b7f27ece99a10e07d72de30 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Apr 2018 12:02:25 +0900
Subject: [PATCH 1143/1250] Support unlisted visibility type

---
 src/remote/activitypub/act/create/note.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/remote/activitypub/act/create/note.ts b/src/remote/activitypub/act/create/note.ts
index 88e3a875a..df9f1d69e 100644
--- a/src/remote/activitypub/act/create/note.ts
+++ b/src/remote/activitypub/act/create/note.ts
@@ -30,6 +30,7 @@ export default async function createNote(resolver: Resolver, actor: IRemoteUser,
 
 	//#region Visibility
 	let visibility = 'public';
+	if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) visibility = 'unlisted';
 	if (note.cc.length == 0) visibility = 'private';
 	//#endergion
 

From 20ae8012cfb64bb7fac477602139ec17fbe652dc Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Apr 2018 12:05:15 +0900
Subject: [PATCH 1144/1250] Ignore post that not public

---
 src/remote/activitypub/act/create/note.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/remote/activitypub/act/create/note.ts b/src/remote/activitypub/act/create/note.ts
index df9f1d69e..c40facea4 100644
--- a/src/remote/activitypub/act/create/note.ts
+++ b/src/remote/activitypub/act/create/note.ts
@@ -32,6 +32,8 @@ export default async function createNote(resolver: Resolver, actor: IRemoteUser,
 	let visibility = 'public';
 	if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) visibility = 'unlisted';
 	if (note.cc.length == 0) visibility = 'private';
+	// TODO
+	if (visibility != 'public') throw new Error('unspported visibility');
 	//#endergion
 
 	//#region 添付メディア

From ac660e3170561f43018d47ac4f77cc3f3076ef86 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Apr 2018 13:05:10 +0900
Subject: [PATCH 1145/1250] oops

---
 src/remote/resolve-user.ts | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/remote/resolve-user.ts b/src/remote/resolve-user.ts
index edb4c7860..9e1ae5195 100644
--- a/src/remote/resolve-user.ts
+++ b/src/remote/resolve-user.ts
@@ -1,7 +1,6 @@
 import { toUnicode, toASCII } from 'punycode';
 import User from '../models/user';
 import resolvePerson from './activitypub/resolve-person';
-import Resolver from './activitypub/resolver';
 import webFinger from './webfinger';
 
 export default async (username, host, option) => {
@@ -20,7 +19,7 @@ export default async (username, host, option) => {
 			throw new Error('self link not found');
 		}
 
-		user = await resolvePerson(new Resolver(), self.href, acctLower);
+		user = await resolvePerson(self.href, acctLower);
 	}
 
 	return user;

From 5a7e706cabf533a86807f5c16a74b9bc35c8f5bf Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Apr 2018 15:39:40 +0900
Subject: [PATCH 1146/1250] =?UTF-8?q?=E5=90=84=E7=A8=AE=E3=82=AB=E3=82=A6?=
 =?UTF-8?q?=E3=83=B3=E3=83=88=E3=82=92=E5=BE=A9=E6=B4=BB=E3=81=95=E3=81=9B?=
 =?UTF-8?q?=E3=81=9F=E3=82=8A=E3=81=AA=E3=81=A9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/remote/activitypub/resolve-person.ts | 39 +++++++++++++++---------
 src/remote/activitypub/type.ts           | 33 ++++++++++++++++++--
 2 files changed, 56 insertions(+), 16 deletions(-)

diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts
index 39887ef77..b3bac3cd3 100644
--- a/src/remote/activitypub/resolve-person.ts
+++ b/src/remote/activitypub/resolve-person.ts
@@ -6,6 +6,7 @@ import User, { validateUsername, isValidName, isValidDescription } from '../../m
 import webFinger from '../webfinger';
 import Resolver from './resolver';
 import uploadFromUrl from '../../services/drive/upload-from-url';
+import { isCollectionOrOrderedCollection } from './type';
 
 export default async (value, verifier?: string) => {
 	const id = value.id || value;
@@ -30,7 +31,21 @@ export default async (value, verifier?: string) => {
 		throw new Error('invalid person');
 	}
 
-	const finger = await webFinger(id, verifier);
+	const [followersCount = 0, followingCount = 0, postsCount = 0, finger] = await Promise.all([
+		resolver.resolve(object.followers).then(
+			resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
+			() => undefined
+		),
+		resolver.resolve(object.following).then(
+			resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
+			() => undefined
+		),
+		resolver.resolve(object.outbox).then(
+			resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
+			() => undefined
+		),
+		webFinger(id, verifier)
+	]);
 
 	const host = toUnicode(finger.subject.replace(/^.*?@/, ''));
 	const hostLower = host.replace(/[A-Z]+/, matched => matched.toLowerCase());
@@ -42,10 +57,10 @@ export default async (value, verifier?: string) => {
 		bannerId: null,
 		createdAt: Date.parse(object.published) || null,
 		description: summaryDOM.textContent,
-		followersCount: 0,
-		followingCount: 0,
+		followersCount,
+		followingCount,
+		postsCount,
 		name: object.name,
-		postsCount: 0,
 		driveCapacity: 1024 * 1024 * 8, // 8MiB
 		username: object.preferredUsername,
 		usernameLower: object.preferredUsername.toLowerCase(),
@@ -61,18 +76,14 @@ export default async (value, verifier?: string) => {
 		},
 	});
 
-	const [avatarId, bannerId] = await Promise.all([
+	const [avatarId, bannerId] = (await Promise.all([
 		object.icon,
 		object.image
-	].map(async img => {
-		if (img === undefined) {
-			return null;
-		}
-
-		const file = await uploadFromUrl(img.url, user);
-
-		return file._id;
-	}));
+	].map(img =>
+		img == null
+			? Promise.resolve(null)
+			: uploadFromUrl(img.url, user)
+	))).map(file => file != null ? file._id : null);
 
 	User.update({ _id: user._id }, { $set: { avatarId, bannerId } });
 
diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts
index 94e2c350a..cd7f40630 100644
--- a/src/remote/activitypub/type.ts
+++ b/src/remote/activitypub/type.ts
@@ -1,3 +1,32 @@
-export type IObject = {
+export type Object = { [x: string]: any };
+
+export type ActivityType =
+	'Create';
+
+export interface IObject {
+	'@context': string | object | any[];
 	type: string;
-};
+	id?: string;
+	summary?: string;
+}
+
+export interface ICollection extends IObject {
+	type: 'Collection';
+	totalItems: number;
+	items: IObject | string | IObject[] | string[];
+}
+
+export interface IOrderedCollection extends IObject {
+	type: 'OrderedCollection';
+	totalItems: number;
+	orderedItems: IObject | string | IObject[] | string[];
+}
+
+export const isCollection = (object: IObject): object is ICollection =>
+	object.type === 'Collection';
+
+export const isOrderedCollection = (object: IObject): object is IOrderedCollection =>
+	object.type === 'OrderedCollection';
+
+export const isCollectionOrOrderedCollection = (object: IObject): object is ICollection | IOrderedCollection =>
+	isCollection(object) || isOrderedCollection(object);

From eaffa118b4eeb922bbee566248d4d230f0ffdbeb Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Apr 2018 15:45:03 +0900
Subject: [PATCH 1147/1250] oops

---
 src/queue/processors/http/index.ts | 25 ++++++++++++++-----------
 1 file changed, 14 insertions(+), 11 deletions(-)

diff --git a/src/queue/processors/http/index.ts b/src/queue/processors/http/index.ts
index 0ea79305c..3dc259537 100644
--- a/src/queue/processors/http/index.ts
+++ b/src/queue/processors/http/index.ts
@@ -1,17 +1,20 @@
-import deliverPost from './deliver-post';
-import follow from './follow';
-import performActivityPub from './perform-activitypub';
+import deliver from './deliver';
 import processInbox from './process-inbox';
 import reportGitHubFailure from './report-github-failure';
-import unfollow from './unfollow';
 
 const handlers = {
-  deliverPost,
-  follow,
-  performActivityPub,
-  processInbox,
-  reportGitHubFailure,
-  unfollow
+	deliver,
+	processInbox,
+	reportGitHubFailure
 };
 
-export default (job, done) => handlers[job.data.type](job, done);
+export default (job, done) => {
+	const handler = handlers[job.data.type];
+
+	if (handler) {
+		handler(job, done);
+	} else {
+		console.error(`Unknown job: ${job.data.type}`);
+		done();
+	}
+};

From 8e8a9175ad21e4314b489380e512417eb95063f4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Apr 2018 15:52:42 +0900
Subject: [PATCH 1148/1250] Add todo

---
 src/remote/activitypub/act/create/note.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/remote/activitypub/act/create/note.ts b/src/remote/activitypub/act/create/note.ts
index c40facea4..364fddfe0 100644
--- a/src/remote/activitypub/act/create/note.ts
+++ b/src/remote/activitypub/act/create/note.ts
@@ -40,6 +40,7 @@ export default async function createNote(resolver: Resolver, actor: IRemoteUser,
 	const media = [];
 	if ('attachment' in note && note.attachment != null) {
 		// TODO: attachmentは必ずしもImageではない
+		// TODO: attachmentは必ずしも配列ではない
 		// TODO: ループの中でawaitはすべきでない
 		note.attachment.forEach(async media => {
 			const created = await createImage(resolver, note.actor, media);

From 6113f7260cc5ffb3d64981c1fd9ea0a38488c6b9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Apr 2018 15:54:11 +0900
Subject: [PATCH 1149/1250] Refactor

---
 src/remote/activitypub/act/undo/follow.ts     | 25 +++++++++++++++++++
 .../act/{undo.ts => undo/index.ts}            |  2 +-
 2 files changed, 26 insertions(+), 1 deletion(-)
 create mode 100644 src/remote/activitypub/act/undo/follow.ts
 rename src/remote/activitypub/act/{undo.ts => undo/index.ts} (89%)

diff --git a/src/remote/activitypub/act/undo/follow.ts b/src/remote/activitypub/act/undo/follow.ts
new file mode 100644
index 000000000..b1c462a3b
--- /dev/null
+++ b/src/remote/activitypub/act/undo/follow.ts
@@ -0,0 +1,25 @@
+import parseAcct from '../../../../acct/parse';
+import User from '../../../../models/user';
+import config from '../../../../config';
+import unfollow from '../../../../services/following/delete';
+
+export default async (actor, activity): Promise<void> => {
+	const prefix = config.url + '/@';
+	const id = activity.object.id || activity.object;
+
+	if (!id.startsWith(prefix)) {
+		return null;
+	}
+
+	const { username, host } = parseAcct(id.slice(prefix.length));
+	if (host !== null) {
+		throw new Error();
+	}
+
+	const followee = await User.findOne({ username, host });
+	if (followee === null) {
+		throw new Error();
+	}
+
+	await unfollow(actor, followee, activity);
+};
diff --git a/src/remote/activitypub/act/undo.ts b/src/remote/activitypub/act/undo/index.ts
similarity index 89%
rename from src/remote/activitypub/act/undo.ts
rename to src/remote/activitypub/act/undo/index.ts
index 9d9f6b035..ecd9944b4 100644
--- a/src/remote/activitypub/act/undo.ts
+++ b/src/remote/activitypub/act/undo/index.ts
@@ -1,4 +1,4 @@
-import unfollow from './unfollow';
+import unfollow from './follow';
 
 export default async (actor, activity): Promise<void> => {
 	if ('actor' in activity && actor.account.uri !== activity.actor) {

From 7fd7751c08a26a99edea809e521a9bb4895c9c0a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Apr 2018 16:14:35 +0900
Subject: [PATCH 1150/1250] Refactor

---
 src/remote/activitypub/act/create/image.ts |  3 +--
 src/remote/activitypub/act/create/index.ts |  5 ++--
 src/remote/activitypub/act/create/note.ts  |  2 +-
 src/remote/activitypub/act/follow.ts       |  7 ++---
 src/remote/activitypub/act/undo/follow.ts  |  7 ++---
 src/remote/activitypub/act/undo/index.ts   | 30 +++++++++++++++++++---
 src/remote/activitypub/act/unfollow.ts     | 25 ------------------
 src/remote/activitypub/type.ts             | 22 +++++++++++++---
 8 files changed, 58 insertions(+), 43 deletions(-)
 delete mode 100644 src/remote/activitypub/act/unfollow.ts

diff --git a/src/remote/activitypub/act/create/image.ts b/src/remote/activitypub/act/create/image.ts
index cd9e7b4e0..30a75e737 100644
--- a/src/remote/activitypub/act/create/image.ts
+++ b/src/remote/activitypub/act/create/image.ts
@@ -1,13 +1,12 @@
 import * as debug from 'debug';
 
-import Resolver from '../../resolver';
 import uploadFromUrl from '../../../../services/drive/upload-from-url';
 import { IRemoteUser } from '../../../../models/user';
 import { IDriveFile } from '../../../../models/drive-file';
 
 const log = debug('misskey:activitypub');
 
-export default async function(resolver: Resolver, actor: IRemoteUser, image): Promise<IDriveFile> {
+export default async function(actor: IRemoteUser, image): Promise<IDriveFile> {
 	if ('attributedTo' in image && actor.account.uri !== image.attributedTo) {
 		log(`invalid image: ${JSON.stringify(image, null, 2)}`);
 		throw new Error('invalid image');
diff --git a/src/remote/activitypub/act/create/index.ts b/src/remote/activitypub/act/create/index.ts
index 7ab4c2aba..dd0b11214 100644
--- a/src/remote/activitypub/act/create/index.ts
+++ b/src/remote/activitypub/act/create/index.ts
@@ -4,10 +4,11 @@ import Resolver from '../../resolver';
 import { IRemoteUser } from '../../../../models/user';
 import createNote from './note';
 import createImage from './image';
+import { ICreate } from '../../type';
 
 const log = debug('misskey:activitypub');
 
-export default async (actor: IRemoteUser, activity): Promise<void> => {
+export default async (actor: IRemoteUser, activity: ICreate): Promise<void> => {
 	if ('actor' in activity && actor.account.uri !== activity.actor) {
 		throw new Error('invalid actor');
 	}
@@ -29,7 +30,7 @@ export default async (actor: IRemoteUser, activity): Promise<void> => {
 
 	switch (object.type) {
 	case 'Image':
-		createImage(resolver, actor, object);
+		createImage(actor, object);
 		break;
 
 	case 'Note':
diff --git a/src/remote/activitypub/act/create/note.ts b/src/remote/activitypub/act/create/note.ts
index 364fddfe0..82a620703 100644
--- a/src/remote/activitypub/act/create/note.ts
+++ b/src/remote/activitypub/act/create/note.ts
@@ -43,7 +43,7 @@ export default async function createNote(resolver: Resolver, actor: IRemoteUser,
 		// TODO: attachmentは必ずしも配列ではない
 		// TODO: ループの中でawaitはすべきでない
 		note.attachment.forEach(async media => {
-			const created = await createImage(resolver, note.actor, media);
+			const created = await createImage(note.actor, media);
 			media.push(created);
 		});
 	}
diff --git a/src/remote/activitypub/act/follow.ts b/src/remote/activitypub/act/follow.ts
index 4fc423d15..3dd029af5 100644
--- a/src/remote/activitypub/act/follow.ts
+++ b/src/remote/activitypub/act/follow.ts
@@ -1,11 +1,12 @@
 import parseAcct from '../../../acct/parse';
-import User from '../../../models/user';
+import User, { IRemoteUser } from '../../../models/user';
 import config from '../../../config';
 import follow from '../../../services/following/create';
+import { IFollow } from '../type';
 
-export default async (actor, activity): Promise<void> => {
+export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
 	const prefix = config.url + '/@';
-	const id = activity.object.id || activity.object;
+	const id = typeof activity == 'string' ? activity : activity.id;
 
 	if (!id.startsWith(prefix)) {
 		return null;
diff --git a/src/remote/activitypub/act/undo/follow.ts b/src/remote/activitypub/act/undo/follow.ts
index b1c462a3b..fcf27c950 100644
--- a/src/remote/activitypub/act/undo/follow.ts
+++ b/src/remote/activitypub/act/undo/follow.ts
@@ -1,11 +1,12 @@
 import parseAcct from '../../../../acct/parse';
-import User from '../../../../models/user';
+import User, { IRemoteUser } from '../../../../models/user';
 import config from '../../../../config';
 import unfollow from '../../../../services/following/delete';
+import { IFollow } from '../../type';
 
-export default async (actor, activity): Promise<void> => {
+export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
 	const prefix = config.url + '/@';
-	const id = activity.object.id || activity.object;
+	const id = typeof activity == 'string' ? activity : activity.id;
 
 	if (!id.startsWith(prefix)) {
 		return null;
diff --git a/src/remote/activitypub/act/undo/index.ts b/src/remote/activitypub/act/undo/index.ts
index ecd9944b4..3ede9fcfb 100644
--- a/src/remote/activitypub/act/undo/index.ts
+++ b/src/remote/activitypub/act/undo/index.ts
@@ -1,13 +1,35 @@
-import unfollow from './follow';
+import * as debug from 'debug';
 
-export default async (actor, activity): Promise<void> => {
+import { IRemoteUser } from '../../../../models/user';
+import { IUndo } from '../../type';
+import unfollow from './follow';
+import Resolver from '../../resolver';
+
+const log = debug('misskey:activitypub');
+
+export default async (actor: IRemoteUser, activity: IUndo): Promise<void> => {
 	if ('actor' in activity && actor.account.uri !== activity.actor) {
 		throw new Error('invalid actor');
 	}
 
-	switch (activity.object.type) {
+	const uri = activity.id || activity;
+
+	log(`Undo: ${uri}`);
+
+	const resolver = new Resolver();
+
+	let object;
+
+	try {
+		object = await resolver.resolve(activity.object);
+	} catch (e) {
+		log(`Resolution failed: ${e}`);
+		throw e;
+	}
+
+	switch (object.type) {
 		case 'Follow':
-			unfollow(actor, activity.object);
+			unfollow(actor, object);
 			break;
 	}
 
diff --git a/src/remote/activitypub/act/unfollow.ts b/src/remote/activitypub/act/unfollow.ts
deleted file mode 100644
index 66c15e9a9..000000000
--- a/src/remote/activitypub/act/unfollow.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import parseAcct from '../../../acct/parse';
-import User from '../../../models/user';
-import config from '../../../config';
-import unfollow from '../../../services/following/delete';
-
-export default async (actor, activity): Promise<void> => {
-	const prefix = config.url + '/@';
-	const id = activity.object.id || activity.object;
-
-	if (!id.startsWith(prefix)) {
-		return null;
-	}
-
-	const { username, host } = parseAcct(id.slice(prefix.length));
-	if (host !== null) {
-		throw new Error();
-	}
-
-	const followee = await User.findOne({ username, host });
-	if (followee === null) {
-		throw new Error();
-	}
-
-	await unfollow(actor, followee, activity);
-};
diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts
index cd7f40630..9a4b3c75f 100644
--- a/src/remote/activitypub/type.ts
+++ b/src/remote/activitypub/type.ts
@@ -1,8 +1,5 @@
 export type Object = { [x: string]: any };
 
-export type ActivityType =
-	'Create';
-
 export interface IObject {
 	'@context': string | object | any[];
 	type: string;
@@ -10,6 +7,13 @@ export interface IObject {
 	summary?: string;
 }
 
+export interface IActivity extends IObject {
+	//type: 'Activity';
+	actor: IObject | string;
+	object: IObject | string;
+	target?: IObject | string;
+}
+
 export interface ICollection extends IObject {
 	type: 'Collection';
 	totalItems: number;
@@ -30,3 +34,15 @@ export const isOrderedCollection = (object: IObject): object is IOrderedCollecti
 
 export const isCollectionOrOrderedCollection = (object: IObject): object is ICollection | IOrderedCollection =>
 	isCollection(object) || isOrderedCollection(object);
+
+export interface ICreate extends IActivity {
+	type: 'Create';
+}
+
+export interface IUndo extends IActivity {
+	type: 'Undo';
+}
+
+export interface IFollow extends IActivity {
+	type: 'Follow';
+}

From a85c9b8e6fdb8e0c528f21a65fb1d0528ceac9b4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Apr 2018 16:36:40 +0900
Subject: [PATCH 1151/1250] Refactor

---
 src/remote/activitypub/act/index.ts |  4 ++--
 src/remote/activitypub/type.ts      | 21 +++++++++++++++++++--
 2 files changed, 21 insertions(+), 4 deletions(-)

diff --git a/src/remote/activitypub/act/index.ts b/src/remote/activitypub/act/index.ts
index 5be07c478..5fcdb6174 100644
--- a/src/remote/activitypub/act/index.ts
+++ b/src/remote/activitypub/act/index.ts
@@ -2,10 +2,10 @@ import create from './create';
 import performDeleteActivity from './delete';
 import follow from './follow';
 import undo from './undo';
-import { IObject } from '../type';
+import { Object } from '../type';
 import { IRemoteUser } from '../../../models/user';
 
-const self = async (actor: IRemoteUser, activity: IObject): Promise<void> => {
+const self = async (actor: IRemoteUser, activity: Object): Promise<void> => {
 	switch (activity.type) {
 	case 'Create':
 		await create(actor, activity);
diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts
index 9a4b3c75f..c07b806be 100644
--- a/src/remote/activitypub/type.ts
+++ b/src/remote/activitypub/type.ts
@@ -1,7 +1,7 @@
-export type Object = { [x: string]: any };
+export type obj = { [x: string]: any };
 
 export interface IObject {
-	'@context': string | object | any[];
+	'@context': string | obj | obj[];
 	type: string;
 	id?: string;
 	summary?: string;
@@ -39,6 +39,10 @@ export interface ICreate extends IActivity {
 	type: 'Create';
 }
 
+export interface IDelete extends IActivity {
+	type: 'Delete';
+}
+
 export interface IUndo extends IActivity {
 	type: 'Undo';
 }
@@ -46,3 +50,16 @@ export interface IUndo extends IActivity {
 export interface IFollow extends IActivity {
 	type: 'Follow';
 }
+
+export interface IAccept extends IActivity {
+	type: 'Accept';
+}
+
+export type Object =
+	ICollection |
+	IOrderedCollection |
+	ICreate |
+	IDelete |
+	IUndo |
+	IFollow |
+	IAccept;

From f79dc2cbc04114ae68f12abb9851bdbf7b4f9e74 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Apr 2018 17:05:14 +0900
Subject: [PATCH 1152/1250] Implement like

---
 src/remote/activitypub/act/index.ts           | 11 ++-
 src/remote/activitypub/act/like.ts            | 55 ++---------
 src/remote/activitypub/renderer/like.ts       |  9 ++
 src/remote/activitypub/renderer/note.ts       |  4 +-
 src/remote/activitypub/type.ts                |  7 +-
 .../api/endpoints/posts/reactions/create.ts   | 88 ++---------------
 src/services/post/reaction/create.ts          | 94 +++++++++++++++++++
 7 files changed, 131 insertions(+), 137 deletions(-)
 create mode 100644 src/remote/activitypub/renderer/like.ts

diff --git a/src/remote/activitypub/act/index.ts b/src/remote/activitypub/act/index.ts
index 5fcdb6174..45d7bd16a 100644
--- a/src/remote/activitypub/act/index.ts
+++ b/src/remote/activitypub/act/index.ts
@@ -1,9 +1,10 @@
+import { Object } from '../type';
+import { IRemoteUser } from '../../../models/user';
 import create from './create';
 import performDeleteActivity from './delete';
 import follow from './follow';
 import undo from './undo';
-import { Object } from '../type';
-import { IRemoteUser } from '../../../models/user';
+import like from './like';
 
 const self = async (actor: IRemoteUser, activity: Object): Promise<void> => {
 	switch (activity.type) {
@@ -23,6 +24,10 @@ const self = async (actor: IRemoteUser, activity: Object): Promise<void> => {
 		// noop
 		break;
 
+	case 'Like':
+		await like(actor, activity);
+		break;
+
 	case 'Undo':
 		await undo(actor, activity);
 		break;
@@ -33,7 +38,7 @@ const self = async (actor: IRemoteUser, activity: Object): Promise<void> => {
 		break;
 
 	default:
-		console.warn(`unknown activity type: ${activity.type}`);
+		console.warn(`unknown activity type: ${(activity as any).type}`);
 		return null;
 	}
 };
diff --git a/src/remote/activitypub/act/like.ts b/src/remote/activitypub/act/like.ts
index ea5324201..2f5e3f807 100644
--- a/src/remote/activitypub/act/like.ts
+++ b/src/remote/activitypub/act/like.ts
@@ -1,10 +1,10 @@
-import { MongoError } from 'mongodb';
-import Reaction, { IPostReaction } from '../../../models/post-reaction';
 import Post from '../../../models/post';
-import queue from '../../../queue';
+import { IRemoteUser } from '../../../models/user';
+import { ILike } from '../type';
+import create from '../../../services/post/reaction/create';
 
-export default async (resolver, actor, activity, distribute) => {
-	const id = activity.object.id || activity.object;
+export default async (actor: IRemoteUser, activity: ILike) => {
+	const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
 
 	// Transform:
 	// https://misskey.ex/@syuilo/xxxx to
@@ -16,48 +16,5 @@ export default async (resolver, actor, activity, distribute) => {
 		throw new Error();
 	}
 
-	if (!distribute) {
-		const { _id } = await Reaction.findOne({
-			userId: actor._id,
-			postId: post._id
-		});
-
-		return {
-			resolver,
-			object: { $ref: 'postPeactions', $id: _id }
-		};
-	}
-
-	const promisedReaction = Reaction.insert({
-		createdAt: new Date(),
-		userId: actor._id,
-		postId: post._id,
-		reaction: 'pudding'
-	}).then(reaction => new Promise<IPostReaction>((resolve, reject) => {
-		queue.create('http', {
-			type: 'reaction',
-			reactionId: reaction._id
-		}).save(error => {
-			if (error) {
-				reject(error);
-			} else {
-				resolve(reaction);
-			}
-		});
-	}), async error => {
-		// duplicate key error
-		if (error instanceof MongoError && error.code === 11000) {
-			return Reaction.findOne({
-				userId: actor._id,
-				postId: post._id
-			});
-		}
-
-		throw error;
-	});
-
-	return promisedReaction.then(({ _id }) => ({
-		resolver,
-		object: { $ref: 'postPeactions', $id: _id }
-	}));
+	await create(actor, post, 'pudding');
 };
diff --git a/src/remote/activitypub/renderer/like.ts b/src/remote/activitypub/renderer/like.ts
new file mode 100644
index 000000000..903b10789
--- /dev/null
+++ b/src/remote/activitypub/renderer/like.ts
@@ -0,0 +1,9 @@
+import config from '../../../config';
+
+export default (user, post) => {
+	return {
+		type: 'Like',
+		actor: `${config.url}/@${user.username}`,
+		object: post.uri ? post.uri : `${config.url}/posts/${post._id}`
+	};
+};
diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts
index b971a5395..bbab63db3 100644
--- a/src/remote/activitypub/renderer/note.ts
+++ b/src/remote/activitypub/renderer/note.ts
@@ -23,7 +23,7 @@ export default async (user: IUser, post: IPost) => {
 			});
 
 			if (inReplyToUser !== null) {
-				inReplyTo = inReplyToPost.uri || `${config.url}/@${inReplyToUser.username}/${inReplyToPost._id}`;
+				inReplyTo = inReplyToPost.uri || `${config.url}/posts/${inReplyToPost._id}`;
 			}
 		}
 	} else {
@@ -33,7 +33,7 @@ export default async (user: IUser, post: IPost) => {
 	const attributedTo = `${config.url}/@${user.username}`;
 
 	return {
-		id: `${attributedTo}/${post._id}`,
+		id: `${config.url}/posts/${post._id}`,
 		type: 'Note',
 		attributedTo,
 		content: post.textHtml,
diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts
index c07b806be..450d5906d 100644
--- a/src/remote/activitypub/type.ts
+++ b/src/remote/activitypub/type.ts
@@ -55,6 +55,10 @@ export interface IAccept extends IActivity {
 	type: 'Accept';
 }
 
+export interface ILike extends IActivity {
+	type: 'Like';
+}
+
 export type Object =
 	ICollection |
 	IOrderedCollection |
@@ -62,4 +66,5 @@ export type Object =
 	IDelete |
 	IUndo |
 	IFollow |
-	IAccept;
+	IAccept |
+	ILike;
diff --git a/src/server/api/endpoints/posts/reactions/create.ts b/src/server/api/endpoints/posts/reactions/create.ts
index f1b0c7dd2..71fa6a295 100644
--- a/src/server/api/endpoints/posts/reactions/create.ts
+++ b/src/server/api/endpoints/posts/reactions/create.ts
@@ -3,20 +3,11 @@
  */
 import $ from 'cafy';
 import Reaction from '../../../../../models/post-reaction';
-import Post, { pack as packPost } from '../../../../../models/post';
-import { pack as packUser } from '../../../../../models/user';
-import Watching from '../../../../../models/post-watching';
-import watch from '../../../../../post/watch';
-import { publishPostStream } from '../../../../../publishers/stream';
-import notify from '../../../../../publishers/notify';
-import pushSw from '../../../../../publishers/push-sw';
+import Post from '../../../../../models/post';
+import create from '../../../../../services/post/reaction/create';
 
 /**
  * React to a post
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Get 'postId' parameter
@@ -46,78 +37,11 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		return rej('post not found');
 	}
 
-	// Myself
-	if (post.userId.equals(user._id)) {
-		return rej('cannot react to my post');
+	try {
+		await create(user, post, reaction);
+	} catch (e) {
+		rej(e);
 	}
 
-	// if already reacted
-	const exist = await Reaction.findOne({
-		postId: post._id,
-		userId: user._id,
-		deletedAt: { $exists: false }
-	});
-
-	if (exist !== null) {
-		return rej('already reacted');
-	}
-
-	// Create reaction
-	await Reaction.insert({
-		createdAt: new Date(),
-		postId: post._id,
-		userId: user._id,
-		reaction: reaction
-	});
-
-	// Send response
 	res();
-
-	const inc = {};
-	inc[`reactionCounts.${reaction}`] = 1;
-
-	// Increment reactions count
-	await Post.update({ _id: post._id }, {
-		$inc: inc
-	});
-
-	publishPostStream(post._id, 'reacted');
-
-	// Notify
-	notify(post.userId, user._id, 'reaction', {
-		postId: post._id,
-		reaction: reaction
-	});
-
-	pushSw(post.userId, 'reaction', {
-		user: await packUser(user, post.userId),
-		post: await packPost(post, post.userId),
-		reaction: reaction
-	});
-
-	// Fetch watchers
-	Watching
-		.find({
-			postId: post._id,
-			userId: { $ne: user._id },
-			// 削除されたドキュメントは除く
-			deletedAt: { $exists: false }
-		}, {
-			fields: {
-				userId: true
-			}
-		})
-		.then(watchers => {
-			watchers.forEach(watcher => {
-				notify(watcher.userId, user._id, 'reaction', {
-					postId: post._id,
-					reaction: reaction
-				});
-			});
-		});
-
-	// この投稿をWatchする
-	if (user.account.settings.autoWatch !== false) {
-		watch(user._id, post);
-	}
 });
diff --git a/src/services/post/reaction/create.ts b/src/services/post/reaction/create.ts
index e69de29bb..c26efcfc7 100644
--- a/src/services/post/reaction/create.ts
+++ b/src/services/post/reaction/create.ts
@@ -0,0 +1,94 @@
+import { IUser, pack as packUser, isLocalUser, isRemoteUser } from '../../../models/user';
+import Post, { IPost, pack as packPost } from '../../../models/post';
+import PostReaction from '../../../models/post-reaction';
+import { publishPostStream } from '../../../publishers/stream';
+import notify from '../../../publishers/notify';
+import pushSw from '../../../publishers/push-sw';
+import PostWatching from '../../../models/post-watching';
+import watch from '../watch';
+import renderLike from '../../../remote/activitypub/renderer/like';
+import { deliver } from '../../../queue';
+import context from '../../../remote/activitypub/renderer/context';
+
+export default async (user: IUser, post: IPost, reaction: string) => new Promise(async (res, rej) => {
+	// Myself
+	if (post.userId.equals(user._id)) {
+		return rej('cannot react to my post');
+	}
+
+	// if already reacted
+	const exist = await PostReaction.findOne({
+		postId: post._id,
+		userId: user._id
+	});
+
+	if (exist !== null) {
+		return rej('already reacted');
+	}
+
+	// Create reaction
+	await PostReaction.insert({
+		createdAt: new Date(),
+		postId: post._id,
+		userId: user._id,
+		reaction
+	});
+
+	res();
+
+	const inc = {};
+	inc[`reactionCounts.${reaction}`] = 1;
+
+	// Increment reactions count
+	await Post.update({ _id: post._id }, {
+		$inc: inc
+	});
+
+	publishPostStream(post._id, 'reacted');
+
+	// Notify
+	notify(post.userId, user._id, 'reaction', {
+		postId: post._id,
+		reaction: reaction
+	});
+
+	pushSw(post.userId, 'reaction', {
+		user: await packUser(user, post.userId),
+		post: await packPost(post, post.userId),
+		reaction: reaction
+	});
+
+	// Fetch watchers
+	PostWatching
+		.find({
+			postId: post._id,
+			userId: { $ne: user._id }
+		}, {
+			fields: {
+				userId: true
+			}
+		})
+		.then(watchers => {
+			watchers.forEach(watcher => {
+				notify(watcher.userId, user._id, 'reaction', {
+					postId: post._id,
+					reaction: reaction
+				});
+			});
+		});
+
+	// ユーザーがローカルユーザーかつ自動ウォッチ設定がオンならばこの投稿をWatchする
+	if (isLocalUser(user) && user.account.settings.autoWatch !== false) {
+		watch(user._id, post);
+	}
+
+	//#region 配信
+	const content = renderLike(user, post);
+	content['@context'] = context;
+
+	// リアクターがローカルユーザーかつリアクション対象がリモートユーザーの投稿なら配送
+	if (isLocalUser(user) && isRemoteUser(post._user)) {
+		deliver(user, content, post._user.account.inbox).save();
+	}
+	//#endregion
+});

From 2ebbb70356696e69b0581253888608cdbf6d44ee Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 02:30:37 +0900
Subject: [PATCH 1153/1250] Post --> Note

Closes #1411
---
 locales/en.yml                                |  86 +--
 locales/ja.yml                                |  74 +--
 src/client/app/auth/views/form.vue            |   2 +-
 src/client/app/ch/tags/channel.tag            |  68 +-
 src/client/app/common/mios.ts                 |   4 +-
 .../common/scripts/compose-notification.ts    |  10 +-
 .../app/common/scripts/parse-search-query.ts  |   4 +-
 .../app/common/views/components/index.ts      |   4 +-
 .../components/messaging-room.message.vue     |   2 +-
 .../components/{post-html.ts => note-html.ts} |   2 +-
 .../{post-menu.vue => note-menu.vue}          |  10 +-
 .../app/common/views/components/poll.vue      |   8 +-
 .../views/components/reaction-picker.vue      |   6 +-
 .../views/components/reactions-viewer.vue     |   4 +-
 .../views/components/welcome-timeline.vue     |  26 +-
 src/client/app/desktop/api/post.ts            |   8 +-
 src/client/app/desktop/script.ts              |  16 +-
 .../views/components/activity.calendar.vue    |   2 +-
 .../views/components/activity.chart.vue       |  16 +-
 .../app/desktop/views/components/index.ts     |  32 +-
 .../app/desktop/views/components/mentions.vue |  24 +-
 ...ost-detail.sub.vue => note-detail.sub.vue} |  22 +-
 .../desktop/views/components/note-detail.vue  | 448 +++++++++++++
 .../{post-preview.vue => note-preview.vue}    |  22 +-
 ...{posts.post.sub.vue => notes.note.sub.vue} |  18 +-
 .../desktop/views/components/notes.note.vue   | 596 ++++++++++++++++++
 .../views/components/{posts.vue => notes.vue} |  38 +-
 .../views/components/notifications.vue        |  54 +-
 .../desktop/views/components/post-detail.vue  |  84 +--
 .../views/components/post-form-window.vue     |   6 +-
 .../desktop/views/components/post-form.vue    |  32 +-
 .../desktop/views/components/posts.post.vue   | 104 +--
 .../views/components/renote-form-window.vue   |  42 ++
 .../desktop/views/components/renote-form.vue  | 131 ++++
 .../views/components/repost-form-window.vue   |   6 +-
 .../desktop/views/components/repost-form.vue  |  26 +-
 .../views/components/sub-note-content.vue     |  44 ++
 .../views/components/sub-post-content.vue     |  44 --
 .../app/desktop/views/components/timeline.vue |  42 +-
 .../views/components/ui.header.post.vue       |   6 +-
 .../desktop/views/components/user-preview.vue |   2 +-
 src/client/app/desktop/views/pages/home.vue   |  12 +-
 .../views/pages/{post.vue => note.vue}        |  18 +-
 src/client/app/desktop/views/pages/search.vue |  32 +-
 .../desktop/views/pages/user/user.home.vue    |   2 +-
 .../desktop/views/pages/user/user.photos.vue  |   8 +-
 .../desktop/views/pages/user/user.profile.vue |   2 +-
 .../views/pages/user/user.timeline.vue        |  24 +-
 .../views/widgets/channel.channel.form.vue    |   4 +-
 ...nnel.post.vue => channel.channel.note.vue} |  24 +-
 .../desktop/views/widgets/channel.channel.vue |  36 +-
 .../app/desktop/views/widgets/polls.vue       |   8 +-
 .../app/desktop/views/widgets/post-form.vue   |   4 +-
 .../app/desktop/views/widgets/trends.vue      |  24 +-
 src/client/app/dev/views/new-app.vue          |   2 +-
 src/client/app/mobile/api/post.ts             |  20 +-
 src/client/app/mobile/script.ts               |   4 +-
 .../app/mobile/views/components/activity.vue  |  16 +-
 .../app/mobile/views/components/index.ts      |  24 +-
 .../{post-card.vue => note-card.vue}          |  18 +-
 ...ost-detail.sub.vue => note-detail.sub.vue} |  16 +-
 .../mobile/views/components/note-detail.vue   | 462 ++++++++++++++
 .../{post-preview.vue => note-preview.vue}    |  18 +-
 .../components/{post.sub.vue => note.sub.vue} |  16 +-
 .../app/mobile/views/components/note.vue      | 540 ++++++++++++++++
 .../views/components/{posts.vue => notes.vue} |  34 +-
 .../views/components/notification-preview.vue |  42 +-
 .../mobile/views/components/notification.vue  |  38 +-
 .../mobile/views/components/post-detail.vue   |  80 +--
 .../app/mobile/views/components/post-form.vue |  10 +-
 .../app/mobile/views/components/post.vue      |  90 +--
 .../views/components/sub-note-content.vue     |  43 ++
 .../views/components/sub-post-content.vue     |  43 --
 .../app/mobile/views/components/timeline.vue  |  38 +-
 .../mobile/views/components/user-timeline.vue |  32 +-
 src/client/app/mobile/views/pages/home.vue    |  12 +-
 .../mobile/views/pages/{post.vue => note.vue} |  18 +-
 src/client/app/mobile/views/pages/search.vue  |  30 +-
 src/client/app/mobile/views/pages/user.vue    |   8 +-
 .../user/{home.posts.vue => home.notes.vue}   |  20 +-
 .../mobile/views/pages/user/home.photos.vue   |  12 +-
 .../app/mobile/views/pages/user/home.vue      |  14 +-
 src/client/app/stats/tags/index.tag           |  28 +-
 .../endpoints/{posts => notes}/create.yaml    |  22 +-
 .../endpoints/{posts => notes}/timeline.yaml  |   6 +-
 src/client/docs/api/entities/note.yaml        | 174 +++++
 src/client/docs/api/entities/post.yaml        |  40 +-
 src/client/docs/api/entities/user.yaml        |  16 +-
 src/client/docs/mute.ja.pug                   |   2 +-
 src/client/docs/search.ja.pug                 |  16 +-
 src/models/favorite.ts                        |   2 +-
 .../{post-reaction.ts => note-reaction.ts}    |  12 +-
 src/models/note-watching.ts                   |  13 +
 src/models/{post.ts => note.ts}               | 116 ++--
 src/models/notification.ts                    |  14 +-
 src/models/poll-vote.ts                       |   2 +-
 src/models/post-watching.ts                   |  13 -
 src/models/user.ts                            |  18 +-
 src/othello/ai/back.ts                        |  10 +-
 src/othello/ai/front.ts                       |  18 +-
 src/publishers/stream.ts                      |   6 +-
 .../processors/http/report-github-failure.ts  |   4 +-
 src/remote/activitypub/act/create/note.ts     |  22 +-
 src/remote/activitypub/act/delete/index.ts    |   6 +-
 src/remote/activitypub/act/delete/note.ts     |  12 +-
 src/remote/activitypub/act/like.ts            |  12 +-
 src/remote/activitypub/renderer/like.ts       |   4 +-
 src/remote/activitypub/renderer/note.ts       |  28 +-
 src/remote/activitypub/resolve-person.ts      |   4 +-
 src/renderers/get-note-summary.ts             |  45 ++
 src/renderers/get-notification-summary.ts     |  16 +-
 src/renderers/get-post-summary.ts             |  45 --
 src/renderers/get-user-summary.ts             |   2 +-
 src/server/activitypub/index.ts               |   4 +-
 src/server/activitypub/{post.ts => note.ts}   |  12 +-
 src/server/activitypub/outbox.ts              |   8 +-
 src/server/api/bot/core.ts                    |  22 +-
 src/server/api/bot/interfaces/line.ts         |  18 +-
 src/server/api/endpoints.ts                   |  54 +-
 .../aggregation/{posts => notes}/reaction.ts  |  24 +-
 .../aggregation/{posts => notes}/reactions.ts |  26 +-
 .../aggregation/{posts => notes}/reply.ts     |  24 +-
 .../aggregation/{posts => notes}/repost.ts    |  24 +-
 src/server/api/endpoints/aggregation/posts.ts |  22 +-
 .../endpoints/aggregation/users/activity.ts   |  20 +-
 .../api/endpoints/aggregation/users/post.ts   |  22 +-
 .../endpoints/aggregation/users/reaction.ts   |   2 +-
 src/server/api/endpoints/app/create.ts        |   2 +-
 .../api/endpoints/app/name_id/available.ts    |   2 +-
 src/server/api/endpoints/app/show.ts          |   2 +-
 src/server/api/endpoints/auth/accept.ts       |   2 +-
 .../api/endpoints/auth/session/generate.ts    |   2 +-
 src/server/api/endpoints/auth/session/show.ts |   2 +-
 .../api/endpoints/auth/session/userkey.ts     |   2 +-
 src/server/api/endpoints/channels/posts.ts    |  10 +-
 src/server/api/endpoints/i/favorites.ts       |   4 +-
 src/server/api/endpoints/i/pin.ts             |  20 +-
 src/server/api/endpoints/meta.ts              |   2 +-
 .../api/endpoints/{posts => notes}/context.ts |  30 +-
 src/server/api/endpoints/notes/create.ts      | 251 ++++++++
 .../{posts => notes}/favorites/create.ts      |  22 +-
 .../{posts => notes}/favorites/delete.ts      |  20 +-
 .../endpoints/{posts => notes}/mentions.ts    |   6 +-
 .../{posts => notes}/polls/recommendation.ts  |  12 +-
 .../endpoints/{posts => notes}/polls/vote.ts  |  48 +-
 .../endpoints/{posts => notes}/reactions.ts   |  24 +-
 .../{posts => notes}/reactions/create.ts      |  24 +-
 .../{posts => notes}/reactions/delete.ts      |  24 +-
 .../api/endpoints/{posts => notes}/replies.ts |  28 +-
 .../api/endpoints/{posts => notes}/reposts.ts |  28 +-
 .../api/endpoints/{posts => notes}/search.ts  |  42 +-
 src/server/api/endpoints/notes/show.ts        |  32 +
 .../endpoints/{posts => notes}/timeline.ts    |  10 +-
 .../api/endpoints/{posts => notes}/trend.ts   |  24 +-
 src/server/api/endpoints/posts.ts             |  18 +-
 src/server/api/endpoints/posts/create.ts      | 108 ++--
 src/server/api/endpoints/posts/show.ts        |  32 -
 src/server/api/endpoints/stats.ts             |  12 +-
 .../users/get_frequently_replied_users.ts     |  18 +-
 src/server/api/endpoints/users/posts.ts       |  10 +-
 src/server/api/private/signup.ts              |   2 +-
 src/server/api/service/github.ts              |   2 +-
 src/server/api/stream/home.ts                 |  24 +-
 src/services/{post => note}/create.ts         | 123 ++--
 .../{post => note}/reaction/create.ts         |  50 +-
 src/services/{post => note}/watch.ts          |  10 +-
 tools/migration/nighthike/11.js               |  35 +
 167 files changed, 4440 insertions(+), 1762 deletions(-)
 rename src/client/app/common/views/components/{post-html.ts => note-html.ts} (98%)
 rename src/client/app/common/views/components/{post-menu.vue => note-menu.vue} (93%)
 rename src/client/app/desktop/views/components/{post-detail.sub.vue => note-detail.sub.vue} (76%)
 create mode 100644 src/client/app/desktop/views/components/note-detail.vue
 rename src/client/app/desktop/views/components/{post-preview.vue => note-preview.vue} (72%)
 rename src/client/app/desktop/views/components/{posts.post.sub.vue => notes.note.sub.vue} (76%)
 create mode 100644 src/client/app/desktop/views/components/notes.note.vue
 rename src/client/app/desktop/views/components/{posts.vue => notes.vue} (56%)
 create mode 100644 src/client/app/desktop/views/components/renote-form-window.vue
 create mode 100644 src/client/app/desktop/views/components/renote-form.vue
 create mode 100644 src/client/app/desktop/views/components/sub-note-content.vue
 delete mode 100644 src/client/app/desktop/views/components/sub-post-content.vue
 rename src/client/app/desktop/views/pages/{post.vue => note.vue} (65%)
 rename src/client/app/desktop/views/widgets/{channel.channel.post.vue => channel.channel.note.vue} (65%)
 rename src/client/app/mobile/views/components/{post-card.vue => note-card.vue} (81%)
 rename src/client/app/mobile/views/components/{post-detail.sub.vue => note-detail.sub.vue} (80%)
 create mode 100644 src/client/app/mobile/views/components/note-detail.vue
 rename src/client/app/mobile/views/components/{post-preview.vue => note-preview.vue} (81%)
 rename src/client/app/mobile/views/components/{post.sub.vue => note.sub.vue} (80%)
 create mode 100644 src/client/app/mobile/views/components/note.vue
 rename src/client/app/mobile/views/components/{posts.vue => notes.vue} (65%)
 create mode 100644 src/client/app/mobile/views/components/sub-note-content.vue
 delete mode 100644 src/client/app/mobile/views/components/sub-post-content.vue
 rename src/client/app/mobile/views/pages/{post.vue => note.vue} (71%)
 rename src/client/app/mobile/views/pages/user/{home.posts.vue => home.notes.vue} (61%)
 rename src/client/docs/api/endpoints/{posts => notes}/create.yaml (77%)
 rename src/client/docs/api/endpoints/{posts => notes}/timeline.yaml (93%)
 create mode 100644 src/client/docs/api/entities/note.yaml
 rename src/models/{post-reaction.ts => note-reaction.ts} (79%)
 create mode 100644 src/models/note-watching.ts
 rename src/models/{post.ts => note.ts} (61%)
 delete mode 100644 src/models/post-watching.ts
 create mode 100644 src/renderers/get-note-summary.ts
 delete mode 100644 src/renderers/get-post-summary.ts
 rename src/server/activitypub/{post.ts => note.ts} (80%)
 rename src/server/api/endpoints/aggregation/{posts => notes}/reaction.ts (72%)
 rename src/server/api/endpoints/aggregation/{posts => notes}/reactions.ts (71%)
 rename src/server/api/endpoints/aggregation/{posts => notes}/reply.ts (74%)
 rename src/server/api/endpoints/aggregation/{posts => notes}/repost.ts (74%)
 rename src/server/api/endpoints/{posts => notes}/context.ts (60%)
 create mode 100644 src/server/api/endpoints/notes/create.ts
 rename src/server/api/endpoints/{posts => notes}/favorites/create.ts (62%)
 rename src/server/api/endpoints/{posts => notes}/favorites/delete.ts (62%)
 rename src/server/api/endpoints/{posts => notes}/mentions.ts (92%)
 rename src/server/api/endpoints/{posts => notes}/polls/recommendation.ts (78%)
 rename src/server/api/endpoints/{posts => notes}/polls/vote.ts (63%)
 rename src/server/api/endpoints/{posts => notes}/reactions.ts (70%)
 rename src/server/api/endpoints/{posts => notes}/reactions/create.ts (50%)
 rename src/server/api/endpoints/{posts => notes}/reactions/delete.ts (63%)
 rename src/server/api/endpoints/{posts => notes}/replies.ts (63%)
 rename src/server/api/endpoints/{posts => notes}/reposts.ts (70%)
 rename src/server/api/endpoints/{posts => notes}/search.ts (91%)
 create mode 100644 src/server/api/endpoints/notes/show.ts
 rename src/server/api/endpoints/{posts => notes}/timeline.ts (93%)
 rename src/server/api/endpoints/{posts => notes}/trend.ts (76%)
 delete mode 100644 src/server/api/endpoints/posts/show.ts
 rename src/services/{post => note}/create.ts (75%)
 rename src/services/{post => note}/reaction/create.ts (59%)
 rename src/services/{post => note}/watch.ts (59%)
 create mode 100644 tools/migration/nighthike/11.js

diff --git a/locales/en.yml b/locales/en.yml
index 2cc857f69..900571124 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -33,7 +33,7 @@ common:
     confused: "Confused"
     pudding: "Pudding"
 
-  post_categories:
+  note_categories:
     music: "Music"
     game: "Video Game"
     anime: "Anime"
@@ -124,7 +124,7 @@ common:
       show-result: "Show result"
       voted: "Voted"
 
-    mk-post-menu:
+    mk-note-menu:
       pin: "Pin"
       pinned: "Pinned"
       select: "Select category"
@@ -211,7 +211,7 @@ ch:
       textarea: "Write here"
       upload: "Upload"
       drive: "Drive"
-      post: "Do"
+      note: "Do"
       posting: "Doing"
 
 desktop:
@@ -304,8 +304,8 @@ desktop:
       settings: "Settings"
       signout: "Sign out"
 
-    mk-ui-header-post-button:
-      post: "Compose new Post"
+    mk-ui-header-note-button:
+      note: "Compose new Post"
 
     mk-ui-header-notifications:
       title: "Notifications"
@@ -350,18 +350,18 @@ desktop:
       no-users: "No muted users"
 
     mk-post-form:
-      post-placeholder: "What's happening?"
-      reply-placeholder: "Reply to this post..."
-      quote-placeholder: "Quote this post..."
-      post: "Post"
+      note-placeholder: "What's happening?"
+      reply-placeholder: "Reply to this note..."
+      quote-placeholder: "Quote this note..."
+      note: "Post"
       reply: "Reply"
-      repost: "Repost"
+      renote: "Renote"
       posted: "Posted!"
       replied: "Replied!"
       reposted: "Reposted!"
-      post-failed: "Failed to post"
+      note-failed: "Failed to note"
       reply-failed: "Failed to reply"
-      repost-failed: "Failed to repost"
+      renote-failed: "Failed to renote"
       posting: "Posting"
       attach-media-from-local: "Attach media from your pc"
       attach-media-from-drive: "Attach media from the drive"
@@ -371,14 +371,14 @@ desktop:
       text-remain: "{} chars remaining"
 
     mk-post-form-window:
-      post: "New post"
+      note: "New note"
       reply: "Reply"
       attaches: "{} media attached"
       uploading-media: "Uploading {} media"
 
-    mk-post-page:
-      prev: "Previous post"
-      next: "Next post"
+    mk-note-page:
+      prev: "Previous note"
+      next: "Next note"
 
     mk-settings:
       profile: "Profile"
@@ -390,10 +390,10 @@ desktop:
       other: "Other"
       license: "License"
 
-    mk-timeline-post:
+    mk-timeline-note:
       reposted-by: "Reposted by {}"
       reply: "Reply"
-      repost: "Repost"
+      renote: "Renote"
       add-reaction: "Add your reaction"
       detail: "Show detail"
 
@@ -448,7 +448,7 @@ desktop:
 
     mk-post-form-home-widget:
       title: "Post"
-      post: "Post"
+      note: "Post"
       placeholder: "What's happening?"
 
     mk-access-log-home-widget:
@@ -463,16 +463,16 @@ desktop:
       have-a-nice-day: "Have a nice day!"
       next: "Next"
 
-    mk-repost-form:
+    mk-renote-form:
       quote: "Quote..."
       cancel: "Cancel"
-      repost: "Repost"
+      renote: "Renote"
       reposting: "Reposting..."
       success: "Reposted!"
-      failure: "Failed to Repost"
+      failure: "Failed to Renote"
 
-    mk-repost-form-window:
-      title: "Are you sure you want to repost this post?"
+    mk-renote-form-window:
+      title: "Are you sure you want to renote this note?"
 
     mk-user:
       last-used-at: "Last used at"
@@ -541,10 +541,10 @@ mobile:
       notifications: "Notifications"
       read-all: "Are you sure you want to mark all unread notifications as read?"
 
-    mk-post-page:
+    mk-note-page:
       title: "Post"
-      prev: "Previous post"
-      next: "Next post"
+      prev: "Previous note"
+      next: "Next note"
 
     mk-search-page:
       search: "Search"
@@ -606,33 +606,33 @@ mobile:
       unfollow: "Unfollow"
 
     mk-home-timeline:
-      empty-timeline: "There is no posts"
+      empty-timeline: "There is no notes"
 
     mk-notifications:
       more: "More"
       empty: "No notifications"
 
-    mk-post-detail:
+    mk-note-detail:
       reply: "Reply"
       reaction: "Reaction"
 
     mk-post-form:
       submit: "Post"
-      reply-placeholder: "Reply to this post..."
-      post-placeholder: "What's happening?"
+      reply-placeholder: "Reply to this note..."
+      note-placeholder: "What's happening?"
 
-    mk-search-posts:
-      empty: "There is no post related to the 「{}」"
+    mk-search-notes:
+      empty: "There is no note related to the 「{}」"
 
-    mk-sub-post-content:
+    mk-sub-note-content:
       media-count: "{} media"
       poll: "Poll"
 
-    mk-timeline-post:
+    mk-timeline-note:
       reposted-by: "Reposted by {}"
 
     mk-timeline:
-      empty: "No posts"
+      empty: "No notes"
       load-more: "More"
 
     mk-ui-nav:
@@ -652,21 +652,21 @@ mobile:
       no-users: "No following."
 
     mk-user-timeline:
-      no-posts: "This user seems never post"
-      no-posts-with-media: "There is no posts with media"
+      no-notes: "This user seems never note"
+      no-notes-with-media: "There is no notes with media"
       load-more: "More"
 
     mk-user:
       follows-you: "Follows you"
       following: "Following"
       followers: "Followers"
-      posts: "Posts"
+      notes: "Posts"
       overview: "Overview"
       timeline: "Timeline"
       media: "Media"
 
     mk-user-overview:
-      recent-posts: "Recent posts"
+      recent-notes: "Recent notes"
       images: "Images"
       activity: "Activity"
       keywords: "Keywords"
@@ -675,9 +675,9 @@ mobile:
       followers-you-know: "Followers you know"
       last-used-at: "Last used at"
 
-    mk-user-overview-posts:
+    mk-user-overview-notes:
       loading: "Loading"
-      no-posts: "No posts"
+      no-notes: "No notes"
 
     mk-user-overview-photos:
       loading: "Loading"
@@ -703,7 +703,7 @@ mobile:
       load-more: "More"
 
 stats:
-  posts-count: "Number of all posts"
+  notes-count: "Number of all notes"
   users-count: "Number of all users"
 
 status:
diff --git a/locales/ja.yml b/locales/ja.yml
index fd140ecc3..84694e3c7 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -33,7 +33,7 @@ common:
     confused: "こまこまのこまり"
     pudding: "Pudding"
 
-  post_categories:
+  note_categories:
     music: "音楽"
     game: "ゲーム"
     anime: "アニメ"
@@ -124,7 +124,7 @@ common:
       show-result: "結果を見る"
       voted: "投票済み"
 
-    mk-post-menu:
+    mk-note-menu:
       pin: "ピン留め"
       pinned: "ピン留めしました"
       select: "カテゴリを選択"
@@ -211,7 +211,7 @@ ch:
       textarea: "書いて"
       upload: "アップロード"
       drive: "ドライブ"
-      post: "やる"
+      note: "やる"
       posting: "やってます"
 
 desktop:
@@ -304,8 +304,8 @@ desktop:
       settings: "設定"
       signout: "サインアウト"
 
-    mk-ui-header-post-button:
-      post: "新規投稿"
+    mk-ui-header-note-button:
+      note: "新規投稿"
 
     mk-ui-header-notifications:
       title: "通知"
@@ -350,18 +350,18 @@ desktop:
       no-users: "ミュートしているユーザーはいません"
 
     mk-post-form:
-      post-placeholder: "いまどうしてる?"
+      note-placeholder: "いまどうしてる?"
       reply-placeholder: "この投稿への返信..."
       quote-placeholder: "この投稿を引用..."
-      post: "投稿"
+      note: "投稿"
       reply: "返信"
-      repost: "Repost"
+      renote: "Renote"
       posted: "投稿しました!"
       replied: "返信しました!"
-      reposted: "Repostしました!"
-      post-failed: "投稿に失敗しました"
+      reposted: "Renoteしました!"
+      note-failed: "投稿に失敗しました"
       reply-failed: "返信に失敗しました"
-      repost-failed: "Repostに失敗しました"
+      renote-failed: "Renoteに失敗しました"
       posting: "投稿中"
       attach-media-from-local: "PCからメディアを添付"
       attach-media-from-drive: "ドライブからメディアを添付"
@@ -371,12 +371,12 @@ desktop:
       text-remain: "のこり{}文字"
 
     mk-post-form-window:
-      post: "新規投稿"
+      note: "新規投稿"
       reply: "返信"
       attaches: "添付: {}メディア"
       uploading-media: "{}個のメディアをアップロード中"
 
-    mk-post-page:
+    mk-note-page:
       prev: "前の投稿"
       next: "次の投稿"
 
@@ -390,10 +390,10 @@ desktop:
       other: "その他"
       license: "ライセンス"
 
-    mk-timeline-post:
-      reposted-by: "{}がRepost"
+    mk-timeline-note:
+      reposted-by: "{}がRenote"
       reply: "返信"
-      repost: "Repost"
+      renote: "Renote"
       add-reaction: "リアクション"
       detail: "詳細"
 
@@ -448,7 +448,7 @@ desktop:
 
     mk-post-form-home-widget:
       title: "投稿"
-      post: "投稿"
+      note: "投稿"
       placeholder: "いまどうしてる?"
 
     mk-access-log-home-widget:
@@ -463,16 +463,16 @@ desktop:
       have-a-nice-day: "良い一日を!"
       next: "次"
 
-    mk-repost-form:
+    mk-renote-form:
       quote: "引用する..."
       cancel: "キャンセル"
-      repost: "Repost"
+      renote: "Renote"
       reposting: "しています..."
-      success: "Repostしました!"
-      failure: "Repostに失敗しました"
+      success: "Renoteしました!"
+      failure: "Renoteに失敗しました"
 
-    mk-repost-form-window:
-      title: "この投稿をRepostしますか?"
+    mk-renote-form-window:
+      title: "この投稿をRenoteしますか?"
 
     mk-user:
       last-used-at: "最終アクセス"
@@ -541,7 +541,7 @@ mobile:
       notifications: "通知"
       read-all: "すべての通知を既読にしますか?"
 
-    mk-post-page:
+    mk-note-page:
       title: "投稿"
       prev: "前の投稿"
       next: "次の投稿"
@@ -612,24 +612,24 @@ mobile:
       more: "もっと見る"
       empty: "ありません!"
 
-    mk-post-detail:
+    mk-note-detail:
       reply: "返信"
       reaction: "リアクション"
 
     mk-post-form:
       submit: "投稿"
       reply-placeholder: "この投稿への返信..."
-      post-placeholder: "いまどうしてる?"
+      note-placeholder: "いまどうしてる?"
 
-    mk-search-posts:
+    mk-search-notes:
       empty: "「{}」に関する投稿は見つかりませんでした。"
 
-    mk-sub-post-content:
+    mk-sub-note-content:
       media-count: "{}個のメディア"
       poll: "投票"
 
-    mk-timeline-post:
-      reposted-by: "{}がRepost"
+    mk-timeline-note:
+      reposted-by: "{}がRenote"
 
     mk-timeline:
       empty: "表示するものがありません"
@@ -652,21 +652,21 @@ mobile:
       no-users: "フォロー中のユーザーはいないようです。"
 
     mk-user-timeline:
-      no-posts: "このユーザーはまだ投稿していないようです。"
-      no-posts-with-media: "メディア付き投稿はありません。"
+      no-notes: "このユーザーはまだ投稿していないようです。"
+      no-notes-with-media: "メディア付き投稿はありません。"
       load-more: "もっとみる"
 
     mk-user:
       follows-you: "フォローされています"
       following: "フォロー"
       followers: "フォロワー"
-      posts: "投稿"
+      notes: "投稿"
       overview: "概要"
       timeline: "タイムライン"
       media: "メディア"
 
     mk-user-overview:
-      recent-posts: "最近の投稿"
+      recent-notes: "最近の投稿"
       images: "画像"
       activity: "アクティビティ"
       keywords: "キーワード"
@@ -675,9 +675,9 @@ mobile:
       followers-you-know: "知り合いのフォロワー"
       last-used-at: "最終ログイン"
 
-    mk-user-overview-posts:
+    mk-user-overview-notes:
       loading: "読み込み中"
-      no-posts: "投稿はありません"
+      no-notes: "投稿はありません"
 
     mk-user-overview-photos:
       loading: "読み込み中"
@@ -703,7 +703,7 @@ mobile:
       load-more: "もっと"
 
 stats:
-  posts-count: "投稿の数"
+  notes-count: "投稿の数"
   users-count: "アカウントの数"
 
 status:
diff --git a/src/client/app/auth/views/form.vue b/src/client/app/auth/views/form.vue
index eb55b9035..b323907eb 100644
--- a/src/client/app/auth/views/form.vue
+++ b/src/client/app/auth/views/form.vue
@@ -16,7 +16,7 @@
 				<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 == 'note-write'">投稿する。</li>
 					<li v-if="p == 'like-write'">いいねしたりいいね解除する。</li>
 					<li v-if="p == 'following-write'">フォローしたりフォロー解除する。</li>
 					<li v-if="p == 'drive-read'">ドライブを見る。</li>
diff --git a/src/client/app/ch/tags/channel.tag b/src/client/app/ch/tags/channel.tag
index 4856728de..c0561c9b9 100644
--- a/src/client/app/ch/tags/channel.tag
+++ b/src/client/app/ch/tags/channel.tag
@@ -15,11 +15,11 @@
 		</div>
 
 		<div class="body">
-			<p v-if="postsFetching">読み込み中<mk-ellipsis/></p>
-			<div v-if="!postsFetching">
-				<p v-if="posts == null || posts.length == 0">まだ投稿がありません</p>
-				<template v-if="posts != null">
-					<mk-channel-post each={ post in posts.slice().reverse() } post={ post } form={ parent.refs.form }/>
+			<p v-if="notesFetching">読み込み中<mk-ellipsis/></p>
+			<div v-if="!notesFetching">
+				<p v-if="notes == null || notes.length == 0">まだ投稿がありません</p>
+				<template v-if="notes != null">
+					<mk-channel-note each={ note in notes.slice().reverse() } note={ note } form={ parent.refs.form }/>
 				</template>
 			</div>
 		</div>
@@ -62,9 +62,9 @@
 
 		this.id = this.opts.id;
 		this.fetching = true;
-		this.postsFetching = true;
+		this.notesFetching = true;
 		this.channel = null;
-		this.posts = null;
+		this.notes = null;
 		this.connection = new ChannelStream(this.id);
 		this.unreadCount = 0;
 
@@ -95,9 +95,9 @@
 			});
 
 			// 投稿読み込み
-			this.$root.$data.os.api('channels/posts', {
+			this.$root.$data.os.api('channels/notes', {
 				channelId: this.id
-			}).then(posts => {
+			}).then(notes => {
 				if (fetched) {
 					Progress.done();
 				} else {
@@ -106,26 +106,26 @@
 				}
 
 				this.update({
-					postsFetching: false,
-					posts: posts
+					notesFetching: false,
+					notes: notes
 				});
 			});
 
-			this.connection.on('post', this.onPost);
+			this.connection.on('note', this.onNote);
 			document.addEventListener('visibilitychange', this.onVisibilitychange, false);
 		});
 
 		this.on('unmount', () => {
-			this.connection.off('post', this.onPost);
+			this.connection.off('note', this.onNote);
 			this.connection.close();
 			document.removeEventListener('visibilitychange', this.onVisibilitychange);
 		});
 
-		this.onPost = post => {
-			this.posts.unshift(post);
+		this.onNote = note => {
+			this.notes.unshift(note);
 			this.update();
 
-			if (document.hidden && this.$root.$data.os.isSignedIn && post.userId !== this.$root.$data.os.i.id) {
+			if (document.hidden && this.$root.$data.os.isSignedIn && note.userId !== this.$root.$data.os.i.id) {
 				this.unreadCount++;
 				document.title = `(${this.unreadCount}) ${this.channel.title} | Misskey`;
 			}
@@ -162,19 +162,19 @@
 	</script>
 </mk-channel>
 
-<mk-channel-post>
+<mk-channel-note>
 	<header>
-		<a class="index" @click="reply">{ post.index }:</a>
-		<a class="name" href={ _URL_ + '/@' + acct }><b>{ getUserName(post.user) }</b></a>
-		<mk-time time={ post.createdAt }/>
-		<mk-time time={ post.createdAt } mode="detail"/>
+		<a class="index" @click="reply">{ note.index }:</a>
+		<a class="name" href={ _URL_ + '/@' + acct }><b>{ getUserName(note.user) }</b></a>
+		<mk-time time={ note.createdAt }/>
+		<mk-time time={ note.createdAt } mode="detail"/>
 		<span>ID:<i>{ acct }</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 v-if="note.reply">&gt;&gt;{ note.reply.index }</a>
+		{ note.text }
+		<div class="media" v-if="note.media">
+			<template each={ file in note.media }>
 				<a href={ file.url } target="_blank">
 					<img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/>
 				</a>
@@ -232,18 +232,18 @@
 		import getAcct from '../../../../acct/render';
 		import getUserName from '../../../../renderers/get-user-name';
 
-		this.post = this.opts.post;
+		this.note = this.opts.note;
 		this.form = this.opts.form;
-		this.acct = getAcct(this.post.user);
-		this.name = getUserName(this.post.user);
+		this.acct = getAcct(this.note.user);
+		this.name = getUserName(this.note.user);
 
 		this.reply = () => {
 			this.form.update({
-				reply: this.post
+				reply: this.note
 			});
 		};
 	</script>
-</mk-channel-post>
+</mk-channel-note>
 
 <mk-channel-form>
 	<p v-if="reply"><b>&gt;&gt;{ reply.index }</b> ({ getUserName(reply.user) }): <a @click="clearReply">[x]</a></p>
@@ -251,8 +251,8 @@
 	<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">
-			<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 :class="{ wait: wait }" ref="submit" disabled={ wait || (refs.text.value.length == 0) } @click="note">
+			<template v-if="!wait">%fa:paper-plane%</template>{ wait ? '%i18n:ch.tags.mk-channel-form.posting%' : '%i18n:ch.tags.mk-channel-form.note%' }<mk-ellipsis v-if="wait"/>
 		</button>
 	</div>
 	<mk-uploader ref="uploader"/>
@@ -321,7 +321,7 @@
 			this.$refs.text.value = '';
 		};
 
-		this.post = () => {
+		this.note = () => {
 			this.update({
 				wait: true
 			});
@@ -330,7 +330,7 @@
 				? this.files.map(f => f.id)
 				: undefined;
 
-			this.$root.$data.os.api('posts/create', {
+			this.$root.$data.os.api('notes/create', {
 				text: this.$refs.text.value == '' ? undefined : this.$refs.text.value,
 				mediaIds: files,
 				replyId: this.reply ? this.reply.id : undefined,
diff --git a/src/client/app/common/mios.ts b/src/client/app/common/mios.ts
index bcb8b6067..48e830810 100644
--- a/src/client/app/common/mios.ts
+++ b/src/client/app/common/mios.ts
@@ -49,7 +49,7 @@ export type API = {
 
 	post: (opts?: {
 		reply?: any;
-		repost?: any;
+		renote?: any;
 	}) => void;
 
 	notify: (message: string) => void;
@@ -312,7 +312,7 @@ export default class MiOS extends EventEmitter {
 			// Finish init
 			callback();
 
-			//#region Post
+			//#region Note
 
 			// Init service worker
 			if (this.shouldRegisterSw) this.registerSw();
diff --git a/src/client/app/common/scripts/compose-notification.ts b/src/client/app/common/scripts/compose-notification.ts
index e99d50296..c19b1c5ad 100644
--- a/src/client/app/common/scripts/compose-notification.ts
+++ b/src/client/app/common/scripts/compose-notification.ts
@@ -1,4 +1,4 @@
-import getPostSummary from '../../../../renderers/get-post-summary';
+import getNoteSummary from '../../../../renderers/get-note-summary';
 import getReactionEmoji from '../../../../renderers/get-reaction-emoji';
 import getUserName from '../../../../renderers/get-user-name';
 
@@ -23,28 +23,28 @@ export default function(type, data): Notification {
 		case 'mention':
 			return {
 				title: `${getUserName(data.user)}さんから:`,
-				body: getPostSummary(data),
+				body: getNoteSummary(data),
 				icon: data.user.avatarUrl + '?thumbnail&size=64'
 			};
 
 		case 'reply':
 			return {
 				title: `${getUserName(data.user)}さんから返信:`,
-				body: getPostSummary(data),
+				body: getNoteSummary(data),
 				icon: data.user.avatarUrl + '?thumbnail&size=64'
 			};
 
 		case 'quote':
 			return {
 				title: `${getUserName(data.user)}さんが引用:`,
-				body: getPostSummary(data),
+				body: getNoteSummary(data),
 				icon: data.user.avatarUrl + '?thumbnail&size=64'
 			};
 
 		case 'reaction':
 			return {
 				title: `${getUserName(data.user)}: ${getReactionEmoji(data.reaction)}:`,
-				body: getPostSummary(data.post),
+				body: getNoteSummary(data.note),
 				icon: data.user.avatarUrl + '?thumbnail&size=64'
 			};
 
diff --git a/src/client/app/common/scripts/parse-search-query.ts b/src/client/app/common/scripts/parse-search-query.ts
index 4f09d2b93..5f6ae3320 100644
--- a/src/client/app/common/scripts/parse-search-query.ts
+++ b/src/client/app/common/scripts/parse-search-query.ts
@@ -19,8 +19,8 @@ export default function(qs: string) {
 				case 'reply':
 					q['reply'] = value == 'null' ? null : value == 'true';
 					break;
-				case 'repost':
-					q['repost'] = value == 'null' ? null : value == 'true';
+				case 'renote':
+					q['renote'] = value == 'null' ? null : value == 'true';
 					break;
 				case 'media':
 					q['media'] = value == 'null' ? null : value == 'true';
diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts
index b58ba37ec..6bfe43a80 100644
--- a/src/client/app/common/views/components/index.ts
+++ b/src/client/app/common/views/components/index.ts
@@ -4,7 +4,7 @@ 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';
+import noteHtml from './note-html';
 import poll from './poll.vue';
 import pollEditor from './poll-editor.vue';
 import reactionIcon from './reaction-icon.vue';
@@ -29,7 +29,7 @@ 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-note-html', noteHtml);
 Vue.component('mk-poll', poll);
 Vue.component('mk-poll-editor', pollEditor);
 Vue.component('mk-reaction-icon', reactionIcon);
diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue
index 1518602c6..7200b59bb 100644
--- a/src/client/app/common/views/components/messaging-room.message.vue
+++ b/src/client/app/common/views/components/messaging-room.message.vue
@@ -10,7 +10,7 @@
 				<img src="/assets/desktop/messaging/delete.png" alt="Delete"/>
 			</button>
 			<div class="content" v-if="!message.isDeleted">
-				<mk-post-html class="text" v-if="message.text" ref="text" :text="message.text" :i="os.i"/>
+				<mk-note-html class="text" v-if="message.text" ref="text" :text="message.text" :i="os.i"/>
 				<div class="file" v-if="message.file">
 					<a :href="message.file.url" target="_blank" :title="message.file.name">
 						<img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/>
diff --git a/src/client/app/common/views/components/post-html.ts b/src/client/app/common/views/components/note-html.ts
similarity index 98%
rename from src/client/app/common/views/components/post-html.ts
rename to src/client/app/common/views/components/note-html.ts
index 8d8531652..24e750a67 100644
--- a/src/client/app/common/views/components/post-html.ts
+++ b/src/client/app/common/views/components/note-html.ts
@@ -9,7 +9,7 @@ const flatten = list => list.reduce(
 	(a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []
 );
 
-export default Vue.component('mk-post-html', {
+export default Vue.component('mk-note-html', {
 	props: {
 		text: {
 			type: String,
diff --git a/src/client/app/common/views/components/post-menu.vue b/src/client/app/common/views/components/note-menu.vue
similarity index 93%
rename from src/client/app/common/views/components/post-menu.vue
rename to src/client/app/common/views/components/note-menu.vue
index 35116db7e..d05374872 100644
--- a/src/client/app/common/views/components/post-menu.vue
+++ b/src/client/app/common/views/components/note-menu.vue
@@ -1,8 +1,8 @@
 <template>
-<div class="mk-post-menu">
+<div class="mk-note-menu">
 	<div class="backdrop" ref="backdrop" @click="close"></div>
 	<div class="popover" :class="{ compact }" ref="popover">
-		<button v-if="post.userId == os.i.id" @click="pin">%i18n:common.tags.mk-post-menu.pin%</button>
+		<button v-if="note.userId == os.i.id" @click="pin">%i18n:common.tags.mk-note-menu.pin%</button>
 	</div>
 </div>
 </template>
@@ -12,7 +12,7 @@ import Vue from 'vue';
 import * as anime from 'animejs';
 
 export default Vue.extend({
-	props: ['post', 'source', 'compact'],
+	props: ['note', 'source', 'compact'],
 	mounted() {
 		this.$nextTick(() => {
 			const popover = this.$refs.popover as any;
@@ -51,7 +51,7 @@ export default Vue.extend({
 	methods: {
 		pin() {
 			(this as any).api('i/pin', {
-				postId: this.post.id
+				noteId: this.note.id
 			}).then(() => {
 				this.$destroy();
 			});
@@ -83,7 +83,7 @@ export default Vue.extend({
 <style lang="stylus" scoped>
 $border-color = rgba(27, 31, 35, 0.15)
 
-.mk-post-menu
+.mk-note-menu
 	position initial
 
 	> .backdrop
diff --git a/src/client/app/common/views/components/poll.vue b/src/client/app/common/views/components/poll.vue
index 711d89720..eb29aa883 100644
--- a/src/client/app/common/views/components/poll.vue
+++ b/src/client/app/common/views/components/poll.vue
@@ -22,7 +22,7 @@
 <script lang="ts">
 import Vue from 'vue';
 export default Vue.extend({
-	props: ['post'],
+	props: ['note'],
 	data() {
 		return {
 			showResult: false
@@ -30,7 +30,7 @@ export default Vue.extend({
 	},
 	computed: {
 		poll(): any {
-			return this.post.poll;
+			return this.note.poll;
 		},
 		total(): number {
 			return this.poll.choices.reduce((a, b) => a + b.votes, 0);
@@ -48,8 +48,8 @@ export default Vue.extend({
 		},
 		vote(id) {
 			if (this.poll.choices.some(c => c.isVoted)) return;
-			(this as any).api('posts/polls/vote', {
-				postId: this.post.id,
+			(this as any).api('notes/polls/vote', {
+				noteId: this.note.id,
 				choice: id
 			}).then(() => {
 				this.poll.choices.forEach(c => {
diff --git a/src/client/app/common/views/components/reaction-picker.vue b/src/client/app/common/views/components/reaction-picker.vue
index bcb6b2b96..fa1998dca 100644
--- a/src/client/app/common/views/components/reaction-picker.vue
+++ b/src/client/app/common/views/components/reaction-picker.vue
@@ -25,7 +25,7 @@ import * as anime from 'animejs';
 const placeholder = '%i18n:common.tags.mk-reaction-picker.choose-reaction%';
 
 export default Vue.extend({
-	props: ['post', 'source', 'compact', 'cb'],
+	props: ['note', 'source', 'compact', 'cb'],
 	data() {
 		return {
 			title: placeholder
@@ -68,8 +68,8 @@ export default Vue.extend({
 	},
 	methods: {
 		react(reaction) {
-			(this as any).api('posts/reactions/create', {
-				postId: this.post.id,
+			(this as any).api('notes/reactions/create', {
+				noteId: this.note.id,
 				reaction: reaction
 			}).then(() => {
 				if (this.cb) this.cb();
diff --git a/src/client/app/common/views/components/reactions-viewer.vue b/src/client/app/common/views/components/reactions-viewer.vue
index 246451008..1afcf525d 100644
--- a/src/client/app/common/views/components/reactions-viewer.vue
+++ b/src/client/app/common/views/components/reactions-viewer.vue
@@ -17,10 +17,10 @@
 <script lang="ts">
 import Vue from 'vue';
 export default Vue.extend({
-	props: ['post'],
+	props: ['note'],
 	computed: {
 		reactions(): number {
-			return this.post.reactionCounts;
+			return this.note.reactionCounts;
 		}
 	}
 });
diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue
index bfb4b2bbe..61616da14 100644
--- a/src/client/app/common/views/components/welcome-timeline.vue
+++ b/src/client/app/common/views/components/welcome-timeline.vue
@@ -1,21 +1,21 @@
 <template>
 <div class="mk-welcome-timeline">
-	<div v-for="post in posts">
-		<router-link class="avatar-anchor" :to="`/@${getAcct(post.user)}`" v-user-preview="post.user.id">
-			<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
+	<div v-for="note in notes">
+		<router-link class="avatar-anchor" :to="`/@${getAcct(note.user)}`" v-user-preview="note.user.id">
+			<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
 		</router-link>
 		<div class="body">
 			<header>
-				<router-link class="name" :to="`/@${getAcct(post.user)}`" v-user-preview="post.user.id">{{ getUserName(post.user) }}</router-link>
-				<span class="username">@{{ getAcct(post.user) }}</span>
+				<router-link class="name" :to="`/@${getAcct(note.user)}`" v-user-preview="note.user.id">{{ getUserName(note.user) }}</router-link>
+				<span class="username">@{{ getAcct(note.user) }}</span>
 				<div class="info">
-					<router-link class="created-at" :to="`/@${getAcct(post.user)}/${post.id}`">
-						<mk-time :time="post.createdAt"/>
+					<router-link class="created-at" :to="`/@${getAcct(note.user)}/${note.id}`">
+						<mk-time :time="note.createdAt"/>
 					</router-link>
 				</div>
 			</header>
 			<div class="text">
-				<mk-post-html :text="post.text"/>
+				<mk-note-html :text="note.text"/>
 			</div>
 		</div>
 	</div>
@@ -31,7 +31,7 @@ export default Vue.extend({
 	data() {
 		return {
 			fetching: true,
-			posts: []
+			notes: []
 		};
 	},
 	mounted() {
@@ -42,14 +42,14 @@ export default Vue.extend({
 		getUserName,
 		fetch(cb?) {
 			this.fetching = true;
-			(this as any).api('posts', {
+			(this as any).api('notes', {
 				reply: false,
-				repost: false,
+				renote: false,
 				media: false,
 				poll: false,
 				bot: false
-			}).then(posts => {
-				this.posts = posts;
+			}).then(notes => {
+				this.notes = notes;
 				this.fetching = false;
 			});
 		}
diff --git a/src/client/app/desktop/api/post.ts b/src/client/app/desktop/api/post.ts
index cf49615df..b569610e1 100644
--- a/src/client/app/desktop/api/post.ts
+++ b/src/client/app/desktop/api/post.ts
@@ -1,12 +1,12 @@
 import PostFormWindow from '../views/components/post-form-window.vue';
-import RepostFormWindow from '../views/components/repost-form-window.vue';
+import RenoteFormWindow from '../views/components/renote-form-window.vue';
 
 export default function(opts) {
 	const o = opts || {};
-	if (o.repost) {
-		const vm = new RepostFormWindow({
+	if (o.renote) {
+		const vm = new RenoteFormWindow({
 			propsData: {
-				repost: o.repost
+				renote: o.renote
 			}
 		}).$mount();
 		document.body.appendChild(vm.$el);
diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts
index b95e16854..f57d42aa6 100644
--- a/src/client/app/desktop/script.ts
+++ b/src/client/app/desktop/script.ts
@@ -28,7 +28,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';
+import MkNote from './views/pages/note.vue';
 import MkSearch from './views/pages/search.vue';
 import MkOthello from './views/pages/othello.vue';
 
@@ -57,7 +57,7 @@ init(async (launch) => {
 			{ path: '/othello', component: MkOthello },
 			{ path: '/othello/:game', component: MkOthello },
 			{ path: '/@:user', component: MkUser },
-			{ path: '/@:user/:post', component: MkPost }
+			{ path: '/@:user/:note', component: MkNote }
 		]
 	});
 
@@ -114,8 +114,8 @@ function registerNotifications(stream: HomeStreamManager) {
 			setTimeout(n.close.bind(n), 5000);
 		});
 
-		connection.on('mention', post => {
-			const _n = composeNotification('mention', post);
+		connection.on('mention', note => {
+			const _n = composeNotification('mention', note);
 			const n = new Notification(_n.title, {
 				body: _n.body,
 				icon: _n.icon
@@ -123,8 +123,8 @@ function registerNotifications(stream: HomeStreamManager) {
 			setTimeout(n.close.bind(n), 6000);
 		});
 
-		connection.on('reply', post => {
-			const _n = composeNotification('reply', post);
+		connection.on('reply', note => {
+			const _n = composeNotification('reply', note);
 			const n = new Notification(_n.title, {
 				body: _n.body,
 				icon: _n.icon
@@ -132,8 +132,8 @@ function registerNotifications(stream: HomeStreamManager) {
 			setTimeout(n.close.bind(n), 6000);
 		});
 
-		connection.on('quote', post => {
-			const _n = composeNotification('quote', post);
+		connection.on('quote', note => {
+			const _n = composeNotification('quote', note);
 			const n = new Notification(_n.title, {
 				body: _n.body,
 				icon: _n.icon
diff --git a/src/client/app/desktop/views/components/activity.calendar.vue b/src/client/app/desktop/views/components/activity.calendar.vue
index 72233e9ac..8b43536c2 100644
--- a/src/client/app/desktop/views/components/activity.calendar.vue
+++ b/src/client/app/desktop/views/components/activity.calendar.vue
@@ -29,7 +29,7 @@ import Vue from 'vue';
 export default Vue.extend({
 	props: ['data'],
 	created() {
-		this.data.forEach(d => d.total = d.posts + d.replies + d.reposts);
+		this.data.forEach(d => d.total = d.notes + d.replies + d.renotes);
 		const peak = Math.max.apply(null, this.data.map(d => d.total));
 
 		let x = 0;
diff --git a/src/client/app/desktop/views/components/activity.chart.vue b/src/client/app/desktop/views/components/activity.chart.vue
index 5057786ed..175c5d37e 100644
--- a/src/client/app/desktop/views/components/activity.chart.vue
+++ b/src/client/app/desktop/views/components/activity.chart.vue
@@ -1,8 +1,8 @@
 <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>
+	<title>Black ... Total<br/>Blue ... Notes<br/>Red ... Replies<br/>Green ... Renotes</title>
 	<polyline
-		:points="pointsPost"
+		:points="pointsNote"
 		fill="none"
 		stroke-width="1"
 		stroke="#41ddde"/>
@@ -12,7 +12,7 @@
 		stroke-width="1"
 		stroke="#f7796c"/>
 	<polyline
-		:points="pointsRepost"
+		:points="pointsRenote"
 		fill="none"
 		stroke-width="1"
 		stroke="#a1de41"/>
@@ -48,24 +48,24 @@ export default Vue.extend({
 			viewBoxY: 60,
 			zoom: 1,
 			pos: 0,
-			pointsPost: null,
+			pointsNote: null,
 			pointsReply: null,
-			pointsRepost: null,
+			pointsRenote: null,
 			pointsTotal: null
 		};
 	},
 	created() {
 		this.data.reverse();
-		this.data.forEach(d => d.total = d.posts + d.replies + d.reposts);
+		this.data.forEach(d => d.total = d.notes + d.replies + d.renotes);
 		this.render();
 	},
 	methods: {
 		render() {
 			const peak = Math.max.apply(null, this.data.map(d => d.total));
 			if (peak != 0) {
-				this.pointsPost = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.posts / peak)) * this.viewBoxY}`).join(' ');
+				this.pointsNote = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.notes / 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.pointsRenote = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.renotes / peak)) * this.viewBoxY}`).join(' ');
 				this.pointsTotal = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ');
 			}
 		},
diff --git a/src/client/app/desktop/views/components/index.ts b/src/client/app/desktop/views/components/index.ts
index 3798bf6d2..4f61f4369 100644
--- a/src/client/app/desktop/views/components/index.ts
+++ b/src/client/app/desktop/views/components/index.ts
@@ -4,23 +4,23 @@ import ui from './ui.vue';
 import uiNotification from './ui-notification.vue';
 import home from './home.vue';
 import timeline from './timeline.vue';
-import posts from './posts.vue';
-import subPostContent from './sub-post-content.vue';
+import notes from './notes.vue';
+import subNoteContent from './sub-note-content.vue';
 import window from './window.vue';
-import postFormWindow from './post-form-window.vue';
-import repostFormWindow from './repost-form-window.vue';
+import noteFormWindow from './post-form-window.vue';
+import renoteFormWindow from './renote-form-window.vue';
 import analogClock from './analog-clock.vue';
 import ellipsisIcon from './ellipsis-icon.vue';
 import mediaImage from './media-image.vue';
 import mediaImageDialog from './media-image-dialog.vue';
 import mediaVideo from './media-video.vue';
 import notifications from './notifications.vue';
-import postForm from './post-form.vue';
-import repostForm from './repost-form.vue';
+import noteForm from './post-form.vue';
+import renoteForm from './renote-form.vue';
 import followButton from './follow-button.vue';
-import postPreview from './post-preview.vue';
+import notePreview from './note-preview.vue';
 import drive from './drive.vue';
-import postDetail from './post-detail.vue';
+import noteDetail from './note-detail.vue';
 import settings from './settings.vue';
 import calendar from './calendar.vue';
 import activity from './activity.vue';
@@ -34,23 +34,23 @@ Vue.component('mk-ui', ui);
 Vue.component('mk-ui-notification', uiNotification);
 Vue.component('mk-home', home);
 Vue.component('mk-timeline', timeline);
-Vue.component('mk-posts', posts);
-Vue.component('mk-sub-post-content', subPostContent);
+Vue.component('mk-notes', notes);
+Vue.component('mk-sub-note-content', subNoteContent);
 Vue.component('mk-window', window);
-Vue.component('mk-post-form-window', postFormWindow);
-Vue.component('mk-repost-form-window', repostFormWindow);
+Vue.component('mk-post-form-window', noteFormWindow);
+Vue.component('mk-renote-form-window', renoteFormWindow);
 Vue.component('mk-analog-clock', analogClock);
 Vue.component('mk-ellipsis-icon', ellipsisIcon);
 Vue.component('mk-media-image', mediaImage);
 Vue.component('mk-media-image-dialog', mediaImageDialog);
 Vue.component('mk-media-video', mediaVideo);
 Vue.component('mk-notifications', notifications);
-Vue.component('mk-post-form', postForm);
-Vue.component('mk-repost-form', repostForm);
+Vue.component('mk-post-form', noteForm);
+Vue.component('mk-renote-form', renoteForm);
 Vue.component('mk-follow-button', followButton);
-Vue.component('mk-post-preview', postPreview);
+Vue.component('mk-note-preview', notePreview);
 Vue.component('mk-drive', drive);
-Vue.component('mk-post-detail', postDetail);
+Vue.component('mk-note-detail', noteDetail);
 Vue.component('mk-settings', settings);
 Vue.component('mk-calendar', calendar);
 Vue.component('mk-activity', activity);
diff --git a/src/client/app/desktop/views/components/mentions.vue b/src/client/app/desktop/views/components/mentions.vue
index 90a92495b..fc3a7af75 100644
--- a/src/client/app/desktop/views/components/mentions.vue
+++ b/src/client/app/desktop/views/components/mentions.vue
@@ -7,12 +7,12 @@
 	<div class="fetching" v-if="fetching">
 		<mk-ellipsis-icon/>
 	</div>
-	<p class="empty" v-if="posts.length == 0 && !fetching">
+	<p class="empty" v-if="notes.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"/>
+	<mk-notes :notes="notes" ref="timeline"/>
 </div>
 </template>
 
@@ -24,7 +24,7 @@ export default Vue.extend({
 			fetching: true,
 			moreFetching: false,
 			mode: 'all',
-			posts: []
+			notes: []
 		};
 	},
 	watch: {
@@ -56,23 +56,23 @@ export default Vue.extend({
 		},
 		fetch(cb?) {
 			this.fetching = true;
-			this.posts =  [];
-			(this as any).api('posts/mentions', {
+			this.notes =  [];
+			(this as any).api('notes/mentions', {
 				following: this.mode == 'following'
-			}).then(posts => {
-				this.posts = posts;
+			}).then(notes => {
+				this.notes = notes;
 				this.fetching = false;
 				if (cb) cb();
 			});
 		},
 		more() {
-			if (this.moreFetching || this.fetching || this.posts.length == 0) return;
+			if (this.moreFetching || this.fetching || this.notes.length == 0) return;
 			this.moreFetching = true;
-			(this as any).api('posts/mentions', {
+			(this as any).api('notes/mentions', {
 				following: this.mode == 'following',
-				untilId: this.posts[this.posts.length - 1].id
-			}).then(posts => {
-				this.posts = this.posts.concat(posts);
+				untilId: this.notes[this.notes.length - 1].id
+			}).then(notes => {
+				this.notes = this.notes.concat(notes);
 				this.moreFetching = false;
 			});
 		}
diff --git a/src/client/app/desktop/views/components/post-detail.sub.vue b/src/client/app/desktop/views/components/note-detail.sub.vue
similarity index 76%
rename from src/client/app/desktop/views/components/post-detail.sub.vue
rename to src/client/app/desktop/views/components/note-detail.sub.vue
index 496003eb8..0159d1ad4 100644
--- a/src/client/app/desktop/views/components/post-detail.sub.vue
+++ b/src/client/app/desktop/views/components/note-detail.sub.vue
@@ -1,24 +1,24 @@
 <template>
 <div class="sub" :title="title">
 	<router-link class="avatar-anchor" :to="`/@${acct}`">
-		<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="post.userId"/>
+		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
 	</router-link>
 	<div class="main">
 		<header>
 			<div class="left">
-				<router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ getUserName(post.user) }}</router-link>
+				<router-link class="name" :to="`/@${acct}`" v-user-preview="note.userId">{{ getUserName(note.user) }}</router-link>
 				<span class="username">@{{ acct }}</span>
 			</div>
 			<div class="right">
-				<router-link class="time" :to="`/@${acct}/${post.id}`">
-					<mk-time :time="post.createdAt"/>
+				<router-link class="time" :to="`/@${acct}/${note.id}`">
+					<mk-time :time="note.createdAt"/>
 				</router-link>
 			</div>
 		</header>
 		<div class="body">
-			<mk-post-html v-if="post.text" :text="post.text" :i="os.i" :class="$style.text"/>
-			<div class="media" v-if="post.media > 0">
-				<mk-media-list :media-list="post.media"/>
+			<mk-note-html v-if="note.text" :text="note.text" :i="os.i" :class="$style.text"/>
+			<div class="media" v-if="note.media > 0">
+				<mk-media-list :media-list="note.media"/>
 			</div>
 		</div>
 	</div>
@@ -32,16 +32,16 @@ import getAcct from '../../../../../acct/render';
 import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
-	props: ['post'],
+	props: ['note'],
 	computed: {
 		acct() {
-			return getAcct(this.post.user);
+			return getAcct(this.note.user);
 		},
 		name() {
-			return getUserName(this.post.user);
+			return getUserName(this.note.user);
 		},
 		title(): string {
-			return dateStringify(this.post.createdAt);
+			return dateStringify(this.note.createdAt);
 		}
 	}
 });
diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue
new file mode 100644
index 000000000..790f797ad
--- /dev/null
+++ b/src/client/app/desktop/views/components/note-detail.vue
@@ -0,0 +1,448 @@
+<template>
+<div class="mk-note-detail" :title="title">
+	<button
+		class="read-more"
+		v-if="p.reply && p.reply.replyId && context == null"
+		title="会話をもっと読み込む"
+		@click="fetchContext"
+		:disabled="contextFetching"
+	>
+		<template v-if="!contextFetching">%fa:ellipsis-v%</template>
+		<template v-if="contextFetching">%fa:spinner .pulse%</template>
+	</button>
+	<div class="context">
+		<x-sub v-for="note in context" :key="note.id" :note="note"/>
+	</div>
+	<div class="reply-to" v-if="p.reply">
+		<x-sub :note="p.reply"/>
+	</div>
+	<div class="renote" v-if="isRenote">
+		<p>
+			<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="note.userId">
+				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
+			</router-link>
+			%fa:retweet%
+			<router-link class="name" :href="`/@${acct}`">{{ getUserName(note.user) }}</router-link>
+			がRenote
+		</p>
+	</div>
+	<article>
+		<router-link class="avatar-anchor" :to="`/@${pAcct}`">
+			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
+		</router-link>
+		<header>
+			<router-link class="name" :to="`/@${pAcct}`" v-user-preview="p.user.id">{{ getUserName(p.user) }}</router-link>
+			<span class="username">@{{ pAcct }}</span>
+			<router-link class="time" :to="`/@${pAcct}/${p.id}`">
+				<mk-time :time="p.createdAt"/>
+			</router-link>
+		</header>
+		<div class="body">
+			<mk-note-html :class="$style.text" v-if="p.text" :text="p.text" :i="os.i"/>
+			<div class="media" v-if="p.media.length > 0">
+				<mk-media-list :media-list="p.media"/>
+			</div>
+			<mk-poll v-if="p.poll" :note="p"/>
+			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
+			<div class="tags" v-if="p.tags && p.tags.length > 0">
+				<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
+			</div>
+			<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
+			<div class="map" v-if="p.geo" ref="map"></div>
+			<div class="renote" v-if="p.renote">
+				<mk-note-preview :note="p.renote"/>
+			</div>
+		</div>
+		<footer>
+			<mk-reactions-viewer :note="p"/>
+			<button @click="reply" title="返信">
+				%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
+			</button>
+			<button @click="renote" title="Renote">
+				%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
+			</button>
+			<button :class="{ reacted: p.myReaction != 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">
+		<x-sub v-for="note in replies" :key="note.id" :note="note"/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import dateStringify from '../../../common/scripts/date-stringify';
+import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
+import parse from '../../../../../text/parse';
+
+import MkPostFormWindow from './post-form-window.vue';
+import MkRenoteFormWindow from './renote-form-window.vue';
+import MkNoteMenu from '../../../common/views/components/note-menu.vue';
+import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
+import XSub from './note-detail.sub.vue';
+
+export default Vue.extend({
+	components: {
+		XSub
+	},
+
+	props: {
+		note: {
+			type: Object,
+			required: true
+		},
+		compact: {
+			default: false
+		}
+	},
+
+	data() {
+		return {
+			context: [],
+			contextFetching: false,
+			replies: []
+		};
+	},
+
+	computed: {
+		isRenote(): boolean {
+			return (this.note.renote &&
+				this.note.text == null &&
+				this.note.mediaIds == null &&
+				this.note.poll == null);
+		},
+		p(): any {
+			return this.isRenote ? this.note.renote : this.note;
+		},
+		reactionsCount(): number {
+			return this.p.reactionCounts
+				? Object.keys(this.p.reactionCounts)
+					.map(key => this.p.reactionCounts[key])
+					.reduce((a, b) => a + b)
+				: 0;
+		},
+		title(): string {
+			return dateStringify(this.p.createdAt);
+		},
+		acct(): string {
+			return getAcct(this.note.user);
+		},
+		name(): string {
+			return getUserName(this.note.user);
+		},
+		pAcct(): string {
+			return getAcct(this.p.user);
+		},
+		pName(): string {
+			return getUserName(this.p.user);
+		},
+		urls(): string[] {
+			if (this.p.text) {
+				const ast = parse(this.p.text);
+				return 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 as any).api('notes/replies', {
+				noteId: this.p.id,
+				limit: 8
+			}).then(replies => {
+				this.replies = replies;
+			});
+		}
+
+		// Draw map
+		if (this.p.geo) {
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true;
+			if (shouldShowMap) {
+				(this as any).os.getGoogleMaps().then(maps => {
+					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
+					const map = new maps.Map(this.$refs.map, {
+						center: uluru,
+						zoom: 15
+					});
+					new maps.Marker({
+						position: uluru,
+						map: map
+					});
+				});
+			}
+		}
+	},
+
+	methods: {
+		fetchContext() {
+			this.contextFetching = true;
+
+			// Fetch context
+			(this as any).api('notes/context', {
+				noteId: this.p.replyId
+			}).then(context => {
+				this.contextFetching = false;
+				this.context = context.reverse();
+			});
+		},
+		reply() {
+			(this as any).os.new(MkPostFormWindow, {
+				reply: this.p
+			});
+		},
+		renote() {
+			(this as any).os.new(MkRenoteFormWindow, {
+				note: this.p
+			});
+		},
+		react() {
+			(this as any).os.new(MkReactionPicker, {
+				source: this.$refs.reactButton,
+				note: this.p
+			});
+		},
+		menu() {
+			(this as any).os.new(MkNoteMenu, {
+				source: this.$refs.menuButton,
+				note: this.p
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+.mk-note-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
+
+	> .renote
+		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
+
+			> .renote
+				margin 8px 0
+
+				> .mk-note-preview
+					padding 16px
+					border dashed 1px #c0dac6
+					border-radius 8px
+
+			> .location
+				margin 4px 0
+				font-size 12px
+				color #ccc
+
+			> .map
+				width 100%
+				height 300px
+
+				&:empty
+					display none
+
+			> .mk-url-preview
+				margin-top 8px
+
+			> .tags
+				margin 4px 0 0 0
+
+				> *
+					display inline-block
+					margin 0 8px 0 0
+					padding 2px 8px 2px 16px
+					font-size 90%
+					color #8d969e
+					background #edf0f3
+					border-radius 4px
+
+					&:before
+						content ""
+						display block
+						position absolute
+						top 0
+						bottom 0
+						left 4px
+						width 8px
+						height 8px
+						margin auto 0
+						background #fff
+						border-radius 100%
+
+					&:hover
+						text-decoration none
+						background #e2e7ec
+
+		> 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>
+
+<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/client/app/desktop/views/components/post-preview.vue b/src/client/app/desktop/views/components/note-preview.vue
similarity index 72%
rename from src/client/app/desktop/views/components/post-preview.vue
rename to src/client/app/desktop/views/components/note-preview.vue
index 99d9442d9..bff199c09 100644
--- a/src/client/app/desktop/views/components/post-preview.vue
+++ b/src/client/app/desktop/views/components/note-preview.vue
@@ -1,18 +1,18 @@
 <template>
-<div class="mk-post-preview" :title="title">
+<div class="mk-note-preview" :title="title">
 	<router-link class="avatar-anchor" :to="`/@${acct}`">
-		<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="post.userId"/>
+		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ name }}</router-link>
+			<router-link class="name" :to="`/@${acct}`" v-user-preview="note.userId">{{ name }}</router-link>
 			<span class="username">@{{ acct }}</span>
-			<router-link class="time" :to="`/@${acct}/${post.id}`">
-				<mk-time :time="post.createdAt"/>
+			<router-link class="time" :to="`/@${acct}/${note.id}`">
+				<mk-time :time="note.createdAt"/>
 			</router-link>
 		</header>
 		<div class="body">
-			<mk-sub-post-content class="text" :post="post"/>
+			<mk-sub-note-content class="text" :note="note"/>
 		</div>
 	</div>
 </div>
@@ -25,23 +25,23 @@ import getAcct from '../../../../../acct/render';
 import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
-	props: ['post'],
+	props: ['note'],
 	computed: {
 		acct() {
-			return getAcct(this.post.user);
+			return getAcct(this.note.user);
 		},
 		name() {
-			return getUserName(this.post.user);
+			return getUserName(this.note.user);
 		},
 		title(): string {
-			return dateStringify(this.post.createdAt);
+			return dateStringify(this.note.createdAt);
 		}
 	}
 });
 </script>
 
 <style lang="stylus" scoped>
-.mk-post-preview
+.mk-note-preview
 	font-size 0.9em
 	background #fff
 
diff --git a/src/client/app/desktop/views/components/posts.post.sub.vue b/src/client/app/desktop/views/components/notes.note.sub.vue
similarity index 76%
rename from src/client/app/desktop/views/components/posts.post.sub.vue
rename to src/client/app/desktop/views/components/notes.note.sub.vue
index a9cd0a927..b49d12b92 100644
--- a/src/client/app/desktop/views/components/posts.post.sub.vue
+++ b/src/client/app/desktop/views/components/notes.note.sub.vue
@@ -1,18 +1,18 @@
 <template>
 <div class="sub" :title="title">
 	<router-link class="avatar-anchor" :to="`/@${acct}`">
-		<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="post.userId"/>
+		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ name }}</router-link>
+			<router-link class="name" :to="`/@${acct}`" v-user-preview="note.userId">{{ name }}</router-link>
 			<span class="username">@{{ acct }}</span>
-			<router-link class="created-at" :to="`/@${acct}/${post.id}`">
-				<mk-time :time="post.createdAt"/>
+			<router-link class="created-at" :to="`/@${acct}/${note.id}`">
+				<mk-time :time="note.createdAt"/>
 			</router-link>
 		</header>
 		<div class="body">
-			<mk-sub-post-content class="text" :post="post"/>
+			<mk-sub-note-content class="text" :note="note"/>
 		</div>
 	</div>
 </div>
@@ -25,16 +25,16 @@ import getAcct from '../../../../../acct/render';
 import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
-	props: ['post'],
+	props: ['note'],
 	computed: {
 		acct() {
-			return getAcct(this.post.user);
+			return getAcct(this.note.user);
 		},
 		name(): string {
-			return getUserName(this.post.user);
+			return getUserName(this.note.user);
 		},
 		title(): string {
-			return dateStringify(this.post.createdAt);
+			return dateStringify(this.note.createdAt);
 		}
 	}
 });
diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue
new file mode 100644
index 000000000..924642c01
--- /dev/null
+++ b/src/client/app/desktop/views/components/notes.note.vue
@@ -0,0 +1,596 @@
+<template>
+<div class="note" tabindex="-1" :title="title" @keydown="onKeydown">
+	<div class="reply-to" v-if="p.reply">
+		<x-sub :note="p.reply"/>
+	</div>
+	<div class="renote" v-if="isRenote">
+		<p>
+			<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="note.userId">
+				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
+			</router-link>
+			%fa:retweet%
+			<span>{{ '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.indexOf('{')) }}</span>
+			<a class="name" :href="`/@${acct}`" v-user-preview="note.userId">{{ getUserName(note.user) }}</a>
+			<span>{{ '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-note.reposted-by%'.indexOf('}') + 1) }}</span>
+		</p>
+		<mk-time :time="note.createdAt"/>
+	</div>
+	<article>
+		<router-link class="avatar-anchor" :to="`/@${acct}`">
+			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
+		</router-link>
+		<div class="main">
+			<header>
+				<router-link class="name" :to="`/@${acct}`" v-user-preview="p.user.id">{{ acct }}</router-link>
+				<span class="is-bot" v-if="p.user.host === null && p.user.account.isBot">bot</span>
+				<span class="username">@{{ acct }}</span>
+				<div class="info">
+					<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
+					<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
+					<router-link class="created-at" :to="url">
+						<mk-time :time="p.createdAt"/>
+					</router-link>
+				</div>
+			</header>
+			<div class="body">
+				<p class="channel" v-if="p.channel">
+					<a :href="`${_CH_URL_}/${p.channel.id}`" target="_blank">{{ p.channel.title }}</a>:
+				</p>
+				<div class="text">
+					<a class="reply" v-if="p.reply">%fa:reply%</a>
+					<mk-note-html v-if="p.textHtml" :text="p.text" :i="os.i" :class="$style.text"/>
+					<a class="rp" v-if="p.renote">RP:</a>
+				</div>
+				<div class="media" v-if="p.media.length > 0">
+					<mk-media-list :media-list="p.media"/>
+				</div>
+				<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
+				<div class="tags" v-if="p.tags && p.tags.length > 0">
+					<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
+				</div>
+				<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
+				<div class="map" v-if="p.geo" ref="map"></div>
+				<div class="renote" v-if="p.renote">
+					<mk-note-preview :note="p.renote"/>
+				</div>
+				<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
+			</div>
+			<footer>
+				<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
+				<button @click="reply" title="%i18n:desktop.tags.mk-timeline-note.reply%">
+					%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
+				</button>
+				<button @click="renote" title="%i18n:desktop.tags.mk-timeline-note.renote%">
+					%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
+				</button>
+				<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:desktop.tags.mk-timeline-note.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 title="%i18n:desktop.tags.mk-timeline-note.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-note-status-graph width="462" height="130" :note="p"/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import dateStringify from '../../../common/scripts/date-stringify';
+import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
+import parse from '../../../../../text/parse';
+
+import MkPostFormWindow from './post-form-window.vue';
+import MkRenoteFormWindow from './renote-form-window.vue';
+import MkNoteMenu from '../../../common/views/components/note-menu.vue';
+import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
+import XSub from './notes.note.sub.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({
+	components: {
+		XSub
+	},
+
+	props: ['note'],
+
+	data() {
+		return {
+			isDetailOpened: false,
+			connection: null,
+			connectionId: null
+		};
+	},
+
+	computed: {
+		acct(): string {
+			return getAcct(this.p.user);
+		},
+		name(): string {
+			return getUserName(this.p.user);
+		},
+		isRenote(): boolean {
+			return (this.note.renote &&
+				this.note.text == null &&
+				this.note.mediaIds == null &&
+				this.note.poll == null);
+		},
+		p(): any {
+			return this.isRenote ? this.note.renote : this.note;
+		},
+		reactionsCount(): number {
+			return this.p.reactionCounts
+				? Object.keys(this.p.reactionCounts)
+					.map(key => this.p.reactionCounts[key])
+					.reduce((a, b) => a + b)
+				: 0;
+		},
+		title(): string {
+			return dateStringify(this.p.createdAt);
+		},
+		url(): string {
+			return `/@${this.acct}/${this.p.id}`;
+		},
+		urls(): string[] {
+			if (this.p.text) {
+				const ast = parse(this.p.text);
+				return ast
+					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
+					.map(t => t.url);
+			} else {
+				return null;
+			}
+		}
+	},
+
+	created() {
+		if ((this as any).os.isSignedIn) {
+			this.connection = (this as any).os.stream.getConnection();
+			this.connectionId = (this as any).os.stream.use();
+		}
+	},
+
+	mounted() {
+		this.capture(true);
+
+		if ((this as any).os.isSignedIn) {
+			this.connection.on('_connected_', this.onStreamConnected);
+		}
+
+		// Draw map
+		if (this.p.geo) {
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true;
+			if (shouldShowMap) {
+				(this as any).os.getGoogleMaps().then(maps => {
+					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
+					const map = new maps.Map(this.$refs.map, {
+						center: uluru,
+						zoom: 15
+					});
+					new maps.Marker({
+						position: uluru,
+						map: map
+					});
+				});
+			}
+		}
+	},
+
+	beforeDestroy() {
+		this.decapture(true);
+
+		if ((this as any).os.isSignedIn) {
+			this.connection.off('_connected_', this.onStreamConnected);
+			(this as any).os.stream.dispose(this.connectionId);
+		}
+	},
+
+	methods: {
+		capture(withHandler = false) {
+			if ((this as any).os.isSignedIn) {
+				this.connection.send({
+					type: 'capture',
+					id: this.p.id
+				});
+				if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
+			}
+		},
+		decapture(withHandler = false) {
+			if ((this as any).os.isSignedIn) {
+				this.connection.send({
+					type: 'decapture',
+					id: this.p.id
+				});
+				if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated);
+			}
+		},
+		onStreamConnected() {
+			this.capture();
+		},
+		onStreamNoteUpdated(data) {
+			const note = data.note;
+			if (note.id == this.note.id) {
+				this.$emit('update:note', note);
+			} else if (note.id == this.note.renoteId) {
+				this.note.renote = note;
+			}
+		},
+		reply() {
+			(this as any).os.new(MkPostFormWindow, {
+				reply: this.p
+			});
+		},
+		renote() {
+			(this as any).os.new(MkRenoteFormWindow, {
+				note: this.p
+			});
+		},
+		react() {
+			(this as any).os.new(MkReactionPicker, {
+				source: this.$refs.reactButton,
+				note: this.p
+			});
+		},
+		menu() {
+			(this as any).os.new(MkNoteMenu, {
+				source: this.$refs.menuButton,
+				note: this.p
+			});
+		},
+		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.renote();
+					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>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+.note
+	margin 0
+	padding 0
+	background #fff
+	border-bottom solid 1px #eaeaea
+
+	&:first-child
+		border-top-left-radius 6px
+		border-top-right-radius 6px
+
+		> .renote
+			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
+
+	> .renote
+		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-note-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
+				align-items center
+				margin-bottom 4px
+				white-space nowrap
+
+				> .name
+					display block
+					margin 0 .5em 0 0
+					padding 0
+					overflow hidden
+					color #627079
+					font-size 1em
+					font-weight bold
+					text-decoration none
+					text-overflow ellipsis
+
+					&:hover
+						text-decoration underline
+
+				> .is-bot
+					margin 0 .5em 0 0
+					padding 1px 6px
+					font-size 12px
+					color #aaa
+					border solid 1px #ddd
+					border-radius 3px
+
+				> .username
+					margin 0 .5em 0 0
+					color #ccc
+
+				> .info
+					margin-left auto
+					font-size 0.9em
+
+					> .mobile
+						margin-right 8px
+						color #ccc
+
+					> .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
+
+					>>> .quote
+						margin 8px
+						padding 6px 12px
+						color #aaa
+						border-left solid 3px #eee
+
+					> .reply
+						margin-right 8px
+						color #717171
+
+					> .rp
+						margin-left 4px
+						font-style oblique
+						color #a0bf46
+
+				> .location
+					margin 4px 0
+					font-size 12px
+					color #ccc
+
+				> .map
+					width 100%
+					height 300px
+
+					&:empty
+						display none
+
+				> .tags
+					margin 4px 0 0 0
+
+					> *
+						display inline-block
+						margin 0 8px 0 0
+						padding 2px 8px 2px 16px
+						font-size 90%
+						color #8d969e
+						background #edf0f3
+						border-radius 4px
+
+						&:before
+							content ""
+							display block
+							position absolute
+							top 0
+							bottom 0
+							left 4px
+							width 8px
+							height 8px
+							margin auto 0
+							background #fff
+							border-radius 100%
+
+						&:hover
+							text-decoration none
+							background #e2e7ec
+
+				.mk-url-preview
+					margin-top 8px
+
+				> .channel
+					margin 0
+
+				> .mk-poll
+					font-size 80%
+
+				> .renote
+					margin 8px 0
+
+					> .mk-note-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>
+
+<style lang="stylus" module>
+.text
+
+	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
+</style>
diff --git a/src/client/app/desktop/views/components/posts.vue b/src/client/app/desktop/views/components/notes.vue
similarity index 56%
rename from src/client/app/desktop/views/components/posts.vue
rename to src/client/app/desktop/views/components/notes.vue
index 5031667c7..b5f6957a1 100644
--- a/src/client/app/desktop/views/components/posts.vue
+++ b/src/client/app/desktop/views/components/notes.vue
@@ -1,10 +1,10 @@
 <template>
-<div class="mk-posts">
-	<template v-for="(post, i) in _posts">
-		<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>
+<div class="mk-notes">
+	<template v-for="(note, i) in _notes">
+		<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
+		<p class="date" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
+			<span>%fa:angle-up%{{ note._datetext }}</span>
+			<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
 		</p>
 	</template>
 	<footer>
@@ -15,26 +15,26 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import XPost from './posts.post.vue';
+import XNote from './notes.note.vue';
 
 export default Vue.extend({
 	components: {
-		XPost
+		XNote
 	},
 	props: {
-		posts: {
+		notes: {
 			type: Array,
 			default: () => []
 		}
 	},
 	computed: {
-		_posts(): any[] {
-			return (this.posts as any).map(post => {
-				const date = new Date(post.createdAt).getDate();
-				const month = new Date(post.createdAt).getMonth() + 1;
-				post._date = date;
-				post._datetext = `${month}月 ${date}日`;
-				return post;
+		_notes(): any[] {
+			return (this.notes as any).map(note => {
+				const date = new Date(note.createdAt).getDate();
+				const month = new Date(note.createdAt).getMonth() + 1;
+				note._date = date;
+				note._datetext = `${month}月 ${date}日`;
+				return note;
 			});
 		}
 	},
@@ -42,15 +42,15 @@ export default Vue.extend({
 		focus() {
 			(this.$el as any).children[0].focus();
 		},
-		onPostUpdated(i, post) {
-			Vue.set((this as any).posts, i, post);
+		onNoteUpdated(i, note) {
+			Vue.set((this as any).notes, i, note);
 		}
 	}
 });
 </script>
 
 <style lang="stylus" scoped>
-.mk-posts
+.mk-notes
 
 	> .date
 		display block
diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
index d8b8eab21..100a803cc 100644
--- a/src/client/app/desktop/views/components/notifications.vue
+++ b/src/client/app/desktop/views/components/notifications.vue
@@ -13,33 +13,33 @@
 							<mk-reaction-icon :reaction="notification.reaction"/>
 							<router-link :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ getUserName(notification.user) }}</router-link>
 						</p>
-						<router-link class="post-ref" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">
-							%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%
+						<router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">
+							%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%
 						</router-link>
 					</div>
 				</template>
-				<template v-if="notification.type == 'repost'">
-					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">
-						<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
+				<template v-if="notification.type == 'renote'">
+					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">
+						<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:retweet%
-							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ getUserName(notification.post.user) }}</router-link>
+							<router-link :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">{{ getUserName(notification.note.user) }}</router-link>
 						</p>
-						<router-link class="post-ref" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">
-							%fa:quote-left%{{ getPostSummary(notification.post.repost) }}%fa:quote-right%
+						<router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">
+							%fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right%
 						</router-link>
 					</div>
 				</template>
 				<template v-if="notification.type == 'quote'">
-					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">
-						<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
+					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">
+						<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:quote-left%
-							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ getUserName(notification.post.user) }}</router-link>
+							<router-link :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">{{ getUserName(notification.note.user) }}</router-link>
 						</p>
-						<router-link class="post-preview" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link>
+						<router-link class="note-preview" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">{{ getNoteSummary(notification.note) }}</router-link>
 					</div>
 				</template>
 				<template v-if="notification.type == 'follow'">
@@ -53,25 +53,25 @@
 					</div>
 				</template>
 				<template v-if="notification.type == 'reply'">
-					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">
-						<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
+					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">
+						<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:reply%
-							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ getUserName(notification.post.user) }}</router-link>
+							<router-link :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">{{ getUserName(notification.note.user) }}</router-link>
 						</p>
-						<router-link class="post-preview" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link>
+						<router-link class="note-preview" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">{{ getNoteSummary(notification.note) }}</router-link>
 					</div>
 				</template>
 				<template v-if="notification.type == 'mention'">
-					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">
-						<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
+					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">
+						<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:at%
-							<router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ getUserName(notification.post.user) }}</router-link>
+							<router-link :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">{{ getUserName(notification.note.user) }}</router-link>
 						</p>
-						<a class="post-preview" :href="`/@${getAcct(notification.post.user)}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</a>
+						<a class="note-preview" :href="`/@${getAcct(notification.note.user)}/${notification.note.id}`">{{ getNoteSummary(notification.note) }}</a>
 					</div>
 				</template>
 				<template v-if="notification.type == 'poll_vote'">
@@ -80,8 +80,8 @@
 					</router-link>
 					<div class="text">
 						<p>%fa:chart-pie%<a :href="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ getUserName(notification.user) }}</a></p>
-						<router-link class="post-ref" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">
-							%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%
+						<router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">
+							%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%
 						</router-link>
 					</div>
 				</template>
@@ -103,7 +103,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../acct/render';
-import getPostSummary from '../../../../../renderers/get-post-summary';
+import getNoteSummary from '../../../../../renderers/get-note-summary';
 import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
@@ -115,7 +115,7 @@ export default Vue.extend({
 			moreNotifications: false,
 			connection: null,
 			connectionId: null,
-			getPostSummary
+			getNoteSummary
 		};
 	},
 	computed: {
@@ -241,10 +241,10 @@ export default Vue.extend({
 					i, .mk-reaction-icon
 						margin-right 4px
 
-			.post-preview
+			.note-preview
 				color rgba(0, 0, 0, 0.7)
 
-			.post-ref
+			.note-ref
 				color rgba(0, 0, 0, 0.7)
 
 				[data-fa]
@@ -254,7 +254,7 @@ export default Vue.extend({
 					display inline-block
 					margin-right 3px
 
-			&.repost, &.quote
+			&.renote, &.quote
 				.text p i
 					color #77B255
 
diff --git a/src/client/app/desktop/views/components/post-detail.vue b/src/client/app/desktop/views/components/post-detail.vue
index 1a3c0d1b6..790f797ad 100644
--- a/src/client/app/desktop/views/components/post-detail.vue
+++ b/src/client/app/desktop/views/components/post-detail.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-post-detail" :title="title">
+<div class="mk-note-detail" :title="title">
 	<button
 		class="read-more"
 		v-if="p.reply && p.reply.replyId && context == null"
@@ -11,19 +11,19 @@
 		<template v-if="contextFetching">%fa:spinner .pulse%</template>
 	</button>
 	<div class="context">
-		<x-sub v-for="post in context" :key="post.id" :post="post"/>
+		<x-sub v-for="note in context" :key="note.id" :note="note"/>
 	</div>
 	<div class="reply-to" v-if="p.reply">
-		<x-sub :post="p.reply"/>
+		<x-sub :note="p.reply"/>
 	</div>
-	<div class="repost" v-if="isRepost">
+	<div class="renote" v-if="isRenote">
 		<p>
-			<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="post.userId">
-				<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
+			<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="note.userId">
+				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
 			</router-link>
 			%fa:retweet%
-			<router-link class="name" :href="`/@${acct}`">{{ getUserName(post.user) }}</router-link>
-			がRepost
+			<router-link class="name" :href="`/@${acct}`">{{ getUserName(note.user) }}</router-link>
+			がRenote
 		</p>
 	</div>
 	<article>
@@ -38,28 +38,28 @@
 			</router-link>
 		</header>
 		<div class="body">
-			<mk-post-html :class="$style.text" v-if="p.text" :text="p.text" :i="os.i"/>
+			<mk-note-html :class="$style.text" v-if="p.text" :text="p.text" :i="os.i"/>
 			<div class="media" v-if="p.media.length > 0">
 				<mk-media-list :media-list="p.media"/>
 			</div>
-			<mk-poll v-if="p.poll" :post="p"/>
+			<mk-poll v-if="p.poll" :note="p"/>
 			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 			<div class="tags" v-if="p.tags && p.tags.length > 0">
 				<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
 			</div>
 			<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
 			<div class="map" v-if="p.geo" ref="map"></div>
-			<div class="repost" v-if="p.repost">
-				<mk-post-preview :post="p.repost"/>
+			<div class="renote" v-if="p.renote">
+				<mk-note-preview :note="p.renote"/>
 			</div>
 		</div>
 		<footer>
-			<mk-reactions-viewer :post="p"/>
+			<mk-reactions-viewer :note="p"/>
 			<button @click="reply" title="返信">
 				%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
 			</button>
-			<button @click="repost" title="Repost">
-				%fa:retweet%<p class="count" v-if="p.repostCount > 0">{{ p.repostCount }}</p>
+			<button @click="renote" title="Renote">
+				%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
 			</button>
 			<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="リアクション">
 				%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
@@ -70,7 +70,7 @@
 		</footer>
 	</article>
 	<div class="replies" v-if="!compact">
-		<x-sub v-for="post in replies" :key="post.id" :post="post"/>
+		<x-sub v-for="note in replies" :key="note.id" :note="note"/>
 	</div>
 </div>
 </template>
@@ -83,10 +83,10 @@ import getUserName from '../../../../../renderers/get-user-name';
 import parse from '../../../../../text/parse';
 
 import MkPostFormWindow from './post-form-window.vue';
-import MkRepostFormWindow from './repost-form-window.vue';
-import MkPostMenu from '../../../common/views/components/post-menu.vue';
+import MkRenoteFormWindow from './renote-form-window.vue';
+import MkNoteMenu from '../../../common/views/components/note-menu.vue';
 import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
-import XSub from './post-detail.sub.vue';
+import XSub from './note-detail.sub.vue';
 
 export default Vue.extend({
 	components: {
@@ -94,7 +94,7 @@ export default Vue.extend({
 	},
 
 	props: {
-		post: {
+		note: {
 			type: Object,
 			required: true
 		},
@@ -112,14 +112,14 @@ export default Vue.extend({
 	},
 
 	computed: {
-		isRepost(): boolean {
-			return (this.post.repost &&
-				this.post.text == null &&
-				this.post.mediaIds == null &&
-				this.post.poll == null);
+		isRenote(): boolean {
+			return (this.note.renote &&
+				this.note.text == null &&
+				this.note.mediaIds == null &&
+				this.note.poll == null);
 		},
 		p(): any {
-			return this.isRepost ? this.post.repost : this.post;
+			return this.isRenote ? this.note.renote : this.note;
 		},
 		reactionsCount(): number {
 			return this.p.reactionCounts
@@ -132,10 +132,10 @@ export default Vue.extend({
 			return dateStringify(this.p.createdAt);
 		},
 		acct(): string {
-			return getAcct(this.post.user);
+			return getAcct(this.note.user);
 		},
 		name(): string {
-			return getUserName(this.post.user);
+			return getUserName(this.note.user);
 		},
 		pAcct(): string {
 			return getAcct(this.p.user);
@@ -158,8 +158,8 @@ export default Vue.extend({
 	mounted() {
 		// Get replies
 		if (!this.compact) {
-			(this as any).api('posts/replies', {
-				postId: this.p.id,
+			(this as any).api('notes/replies', {
+				noteId: this.p.id,
 				limit: 8
 			}).then(replies => {
 				this.replies = replies;
@@ -190,8 +190,8 @@ export default Vue.extend({
 			this.contextFetching = true;
 
 			// Fetch context
-			(this as any).api('posts/context', {
-				postId: this.p.replyId
+			(this as any).api('notes/context', {
+				noteId: this.p.replyId
 			}).then(context => {
 				this.contextFetching = false;
 				this.context = context.reverse();
@@ -202,21 +202,21 @@ export default Vue.extend({
 				reply: this.p
 			});
 		},
-		repost() {
-			(this as any).os.new(MkRepostFormWindow, {
-				post: this.p
+		renote() {
+			(this as any).os.new(MkRenoteFormWindow, {
+				note: this.p
 			});
 		},
 		react() {
 			(this as any).os.new(MkReactionPicker, {
 				source: this.$refs.reactButton,
-				post: this.p
+				note: this.p
 			});
 		},
 		menu() {
-			(this as any).os.new(MkPostMenu, {
+			(this as any).os.new(MkNoteMenu, {
 				source: this.$refs.menuButton,
-				post: this.p
+				note: this.p
 			});
 		}
 	}
@@ -226,7 +226,7 @@ export default Vue.extend({
 <style lang="stylus" scoped>
 @import '~const.styl'
 
-.mk-post-detail
+.mk-note-detail
 	margin 0
 	padding 0
 	overflow hidden
@@ -263,7 +263,7 @@ export default Vue.extend({
 		> *
 			border-bottom 1px solid #eef0f2
 
-	> .repost
+	> .renote
 		color #9dbb00
 		background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
 
@@ -355,10 +355,10 @@ export default Vue.extend({
 		> .body
 			padding 8px 0
 
-			> .repost
+			> .renote
 				margin 8px 0
 
-				> .mk-post-preview
+				> .mk-note-preview
 					padding 16px
 					border dashed 1px #c0dac6
 					border-radius 8px
diff --git a/src/client/app/desktop/views/components/post-form-window.vue b/src/client/app/desktop/views/components/post-form-window.vue
index d0b115e85..c890592a5 100644
--- a/src/client/app/desktop/views/components/post-form-window.vue
+++ b/src/client/app/desktop/views/components/post-form-window.vue
@@ -2,13 +2,13 @@
 <mk-window ref="window" is-modal @closed="$destroy">
 	<span slot="header">
 		<span :class="$style.icon" v-if="geo">%fa:map-marker-alt%</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.note%</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>
 
-	<mk-post-preview v-if="reply" :class="$style.postPreview" :post="reply"/>
+	<mk-note-preview v-if="reply" :class="$style.notePreview" :note="reply"/>
 	<mk-post-form ref="form"
 		:reply="reply"
 		@posted="onPosted"
@@ -70,7 +70,7 @@ export default Vue.extend({
 	&:after
 		content ')'
 
-.postPreview
+.notePreview
 	margin 16px 22px
 
 </style>
diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue
index 1c83a38b6..8e3ec24bc 100644
--- a/src/client/app/desktop/views/components/post-form.vue
+++ b/src/client/app/desktop/views/components/post-form.vue
@@ -46,7 +46,7 @@ export default Vue.extend({
 	components: {
 		XDraggable
 	},
-	props: ['reply', 'repost'],
+	props: ['reply', 'renote'],
 	data() {
 		return {
 			posting: false,
@@ -61,28 +61,28 @@ export default Vue.extend({
 	},
 	computed: {
 		draftId(): string {
-			return this.repost
-				? 'repost:' + this.repost.id
+			return this.renote
+				? 'renote:' + this.renote.id
 				: this.reply
 					? 'reply:' + this.reply.id
-					: 'post';
+					: 'note';
 		},
 		placeholder(): string {
-			return this.repost
+			return this.renote
 				? '%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%';
+					: '%i18n:desktop.tags.mk-post-form.note-placeholder%';
 		},
 		submitText(): string {
-			return this.repost
-				? '%i18n:desktop.tags.mk-post-form.repost%'
+			return this.renote
+				? '%i18n:desktop.tags.mk-post-form.renote%'
 				: this.reply
 					? '%i18n:desktop.tags.mk-post-form.reply%'
-					: '%i18n:desktop.tags.mk-post-form.post%';
+					: '%i18n:desktop.tags.mk-post-form.note%';
 		},
 		canPost(): boolean {
-			return !this.posting && (this.text.length != 0 || this.files.length != 0 || this.poll || this.repost);
+			return !this.posting && (this.text.length != 0 || this.files.length != 0 || this.poll || this.renote);
 		}
 	},
 	watch: {
@@ -217,11 +217,11 @@ export default Vue.extend({
 		post() {
 			this.posting = true;
 
-			(this as any).api('posts/create', {
+			(this as any).api('notes/create', {
 				text: this.text == '' ? undefined : this.text,
 				mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
 				replyId: this.reply ? this.reply.id : undefined,
-				repostId: this.repost ? this.repost.id : undefined,
+				renoteId: this.renote ? this.renote.id : undefined,
 				poll: this.poll ? (this.$refs.poll as any).get() : undefined,
 				geo: this.geo ? {
 					coordinates: [this.geo.longitude, this.geo.latitude],
@@ -235,17 +235,17 @@ export default Vue.extend({
 				this.clear();
 				this.deleteDraft();
 				this.$emit('posted');
-				(this as any).apis.notify(this.repost
+				(this as any).apis.notify(this.renote
 					? '%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 => {
-				(this as any).apis.notify(this.repost
-					? '%i18n:desktop.tags.mk-post-form.repost-failed%'
+				(this as any).apis.notify(this.renote
+					? '%i18n:desktop.tags.mk-post-form.renote-failed%'
 					: this.reply
 						? '%i18n:desktop.tags.mk-post-form.reply-failed%'
-						: '%i18n:desktop.tags.mk-post-form.post-failed%');
+						: '%i18n:desktop.tags.mk-post-form.note-failed%');
 			}).then(() => {
 				this.posting = false;
 			});
diff --git a/src/client/app/desktop/views/components/posts.post.vue b/src/client/app/desktop/views/components/posts.post.vue
index 17fe33042..924642c01 100644
--- a/src/client/app/desktop/views/components/posts.post.vue
+++ b/src/client/app/desktop/views/components/posts.post.vue
@@ -1,19 +1,19 @@
 <template>
-<div class="post" tabindex="-1" :title="title" @keydown="onKeydown">
+<div class="note" tabindex="-1" :title="title" @keydown="onKeydown">
 	<div class="reply-to" v-if="p.reply">
-		<x-sub :post="p.reply"/>
+		<x-sub :note="p.reply"/>
 	</div>
-	<div class="repost" v-if="isRepost">
+	<div class="renote" v-if="isRenote">
 		<p>
-			<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="post.userId">
-				<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
+			<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="note.userId">
+				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
 			</router-link>
 			%fa:retweet%
-			<span>{{ '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('{')) }}</span>
-			<a class="name" :href="`/@${acct}`" v-user-preview="post.userId">{{ getUserName(post.user) }}</a>
-			<span>{{ '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1) }}</span>
+			<span>{{ '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.indexOf('{')) }}</span>
+			<a class="name" :href="`/@${acct}`" v-user-preview="note.userId">{{ getUserName(note.user) }}</a>
+			<span>{{ '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-note.reposted-by%'.indexOf('}') + 1) }}</span>
 		</p>
-		<mk-time :time="post.createdAt"/>
+		<mk-time :time="note.createdAt"/>
 	</div>
 	<article>
 		<router-link class="avatar-anchor" :to="`/@${acct}`">
@@ -38,38 +38,38 @@
 				</p>
 				<div class="text">
 					<a class="reply" v-if="p.reply">%fa:reply%</a>
-					<mk-post-html v-if="p.textHtml" :text="p.text" :i="os.i" :class="$style.text"/>
-					<a class="rp" v-if="p.repost">RP:</a>
+					<mk-note-html v-if="p.textHtml" :text="p.text" :i="os.i" :class="$style.text"/>
+					<a class="rp" v-if="p.renote">RP:</a>
 				</div>
 				<div class="media" v-if="p.media.length > 0">
 					<mk-media-list :media-list="p.media"/>
 				</div>
-				<mk-poll v-if="p.poll" :post="p" ref="pollViewer"/>
+				<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
 				<div class="tags" v-if="p.tags && p.tags.length > 0">
 					<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
 				</div>
 				<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
 				<div class="map" v-if="p.geo" ref="map"></div>
-				<div class="repost" v-if="p.repost">
-					<mk-post-preview :post="p.repost"/>
+				<div class="renote" v-if="p.renote">
+					<mk-note-preview :note="p.renote"/>
 				</div>
 				<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 			</div>
 			<footer>
-				<mk-reactions-viewer :post="p" ref="reactionsViewer"/>
-				<button @click="reply" title="%i18n:desktop.tags.mk-timeline-post.reply%">
+				<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
+				<button @click="reply" title="%i18n:desktop.tags.mk-timeline-note.reply%">
 					%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
 				</button>
-				<button @click="repost" title="%i18n:desktop.tags.mk-timeline-post.repost%">
-					%fa:retweet%<p class="count" v-if="p.repostCount > 0">{{ p.repostCount }}</p>
+				<button @click="renote" title="%i18n:desktop.tags.mk-timeline-note.renote%">
+					%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
 				</button>
-				<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:desktop.tags.mk-timeline-post.add-reaction%">
+				<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:desktop.tags.mk-timeline-note.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 title="%i18n:desktop.tags.mk-timeline-post.detail">
+				<button title="%i18n:desktop.tags.mk-timeline-note.detail">
 					<template v-if="!isDetailOpened">%fa:caret-down%</template>
 					<template v-if="isDetailOpened">%fa:caret-up%</template>
 				</button>
@@ -77,7 +77,7 @@
 		</div>
 	</article>
 	<div class="detail" v-if="isDetailOpened">
-		<mk-post-status-graph width="462" height="130" :post="p"/>
+		<mk-note-status-graph width="462" height="130" :note="p"/>
 	</div>
 </div>
 </template>
@@ -90,10 +90,10 @@ import getUserName from '../../../../../renderers/get-user-name';
 import parse from '../../../../../text/parse';
 
 import MkPostFormWindow from './post-form-window.vue';
-import MkRepostFormWindow from './repost-form-window.vue';
-import MkPostMenu from '../../../common/views/components/post-menu.vue';
+import MkRenoteFormWindow from './renote-form-window.vue';
+import MkNoteMenu from '../../../common/views/components/note-menu.vue';
 import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
-import XSub from './posts.post.sub.vue';
+import XSub from './notes.note.sub.vue';
 
 function focus(el, fn) {
 	const target = fn(el);
@@ -111,7 +111,7 @@ export default Vue.extend({
 		XSub
 	},
 
-	props: ['post'],
+	props: ['note'],
 
 	data() {
 		return {
@@ -128,14 +128,14 @@ export default Vue.extend({
 		name(): string {
 			return getUserName(this.p.user);
 		},
-		isRepost(): boolean {
-			return (this.post.repost &&
-				this.post.text == null &&
-				this.post.mediaIds == null &&
-				this.post.poll == null);
+		isRenote(): boolean {
+			return (this.note.renote &&
+				this.note.text == null &&
+				this.note.mediaIds == null &&
+				this.note.poll == null);
 		},
 		p(): any {
-			return this.isRepost ? this.post.repost : this.post;
+			return this.isRenote ? this.note.renote : this.note;
 		},
 		reactionsCount(): number {
 			return this.p.reactionCounts
@@ -211,7 +211,7 @@ export default Vue.extend({
 					type: 'capture',
 					id: this.p.id
 				});
-				if (withHandler) this.connection.on('post-updated', this.onStreamPostUpdated);
+				if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
 			}
 		},
 		decapture(withHandler = false) {
@@ -220,18 +220,18 @@ export default Vue.extend({
 					type: 'decapture',
 					id: this.p.id
 				});
-				if (withHandler) this.connection.off('post-updated', this.onStreamPostUpdated);
+				if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated);
 			}
 		},
 		onStreamConnected() {
 			this.capture();
 		},
-		onStreamPostUpdated(data) {
-			const post = data.post;
-			if (post.id == this.post.id) {
-				this.$emit('update:post', post);
-			} else if (post.id == this.post.repostId) {
-				this.post.repost = post;
+		onStreamNoteUpdated(data) {
+			const note = data.note;
+			if (note.id == this.note.id) {
+				this.$emit('update:note', note);
+			} else if (note.id == this.note.renoteId) {
+				this.note.renote = note;
 			}
 		},
 		reply() {
@@ -239,21 +239,21 @@ export default Vue.extend({
 				reply: this.p
 			});
 		},
-		repost() {
-			(this as any).os.new(MkRepostFormWindow, {
-				post: this.p
+		renote() {
+			(this as any).os.new(MkRenoteFormWindow, {
+				note: this.p
 			});
 		},
 		react() {
 			(this as any).os.new(MkReactionPicker, {
 				source: this.$refs.reactButton,
-				post: this.p
+				note: this.p
 			});
 		},
 		menu() {
-			(this as any).os.new(MkPostMenu, {
+			(this as any).os.new(MkNoteMenu, {
 				source: this.$refs.menuButton,
-				post: this.p
+				note: this.p
 			});
 		},
 		onKeydown(e) {
@@ -274,7 +274,7 @@ export default Vue.extend({
 
 				case e.which == 81: // [q]
 				case e.which == 69: // [e]
-					this.repost();
+					this.renote();
 					break;
 
 				case e.which == 70: // [f]
@@ -299,7 +299,7 @@ export default Vue.extend({
 <style lang="stylus" scoped>
 @import '~const.styl'
 
-.post
+.note
 	margin 0
 	padding 0
 	background #fff
@@ -309,7 +309,7 @@ export default Vue.extend({
 		border-top-left-radius 6px
 		border-top-right-radius 6px
 
-		> .repost
+		> .renote
 			border-top-left-radius 6px
 			border-top-right-radius 6px
 
@@ -330,7 +330,7 @@ export default Vue.extend({
 			border 2px solid rgba($theme-color, 0.3)
 			border-radius 4px
 
-	> .repost
+	> .renote
 		color #9dbb00
 		background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
 
@@ -369,7 +369,7 @@ export default Vue.extend({
 		padding 0 16px
 		background rgba(0, 0, 0, 0.0125)
 
-		> .mk-post-preview
+		> .mk-note-preview
 			background transparent
 
 	> article
@@ -529,10 +529,10 @@ export default Vue.extend({
 				> .mk-poll
 					font-size 80%
 
-				> .repost
+				> .renote
 					margin 8px 0
 
-					> .mk-post-preview
+					> .mk-note-preview
 						padding 16px
 						border dashed 1px #c0dac6
 						border-radius 8px
diff --git a/src/client/app/desktop/views/components/renote-form-window.vue b/src/client/app/desktop/views/components/renote-form-window.vue
new file mode 100644
index 000000000..09176b5ba
--- /dev/null
+++ b/src/client/app/desktop/views/components/renote-form-window.vue
@@ -0,0 +1,42 @@
+<template>
+<mk-window ref="window" is-modal @closed="$destroy">
+	<span slot="header" :class="$style.header">%fa:retweet%%i18n:desktop.tags.mk-renote-form-window.title%</span>
+	<mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled"/>
+</mk-window>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: ['note'],
+	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();
+				}
+			}
+		},
+		onPosted() {
+			(this.$refs.window as any).close();
+		},
+		onCanceled() {
+			(this.$refs.window as any).close();
+		}
+	}
+});
+</script>
+
+<style lang="stylus" module>
+.header
+	> [data-fa]
+		margin-right 4px
+
+</style>
diff --git a/src/client/app/desktop/views/components/renote-form.vue b/src/client/app/desktop/views/components/renote-form.vue
new file mode 100644
index 000000000..c6b907415
--- /dev/null
+++ b/src/client/app/desktop/views/components/renote-form.vue
@@ -0,0 +1,131 @@
+<template>
+<div class="mk-renote-form">
+	<mk-note-preview :note="note"/>
+	<template v-if="!quote">
+		<footer>
+			<a class="quote" v-if="!quote" @click="onQuote">%i18n:desktop.tags.mk-renote-form.quote%</a>
+			<button class="cancel" @click="cancel">%i18n:desktop.tags.mk-renote-form.cancel%</button>
+			<button class="ok" @click="ok" :disabled="wait">{{ wait ? '%i18n:desktop.tags.mk-renote-form.reposting%' : '%i18n:desktop.tags.mk-renote-form.renote%' }}</button>
+		</footer>
+	</template>
+	<template v-if="quote">
+		<mk-post-form ref="form" :renote="note" @posted="onChildFormPosted"/>
+	</template>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: ['note'],
+	data() {
+		return {
+			wait: false,
+			quote: false
+		};
+	},
+	methods: {
+		ok() {
+			this.wait = true;
+			(this as any).api('notes/create', {
+				renoteId: this.note.id
+			}).then(data => {
+				this.$emit('posted');
+				(this as any).apis.notify('%i18n:desktop.tags.mk-renote-form.success%');
+			}).catch(err => {
+				(this as any).apis.notify('%i18n:desktop.tags.mk-renote-form.failure%');
+			}).then(() => {
+				this.wait = false;
+			});
+		},
+		cancel() {
+			this.$emit('canceled');
+		},
+		onQuote() {
+			this.quote = true;
+
+			this.$nextTick(() => {
+				(this.$refs.form as any).focus();
+			});
+		},
+		onChildFormPosted() {
+			this.$emit('posted');
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+.mk-renote-form
+
+	> .mk-note-preview
+		margin 16px 22px
+
+	> 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>
diff --git a/src/client/app/desktop/views/components/repost-form-window.vue b/src/client/app/desktop/views/components/repost-form-window.vue
index 7db5adbff..09176b5ba 100644
--- a/src/client/app/desktop/views/components/repost-form-window.vue
+++ b/src/client/app/desktop/views/components/repost-form-window.vue
@@ -1,7 +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>
-	<mk-repost-form ref="form" :post="post" @posted="onPosted" @canceled="onCanceled"/>
+	<span slot="header" :class="$style.header">%fa:retweet%%i18n:desktop.tags.mk-renote-form-window.title%</span>
+	<mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled"/>
 </mk-window>
 </template>
 
@@ -9,7 +9,7 @@
 import Vue from 'vue';
 
 export default Vue.extend({
-	props: ['post'],
+	props: ['note'],
 	mounted() {
 		document.addEventListener('keydown', this.onDocumentKeydown);
 	},
diff --git a/src/client/app/desktop/views/components/repost-form.vue b/src/client/app/desktop/views/components/repost-form.vue
index 3a5e3a7c5..c6b907415 100644
--- a/src/client/app/desktop/views/components/repost-form.vue
+++ b/src/client/app/desktop/views/components/repost-form.vue
@@ -1,15 +1,15 @@
 <template>
-<div class="mk-repost-form">
-	<mk-post-preview :post="post"/>
+<div class="mk-renote-form">
+	<mk-note-preview :note="note"/>
 	<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>
+			<a class="quote" v-if="!quote" @click="onQuote">%i18n:desktop.tags.mk-renote-form.quote%</a>
+			<button class="cancel" @click="cancel">%i18n:desktop.tags.mk-renote-form.cancel%</button>
+			<button class="ok" @click="ok" :disabled="wait">{{ wait ? '%i18n:desktop.tags.mk-renote-form.reposting%' : '%i18n:desktop.tags.mk-renote-form.renote%' }}</button>
 		</footer>
 	</template>
 	<template v-if="quote">
-		<mk-post-form ref="form" :repost="post" @posted="onChildFormPosted"/>
+		<mk-post-form ref="form" :renote="note" @posted="onChildFormPosted"/>
 	</template>
 </div>
 </template>
@@ -18,7 +18,7 @@
 import Vue from 'vue';
 
 export default Vue.extend({
-	props: ['post'],
+	props: ['note'],
 	data() {
 		return {
 			wait: false,
@@ -28,13 +28,13 @@ export default Vue.extend({
 	methods: {
 		ok() {
 			this.wait = true;
-			(this as any).api('posts/create', {
-				repostId: this.post.id
+			(this as any).api('notes/create', {
+				renoteId: this.note.id
 			}).then(data => {
 				this.$emit('posted');
-				(this as any).apis.notify('%i18n:desktop.tags.mk-repost-form.success%');
+				(this as any).apis.notify('%i18n:desktop.tags.mk-renote-form.success%');
 			}).catch(err => {
-				(this as any).apis.notify('%i18n:desktop.tags.mk-repost-form.failure%');
+				(this as any).apis.notify('%i18n:desktop.tags.mk-renote-form.failure%');
 			}).then(() => {
 				this.wait = false;
 			});
@@ -59,9 +59,9 @@ export default Vue.extend({
 <style lang="stylus" scoped>
 @import '~const.styl'
 
-.mk-repost-form
+.mk-renote-form
 
-	> .mk-post-preview
+	> .mk-note-preview
 		margin 16px 22px
 
 	> footer
diff --git a/src/client/app/desktop/views/components/sub-note-content.vue b/src/client/app/desktop/views/components/sub-note-content.vue
new file mode 100644
index 000000000..51ee93cba
--- /dev/null
+++ b/src/client/app/desktop/views/components/sub-note-content.vue
@@ -0,0 +1,44 @@
+<template>
+<div class="mk-sub-note-content">
+	<div class="body">
+		<a class="reply" v-if="note.replyId">%fa:reply%</a>
+		<mk-note-html :text="note.text" :i="os.i"/>
+		<a class="rp" v-if="note.renoteId" :href="`/note:${note.renoteId}`">RP: ...</a>
+	</div>
+	<details v-if="note.media.length > 0">
+		<summary>({{ note.media.length }}つのメディア)</summary>
+		<mk-media-list :media-list="note.media"/>
+	</details>
+	<details v-if="note.poll">
+		<summary>投票</summary>
+		<mk-poll :note="note"/>
+	</details>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: ['note']
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-sub-note-content
+	overflow-wrap break-word
+
+	> .body
+		> .reply
+			margin-right 6px
+			color #717171
+
+		> .rp
+			margin-left 4px
+			font-style oblique
+			color #a0bf46
+
+	mk-poll
+		font-size 80%
+
+</style>
diff --git a/src/client/app/desktop/views/components/sub-post-content.vue b/src/client/app/desktop/views/components/sub-post-content.vue
deleted file mode 100644
index 17899af28..000000000
--- a/src/client/app/desktop/views/components/sub-post-content.vue
+++ /dev/null
@@ -1,44 +0,0 @@
-<template>
-<div class="mk-sub-post-content">
-	<div class="body">
-		<a class="reply" v-if="post.replyId">%fa:reply%</a>
-		<mk-post-html :text="post.text" :i="os.i"/>
-		<a class="rp" v-if="post.repostId" :href="`/post:${post.repostId}`">RP: ...</a>
-	</div>
-	<details v-if="post.media.length > 0">
-		<summary>({{ post.media.length }}つのメディア)</summary>
-		<mk-media-list :media-list="post.media"/>
-	</details>
-	<details v-if="post.poll">
-		<summary>投票</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
-
-		> .rp
-			margin-left 4px
-			font-style oblique
-			color #a0bf46
-
-	mk-poll
-		font-size 80%
-
-</style>
diff --git a/src/client/app/desktop/views/components/timeline.vue b/src/client/app/desktop/views/components/timeline.vue
index 65b4bd1c7..6d049eee9 100644
--- a/src/client/app/desktop/views/components/timeline.vue
+++ b/src/client/app/desktop/views/components/timeline.vue
@@ -4,15 +4,15 @@
 	<div class="fetching" v-if="fetching">
 		<mk-ellipsis-icon/>
 	</div>
-	<p class="empty" v-if="posts.length == 0 && !fetching">
+	<p class="empty" v-if="notes.length == 0 && !fetching">
 		%fa:R comments%自分の投稿や、自分がフォローしているユーザーの投稿が表示されます。
 	</p>
-	<mk-posts :posts="posts" ref="timeline">
+	<mk-notes :notes="notes" ref="timeline">
 		<button slot="footer" @click="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
 			<template v-if="!moreFetching">もっと見る</template>
 			<template v-if="moreFetching">%fa:spinner .pulse .fw%</template>
 		</button>
-	</mk-posts>
+	</mk-notes>
 </div>
 </template>
 
@@ -26,7 +26,7 @@ export default Vue.extend({
 			fetching: true,
 			moreFetching: false,
 			existMore: false,
-			posts: [],
+			notes: [],
 			connection: null,
 			connectionId: null,
 			date: null
@@ -41,7 +41,7 @@ export default Vue.extend({
 		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('note', this.onNote);
 		this.connection.on('follow', this.onChangeFollowing);
 		this.connection.on('unfollow', this.onChangeFollowing);
 
@@ -51,7 +51,7 @@ export default Vue.extend({
 		this.fetch();
 	},
 	beforeDestroy() {
-		this.connection.off('post', this.onPost);
+		this.connection.off('note', this.onNote);
 		this.connection.off('follow', this.onChangeFollowing);
 		this.connection.off('unfollow', this.onChangeFollowing);
 		(this as any).os.stream.dispose(this.connectionId);
@@ -63,45 +63,45 @@ export default Vue.extend({
 		fetch(cb?) {
 			this.fetching = true;
 
-			(this as any).api('posts/timeline', {
+			(this as any).api('notes/timeline', {
 				limit: 11,
 				untilDate: this.date ? this.date.getTime() : undefined
-			}).then(posts => {
-				if (posts.length == 11) {
-					posts.pop();
+			}).then(notes => {
+				if (notes.length == 11) {
+					notes.pop();
 					this.existMore = true;
 				}
-				this.posts = posts;
+				this.notes = notes;
 				this.fetching = false;
 				this.$emit('loaded');
 				if (cb) cb();
 			});
 		},
 		more() {
-			if (this.moreFetching || this.fetching || this.posts.length == 0 || !this.existMore) return;
+			if (this.moreFetching || this.fetching || this.notes.length == 0 || !this.existMore) return;
 			this.moreFetching = true;
-			(this as any).api('posts/timeline', {
+			(this as any).api('notes/timeline', {
 				limit: 11,
-				untilId: this.posts[this.posts.length - 1].id
-			}).then(posts => {
-				if (posts.length == 11) {
-					posts.pop();
+				untilId: this.notes[this.notes.length - 1].id
+			}).then(notes => {
+				if (notes.length == 11) {
+					notes.pop();
 				} else {
 					this.existMore = false;
 				}
-				this.posts = this.posts.concat(posts);
+				this.notes = this.notes.concat(notes);
 				this.moreFetching = false;
 			});
 		},
-		onPost(post) {
+		onNote(note) {
 			// サウンドを再生する
 			if ((this as any).os.isEnableSounds) {
-				const sound = new Audio(`${url}/assets/post.mp3`);
+				const sound = new Audio(`${url}/assets/note.mp3`);
 				sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 1;
 				sound.play();
 			}
 
-			this.posts.unshift(post);
+			this.notes.unshift(note);
 		},
 		onChangeFollowing() {
 			this.fetch();
diff --git a/src/client/app/desktop/views/components/ui.header.post.vue b/src/client/app/desktop/views/components/ui.header.post.vue
index c2f0e07dd..5c1756b75 100644
--- a/src/client/app/desktop/views/components/ui.header.post.vue
+++ b/src/client/app/desktop/views/components/ui.header.post.vue
@@ -1,6 +1,6 @@
 <template>
-<div class="post">
-	<button @click="post" title="%i18n:desktop.tags.mk-ui-header-post-button.post%">%fa:pencil-alt%</button>
+<div class="note">
+	<button @click="post" title="%i18n:desktop.tags.mk-ui-header-note-button.note%">%fa:pencil-alt%</button>
 </div>
 </template>
 
@@ -19,7 +19,7 @@ export default Vue.extend({
 <style lang="stylus" scoped>
 @import '~const.styl'
 
-.post
+.note
 	display inline-block
 	padding 8px
 	height 100%
diff --git a/src/client/app/desktop/views/components/user-preview.vue b/src/client/app/desktop/views/components/user-preview.vue
index 2f2e78ec6..3cbaa2816 100644
--- a/src/client/app/desktop/views/components/user-preview.vue
+++ b/src/client/app/desktop/views/components/user-preview.vue
@@ -12,7 +12,7 @@
 		<div class="description">{{ u.description }}</div>
 		<div class="status">
 			<div>
-				<p>投稿</p><a>{{ u.postsCount }}</a>
+				<p>投稿</p><a>{{ u.notesCount }}</a>
 			</div>
 			<div>
 				<p>フォロー</p><a>{{ u.followingCount }}</a>
diff --git a/src/client/app/desktop/views/pages/home.vue b/src/client/app/desktop/views/pages/home.vue
index c209af4e4..e4caa2022 100644
--- a/src/client/app/desktop/views/pages/home.vue
+++ b/src/client/app/desktop/views/pages/home.vue
@@ -7,7 +7,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
-import getPostSummary from '../../../../../renderers/get-post-summary';
+import getNoteSummary from '../../../../../renderers/get-note-summary';
 
 export default Vue.extend({
 	props: {
@@ -29,13 +29,13 @@ export default Vue.extend({
 		this.connection = (this as any).os.stream.getConnection();
 		this.connectionId = (this as any).os.stream.use();
 
-		this.connection.on('post', this.onStreamPost);
+		this.connection.on('note', this.onStreamNote);
 		document.addEventListener('visibilitychange', this.onVisibilitychange, false);
 
 		Progress.start();
 	},
 	beforeDestroy() {
-		this.connection.off('post', this.onStreamPost);
+		this.connection.off('note', this.onStreamNote);
 		(this as any).os.stream.dispose(this.connectionId);
 		document.removeEventListener('visibilitychange', this.onVisibilitychange);
 	},
@@ -44,10 +44,10 @@ export default Vue.extend({
 			Progress.done();
 		},
 
-		onStreamPost(post) {
-			if (document.hidden && post.userId != (this as any).os.i.id) {
+		onStreamNote(note) {
+			if (document.hidden && note.userId != (this as any).os.i.id) {
 				this.unreadCount++;
-				document.title = `(${this.unreadCount}) ${getPostSummary(post)}`;
+				document.title = `(${this.unreadCount}) ${getNoteSummary(note)}`;
 			}
 		},
 
diff --git a/src/client/app/desktop/views/pages/post.vue b/src/client/app/desktop/views/pages/note.vue
similarity index 65%
rename from src/client/app/desktop/views/pages/post.vue
rename to src/client/app/desktop/views/pages/note.vue
index dbd707e04..17c2b1e95 100644
--- a/src/client/app/desktop/views/pages/post.vue
+++ b/src/client/app/desktop/views/pages/note.vue
@@ -1,9 +1,9 @@
 <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>
+		<a v-if="note.next" :href="note.next">%fa:angle-up%%i18n:desktop.tags.mk-note-page.next%</a>
+		<mk-note-detail :note="note"/>
+		<a v-if="note.prev" :href="note.prev">%fa:angle-down%%i18n:desktop.tags.mk-note-page.prev%</a>
 	</main>
 </mk-ui>
 </template>
@@ -16,7 +16,7 @@ export default Vue.extend({
 	data() {
 		return {
 			fetching: true,
-			post: null
+			note: null
 		};
 	},
 	watch: {
@@ -30,10 +30,10 @@ export default Vue.extend({
 			Progress.start();
 			this.fetching = true;
 
-			(this as any).api('posts/show', {
-				postId: this.$route.params.post
-			}).then(post => {
-				this.post = post;
+			(this as any).api('notes/show', {
+				noteId: this.$route.params.note
+			}).then(note => {
+				this.note = note;
 				this.fetching = false;
 
 				Progress.done();
@@ -60,7 +60,7 @@ main
 		> [data-fa]
 			margin-right 4px
 
-	> .mk-post-detail
+	> .mk-note-detail
 		margin 0 auto
 		width 640px
 
diff --git a/src/client/app/desktop/views/pages/search.vue b/src/client/app/desktop/views/pages/search.vue
index afd37c8ce..698154e66 100644
--- a/src/client/app/desktop/views/pages/search.vue
+++ b/src/client/app/desktop/views/pages/search.vue
@@ -7,12 +7,12 @@
 		<mk-ellipsis-icon/>
 	</div>
 	<p :class="$style.empty" v-if="!fetching && empty">%fa:search%「{{ q }}」に関する投稿は見つかりませんでした。</p>
-	<mk-posts ref="timeline" :class="$style.posts" :posts="posts">
+	<mk-notes ref="timeline" :class="$style.notes" :notes="notes">
 		<div slot="footer">
 			<template v-if="!moreFetching">%fa:search%</template>
 			<template v-if="moreFetching">%fa:spinner .pulse .fw%</template>
 		</div>
-	</mk-posts>
+	</mk-notes>
 </mk-ui>
 </template>
 
@@ -30,7 +30,7 @@ export default Vue.extend({
 			moreFetching: false,
 			existMore: false,
 			offset: 0,
-			posts: []
+			notes: []
 		};
 	},
 	watch: {
@@ -38,7 +38,7 @@ export default Vue.extend({
 	},
 	computed: {
 		empty(): boolean {
-			return this.posts.length == 0;
+			return this.notes.length == 0;
 		},
 		q(): string {
 			return this.$route.query.q;
@@ -66,33 +66,33 @@ export default Vue.extend({
 			this.fetching = true;
 			Progress.start();
 
-			(this as any).api('posts/search', Object.assign({
+			(this as any).api('notes/search', Object.assign({
 				limit: limit + 1,
 				offset: this.offset
-			}, parse(this.q))).then(posts => {
-				if (posts.length == limit + 1) {
-					posts.pop();
+			}, parse(this.q))).then(notes => {
+				if (notes.length == limit + 1) {
+					notes.pop();
 					this.existMore = true;
 				}
-				this.posts = posts;
+				this.notes = notes;
 				this.fetching = false;
 				Progress.done();
 			});
 		},
 		more() {
-			if (this.moreFetching || this.fetching || this.posts.length == 0 || !this.existMore) return;
+			if (this.moreFetching || this.fetching || this.notes.length == 0 || !this.existMore) return;
 			this.offset += limit;
 			this.moreFetching = true;
-			return (this as any).api('posts/search', Object.assign({
+			return (this as any).api('notes/search', Object.assign({
 				limit: limit + 1,
 				offset: this.offset
-			}, parse(this.q))).then(posts => {
-				if (posts.length == limit + 1) {
-					posts.pop();
+			}, parse(this.q))).then(notes => {
+				if (notes.length == limit + 1) {
+					notes.pop();
 				} else {
 					this.existMore = false;
 				}
-				this.posts = this.posts.concat(posts);
+				this.notes = this.notes.concat(notes);
 				this.moreFetching = false;
 			});
 		},
@@ -111,7 +111,7 @@ export default Vue.extend({
 	margin 0 auto
 	color #555
 
-.posts
+.notes
 	max-width 600px
 	margin 0 auto
 	border solid 1px rgba(0, 0, 0, 0.075)
diff --git a/src/client/app/desktop/views/pages/user/user.home.vue b/src/client/app/desktop/views/pages/user/user.home.vue
index 071c9bb61..ed3b1c710 100644
--- a/src/client/app/desktop/views/pages/user/user.home.vue
+++ b/src/client/app/desktop/views/pages/user/user.home.vue
@@ -9,7 +9,7 @@
 		</div>
 	</div>
 	<main>
-		<mk-post-detail v-if="user.pinnedPost" :post="user.pinnedPost" :compact="true"/>
+		<mk-note-detail v-if="user.pinnedNote" :note="user.pinnedNote" :compact="true"/>
 		<x-timeline class="timeline" ref="tl" :user="user"/>
 	</main>
 	<div>
diff --git a/src/client/app/desktop/views/pages/user/user.photos.vue b/src/client/app/desktop/views/pages/user/user.photos.vue
index 1ff79b4ae..99a1a8d70 100644
--- a/src/client/app/desktop/views/pages/user/user.photos.vue
+++ b/src/client/app/desktop/views/pages/user/user.photos.vue
@@ -22,13 +22,13 @@ export default Vue.extend({
 		};
 	},
 	mounted() {
-		(this as any).api('users/posts', {
+		(this as any).api('users/notes', {
 			userId: this.user.id,
 			withMedia: true,
 			limit: 9
-		}).then(posts => {
-			posts.forEach(post => {
-				post.media.forEach(media => {
+		}).then(notes => {
+			notes.forEach(note => {
+				note.media.forEach(media => {
 					if (this.images.length < 9) this.images.push(media);
 				});
 			});
diff --git a/src/client/app/desktop/views/pages/user/user.profile.vue b/src/client/app/desktop/views/pages/user/user.profile.vue
index f5562d091..2a82ba786 100644
--- a/src/client/app/desktop/views/pages/user/user.profile.vue
+++ b/src/client/app/desktop/views/pages/user/user.profile.vue
@@ -14,7 +14,7 @@
 		<p>%fa:B twitter%<a :href="`https://twitter.com/${user.account.twitter.screenName}`" target="_blank">@{{ user.account.twitter.screenName }}</a></p>
 	</div>
 	<div class="status">
-		<p class="posts-count">%fa:angle-right%<a>{{ user.postsCount }}</a><b>投稿</b></p>
+		<p class="notes-count">%fa:angle-right%<a>{{ user.notesCount }}</a><b>投稿</b></p>
 		<p class="following">%fa:angle-right%<a @click="showFollowing">{{ user.followingCount }}</a>人を<b>フォロー</b></p>
 		<p class="followers">%fa:angle-right%<a @click="showFollowers">{{ user.followersCount }}</a>人の<b>フォロワー</b></p>
 	</div>
diff --git a/src/client/app/desktop/views/pages/user/user.timeline.vue b/src/client/app/desktop/views/pages/user/user.timeline.vue
index 134ad423c..87d133174 100644
--- a/src/client/app/desktop/views/pages/user/user.timeline.vue
+++ b/src/client/app/desktop/views/pages/user/user.timeline.vue
@@ -8,12 +8,12 @@
 		<mk-ellipsis-icon/>
 	</div>
 	<p class="empty" v-if="empty">%fa:R comments%このユーザーはまだ何も投稿していないようです。</p>
-	<mk-posts ref="timeline" :posts="posts">
+	<mk-notes ref="timeline" :notes="notes">
 		<div slot="footer">
 			<template v-if="!moreFetching">%fa:moon%</template>
 			<template v-if="moreFetching">%fa:spinner .pulse .fw%</template>
 		</div>
-	</mk-posts>
+	</mk-notes>
 </div>
 </template>
 
@@ -27,7 +27,7 @@ export default Vue.extend({
 			moreFetching: false,
 			mode: 'default',
 			unreadCount: 0,
-			posts: [],
+			notes: [],
 			date: null
 		};
 	},
@@ -38,7 +38,7 @@ export default Vue.extend({
 	},
 	computed: {
 		empty(): boolean {
-			return this.posts.length == 0;
+			return this.notes.length == 0;
 		}
 	},
 	mounted() {
@@ -60,26 +60,26 @@ export default Vue.extend({
 			}
 		},
 		fetch(cb?) {
-			(this as any).api('users/posts', {
+			(this as any).api('users/notes', {
 				userId: this.user.id,
 				untilDate: this.date ? this.date.getTime() : undefined,
 				with_replies: this.mode == 'with-replies'
-			}).then(posts => {
-				this.posts = posts;
+			}).then(notes => {
+				this.notes = notes;
 				this.fetching = false;
 				if (cb) cb();
 			});
 		},
 		more() {
-			if (this.moreFetching || this.fetching || this.posts.length == 0) return;
+			if (this.moreFetching || this.fetching || this.notes.length == 0) return;
 			this.moreFetching = true;
-			(this as any).api('users/posts', {
+			(this as any).api('users/notes', {
 				userId: this.user.id,
 				with_replies: this.mode == 'with-replies',
-				untilId: this.posts[this.posts.length - 1].id
-			}).then(posts => {
+				untilId: this.notes[this.notes.length - 1].id
+			}).then(notes => {
 				this.moreFetching = false;
-				this.posts = this.posts.concat(posts);
+				this.notes = this.notes.concat(notes);
 			});
 		},
 		onScroll() {
diff --git a/src/client/app/desktop/views/widgets/channel.channel.form.vue b/src/client/app/desktop/views/widgets/channel.channel.form.vue
index aaf327f1e..f2744268b 100644
--- a/src/client/app/desktop/views/widgets/channel.channel.form.vue
+++ b/src/client/app/desktop/views/widgets/channel.channel.form.vue
@@ -24,11 +24,11 @@ export default Vue.extend({
 
 			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);
+				reply = (this.$parent as any).notes.find(p => p.index.toString() == index);
 				this.text = this.text.replace(/^>>([0-9]+) /, '');
 			}
 
-			(this as any).api('posts/create', {
+			(this as any).api('notes/create', {
 				text: this.text,
 				replyId: reply ? reply.id : undefined,
 				channelId: (this.$parent as any).channel.id
diff --git a/src/client/app/desktop/views/widgets/channel.channel.post.vue b/src/client/app/desktop/views/widgets/channel.channel.note.vue
similarity index 65%
rename from src/client/app/desktop/views/widgets/channel.channel.post.vue
rename to src/client/app/desktop/views/widgets/channel.channel.note.vue
index fa6d8c34a..313a2e3f4 100644
--- a/src/client/app/desktop/views/widgets/channel.channel.post.vue
+++ b/src/client/app/desktop/views/widgets/channel.channel.note.vue
@@ -1,15 +1,15 @@
 <template>
-<div class="post">
+<div class="note">
 	<header>
-		<a class="index" @click="reply">{{ post.index }}:</a>
-		<router-link class="name" :to="`/@${acct}`" v-user-preview="post.user.id"><b>{{ name }}</b></router-link>
+		<a class="index" @click="reply">{{ note.index }}:</a>
+		<router-link class="name" :to="`/@${acct}`" v-user-preview="note.user.id"><b>{{ name }}</b></router-link>
 		<span>ID:<i>{{ acct }}</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">
+		<a v-if="note.reply">&gt;&gt;{{ note.reply.index }}</a>
+		{{ note.text }}
+		<div class="media" v-if="note.media">
+			<a v-for="file in note.media" :href="file.url" target="_blank">
 				<img :src="`${file.url}?thumbnail&size=512`" :alt="file.name" :title="file.name"/>
 			</a>
 		</div>
@@ -23,25 +23,25 @@ import getAcct from '../../../../../acct/render';
 import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
-	props: ['post'],
+	props: ['note'],
 	computed: {
 		acct() {
-			return getAcct(this.post.user);
+			return getAcct(this.note.user);
 		},
 		name() {
-			return getUserName(this.post.user);
+			return getUserName(this.note.user);
 		}
 	},
 	methods: {
 		reply() {
-			this.$emit('reply', this.post);
+			this.$emit('reply', this.note);
 		}
 	}
 });
 </script>
 
 <style lang="stylus" scoped>
-.post
+.note
 	margin 0
 	padding 0
 	color #444
diff --git a/src/client/app/desktop/views/widgets/channel.channel.vue b/src/client/app/desktop/views/widgets/channel.channel.vue
index e9fb9e3fd..ea4d8f845 100644
--- a/src/client/app/desktop/views/widgets/channel.channel.vue
+++ b/src/client/app/desktop/views/widgets/channel.channel.vue
@@ -1,9 +1,9 @@
 <template>
 <div class="channel">
 	<p v-if="fetching">読み込み中<mk-ellipsis/></p>
-	<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 v-if="!fetching" ref="notes" class="notes">
+		<p v-if="notes.length == 0">まだ投稿がありません</p>
+		<x-note class="note" v-for="note in notes.slice().reverse()" :note="note" :key="note.id" @reply="reply"/>
 	</div>
 	<x-form class="form" ref="form"/>
 </div>
@@ -13,18 +13,18 @@
 import Vue from 'vue';
 import ChannelStream from '../../../common/scripts/streaming/channel';
 import XForm from './channel.channel.form.vue';
-import XPost from './channel.channel.post.vue';
+import XNote from './channel.channel.note.vue';
 
 export default Vue.extend({
 	components: {
 		XForm,
-		XPost
+		XNote
 	},
 	props: ['channel'],
 	data() {
 		return {
 			fetching: true,
-			posts: [],
+			notes: [],
 			connection: null
 		};
 	},
@@ -43,10 +43,10 @@ export default Vue.extend({
 		zap() {
 			this.fetching = true;
 
-			(this as any).api('channels/posts', {
+			(this as any).api('channels/notes', {
 				channelId: this.channel.id
-			}).then(posts => {
-				this.posts = posts;
+			}).then(notes => {
+				this.notes = notes;
 				this.fetching = false;
 
 				this.$nextTick(() => {
@@ -55,24 +55,24 @@ export default Vue.extend({
 
 				this.disconnect();
 				this.connection = new ChannelStream((this as any).os, this.channel.id);
-				this.connection.on('post', this.onPost);
+				this.connection.on('note', this.onNote);
 			});
 		},
 		disconnect() {
 			if (this.connection) {
-				this.connection.off('post', this.onPost);
+				this.connection.off('note', this.onNote);
 				this.connection.close();
 			}
 		},
-		onPost(post) {
-			this.posts.unshift(post);
+		onNote(note) {
+			this.notes.unshift(note);
 			this.scrollToBottom();
 		},
 		scrollToBottom() {
-			(this.$refs.posts as any).scrollTop = (this.$refs.posts as any).scrollHeight;
+			(this.$refs.notes as any).scrollTop = (this.$refs.notes as any).scrollHeight;
 		},
-		reply(post) {
-			(this.$refs.form as any).text = `>>${ post.index } `;
+		reply(note) {
+			(this.$refs.form as any).text = `>>${ note.index } `;
 		}
 	}
 });
@@ -87,12 +87,12 @@ export default Vue.extend({
 		text-align center
 		color #aaa
 
-	> .posts
+	> .notes
 		height calc(100% - 38px)
 		overflow auto
 		font-size 0.9em
 
-		> .post
+		> .note
 			border-bottom solid 1px #eee
 
 			&:last-child
diff --git a/src/client/app/desktop/views/widgets/polls.vue b/src/client/app/desktop/views/widgets/polls.vue
index c8ba17bd4..eb49a4cd5 100644
--- a/src/client/app/desktop/views/widgets/polls.vue
+++ b/src/client/app/desktop/views/widgets/polls.vue
@@ -7,7 +7,7 @@
 	<div class="poll" v-if="!fetching && poll != null">
 		<p v-if="poll.text"><router-link to="`/@${ acct }/${ poll.id }`">{{ poll.text }}</router-link></p>
 		<p v-if="!poll.text"><router-link to="`/@${ acct }/${ poll.id }`">%fa:link%</router-link></p>
-		<mk-poll :post="poll"/>
+		<mk-poll :note="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>
@@ -47,11 +47,11 @@ export default define({
 			this.fetching = true;
 			this.poll = null;
 
-			(this as any).api('posts/polls/recommendation', {
+			(this as any).api('notes/polls/recommendation', {
 				limit: 1,
 				offset: this.offset
-			}).then(posts => {
-				const poll = posts ? posts[0] : null;
+			}).then(notes => {
+				const poll = notes ? notes[0] : null;
 				if (poll == null) {
 					this.offset = 0;
 				} else {
diff --git a/src/client/app/desktop/views/widgets/post-form.vue b/src/client/app/desktop/views/widgets/post-form.vue
index cf7fd1f2b..5e59582a0 100644
--- a/src/client/app/desktop/views/widgets/post-form.vue
+++ b/src/client/app/desktop/views/widgets/post-form.vue
@@ -4,7 +4,7 @@
 		<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>
+	<button @click="post" :disabled="posting">%i18n:desktop.tags.mk-post-form-home-widget.note%</button>
 </div>
 </template>
 
@@ -36,7 +36,7 @@ export default define({
 		post() {
 			this.posting = true;
 
-			(this as any).api('posts/create', {
+			(this as any).api('notes/create', {
 				text: this.text
 			}).then(data => {
 				this.clear();
diff --git a/src/client/app/desktop/views/widgets/trends.vue b/src/client/app/desktop/views/widgets/trends.vue
index 27c1860b3..c2c7636bb 100644
--- a/src/client/app/desktop/views/widgets/trends.vue
+++ b/src/client/app/desktop/views/widgets/trends.vue
@@ -5,8 +5,8 @@
 		<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"><router-link :to="`/@${ acct }/${ post.id }`">{{ post.text }}</router-link></p>
+	<div class="note" v-else-if="note != null">
+		<p class="text"><router-link :to="`/@${ acct }/${ note.id }`">{{ note.text }}</router-link></p>
 		<p class="author">―<router-link :to="`/@${ acct }`">@{{ acct }}</router-link></p>
 	</div>
 	<p class="empty" v-else>%i18n:desktop.tags.mk-trends-home-widget.nothing%</p>
@@ -25,12 +25,12 @@ export default define({
 }).extend({
 	computed: {
 		acct() {
-			return getAcct(this.post.user);
+			return getAcct(this.note.user);
 		},
 	},
 	data() {
 		return {
-			post: null,
+			note: null,
 			fetching: true,
 			offset: 0
 		};
@@ -44,23 +44,23 @@ export default define({
 		},
 		fetch() {
 			this.fetching = true;
-			this.post = null;
+			this.note = null;
 
-			(this as any).api('posts/trend', {
+			(this as any).api('notes/trend', {
 				limit: 1,
 				offset: this.offset,
-				repost: false,
+				renote: false,
 				reply: false,
 				media: false,
 				poll: false
-			}).then(posts => {
-				const post = posts ? posts[0] : null;
-				if (post == null) {
+			}).then(notes => {
+				const note = notes ? notes[0] : null;
+				if (note == null) {
 					this.offset = 0;
 				} else {
 					this.offset++;
 				}
-				this.post = post;
+				this.note = note;
 				this.fetching = false;
 			});
 		}
@@ -103,7 +103,7 @@ export default define({
 		&:active
 			color #999
 
-	> .post
+	> .note
 		padding 16px
 		font-size 12px
 		font-style oblique
diff --git a/src/client/app/dev/views/new-app.vue b/src/client/app/dev/views/new-app.vue
index e407ca00d..c9d597139 100644
--- a/src/client/app/dev/views/new-app.vue
+++ b/src/client/app/dev/views/new-app.vue
@@ -27,7 +27,7 @@
 					<b-form-checkbox-group v-model="permission" stacked>
 						<b-form-checkbox value="account-read">アカウントの情報を見る。</b-form-checkbox>
 						<b-form-checkbox value="account-write">アカウントの情報を操作する。</b-form-checkbox>
-						<b-form-checkbox value="post-write">投稿する。</b-form-checkbox>
+						<b-form-checkbox value="note-write">投稿する。</b-form-checkbox>
 						<b-form-checkbox value="reaction-write">リアクションしたりリアクションをキャンセルする。</b-form-checkbox>
 						<b-form-checkbox value="following-write">フォローしたりフォロー解除する。</b-form-checkbox>
 						<b-form-checkbox value="drive-read">ドライブを見る。</b-form-checkbox>
diff --git a/src/client/app/mobile/api/post.ts b/src/client/app/mobile/api/post.ts
index 98309ba8d..72919c650 100644
--- a/src/client/app/mobile/api/post.ts
+++ b/src/client/app/mobile/api/post.ts
@@ -1,24 +1,24 @@
 import PostForm from '../views/components/post-form.vue';
-//import RepostForm from '../views/components/repost-form.vue';
-import getPostSummary from '../../../../renderers/get-post-summary';
+//import RenoteForm from '../views/components/renote-form.vue';
+import getNoteSummary from '../../../../renderers/get-note-summary';
 
 export default (os) => (opts) => {
 	const o = opts || {};
 
-	if (o.repost) {
-		/*const vm = new RepostForm({
+	if (o.renote) {
+		/*const vm = new RenoteForm({
 			propsData: {
-				repost: o.repost
+				renote: o.renote
 			}
 		}).$mount();
 		vm.$once('cancel', recover);
-		vm.$once('post', recover);
+		vm.$once('note', recover);
 		document.body.appendChild(vm.$el);*/
 
-		const text = window.prompt(`「${getPostSummary(o.repost)}」をRepost`);
+		const text = window.prompt(`「${getNoteSummary(o.renote)}」をRenote`);
 		if (text == null) return;
-		os.api('posts/create', {
-			repostId: o.repost.id,
+		os.api('notes/create', {
+			renoteId: o.renote.id,
 			text: text == '' ? undefined : text
 		});
 	} else {
@@ -36,7 +36,7 @@ export default (os) => (opts) => {
 			}
 		}).$mount();
 		vm.$once('cancel', recover);
-		vm.$once('post', recover);
+		vm.$once('note', recover);
 		document.body.appendChild(vm.$el);
 		(vm as any).focus();
 	}
diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts
index 4776fccdd..6265d0d45 100644
--- a/src/client/app/mobile/script.ts
+++ b/src/client/app/mobile/script.ts
@@ -25,7 +25,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';
+import MkNote from './views/pages/note.vue';
 import MkSearch from './views/pages/search.vue';
 import MkFollowers from './views/pages/followers.vue';
 import MkFollowing from './views/pages/following.vue';
@@ -68,7 +68,7 @@ init((launch) => {
 			{ path: '/@:user', component: MkUser },
 			{ path: '/@:user/followers', component: MkFollowers },
 			{ path: '/@:user/following', component: MkFollowing },
-			{ path: '/@:user/:post', component: MkPost }
+			{ path: '/@:user/:note', component: MkNote }
 		]
 	});
 
diff --git a/src/client/app/mobile/views/components/activity.vue b/src/client/app/mobile/views/components/activity.vue
index 2e44017e7..dcd319cb6 100644
--- a/src/client/app/mobile/views/components/activity.vue
+++ b/src/client/app/mobile/views/components/activity.vue
@@ -2,14 +2,14 @@
 <div class="mk-activity">
 	<svg v-if="data" ref="canvas" viewBox="0 0 30 1" preserveAspectRatio="none">
 		<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"
+			<rect width="0.8" :height="d.notesH"
+				:x="i + 0.1" :y="1 - d.notesH - d.repliesH - d.renotesH"
 				fill="#41ddde"/>
 			<rect width="0.8" :height="d.repliesH"
-				:x="i + 0.1" :y="1 - d.repliesH - d.repostsH"
+				:x="i + 0.1" :y="1 - d.repliesH - d.renotesH"
 				fill="#f7796c"/>
-			<rect width="0.8" :height="d.repostsH"
-				:x="i + 0.1" :y="1 - d.repostsH"
+			<rect width="0.8" :height="d.renotesH"
+				:x="i + 0.1" :y="1 - d.renotesH"
 				fill="#a1de41"/>
 			</g>
 	</svg>
@@ -32,12 +32,12 @@ export default Vue.extend({
 			userId: this.user.id,
 			limit: 30
 		}).then(data => {
-			data.forEach(d => d.total = d.posts + d.replies + d.reposts);
+			data.forEach(d => d.total = d.notes + d.replies + d.renotes);
 			this.peak = Math.max.apply(null, data.map(d => d.total));
 			data.forEach(d => {
-				d.postsH = d.posts / this.peak;
+				d.notesH = d.notes / this.peak;
 				d.repliesH = d.replies / this.peak;
-				d.repostsH = d.reposts / this.peak;
+				d.renotesH = d.renotes / this.peak;
 			});
 			data.reverse();
 			this.data = data;
diff --git a/src/client/app/mobile/views/components/index.ts b/src/client/app/mobile/views/components/index.ts
index fb8f65f47..934670030 100644
--- a/src/client/app/mobile/views/components/index.ts
+++ b/src/client/app/mobile/views/components/index.ts
@@ -2,16 +2,16 @@ import Vue from 'vue';
 
 import ui from './ui.vue';
 import timeline from './timeline.vue';
-import post from './post.vue';
-import posts from './posts.vue';
+import note from './note.vue';
+import notes from './notes.vue';
 import mediaImage from './media-image.vue';
 import mediaVideo from './media-video.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 notePreview from './note-preview.vue';
+import subNoteContent from './sub-note-content.vue';
+import noteCard from './note-card.vue';
 import userCard from './user-card.vue';
-import postDetail from './post-detail.vue';
+import noteDetail from './note-detail.vue';
 import followButton from './follow-button.vue';
 import friendsMaker from './friends-maker.vue';
 import notification from './notification.vue';
@@ -25,16 +25,16 @@ import widgetContainer from './widget-container.vue';
 
 Vue.component('mk-ui', ui);
 Vue.component('mk-timeline', timeline);
-Vue.component('mk-post', post);
-Vue.component('mk-posts', posts);
+Vue.component('mk-note', note);
+Vue.component('mk-notes', notes);
 Vue.component('mk-media-image', mediaImage);
 Vue.component('mk-media-video', mediaVideo);
 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-note-preview', notePreview);
+Vue.component('mk-sub-note-content', subNoteContent);
+Vue.component('mk-note-card', noteCard);
 Vue.component('mk-user-card', userCard);
-Vue.component('mk-post-detail', postDetail);
+Vue.component('mk-note-detail', noteDetail);
 Vue.component('mk-follow-button', followButton);
 Vue.component('mk-friends-maker', friendsMaker);
 Vue.component('mk-notification', notification);
diff --git a/src/client/app/mobile/views/components/post-card.vue b/src/client/app/mobile/views/components/note-card.vue
similarity index 81%
rename from src/client/app/mobile/views/components/post-card.vue
rename to src/client/app/mobile/views/components/note-card.vue
index 68a42ef24..9ad0d3e29 100644
--- a/src/client/app/mobile/views/components/post-card.vue
+++ b/src/client/app/mobile/views/components/note-card.vue
@@ -1,41 +1,41 @@
 <template>
-<div class="mk-post-card">
-	<a :href="`/@${acct}/${post.id}`">
+<div class="mk-note-card">
+	<a :href="`/@${acct}/${note.id}`">
 		<header>
 			<img :src="`${acct}?thumbnail&size=64`" alt="avatar"/><h3>{{ name }}</h3>
 		</header>
 		<div>
 			{{ text }}
 		</div>
-		<mk-time :time="post.createdAt"/>
+		<mk-time :time="note.createdAt"/>
 	</a>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-import summary from '../../../../../renderers/get-post-summary';
+import summary from '../../../../../renderers/get-note-summary';
 import getAcct from '../../../../../acct/render';
 import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
-	props: ['post'],
+	props: ['note'],
 	computed: {
 		acct() {
-			return getAcct(this.post.user);
+			return getAcct(this.note.user);
 		},
 		name() {
-			return getUserName(this.post.user);
+			return getUserName(this.note.user);
 		},
 		text(): string {
-			return summary(this.post);
+			return summary(this.note);
 		}
 	}
 });
 </script>
 
 <style lang="stylus" scoped>
-.mk-post-card
+.mk-note-card
 	display inline-block
 	width 150px
 	//height 120px
diff --git a/src/client/app/mobile/views/components/post-detail.sub.vue b/src/client/app/mobile/views/components/note-detail.sub.vue
similarity index 80%
rename from src/client/app/mobile/views/components/post-detail.sub.vue
rename to src/client/app/mobile/views/components/note-detail.sub.vue
index 98d6a14ca..38aea4ba2 100644
--- a/src/client/app/mobile/views/components/post-detail.sub.vue
+++ b/src/client/app/mobile/views/components/note-detail.sub.vue
@@ -1,18 +1,18 @@
 <template>
 <div class="root sub">
 	<router-link class="avatar-anchor" :to="`/@${acct}`">
-		<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`">{{ getUserName(post.user) }}</router-link>
+			<router-link class="name" :to="`/@${acct}`">{{ getUserName(note.user) }}</router-link>
 			<span class="username">@{{ acct }}</span>
-			<router-link class="time" :to="`/@${acct}/${post.id}`">
-				<mk-time :time="post.createdAt"/>
+			<router-link class="time" :to="`/@${acct}/${note.id}`">
+				<mk-time :time="note.createdAt"/>
 			</router-link>
 		</header>
 		<div class="body">
-			<mk-sub-post-content class="text" :post="post"/>
+			<mk-sub-note-content class="text" :note="note"/>
 		</div>
 	</div>
 </div>
@@ -24,13 +24,13 @@ import getAcct from '../../../../../acct/render';
 import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
-	props: ['post'],
+	props: ['note'],
 	computed: {
 		acct() {
-			return getAcct(this.post.user);
+			return getAcct(this.note.user);
 		},
 		name() {
-			return getUserName(this.post.user);
+			return getUserName(this.note.user);
 		}
 	}
 });
diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue
new file mode 100644
index 000000000..e1682e58e
--- /dev/null
+++ b/src/client/app/mobile/views/components/note-detail.vue
@@ -0,0 +1,462 @@
+<template>
+<div class="mk-note-detail">
+	<button
+		class="more"
+		v-if="p.reply && p.reply.replyId && 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">
+		<x-sub v-for="note in context" :key="note.id" :note="note"/>
+	</div>
+	<div class="reply-to" v-if="p.reply">
+		<x-sub :note="p.reply"/>
+	</div>
+	<div class="renote" v-if="isRenote">
+		<p>
+			<router-link class="avatar-anchor" :to="`/@${acct}`">
+				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
+			</router-link>
+			%fa:retweet%
+			<router-link class="name" :to="`/@${acct}`">
+				{{ name }}
+			</router-link>
+			がRenote
+		</p>
+	</div>
+	<article>
+		<header>
+			<router-link class="avatar-anchor" :to="`/@${pAcct}`">
+				<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+			</router-link>
+			<div>
+				<router-link class="name" :to="`/@${pAcct}`">{{ pName }}</router-link>
+				<span class="username">@{{ pAcct }}</span>
+			</div>
+		</header>
+		<div class="body">
+			<mk-note-html v-if="p.text" :ast="p.text" :i="os.i" :class="$style.text"/>
+			<div class="tags" v-if="p.tags && p.tags.length > 0">
+				<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
+			</div>
+			<div class="media" v-if="p.media.length > 0">
+				<mk-media-list :media-list="p.media"/>
+			</div>
+			<mk-poll v-if="p.poll" :note="p"/>
+			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
+			<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
+			<div class="map" v-if="p.geo" ref="map"></div>
+			<div class="renote" v-if="p.renote">
+				<mk-note-preview :note="p.renote"/>
+			</div>
+		</div>
+		<router-link class="time" :to="`/@${pAcct}/${p.id}`">
+			<mk-time :time="p.createdAt" mode="detail"/>
+		</router-link>
+		<footer>
+			<mk-reactions-viewer :note="p"/>
+			<button @click="reply" title="%i18n:mobile.tags.mk-note-detail.reply%">
+				%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
+			</button>
+			<button @click="renote" title="Renote">
+				%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
+			</button>
+			<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:mobile.tags.mk-note-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">
+		<x-sub v-for="note in replies" :key="note.id" :note="note"/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
+import parse from '../../../../../text/parse';
+
+import MkNoteMenu from '../../../common/views/components/note-menu.vue';
+import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
+import XSub from './note-detail.sub.vue';
+
+export default Vue.extend({
+	components: {
+		XSub
+	},
+
+	props: {
+		note: {
+			type: Object,
+			required: true
+		},
+		compact: {
+			default: false
+		}
+	},
+
+	data() {
+		return {
+			context: [],
+			contextFetching: false,
+			replies: []
+		};
+	},
+
+	computed: {
+		acct(): string {
+			return getAcct(this.note.user);
+		},
+		name(): string {
+			return getUserName(this.note.user);
+		},
+		pAcct(): string {
+			return getAcct(this.p.user);
+		},
+		pName(): string {
+			return getUserName(this.p.user);
+		},
+		isRenote(): boolean {
+			return (this.note.renote &&
+				this.note.text == null &&
+				this.note.mediaIds == null &&
+				this.note.poll == null);
+		},
+		p(): any {
+			return this.isRenote ? this.note.renote : this.note;
+		},
+		reactionsCount(): number {
+			return this.p.reactionCounts
+				? Object.keys(this.p.reactionCounts)
+					.map(key => this.p.reactionCounts[key])
+					.reduce((a, b) => a + b)
+				: 0;
+		},
+		urls(): string[] {
+			if (this.p.text) {
+				const ast = parse(this.p.text);
+				return 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 as any).api('notes/replies', {
+				noteId: this.p.id,
+				limit: 8
+			}).then(replies => {
+				this.replies = replies;
+			});
+		}
+
+		// Draw map
+		if (this.p.geo) {
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true;
+			if (shouldShowMap) {
+				(this as any).os.getGoogleMaps().then(maps => {
+					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
+					const map = new maps.Map(this.$refs.map, {
+						center: uluru,
+						zoom: 15
+					});
+					new maps.Marker({
+						position: uluru,
+						map: map
+					});
+				});
+			}
+		}
+	},
+
+	methods: {
+		fetchContext() {
+			this.contextFetching = true;
+
+			// Fetch context
+			(this as any).api('notes/context', {
+				noteId: this.p.replyId
+			}).then(context => {
+				this.contextFetching = false;
+				this.context = context.reverse();
+			});
+		},
+		reply() {
+			(this as any).apis.post({
+				reply: this.p
+			});
+		},
+		renote() {
+			(this as any).apis.post({
+				renote: this.p
+			});
+		},
+		react() {
+			(this as any).os.new(MkReactionPicker, {
+				source: this.$refs.reactButton,
+				note: this.p,
+				compact: true
+			});
+		},
+		menu() {
+			(this as any).os.new(MkNoteMenu, {
+				source: this.$refs.menuButton,
+				note: this.p,
+				compact: true
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+.mk-note-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
+
+	> .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
+
+	> .renote
+		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
+
+			> .renote
+				margin 8px 0
+
+				> .mk-note-preview
+					padding 16px
+					border dashed 1px #c0dac6
+					border-radius 8px
+
+			> .location
+				margin 4px 0
+				font-size 12px
+				color #ccc
+
+			> .map
+				width 100%
+				height 200px
+
+				&:empty
+					display none
+
+			> .mk-url-preview
+				margin-top 8px
+
+			> .media
+				> img
+					display block
+					max-width 100%
+
+			> .tags
+				margin 4px 0 0 0
+
+				> *
+					display inline-block
+					margin 0 8px 0 0
+					padding 2px 8px 2px 16px
+					font-size 90%
+					color #8d969e
+					background #edf0f3
+					border-radius 4px
+
+					&:before
+						content ""
+						display block
+						position absolute
+						top 0
+						bottom 0
+						left 4px
+						width 8px
+						height 8px
+						margin auto 0
+						background #fff
+						border-radius 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>
+
+<style lang="stylus" module>
+.text
+	display block
+	margin 0
+	padding 0
+	overflow-wrap break-word
+	font-size 16px
+	color #717171
+
+	@media (min-width 500px)
+		font-size 24px
+
+</style>
diff --git a/src/client/app/mobile/views/components/post-preview.vue b/src/client/app/mobile/views/components/note-preview.vue
similarity index 81%
rename from src/client/app/mobile/views/components/post-preview.vue
rename to src/client/app/mobile/views/components/note-preview.vue
index 96bd4d5c1..8c8d8645b 100644
--- a/src/client/app/mobile/views/components/post-preview.vue
+++ b/src/client/app/mobile/views/components/note-preview.vue
@@ -1,18 +1,18 @@
 <template>
-<div class="mk-post-preview">
+<div class="mk-note-preview">
 	<router-link class="avatar-anchor" :to="`/@${acct}`">
-		<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 	</router-link>
 	<div class="main">
 		<header>
 			<router-link class="name" :to="`/@${acct}`">{{ name }}</router-link>
 			<span class="username">@{{ acct }}</span>
-			<router-link class="time" :to="`/@${acct}/${post.id}`">
-				<mk-time :time="post.createdAt"/>
+			<router-link class="time" :to="`/@${acct}/${note.id}`">
+				<mk-time :time="note.createdAt"/>
 			</router-link>
 		</header>
 		<div class="body">
-			<mk-sub-post-content class="text" :post="post"/>
+			<mk-sub-note-content class="text" :note="note"/>
 		</div>
 	</div>
 </div>
@@ -24,20 +24,20 @@ import getAcct from '../../../../../acct/render';
 import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
-	props: ['post'],
+	props: ['note'],
 	computed: {
 		acct() {
-			return getAcct(this.post.user);
+			return getAcct(this.note.user);
 		},
 		name() {
-			return getUserName(this.post.user);
+			return getUserName(this.note.user);
 		}
 	}
 });
 </script>
 
 <style lang="stylus" scoped>
-.mk-post-preview
+.mk-note-preview
 	margin 0
 	padding 0
 	font-size 0.9em
diff --git a/src/client/app/mobile/views/components/post.sub.vue b/src/client/app/mobile/views/components/note.sub.vue
similarity index 80%
rename from src/client/app/mobile/views/components/post.sub.vue
rename to src/client/app/mobile/views/components/note.sub.vue
index 909d5cb59..a37d0dea0 100644
--- a/src/client/app/mobile/views/components/post.sub.vue
+++ b/src/client/app/mobile/views/components/note.sub.vue
@@ -1,18 +1,18 @@
 <template>
 <div class="sub">
 	<router-link class="avatar-anchor" :to="`/@${acct}`">
-		<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
+		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`">{{ getUserName(post.user) }}</router-link>
+			<router-link class="name" :to="`/@${acct}`">{{ getUserName(note.user) }}</router-link>
 			<span class="username">@{{ acct }}</span>
-			<router-link class="created-at" :to="`/@${acct}/${post.id}`">
-				<mk-time :time="post.createdAt"/>
+			<router-link class="created-at" :to="`/@${acct}/${note.id}`">
+				<mk-time :time="note.createdAt"/>
 			</router-link>
 		</header>
 		<div class="body">
-			<mk-sub-post-content class="text" :post="post"/>
+			<mk-sub-note-content class="text" :note="note"/>
 		</div>
 	</div>
 </div>
@@ -24,13 +24,13 @@ import getAcct from '../../../../../acct/render';
 import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
-	props: ['post'],
+	props: ['note'],
 	computed: {
 		acct() {
-			return getAcct(this.post.user);
+			return getAcct(this.note.user);
 		},
 		name() {
-			return getUserName(this.post.user);
+			return getUserName(this.note.user);
 		}
 	}
 });
diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue
new file mode 100644
index 000000000..4b33c6f07
--- /dev/null
+++ b/src/client/app/mobile/views/components/note.vue
@@ -0,0 +1,540 @@
+<template>
+<div class="note" :class="{ renote: isRenote }">
+	<div class="reply-to" v-if="p.reply">
+		<x-sub :note="p.reply"/>
+	</div>
+	<div class="renote" v-if="isRenote">
+		<p>
+			<router-link class="avatar-anchor" :to="`/@${acct}`">
+				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+			</router-link>
+			%fa:retweet%
+			<span>{{ '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.substr(0, '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.indexOf('{')) }}</span>
+			<router-link class="name" :to="`/@${acct}`">{{ name }}</router-link>
+			<span>{{ '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.substr('%i18n:mobile.tags.mk-timeline-note.reposted-by%'.indexOf('}') + 1) }}</span>
+		</p>
+		<mk-time :time="note.createdAt"/>
+	</div>
+	<article>
+		<router-link class="avatar-anchor" :to="`/@${pAcct}`">
+			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
+		</router-link>
+		<div class="main">
+			<header>
+				<router-link class="name" :to="`/@${pAcct}`">{{ pName }}</router-link>
+				<span class="is-bot" v-if="p.user.host === null && p.user.account.isBot">bot</span>
+				<span class="username">@{{ pAcct }}</span>
+				<div class="info">
+					<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
+					<router-link class="created-at" :to="url">
+						<mk-time :time="p.createdAt"/>
+					</router-link>
+				</div>
+			</header>
+			<div class="body">
+				<p class="channel" v-if="p.channel != null"><a target="_blank">{{ p.channel.title }}</a>:</p>
+				<div class="text">
+					<a class="reply" v-if="p.reply">
+						%fa:reply%
+					</a>
+					<mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/>
+					<a class="rp" v-if="p.renote != null">RP:</a>
+				</div>
+				<div class="media" v-if="p.media.length > 0">
+					<mk-media-list :media-list="p.media"/>
+				</div>
+				<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
+				<div class="tags" v-if="p.tags && p.tags.length > 0">
+					<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
+				</div>
+				<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
+				<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
+				<div class="map" v-if="p.geo" ref="map"></div>
+				<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
+				<div class="renote" v-if="p.renote">
+					<mk-note-preview :note="p.renote"/>
+				</div>
+			</div>
+			<footer>
+				<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
+				<button @click="reply">
+					%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
+				</button>
+				<button @click="renote" title="Renote">
+					%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
+				</button>
+				<button :class="{ reacted: p.myReaction != 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 getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
+import parse from '../../../../../text/parse';
+
+import MkNoteMenu from '../../../common/views/components/note-menu.vue';
+import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
+import XSub from './note.sub.vue';
+
+export default Vue.extend({
+	components: {
+		XSub
+	},
+
+	props: ['note'],
+
+	data() {
+		return {
+			connection: null,
+			connectionId: null
+		};
+	},
+
+	computed: {
+		acct(): string {
+			return getAcct(this.note.user);
+		},
+		name(): string {
+			return getUserName(this.note.user);
+		},
+		pAcct(): string {
+			return getAcct(this.p.user);
+		},
+		pName(): string {
+			return getUserName(this.p.user);
+		},
+		isRenote(): boolean {
+			return (this.note.renote &&
+				this.note.text == null &&
+				this.note.mediaIds == null &&
+				this.note.poll == null);
+		},
+		p(): any {
+			return this.isRenote ? this.note.renote : this.note;
+		},
+		reactionsCount(): number {
+			return this.p.reactionCounts
+				? Object.keys(this.p.reactionCounts)
+					.map(key => this.p.reactionCounts[key])
+					.reduce((a, b) => a + b)
+				: 0;
+		},
+		url(): string {
+			return `/@${this.pAcct}/${this.p.id}`;
+		},
+		urls(): string[] {
+			if (this.p.text) {
+				const ast = parse(this.p.text);
+				return ast
+					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
+					.map(t => t.url);
+			} else {
+				return null;
+			}
+		}
+	},
+
+	created() {
+		if ((this as any).os.isSignedIn) {
+			this.connection = (this as any).os.stream.getConnection();
+			this.connectionId = (this as any).os.stream.use();
+		}
+	},
+
+	mounted() {
+		this.capture(true);
+
+		if ((this as any).os.isSignedIn) {
+			this.connection.on('_connected_', this.onStreamConnected);
+		}
+
+		// Draw map
+		if (this.p.geo) {
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true;
+			if (shouldShowMap) {
+				(this as any).os.getGoogleMaps().then(maps => {
+					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
+					const map = new maps.Map(this.$refs.map, {
+						center: uluru,
+						zoom: 15
+					});
+					new maps.Marker({
+						position: uluru,
+						map: map
+					});
+				});
+			}
+		}
+	},
+
+	beforeDestroy() {
+		this.decapture(true);
+
+		if ((this as any).os.isSignedIn) {
+			this.connection.off('_connected_', this.onStreamConnected);
+			(this as any).os.stream.dispose(this.connectionId);
+		}
+	},
+
+	methods: {
+		capture(withHandler = false) {
+			if ((this as any).os.isSignedIn) {
+				this.connection.send({
+					type: 'capture',
+					id: this.p.id
+				});
+				if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
+			}
+		},
+		decapture(withHandler = false) {
+			if ((this as any).os.isSignedIn) {
+				this.connection.send({
+					type: 'decapture',
+					id: this.p.id
+				});
+				if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated);
+			}
+		},
+		onStreamConnected() {
+			this.capture();
+		},
+		onStreamNoteUpdated(data) {
+			const note = data.note;
+			if (note.id == this.note.id) {
+				this.$emit('update:note', note);
+			} else if (note.id == this.note.renoteId) {
+				this.note.renote = note;
+			}
+		},
+		reply() {
+			(this as any).apis.post({
+				reply: this.p
+			});
+		},
+		renote() {
+			(this as any).apis.post({
+				renote: this.p
+			});
+		},
+		react() {
+			(this as any).os.new(MkReactionPicker, {
+				source: this.$refs.reactButton,
+				note: this.p,
+				compact: true
+			});
+		},
+		menu() {
+			(this as any).os.new(MkNoteMenu, {
+				source: this.$refs.menuButton,
+				note: this.p,
+				compact: true
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+.note
+	font-size 12px
+	border-bottom solid 1px #eaeaea
+
+	&:first-child
+		border-radius 8px 8px 0 0
+
+		> .renote
+			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
+
+	> .renote
+		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-note-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
+				align-items center
+				white-space nowrap
+
+				@media (min-width 500px)
+					margin-bottom 2px
+
+				> .name
+					display block
+					margin 0 0.5em 0 0
+					padding 0
+					overflow hidden
+					color #627079
+					font-size 1em
+					font-weight bold
+					text-decoration none
+					text-overflow ellipsis
+
+					&:hover
+						text-decoration underline
+
+				> .is-bot
+					margin 0 0.5em 0 0
+					padding 1px 6px
+					font-size 12px
+					color #aaa
+					border solid 1px #ddd
+					border-radius 3px
+
+				> .username
+					margin 0 0.5em 0 0
+					color #ccc
+
+				> .info
+					margin-left auto
+					font-size 0.9em
+
+					> .mobile
+						margin-right 6px
+						color #c0c0c0
+
+					> .created-at
+						color #c0c0c0
+
+			> .body
+
+				> .text
+					display block
+					margin 0
+					padding 0
+					overflow-wrap break-word
+					font-size 1.1em
+					color #717171
+
+					>>> .quote
+						margin 8px
+						padding 6px 12px
+						color #aaa
+						border-left solid 3px #eee
+
+					> .reply
+						margin-right 8px
+						color #717171
+
+					> .rp
+						margin-left 4px
+						font-style oblique
+						color #a0bf46
+
+					[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-url-preview
+					margin-top 8px
+
+				> .channel
+					margin 0
+
+				> .tags
+					margin 4px 0 0 0
+
+					> *
+						display inline-block
+						margin 0 8px 0 0
+						padding 2px 8px 2px 16px
+						font-size 90%
+						color #8d969e
+						background #edf0f3
+						border-radius 4px
+
+						&:before
+							content ""
+							display block
+							position absolute
+							top 0
+							bottom 0
+							left 4px
+							width 8px
+							height 8px
+							margin auto 0
+							background #fff
+							border-radius 100%
+
+				> .media
+					> img
+						display block
+						max-width 100%
+
+				> .location
+					margin 4px 0
+					font-size 12px
+					color #ccc
+
+				> .map
+					width 100%
+					height 200px
+
+					&:empty
+						display none
+
+				> .app
+					font-size 12px
+					color #ccc
+
+				> .mk-poll
+					font-size 80%
+
+				> .renote
+					margin 8px 0
+
+					> .mk-note-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>
+
+<style lang="stylus" module>
+.text
+	code
+		padding 4px 8px
+		margin 0 0.5em
+		font-size 80%
+		color #525252
+		background #f8f8f8
+		border-radius 2px
+
+	pre > code
+		padding 16px
+		margin 0
+</style>
diff --git a/src/client/app/mobile/views/components/posts.vue b/src/client/app/mobile/views/components/notes.vue
similarity index 65%
rename from src/client/app/mobile/views/components/posts.vue
rename to src/client/app/mobile/views/components/notes.vue
index 4695f1bea..573026d53 100644
--- a/src/client/app/mobile/views/components/posts.vue
+++ b/src/client/app/mobile/views/components/notes.vue
@@ -1,12 +1,12 @@
 <template>
-<div class="mk-posts">
+<div class="mk-notes">
 	<slot name="head"></slot>
 	<slot></slot>
-	<template v-for="(post, i) in _posts">
-		<mk-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>
+	<template v-for="(note, i) in _notes">
+		<mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
+		<p class="date" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
+			<span>%fa:angle-up%{{ note._datetext }}</span>
+			<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
 		</p>
 	</template>
 	<footer>
@@ -20,25 +20,25 @@ import Vue from 'vue';
 
 export default Vue.extend({
 	props: {
-		posts: {
+		notes: {
 			type: Array,
 			default: () => []
 		}
 	},
 	computed: {
-		_posts(): any[] {
-			return (this.posts as any).map(post => {
-				const date = new Date(post.createdAt).getDate();
-				const month = new Date(post.createdAt).getMonth() + 1;
-				post._date = date;
-				post._datetext = `${month}月 ${date}日`;
-				return post;
+		_notes(): any[] {
+			return (this.notes as any).map(note => {
+				const date = new Date(note.createdAt).getDate();
+				const month = new Date(note.createdAt).getMonth() + 1;
+				note._date = date;
+				note._datetext = `${month}月 ${date}日`;
+				return note;
 			});
 		}
 	},
 	methods: {
-		onPostUpdated(i, post) {
-			Vue.set((this as any).posts, i, post);
+		onNoteUpdated(i, note) {
+			Vue.set((this as any).notes, i, note);
 		}
 	}
 });
@@ -47,7 +47,7 @@ export default Vue.extend({
 <style lang="stylus" scoped>
 @import '~const.styl'
 
-.mk-posts
+.mk-notes
 	background #fff
 	border-radius 8px
 	box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
diff --git a/src/client/app/mobile/views/components/notification-preview.vue b/src/client/app/mobile/views/components/notification-preview.vue
index 0492c5d86..79ca3321e 100644
--- a/src/client/app/mobile/views/components/notification-preview.vue
+++ b/src/client/app/mobile/views/components/notification-preview.vue
@@ -4,23 +4,23 @@
 		<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
 			<p><mk-reaction-icon :reaction="notification.reaction"/>{{ name }}</p>
-			<p class="post-ref">%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%</p>
+			<p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%</p>
 		</div>
 	</template>
 
-	<template v-if="notification.type == 'repost'">
-		<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+	<template v-if="notification.type == 'renote'">
+		<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:retweet%{{ posterName }}</p>
-			<p class="post-ref">%fa:quote-left%{{ getPostSummary(notification.post.repost) }}%fa:quote-right%</p>
+			<p>%fa:retweet%{{ noteerName }}</p>
+			<p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right%</p>
 		</div>
 	</template>
 
 	<template v-if="notification.type == 'quote'">
-		<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+		<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:quote-left%{{ posterName }}</p>
-			<p class="post-preview">{{ getPostSummary(notification.post) }}</p>
+			<p>%fa:quote-left%{{ noteerName }}</p>
+			<p class="note-preview">{{ getNoteSummary(notification.note) }}</p>
 		</div>
 	</template>
 
@@ -32,18 +32,18 @@
 	</template>
 
 	<template v-if="notification.type == 'reply'">
-		<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+		<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:reply%{{ posterName }}</p>
-			<p class="post-preview">{{ getPostSummary(notification.post) }}</p>
+			<p>%fa:reply%{{ noteerName }}</p>
+			<p class="note-preview">{{ getNoteSummary(notification.note) }}</p>
 		</div>
 	</template>
 
 	<template v-if="notification.type == 'mention'">
-		<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+		<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:at%{{ posterName }}</p>
-			<p class="post-preview">{{ getPostSummary(notification.post) }}</p>
+			<p>%fa:at%{{ noteerName }}</p>
+			<p class="note-preview">{{ getNoteSummary(notification.note) }}</p>
 		</div>
 	</template>
 
@@ -51,7 +51,7 @@
 		<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
 			<p>%fa:chart-pie%{{ name }}</p>
-			<p class="post-ref">%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%</p>
+			<p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%</p>
 		</div>
 	</template>
 </div>
@@ -59,7 +59,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getPostSummary from '../../../../../renderers/get-post-summary';
+import getNoteSummary from '../../../../../renderers/get-note-summary';
 import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
@@ -68,13 +68,13 @@ export default Vue.extend({
 		name() {
 			return getUserName(this.notification.user);
 		},
-		posterName() {
-			return getUserName(this.notification.post.user);
+		noteerName() {
+			return getUserName(this.notification.note.user);
 		}
 	},
 	data() {
 		return {
-			getPostSummary
+			getNoteSummary
 		};
 	}
 });
@@ -112,7 +112,7 @@ export default Vue.extend({
 			i, mk-reaction-icon
 				margin-right 4px
 
-	.post-ref
+	.note-ref
 
 		[data-fa]
 			font-size 1em
@@ -121,7 +121,7 @@ export default Vue.extend({
 			display inline-block
 			margin-right 3px
 
-	&.repost, &.quote
+	&.renote, &.quote
 		.text p i
 			color #77B255
 
diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue
index 62a0e6425..e1294a877 100644
--- a/src/client/app/mobile/views/components/notification.vue
+++ b/src/client/app/mobile/views/components/notification.vue
@@ -10,31 +10,31 @@
 				<mk-reaction-icon :reaction="notification.reaction"/>
 				<router-link :to="`/@${acct}`">{{ getUserName(notification.user) }}</router-link>
 			</p>
-			<router-link class="post-ref" :to="`/@${acct}/${notification.post.id}`">
-				%fa:quote-left%{{ getPostSummary(notification.post) }}
+			<router-link class="note-ref" :to="`/@${acct}/${notification.note.id}`">
+				%fa:quote-left%{{ getNoteSummary(notification.note) }}
 				%fa:quote-right%
 			</router-link>
 		</div>
 	</div>
 
-	<div class="notification repost" v-if="notification.type == 'repost'">
+	<div class="notification renote" v-if="notification.type == 'renote'">
 		<mk-time :time="notification.createdAt"/>
 		<router-link class="avatar-anchor" :to="`/@${acct}`">
-			<img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+			<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="text">
 			<p>
 				%fa:retweet%
-				<router-link :to="`/@${acct}`">{{ getUserName(notification.post.user) }}</router-link>
+				<router-link :to="`/@${acct}`">{{ getUserName(notification.note.user) }}</router-link>
 			</p>
-			<router-link class="post-ref" :to="`/@${acct}/${notification.post.id}`">
-				%fa:quote-left%{{ getPostSummary(notification.post.repost) }}%fa:quote-right%
+			<router-link class="note-ref" :to="`/@${acct}/${notification.note.id}`">
+				%fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right%
 			</router-link>
 		</div>
 	</div>
 
 	<template v-if="notification.type == 'quote'">
-		<mk-post :post="notification.post"/>
+		<mk-note :note="notification.note"/>
 	</template>
 
 	<div class="notification follow" v-if="notification.type == 'follow'">
@@ -51,11 +51,11 @@
 	</div>
 
 	<template v-if="notification.type == 'reply'">
-		<mk-post :post="notification.post"/>
+		<mk-note :note="notification.note"/>
 	</template>
 
 	<template v-if="notification.type == 'mention'">
-		<mk-post :post="notification.post"/>
+		<mk-note :note="notification.note"/>
 	</template>
 
 	<div class="notification poll_vote" v-if="notification.type == 'poll_vote'">
@@ -68,8 +68,8 @@
 				%fa:chart-pie%
 				<router-link :to="`/@${acct}`">{{ getUserName(notification.user) }}</router-link>
 			</p>
-			<router-link class="post-ref" :to="`/@${acct}/${notification.post.id}`">
-				%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%
+			<router-link class="note-ref" :to="`/@${acct}/${notification.note.id}`">
+				%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%
 			</router-link>
 		</div>
 	</div>
@@ -78,7 +78,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getPostSummary from '../../../../../renderers/get-post-summary';
+import getNoteSummary from '../../../../../renderers/get-note-summary';
 import getAcct from '../../../../../acct/render';
 import getUserName from '../../../../../renderers/get-user-name';
 
@@ -91,13 +91,13 @@ export default Vue.extend({
 		name() {
 			return getUserName(this.notification.user);
 		},
-		posterName() {
- 			return getUserName(this.notification.post.user);
+		noteerName() {
+ 			return getUserName(this.notification.note.user);
 		}
 	},
 	data() {
 		return {
-			getPostSummary
+			getNoteSummary
 		};
 	}
 });
@@ -146,10 +146,10 @@ export default Vue.extend({
 				i, .mk-reaction-icon
 					margin-right 4px
 
-			> .post-preview
+			> .note-preview
 				color rgba(0, 0, 0, 0.7)
 
-			> .post-ref
+			> .note-ref
 				color rgba(0, 0, 0, 0.7)
 
 				[data-fa]
@@ -159,7 +159,7 @@ export default Vue.extend({
 					display inline-block
 					margin-right 3px
 
-		&.repost
+		&.renote
 			.text p i
 				color #77B255
 
diff --git a/src/client/app/mobile/views/components/post-detail.vue b/src/client/app/mobile/views/components/post-detail.vue
index 0226ce081..e1682e58e 100644
--- a/src/client/app/mobile/views/components/post-detail.vue
+++ b/src/client/app/mobile/views/components/post-detail.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-post-detail">
+<div class="mk-note-detail">
 	<button
 		class="more"
 		v-if="p.reply && p.reply.replyId && context == null"
@@ -10,21 +10,21 @@
 		<template v-if="contextFetching">%fa:spinner .pulse%</template>
 	</button>
 	<div class="context">
-		<x-sub v-for="post in context" :key="post.id" :post="post"/>
+		<x-sub v-for="note in context" :key="note.id" :note="note"/>
 	</div>
 	<div class="reply-to" v-if="p.reply">
-		<x-sub :post="p.reply"/>
+		<x-sub :note="p.reply"/>
 	</div>
-	<div class="repost" v-if="isRepost">
+	<div class="renote" v-if="isRenote">
 		<p>
 			<router-link class="avatar-anchor" :to="`/@${acct}`">
-				<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
+				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
 			</router-link>
 			%fa:retweet%
 			<router-link class="name" :to="`/@${acct}`">
 				{{ name }}
 			</router-link>
-			がRepost
+			がRenote
 		</p>
 	</div>
 	<article>
@@ -38,33 +38,33 @@
 			</div>
 		</header>
 		<div class="body">
-			<mk-post-html v-if="p.text" :ast="p.text" :i="os.i" :class="$style.text"/>
+			<mk-note-html v-if="p.text" :ast="p.text" :i="os.i" :class="$style.text"/>
 			<div class="tags" v-if="p.tags && p.tags.length > 0">
 				<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
 			</div>
 			<div class="media" v-if="p.media.length > 0">
 				<mk-media-list :media-list="p.media"/>
 			</div>
-			<mk-poll v-if="p.poll" :post="p"/>
+			<mk-poll v-if="p.poll" :note="p"/>
 			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 			<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
 			<div class="map" v-if="p.geo" ref="map"></div>
-			<div class="repost" v-if="p.repost">
-				<mk-post-preview :post="p.repost"/>
+			<div class="renote" v-if="p.renote">
+				<mk-note-preview :note="p.renote"/>
 			</div>
 		</div>
 		<router-link class="time" :to="`/@${pAcct}/${p.id}`">
 			<mk-time :time="p.createdAt" mode="detail"/>
 		</router-link>
 		<footer>
-			<mk-reactions-viewer :post="p"/>
-			<button @click="reply" title="%i18n:mobile.tags.mk-post-detail.reply%">
+			<mk-reactions-viewer :note="p"/>
+			<button @click="reply" title="%i18n:mobile.tags.mk-note-detail.reply%">
 				%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
 			</button>
-			<button @click="repost" title="Repost">
-				%fa:retweet%<p class="count" v-if="p.repostCount > 0">{{ p.repostCount }}</p>
+			<button @click="renote" title="Renote">
+				%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
 			</button>
-			<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:mobile.tags.mk-post-detail.reaction%">
+			<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:mobile.tags.mk-note-detail.reaction%">
 				%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
 			</button>
 			<button @click="menu" ref="menuButton">
@@ -73,7 +73,7 @@
 		</footer>
 	</article>
 	<div class="replies" v-if="!compact">
-		<x-sub v-for="post in replies" :key="post.id" :post="post"/>
+		<x-sub v-for="note in replies" :key="note.id" :note="note"/>
 	</div>
 </div>
 </template>
@@ -84,9 +84,9 @@ import getAcct from '../../../../../acct/render';
 import getUserName from '../../../../../renderers/get-user-name';
 import parse from '../../../../../text/parse';
 
-import MkPostMenu from '../../../common/views/components/post-menu.vue';
+import MkNoteMenu from '../../../common/views/components/note-menu.vue';
 import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
-import XSub from './post-detail.sub.vue';
+import XSub from './note-detail.sub.vue';
 
 export default Vue.extend({
 	components: {
@@ -94,7 +94,7 @@ export default Vue.extend({
 	},
 
 	props: {
-		post: {
+		note: {
 			type: Object,
 			required: true
 		},
@@ -113,10 +113,10 @@ export default Vue.extend({
 
 	computed: {
 		acct(): string {
-			return getAcct(this.post.user);
+			return getAcct(this.note.user);
 		},
 		name(): string {
-			return getUserName(this.post.user);
+			return getUserName(this.note.user);
 		},
 		pAcct(): string {
 			return getAcct(this.p.user);
@@ -124,14 +124,14 @@ export default Vue.extend({
 		pName(): string {
 			return getUserName(this.p.user);
 		},
-		isRepost(): boolean {
-			return (this.post.repost &&
-				this.post.text == null &&
-				this.post.mediaIds == null &&
-				this.post.poll == null);
+		isRenote(): boolean {
+			return (this.note.renote &&
+				this.note.text == null &&
+				this.note.mediaIds == null &&
+				this.note.poll == null);
 		},
 		p(): any {
-			return this.isRepost ? this.post.repost : this.post;
+			return this.isRenote ? this.note.renote : this.note;
 		},
 		reactionsCount(): number {
 			return this.p.reactionCounts
@@ -155,8 +155,8 @@ export default Vue.extend({
 	mounted() {
 		// Get replies
 		if (!this.compact) {
-			(this as any).api('posts/replies', {
-				postId: this.p.id,
+			(this as any).api('notes/replies', {
+				noteId: this.p.id,
 				limit: 8
 			}).then(replies => {
 				this.replies = replies;
@@ -187,8 +187,8 @@ export default Vue.extend({
 			this.contextFetching = true;
 
 			// Fetch context
-			(this as any).api('posts/context', {
-				postId: this.p.replyId
+			(this as any).api('notes/context', {
+				noteId: this.p.replyId
 			}).then(context => {
 				this.contextFetching = false;
 				this.context = context.reverse();
@@ -199,22 +199,22 @@ export default Vue.extend({
 				reply: this.p
 			});
 		},
-		repost() {
+		renote() {
 			(this as any).apis.post({
-				repost: this.p
+				renote: this.p
 			});
 		},
 		react() {
 			(this as any).os.new(MkReactionPicker, {
 				source: this.$refs.reactButton,
-				post: this.p,
+				note: this.p,
 				compact: true
 			});
 		},
 		menu() {
-			(this as any).os.new(MkPostMenu, {
+			(this as any).os.new(MkNoteMenu, {
 				source: this.$refs.menuButton,
-				post: this.p,
+				note: this.p,
 				compact: true
 			});
 		}
@@ -225,7 +225,7 @@ export default Vue.extend({
 <style lang="stylus" scoped>
 @import '~const.styl'
 
-.mk-post-detail
+.mk-note-detail
 	overflow hidden
 	margin 0 auto
 	padding 0
@@ -267,7 +267,7 @@ export default Vue.extend({
 		> *
 			border-bottom 1px solid #eef0f2
 
-	> .repost
+	> .renote
 		color #9dbb00
 		background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
 
@@ -357,10 +357,10 @@ export default Vue.extend({
 		> .body
 			padding 8px 0
 
-			> .repost
+			> .renote
 				margin 8px 0
 
-				> .mk-post-preview
+				> .mk-note-preview
 					padding 16px
 					border dashed 1px #c0dac6
 					border-radius 8px
diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue
index 5b78a2571..6fcbbb47e 100644
--- a/src/client/app/mobile/views/components/post-form.vue
+++ b/src/client/app/mobile/views/components/post-form.vue
@@ -9,8 +9,8 @@
 		</div>
 	</header>
 	<div class="form">
-		<mk-post-preview v-if="reply" :post="reply"/>
-		<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>
+		<mk-note-preview v-if="reply" :note="reply"/>
+		<textarea v-model="text" ref="text" :disabled="posting" :placeholder="reply ? '%i18n:mobile.tags.mk-post-form.reply-placeholder%' : '%i18n:mobile.tags.mk-post-form.note-placeholder%'"></textarea>
 		<div class="attaches" v-show="files.length != 0">
 			<x-draggable class="files" :list="files" :options="{ animation: 150 }">
 				<div class="file" v-for="file in files" :key="file.id">
@@ -112,7 +112,7 @@ export default Vue.extend({
 		post() {
 			this.posting = true;
 			const viaMobile = (this as any).os.i.account.clientSettings.disableViaMobile !== true;
-			(this as any).api('posts/create', {
+			(this as any).api('notes/create', {
 				text: this.text == '' ? undefined : this.text,
 				mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
 				replyId: this.reply ? this.reply.id : undefined,
@@ -127,7 +127,7 @@ export default Vue.extend({
 				} : null,
 				viaMobile: viaMobile
 			}).then(data => {
-				this.$emit('post');
+				this.$emit('note');
 				this.$destroy();
 			}).catch(err => {
 				this.posting = false;
@@ -200,7 +200,7 @@ export default Vue.extend({
 		max-width 500px
 		margin 0 auto
 
-		> .mk-post-preview
+		> .mk-note-preview
 			padding 16px
 
 		> .attaches
diff --git a/src/client/app/mobile/views/components/post.vue b/src/client/app/mobile/views/components/post.vue
index eee1e80fd..4b33c6f07 100644
--- a/src/client/app/mobile/views/components/post.vue
+++ b/src/client/app/mobile/views/components/post.vue
@@ -1,19 +1,19 @@
 <template>
-<div class="post" :class="{ repost: isRepost }">
+<div class="note" :class="{ renote: isRenote }">
 	<div class="reply-to" v-if="p.reply">
-		<x-sub :post="p.reply"/>
+		<x-sub :note="p.reply"/>
 	</div>
-	<div class="repost" v-if="isRepost">
+	<div class="renote" v-if="isRenote">
 		<p>
 			<router-link class="avatar-anchor" :to="`/@${acct}`">
-				<img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 			</router-link>
 			%fa:retweet%
-			<span>{{ '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('{')) }}</span>
+			<span>{{ '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.substr(0, '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.indexOf('{')) }}</span>
 			<router-link class="name" :to="`/@${acct}`">{{ name }}</router-link>
-			<span>{{ '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr('%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1) }}</span>
+			<span>{{ '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.substr('%i18n:mobile.tags.mk-timeline-note.reposted-by%'.indexOf('}') + 1) }}</span>
 		</p>
-		<mk-time :time="post.createdAt"/>
+		<mk-time :time="note.createdAt"/>
 	</div>
 	<article>
 		<router-link class="avatar-anchor" :to="`/@${pAcct}`">
@@ -37,13 +37,13 @@
 					<a class="reply" v-if="p.reply">
 						%fa:reply%
 					</a>
-					<mk-post-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/>
-					<a class="rp" v-if="p.repost != null">RP:</a>
+					<mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/>
+					<a class="rp" v-if="p.renote != null">RP:</a>
 				</div>
 				<div class="media" v-if="p.media.length > 0">
 					<mk-media-list :media-list="p.media"/>
 				</div>
-				<mk-poll v-if="p.poll" :post="p" ref="pollViewer"/>
+				<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
 				<div class="tags" v-if="p.tags && p.tags.length > 0">
 					<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
 				</div>
@@ -51,17 +51,17 @@
 				<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
 				<div class="map" v-if="p.geo" ref="map"></div>
 				<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
-				<div class="repost" v-if="p.repost">
-					<mk-post-preview :post="p.repost"/>
+				<div class="renote" v-if="p.renote">
+					<mk-note-preview :note="p.renote"/>
 				</div>
 			</div>
 			<footer>
-				<mk-reactions-viewer :post="p" ref="reactionsViewer"/>
+				<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
 				<button @click="reply">
 					%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
 				</button>
-				<button @click="repost" title="Repost">
-					%fa:retweet%<p class="count" v-if="p.repostCount > 0">{{ p.repostCount }}</p>
+				<button @click="renote" title="Renote">
+					%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
 				</button>
 				<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton">
 					%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
@@ -81,16 +81,16 @@ import getAcct from '../../../../../acct/render';
 import getUserName from '../../../../../renderers/get-user-name';
 import parse from '../../../../../text/parse';
 
-import MkPostMenu from '../../../common/views/components/post-menu.vue';
+import MkNoteMenu from '../../../common/views/components/note-menu.vue';
 import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
-import XSub from './post.sub.vue';
+import XSub from './note.sub.vue';
 
 export default Vue.extend({
 	components: {
 		XSub
 	},
 
-	props: ['post'],
+	props: ['note'],
 
 	data() {
 		return {
@@ -101,10 +101,10 @@ export default Vue.extend({
 
 	computed: {
 		acct(): string {
-			return getAcct(this.post.user);
+			return getAcct(this.note.user);
 		},
 		name(): string {
-			return getUserName(this.post.user);
+			return getUserName(this.note.user);
 		},
 		pAcct(): string {
 			return getAcct(this.p.user);
@@ -112,14 +112,14 @@ export default Vue.extend({
 		pName(): string {
 			return getUserName(this.p.user);
 		},
-		isRepost(): boolean {
-			return (this.post.repost &&
-				this.post.text == null &&
-				this.post.mediaIds == null &&
-				this.post.poll == null);
+		isRenote(): boolean {
+			return (this.note.renote &&
+				this.note.text == null &&
+				this.note.mediaIds == null &&
+				this.note.poll == null);
 		},
 		p(): any {
-			return this.isRepost ? this.post.repost : this.post;
+			return this.isRenote ? this.note.renote : this.note;
 		},
 		reactionsCount(): number {
 			return this.p.reactionCounts
@@ -192,7 +192,7 @@ export default Vue.extend({
 					type: 'capture',
 					id: this.p.id
 				});
-				if (withHandler) this.connection.on('post-updated', this.onStreamPostUpdated);
+				if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
 			}
 		},
 		decapture(withHandler = false) {
@@ -201,18 +201,18 @@ export default Vue.extend({
 					type: 'decapture',
 					id: this.p.id
 				});
-				if (withHandler) this.connection.off('post-updated', this.onStreamPostUpdated);
+				if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated);
 			}
 		},
 		onStreamConnected() {
 			this.capture();
 		},
-		onStreamPostUpdated(data) {
-			const post = data.post;
-			if (post.id == this.post.id) {
-				this.$emit('update:post', post);
-			} else if (post.id == this.post.repostId) {
-				this.post.repost = post;
+		onStreamNoteUpdated(data) {
+			const note = data.note;
+			if (note.id == this.note.id) {
+				this.$emit('update:note', note);
+			} else if (note.id == this.note.renoteId) {
+				this.note.renote = note;
 			}
 		},
 		reply() {
@@ -220,22 +220,22 @@ export default Vue.extend({
 				reply: this.p
 			});
 		},
-		repost() {
+		renote() {
 			(this as any).apis.post({
-				repost: this.p
+				renote: this.p
 			});
 		},
 		react() {
 			(this as any).os.new(MkReactionPicker, {
 				source: this.$refs.reactButton,
-				post: this.p,
+				note: this.p,
 				compact: true
 			});
 		},
 		menu() {
-			(this as any).os.new(MkPostMenu, {
+			(this as any).os.new(MkNoteMenu, {
 				source: this.$refs.menuButton,
-				post: this.p,
+				note: this.p,
 				compact: true
 			});
 		}
@@ -246,14 +246,14 @@ export default Vue.extend({
 <style lang="stylus" scoped>
 @import '~const.styl'
 
-.post
+.note
 	font-size 12px
 	border-bottom solid 1px #eaeaea
 
 	&:first-child
 		border-radius 8px 8px 0 0
 
-		> .repost
+		> .renote
 			border-radius 8px 8px 0 0
 
 	&:last-of-type
@@ -265,7 +265,7 @@ export default Vue.extend({
 	@media (min-width 500px)
 		font-size 16px
 
-	> .repost
+	> .renote
 		color #9dbb00
 		background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
 
@@ -309,7 +309,7 @@ export default Vue.extend({
 	> .reply-to
 		background rgba(0, 0, 0, 0.0125)
 
-		> .mk-post-preview
+		> .mk-note-preview
 			background transparent
 
 	> article
@@ -485,10 +485,10 @@ export default Vue.extend({
 				> .mk-poll
 					font-size 80%
 
-				> .repost
+				> .renote
 					margin 8px 0
 
-					> .mk-post-preview
+					> .mk-note-preview
 						padding 16px
 						border dashed 1px #c0dac6
 						border-radius 8px
diff --git a/src/client/app/mobile/views/components/sub-note-content.vue b/src/client/app/mobile/views/components/sub-note-content.vue
new file mode 100644
index 000000000..22e6ebe3f
--- /dev/null
+++ b/src/client/app/mobile/views/components/sub-note-content.vue
@@ -0,0 +1,43 @@
+<template>
+<div class="mk-sub-note-content">
+	<div class="body">
+		<a class="reply" v-if="note.replyId">%fa:reply%</a>
+		<mk-note-html v-if="note.text" :text="note.text" :i="os.i"/>
+		<a class="rp" v-if="note.renoteId">RP: ...</a>
+	</div>
+	<details v-if="note.media.length > 0">
+		<summary>({{ note.media.length }}個のメディア)</summary>
+		<mk-media-list :media-list="note.media"/>
+	</details>
+	<details v-if="note.poll">
+		<summary>%i18n:mobile.tags.mk-sub-note-content.poll%</summary>
+		<mk-poll :note="note"/>
+	</details>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['note']
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-sub-note-content
+	overflow-wrap break-word
+
+	> .body
+		> .reply
+			margin-right 6px
+			color #717171
+
+		> .rp
+			margin-left 4px
+			font-style oblique
+			color #a0bf46
+
+	mk-poll
+		font-size 80%
+
+</style>
diff --git a/src/client/app/mobile/views/components/sub-post-content.vue b/src/client/app/mobile/views/components/sub-post-content.vue
deleted file mode 100644
index 97dd987dd..000000000
--- a/src/client/app/mobile/views/components/sub-post-content.vue
+++ /dev/null
@@ -1,43 +0,0 @@
-<template>
-<div class="mk-sub-post-content">
-	<div class="body">
-		<a class="reply" v-if="post.replyId">%fa:reply%</a>
-		<mk-post-html v-if="post.text" :text="post.text" :i="os.i"/>
-		<a class="rp" v-if="post.repostId">RP: ...</a>
-	</div>
-	<details v-if="post.media.length > 0">
-		<summary>({{ post.media.length }}個のメディア)</summary>
-		<mk-media-list :media-list="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
-
-		> .rp
-			margin-left 4px
-			font-style oblique
-			color #a0bf46
-
-	mk-poll
-		font-size 80%
-
-</style>
diff --git a/src/client/app/mobile/views/components/timeline.vue b/src/client/app/mobile/views/components/timeline.vue
index 7b5948faf..4d6abcd16 100644
--- a/src/client/app/mobile/views/components/timeline.vue
+++ b/src/client/app/mobile/views/components/timeline.vue
@@ -1,11 +1,11 @@
 <template>
 <div class="mk-timeline">
 	<mk-friends-maker v-if="alone"/>
-	<mk-posts :posts="posts">
+	<mk-notes :notes="notes">
 		<div class="init" v-if="fetching">
 			%fa:spinner .pulse%%i18n:common.loading%
 		</div>
-		<div class="empty" v-if="!fetching && posts.length == 0">
+		<div class="empty" v-if="!fetching && notes.length == 0">
 			%fa:R comments%
 			%i18n:mobile.tags.mk-home-timeline.empty-timeline%
 		</div>
@@ -13,7 +13,7 @@
 			<span v-if="!moreFetching">%i18n:mobile.tags.mk-timeline.load-more%</span>
 			<span v-if="moreFetching">%i18n:common.loading%<mk-ellipsis/></span>
 		</button>
-	</mk-posts>
+	</mk-notes>
 </div>
 </template>
 
@@ -33,7 +33,7 @@ export default Vue.extend({
 		return {
 			fetching: true,
 			moreFetching: false,
-			posts: [],
+			notes: [],
 			existMore: false,
 			connection: null,
 			connectionId: null
@@ -48,14 +48,14 @@ export default Vue.extend({
 		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('note', this.onNote);
 		this.connection.on('follow', this.onChangeFollowing);
 		this.connection.on('unfollow', this.onChangeFollowing);
 
 		this.fetch();
 	},
 	beforeDestroy() {
-		this.connection.off('post', this.onPost);
+		this.connection.off('note', this.onNote);
 		this.connection.off('follow', this.onChangeFollowing);
 		this.connection.off('unfollow', this.onChangeFollowing);
 		(this as any).os.stream.dispose(this.connectionId);
@@ -63,15 +63,15 @@ export default Vue.extend({
 	methods: {
 		fetch(cb?) {
 			this.fetching = true;
-			(this as any).api('posts/timeline', {
+			(this as any).api('notes/timeline', {
 				limit: limit + 1,
 				untilDate: this.date ? (this.date as any).getTime() : undefined
-			}).then(posts => {
-				if (posts.length == limit + 1) {
-					posts.pop();
+			}).then(notes => {
+				if (notes.length == limit + 1) {
+					notes.pop();
 					this.existMore = true;
 				}
-				this.posts = posts;
+				this.notes = notes;
 				this.fetching = false;
 				this.$emit('loaded');
 				if (cb) cb();
@@ -79,22 +79,22 @@ export default Vue.extend({
 		},
 		more() {
 			this.moreFetching = true;
-			(this as any).api('posts/timeline', {
+			(this as any).api('notes/timeline', {
 				limit: limit + 1,
-				untilId: this.posts[this.posts.length - 1].id
-			}).then(posts => {
-				if (posts.length == limit + 1) {
-					posts.pop();
+				untilId: this.notes[this.notes.length - 1].id
+			}).then(notes => {
+				if (notes.length == limit + 1) {
+					notes.pop();
 					this.existMore = true;
 				} else {
 					this.existMore = false;
 				}
-				this.posts = this.posts.concat(posts);
+				this.notes = this.notes.concat(notes);
 				this.moreFetching = false;
 			});
 		},
-		onPost(post) {
-			this.posts.unshift(post);
+		onNote(note) {
+			this.notes.unshift(note);
 		},
 		onChangeFollowing() {
 			this.fetch();
diff --git a/src/client/app/mobile/views/components/user-timeline.vue b/src/client/app/mobile/views/components/user-timeline.vue
index bd3e3d0c8..7a04441f7 100644
--- a/src/client/app/mobile/views/components/user-timeline.vue
+++ b/src/client/app/mobile/views/components/user-timeline.vue
@@ -1,18 +1,18 @@
 <template>
 <div class="mk-user-timeline">
-	<mk-posts :posts="posts">
+	<mk-notes :notes="notes">
 		<div class="init" v-if="fetching">
 			%fa:spinner .pulse%%i18n:common.loading%
 		</div>
-		<div class="empty" v-if="!fetching && posts.length == 0">
+		<div class="empty" v-if="!fetching && notes.length == 0">
 			%fa:R comments%
-			{{ withMedia ? '%i18n:mobile.tags.mk-user-timeline.no-posts-with-media%' : '%i18n:mobile.tags.mk-user-timeline.no-posts%' }}
+			{{ withMedia ? '%i18n:mobile.tags.mk-user-timeline.no-notes-with-media%' : '%i18n:mobile.tags.mk-user-timeline.no-notes%' }}
 		</div>
 		<button v-if="!fetching && existMore" @click="more" :disabled="moreFetching" slot="tail">
 			<span v-if="!moreFetching">%i18n:mobile.tags.mk-user-timeline.load-more%</span>
 			<span v-if="moreFetching">%i18n:common.loading%<mk-ellipsis/></span>
 		</button>
-	</mk-posts>
+	</mk-notes>
 </div>
 </template>
 
@@ -26,22 +26,22 @@ export default Vue.extend({
 	data() {
 		return {
 			fetching: true,
-			posts: [],
+			notes: [],
 			existMore: false,
 			moreFetching: false
 		};
 	},
 	mounted() {
-		(this as any).api('users/posts', {
+		(this as any).api('users/notes', {
 			userId: this.user.id,
 			withMedia: this.withMedia,
 			limit: limit + 1
-		}).then(posts => {
-			if (posts.length == limit + 1) {
-				posts.pop();
+		}).then(notes => {
+			if (notes.length == limit + 1) {
+				notes.pop();
 				this.existMore = true;
 			}
-			this.posts = posts;
+			this.notes = notes;
 			this.fetching = false;
 			this.$emit('loaded');
 		});
@@ -49,19 +49,19 @@ export default Vue.extend({
 	methods: {
 		more() {
 			this.moreFetching = true;
-			(this as any).api('users/posts', {
+			(this as any).api('users/notes', {
 				userId: this.user.id,
 				withMedia: this.withMedia,
 				limit: limit + 1,
-				untilId: this.posts[this.posts.length - 1].id
-			}).then(posts => {
-				if (posts.length == limit + 1) {
-					posts.pop();
+				untilId: this.notes[this.notes.length - 1].id
+			}).then(notes => {
+				if (notes.length == limit + 1) {
+					notes.pop();
 					this.existMore = true;
 				} else {
 					this.existMore = false;
 				}
-				this.posts = this.posts.concat(posts);
+				this.notes = this.notes.concat(notes);
 				this.moreFetching = false;
 			});
 		}
diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue
index 6fb1c6d4f..ab61166cf 100644
--- a/src/client/app/mobile/views/pages/home.vue
+++ b/src/client/app/mobile/views/pages/home.vue
@@ -64,7 +64,7 @@ import Vue from 'vue';
 import * as XDraggable from 'vuedraggable';
 import * as uuid from 'uuid';
 import Progress from '../../../common/scripts/loading';
-import getPostSummary from '../../../../../renderers/get-post-summary';
+import getNoteSummary from '../../../../../renderers/get-note-summary';
 
 export default Vue.extend({
 	components: {
@@ -124,14 +124,14 @@ export default Vue.extend({
 		this.connection = (this as any).os.stream.getConnection();
 		this.connectionId = (this as any).os.stream.use();
 
-		this.connection.on('post', this.onStreamPost);
+		this.connection.on('note', this.onStreamNote);
 		this.connection.on('mobile_home_updated', this.onHomeUpdated);
 		document.addEventListener('visibilitychange', this.onVisibilitychange, false);
 
 		Progress.start();
 	},
 	beforeDestroy() {
-		this.connection.off('post', this.onStreamPost);
+		this.connection.off('note', this.onStreamNote);
 		this.connection.off('mobile_home_updated', this.onHomeUpdated);
 		(this as any).os.stream.dispose(this.connectionId);
 		document.removeEventListener('visibilitychange', this.onVisibilitychange);
@@ -143,10 +143,10 @@ export default Vue.extend({
 		onLoaded() {
 			Progress.done();
 		},
-		onStreamPost(post) {
-			if (document.hidden && post.userId !== (this as any).os.i.id) {
+		onStreamNote(note) {
+			if (document.hidden && note.userId !== (this as any).os.i.id) {
 				this.unreadCount++;
-				document.title = `(${this.unreadCount}) ${getPostSummary(post)}`;
+				document.title = `(${this.unreadCount}) ${getNoteSummary(note)}`;
 			}
 		},
 		onVisibilitychange() {
diff --git a/src/client/app/mobile/views/pages/post.vue b/src/client/app/mobile/views/pages/note.vue
similarity index 71%
rename from src/client/app/mobile/views/pages/post.vue
rename to src/client/app/mobile/views/pages/note.vue
index 49a4bfd9d..89b8c776f 100644
--- a/src/client/app/mobile/views/pages/post.vue
+++ b/src/client/app/mobile/views/pages/note.vue
@@ -1,12 +1,12 @@
 <template>
 <mk-ui>
-	<span slot="header">%fa:R sticky-note%%i18n:mobile.tags.mk-post-page.title%</span>
+	<span slot="header">%fa:R sticky-note%%i18n:mobile.tags.mk-note-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>
+		<a v-if="note.next" :href="note.next">%fa:angle-up%%i18n:mobile.tags.mk-note-page.next%</a>
 		<div>
-			<mk-post-detail :post="post"/>
+			<mk-note-detail :note="note"/>
 		</div>
-		<a v-if="post.prev" :href="post.prev">%fa:angle-down%%i18n:mobile.tags.mk-post-page.prev%</a>
+		<a v-if="note.prev" :href="note.prev">%fa:angle-down%%i18n:mobile.tags.mk-note-page.prev%</a>
 	</main>
 </mk-ui>
 </template>
@@ -19,7 +19,7 @@ export default Vue.extend({
 	data() {
 		return {
 			fetching: true,
-			post: null
+			note: null
 		};
 	},
 	watch: {
@@ -37,10 +37,10 @@ export default Vue.extend({
 			Progress.start();
 			this.fetching = true;
 
-			(this as any).api('posts/show', {
-				postId: this.$route.params.post
-			}).then(post => {
-				this.post = post;
+			(this as any).api('notes/show', {
+				noteId: this.$route.params.note
+			}).then(note => {
+				this.note = note;
 				this.fetching = false;
 
 				Progress.done();
diff --git a/src/client/app/mobile/views/pages/search.vue b/src/client/app/mobile/views/pages/search.vue
index cbab504e3..a96832bee 100644
--- a/src/client/app/mobile/views/pages/search.vue
+++ b/src/client/app/mobile/views/pages/search.vue
@@ -2,13 +2,13 @@
 <mk-ui>
 	<span slot="header">%fa:search% {{ q }}</span>
 	<main v-if="!fetching">
-		<mk-posts :class="$style.posts" :posts="posts">
-			<span v-if="posts.length == 0">{{ '%i18n:mobile.tags.mk-search-posts.empty%'.replace('{}', q) }}</span>
+		<mk-notes :class="$style.notes" :notes="notes">
+			<span v-if="notes.length == 0">{{ '%i18n:mobile.tags.mk-search-notes.empty%'.replace('{}', q) }}</span>
 			<button v-if="existMore" @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>
+		</mk-notes>
 	</main>
 </mk-ui>
 </template>
@@ -25,7 +25,7 @@ export default Vue.extend({
 		return {
 			fetching: true,
 			existMore: false,
-			posts: [],
+			notes: [],
 			offset: 0
 		};
 	},
@@ -48,30 +48,30 @@ export default Vue.extend({
 			this.fetching = true;
 			Progress.start();
 
-			(this as any).api('posts/search', Object.assign({
+			(this as any).api('notes/search', Object.assign({
 				limit: limit + 1
-			}, parse(this.q))).then(posts => {
-				if (posts.length == limit + 1) {
-					posts.pop();
+			}, parse(this.q))).then(notes => {
+				if (notes.length == limit + 1) {
+					notes.pop();
 					this.existMore = true;
 				}
-				this.posts = posts;
+				this.notes = notes;
 				this.fetching = false;
 				Progress.done();
 			});
 		},
 		more() {
 			this.offset += limit;
-			(this as any).api('posts/search', Object.assign({
+			(this as any).api('notes/search', Object.assign({
 				limit: limit + 1,
 				offset: this.offset
-			}, parse(this.q))).then(posts => {
-				if (posts.length == limit + 1) {
-					posts.pop();
+			}, parse(this.q))).then(notes => {
+				if (notes.length == limit + 1) {
+					notes.pop();
 				} else {
 					this.existMore = false;
 				}
-				this.posts = this.posts.concat(posts);
+				this.notes = this.notes.concat(notes);
 			});
 		}
 	}
@@ -79,7 +79,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" module>
-.posts
+.notes
 	margin 8px auto
 	max-width 500px
 	width calc(100% - 16px)
diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue
index 3b7b4d6c2..08fa4e277 100644
--- a/src/client/app/mobile/views/pages/user.vue
+++ b/src/client/app/mobile/views/pages/user.vue
@@ -27,8 +27,8 @@
 				</div>
 				<div class="status">
 					<a>
-						<b>{{ user.postsCount | number }}</b>
-						<i>%i18n:mobile.tags.mk-user.posts%</i>
+						<b>{{ user.notesCount | number }}</b>
+						<i>%i18n:mobile.tags.mk-user.notes%</i>
 					</a>
 					<a :href="`@${acct}/following`">
 						<b>{{ user.followingCount | number }}</b>
@@ -44,13 +44,13 @@
 		<nav>
 			<div class="nav-container">
 				<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 == 'notes' " @click="page = 'notes'">%i18n:mobile.tags.mk-user.timeline%</a>
 				<a :data-is-active=" page == 'media' " @click="page = 'media'">%i18n:mobile.tags.mk-user.media%</a>
 			</div>
 		</nav>
 		<div class="body">
 			<x-home v-if="page == 'home'" :user="user"/>
-			<mk-user-timeline v-if="page == 'posts'" :user="user"/>
+			<mk-user-timeline v-if="page == 'notes'" :user="user"/>
 			<mk-user-timeline v-if="page == 'media'" :user="user" with-media/>
 		</div>
 	</main>
diff --git a/src/client/app/mobile/views/pages/user/home.posts.vue b/src/client/app/mobile/views/pages/user/home.notes.vue
similarity index 61%
rename from src/client/app/mobile/views/pages/user/home.posts.vue
rename to src/client/app/mobile/views/pages/user/home.notes.vue
index 654f7f63e..02afed9b8 100644
--- a/src/client/app/mobile/views/pages/user/home.posts.vue
+++ b/src/client/app/mobile/views/pages/user/home.notes.vue
@@ -1,10 +1,10 @@
 <template>
-<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 class="root notes">
+	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-notes.loading%<mk-ellipsis/></p>
+	<div v-if="!fetching && notes.length > 0">
+		<mk-note-card v-for="note in notes" :key="note.id" :note="note"/>
 	</div>
-	<p class="empty" v-if="!fetching && posts.length == 0">%i18n:mobile.tags.mk-user-overview-posts.no-posts%</p>
+	<p class="empty" v-if="!fetching && notes.length == 0">%i18n:mobile.tags.mk-user-overview-notes.no-notes%</p>
 </div>
 </template>
 
@@ -15,14 +15,14 @@ export default Vue.extend({
 	data() {
 		return {
 			fetching: true,
-			posts: []
+			notes: []
 		};
 	},
 	mounted() {
-		(this as any).api('users/posts', {
+		(this as any).api('users/notes', {
 			userId: this.user.id
-		}).then(posts => {
-			this.posts = posts;
+		}).then(notes => {
+			this.notes = notes;
 			this.fetching = false;
 		});
 	}
@@ -30,7 +30,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.root.posts
+.root.notes
 
 	> div
 		overflow-x scroll
diff --git a/src/client/app/mobile/views/pages/user/home.photos.vue b/src/client/app/mobile/views/pages/user/home.photos.vue
index ecf508207..1c5926081 100644
--- a/src/client/app/mobile/views/pages/user/home.photos.vue
+++ b/src/client/app/mobile/views/pages/user/home.photos.vue
@@ -5,7 +5,7 @@
 		<a v-for="image in images"
 			class="img"
 			:style="`background-image: url(${image.media.url}?thumbnail&size=256)`"
-			:href="`/@${getAcct(image.post.user)}/${image.post.id}`"
+			:href="`/@${getAcct(image.note.user)}/${image.note.id}`"
 		></a>
 	</div>
 	<p class="empty" v-if="!fetching && images.length == 0">%i18n:mobile.tags.mk-user-overview-photos.no-photos%</p>
@@ -28,15 +28,15 @@ export default Vue.extend({
 		getAcct
 	},
 	mounted() {
-		(this as any).api('users/posts', {
+		(this as any).api('users/notes', {
 			userId: this.user.id,
 			withMedia: true,
 			limit: 6
-		}).then(posts => {
-			posts.forEach(post => {
-				post.media.forEach(media => {
+		}).then(notes => {
+			notes.forEach(note => {
+				note.media.forEach(media => {
 					if (this.images.length < 9) this.images.push({
-						post,
+						note,
 						media
 					});
 				});
diff --git a/src/client/app/mobile/views/pages/user/home.vue b/src/client/app/mobile/views/pages/user/home.vue
index 1afcd1f5b..255408496 100644
--- a/src/client/app/mobile/views/pages/user/home.vue
+++ b/src/client/app/mobile/views/pages/user/home.vue
@@ -1,10 +1,10 @@
 <template>
 <div class="root home">
-	<mk-post-detail v-if="user.pinnedPost" :post="user.pinnedPost" :compact="true"/>
-	<section class="recent-posts">
-		<h2>%fa:R comments%%i18n:mobile.tags.mk-user-overview.recent-posts%</h2>
+	<mk-note-detail v-if="user.pinnedNote" :note="user.pinnedNote" :compact="true"/>
+	<section class="recent-notes">
+		<h2>%fa:R comments%%i18n:mobile.tags.mk-user-overview.recent-notes%</h2>
 		<div>
-			<x-posts :user="user"/>
+			<x-notes :user="user"/>
 		</div>
 	</section>
 	<section class="images">
@@ -37,14 +37,14 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import XPosts from './home.posts.vue';
+import XNotes from './home.notes.vue';
 import XPhotos from './home.photos.vue';
 import XFriends from './home.friends.vue';
 import XFollowersYouKnow from './home.followers-you-know.vue';
 
 export default Vue.extend({
 	components: {
-		XPosts,
+		XNotes,
 		XPhotos,
 		XFriends,
 		XFollowersYouKnow
@@ -58,7 +58,7 @@ export default Vue.extend({
 	max-width 600px
 	margin 0 auto
 
-	> .mk-post-detail
+	> .mk-note-detail
 		margin 0 0 8px 0
 
 	> section
diff --git a/src/client/app/stats/tags/index.tag b/src/client/app/stats/tags/index.tag
index 63fdd2404..f8944c083 100644
--- a/src/client/app/stats/tags/index.tag
+++ b/src/client/app/stats/tags/index.tag
@@ -2,7 +2,7 @@
 	<h1>Misskey<i>Statistics</i></h1>
 	<main v-if="!initializing">
 		<mk-users stats={ stats }/>
-		<mk-posts stats={ stats }/>
+		<mk-notes stats={ stats }/>
 	</main>
 	<footer><a href={ _URL_ }>{ _HOST_ }</a></footer>
 	<style lang="stylus" scoped>
@@ -56,9 +56,9 @@
 	</script>
 </mk-index>
 
-<mk-posts>
-	<h2>%i18n:stats.posts-count% <b>{ stats.postsCount }</b></h2>
-	<mk-posts-chart v-if="!initializing" data={ data }/>
+<mk-notes>
+	<h2>%i18n:stats.notes-count% <b>{ stats.notesCount }</b></h2>
+	<mk-notes-chart v-if="!initializing" data={ data }/>
 	<style lang="stylus" scoped>
 		:scope
 			display block
@@ -70,7 +70,7 @@
 		this.stats = this.opts.stats;
 
 		this.on('mount', () => {
-			this.$root.$data.os.api('aggregation/posts', {
+			this.$root.$data.os.api('aggregation/notes', {
 				limit: 365
 			}).then(data => {
 				this.update({
@@ -80,7 +80,7 @@
 			});
 		});
 	</script>
-</mk-posts>
+</mk-notes>
 
 <mk-users>
 	<h2>%i18n:stats.users-count% <b>{ stats.usersCount }</b></h2>
@@ -108,11 +108,11 @@
 	</script>
 </mk-users>
 
-<mk-posts-chart>
+<mk-notes-chart>
 	<svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none">
-		<title>Black ... Total<br/>Blue ... Posts<br/>Red ... Replies<br/>Green ... Reposts</title>
+		<title>Black ... Total<br/>Blue ... Notes<br/>Red ... Replies<br/>Green ... Renotes</title>
 		<polyline
-			riot-points={ pointsPost }
+			riot-points={ pointsNote }
 			fill="none"
 			stroke-width="1"
 			stroke="#41ddde"/>
@@ -122,7 +122,7 @@
 			stroke-width="1"
 			stroke="#f7796c"/>
 		<polyline
-			riot-points={ pointsRepost }
+			riot-points={ pointsRenote }
 			fill="none"
 			stroke-width="1"
 			stroke="#a1de41"/>
@@ -147,7 +147,7 @@
 		this.viewBoxY = 80;
 
 		this.data = this.opts.data.reverse();
-		this.data.forEach(d => d.total = d.posts + d.replies + d.reposts);
+		this.data.forEach(d => d.total = d.notes + d.replies + d.renotes);
 		const peak = Math.max.apply(null, this.data.map(d => d.total));
 
 		this.on('mount', () => {
@@ -156,14 +156,14 @@
 
 		this.render = () => {
 			this.update({
-				pointsPost: this.data.map((d, i) => `${i},${(1 - (d.posts / peak)) * this.viewBoxY}`).join(' '),
+				pointsNote: this.data.map((d, i) => `${i},${(1 - (d.notes / peak)) * this.viewBoxY}`).join(' '),
 				pointsReply: this.data.map((d, i) => `${i},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' '),
-				pointsRepost: this.data.map((d, i) => `${i},${(1 - (d.reposts / peak)) * this.viewBoxY}`).join(' '),
+				pointsRenote: this.data.map((d, i) => `${i},${(1 - (d.renotes / peak)) * this.viewBoxY}`).join(' '),
 				pointsTotal: this.data.map((d, i) => `${i},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ')
 			});
 		};
 	</script>
-</mk-posts-chart>
+</mk-notes-chart>
 
 <mk-users-chart>
 	<svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none">
diff --git a/src/client/docs/api/endpoints/posts/create.yaml b/src/client/docs/api/endpoints/notes/create.yaml
similarity index 77%
rename from src/client/docs/api/endpoints/posts/create.yaml
rename to src/client/docs/api/endpoints/notes/create.yaml
index d2d6e27fc..04ada2ecd 100644
--- a/src/client/docs/api/endpoints/posts/create.yaml
+++ b/src/client/docs/api/endpoints/notes/create.yaml
@@ -1,8 +1,8 @@
-endpoint: "posts/create"
+endpoint: "notes/create"
 
 desc:
   ja: "投稿します。"
-  en: "Compose new post."
+  en: "Compose new note."
 
 params:
   - name: "text"
@@ -10,7 +10,7 @@ params:
     optional: true
     desc:
       ja: "投稿の本文"
-      en: "The text of your post"
+      en: "The text of your note"
   - name: "cw"
     type: "string"
     optional: true
@@ -24,17 +24,17 @@ params:
       ja: "添付するメディア(1~4つ)"
       en: "Media you want to attach (1~4)"
   - name: "replyId"
-    type: "id(Post)"
+    type: "id(Note)"
     optional: true
     desc:
       ja: "返信する投稿"
-      en: "The post you want to reply"
-  - name: "repostId"
-    type: "id(Post)"
+      en: "The note you want to reply"
+  - name: "renoteId"
+    type: "id(Note)"
     optional: true
     desc:
       ja: "引用する投稿"
-      en: "The post you want to quote"
+      en: "The note you want to quote"
   - name: "poll"
     type: "object"
     optional: true
@@ -51,9 +51,9 @@ params:
           en: "Choices of a poll"
 
 res:
-  - name: "createdPost"
-    type: "entity(Post)"
+  - name: "createdNote"
+    type: "entity(Note)"
     optional: false
     desc:
       ja: "作成した投稿"
-      en: "A post that created"
+      en: "A note that created"
diff --git a/src/client/docs/api/endpoints/posts/timeline.yaml b/src/client/docs/api/endpoints/notes/timeline.yaml
similarity index 93%
rename from src/client/docs/api/endpoints/posts/timeline.yaml
rename to src/client/docs/api/endpoints/notes/timeline.yaml
index 9c44dd736..71c346f35 100644
--- a/src/client/docs/api/endpoints/posts/timeline.yaml
+++ b/src/client/docs/api/endpoints/notes/timeline.yaml
@@ -1,4 +1,4 @@
-endpoint: "posts/timeline"
+endpoint: "notes/timeline"
 
 desc:
   ja: "タイムラインを取得します。"
@@ -11,12 +11,12 @@ params:
     desc:
       ja: "取得する最大の数"
   - name: "sinceId"
-    type: "id(Post)"
+    type: "id(Note)"
     optional: true
     desc:
       ja: "指定すると、この投稿を基点としてより新しい投稿を取得します"
   - name: "untilId"
-    type: "id(Post)"
+    type: "id(Note)"
     optional: true
     desc:
       ja: "指定すると、この投稿を基点としてより古い投稿を取得します"
diff --git a/src/client/docs/api/entities/note.yaml b/src/client/docs/api/entities/note.yaml
new file mode 100644
index 000000000..718d331d1
--- /dev/null
+++ b/src/client/docs/api/entities/note.yaml
@@ -0,0 +1,174 @@
+name: "Note"
+
+desc:
+  ja: "投稿。"
+  en: "A note."
+
+props:
+  - name: "id"
+    type: "id"
+    optional: false
+    desc:
+      ja: "投稿ID"
+      en: "The ID of this note"
+  - name: "createdAt"
+    type: "date"
+    optional: false
+    desc:
+      ja: "投稿日時"
+      en: "The posted date of this note"
+  - name: "viaMobile"
+    type: "boolean"
+    optional: true
+    desc:
+      ja: "モバイル端末から投稿したか否か(自己申告であることに留意)"
+      en: "Whether this note sent via a mobile device"
+  - name: "text"
+    type: "string"
+    optional: true
+    desc:
+      ja: "投稿の本文 (ローカルの場合Markdown風のフォーマット)"
+      en: "The text of this note (in Markdown like format if local)"
+  - name: "textHtml"
+    type: "string"
+    optional: true
+    desc:
+      ja: "投稿の本文 (HTML) (投稿時は無視)"
+      en: "The text of this note (in HTML. Ignored when posting.)"
+  - name: "mediaIds"
+    type: "id(DriveFile)[]"
+    optional: true
+    desc:
+      ja: "添付されているメディアのID (なければレスポンスでは空配列)"
+      en: "The IDs of the attached media (empty array for response if no media is attached)"
+  - name: "media"
+    type: "entity(DriveFile)[]"
+    optional: true
+    desc:
+      ja: "添付されているメディア"
+      en: "The attached media"
+  - name: "userId"
+    type: "id(User)"
+    optional: false
+    desc:
+      ja: "投稿者ID"
+      en: "The ID of author of this note"
+  - name: "user"
+    type: "entity(User)"
+    optional: true
+    desc:
+      ja: "投稿者"
+      en: "The author of this note"
+  - name: "myReaction"
+    type: "string"
+    optional: true
+    desc:
+      ja: "この投稿に対する自分の<a href='/docs/api/reactions'>リアクション</a>"
+      en: "The your <a href='/docs/api/reactions'>reaction</a> of this note"
+  - name: "reactionCounts"
+    type: "object"
+    optional: false
+    desc:
+      ja: "<a href='/docs/api/reactions'>リアクション</a>をキーとし、この投稿に対するそのリアクションの数を値としたオブジェクト"
+  - name: "replyId"
+    type: "id(Note)"
+    optional: true
+    desc:
+      ja: "返信した投稿のID"
+      en: "The ID of the replyed note"
+  - name: "reply"
+    type: "entity(Note)"
+    optional: true
+    desc:
+      ja: "返信した投稿"
+      en: "The replyed note"
+  - name: "renoteId"
+    type: "id(Note)"
+    optional: true
+    desc:
+      ja: "引用した投稿のID"
+      en: "The ID of the quoted note"
+  - name: "renote"
+    type: "entity(Note)"
+    optional: true
+    desc:
+      ja: "引用した投稿"
+      en: "The quoted note"
+  - name: "poll"
+    type: "object"
+    optional: true
+    desc:
+      ja: "投票"
+      en: "The poll"
+    defName: "poll"
+    def:
+      - name: "choices"
+        type: "object[]"
+        optional: false
+        desc:
+          ja: "投票の選択肢"
+          en: "The choices of this poll"
+        defName: "choice"
+        def:
+          - name: "id"
+            type: "number"
+            optional: false
+            desc:
+              ja: "選択肢ID"
+              en: "The ID of this choice"
+          - name: "isVoted"
+            type: "boolean"
+            optional: true
+            desc:
+              ja: "自分がこの選択肢に投票したかどうか"
+              en: "Whether you voted to this choice"
+          - name: "text"
+            type: "string"
+            optional: false
+            desc:
+              ja: "選択肢本文"
+              en: "The text of this choice"
+          - name: "votes"
+            type: "number"
+            optional: false
+            desc:
+              ja: "この選択肢に投票された数"
+              en: "The number voted for this choice"
+  - name: "geo"
+    type: "object"
+    optional: true
+    desc:
+      ja: "位置情報"
+      en: "Geo location"
+    defName: "geo"
+    def:
+      - name: "coordinates"
+        type: "number[]"
+        optional: false
+        desc:
+          ja: "座標。最初に経度:-180〜180で表す。最後に緯度:-90〜90で表す。"
+      - name: "altitude"
+        type: "number"
+        optional: false
+        desc:
+          ja: "高度。メートル単位で表す。"
+      - name: "accuracy"
+        type: "number"
+        optional: false
+        desc:
+          ja: "緯度、経度の精度。メートル単位で表す。"
+      - name: "altitudeAccuracy"
+        type: "number"
+        optional: false
+        desc:
+          ja: "高度の精度。メートル単位で表す。"
+      - name: "heading"
+        type: "number"
+        optional: false
+        desc:
+          ja: "方角。0〜360の角度で表す。0が北、90が東、180が南、270が西。"
+      - name: "speed"
+        type: "number"
+        optional: false
+        desc:
+          ja: "速度。メートル / 秒数で表す。"
diff --git a/src/client/docs/api/entities/post.yaml b/src/client/docs/api/entities/post.yaml
index 707770012..718d331d1 100644
--- a/src/client/docs/api/entities/post.yaml
+++ b/src/client/docs/api/entities/post.yaml
@@ -1,8 +1,8 @@
-name: "Post"
+name: "Note"
 
 desc:
   ja: "投稿。"
-  en: "A post."
+  en: "A note."
 
 props:
   - name: "id"
@@ -10,31 +10,31 @@ props:
     optional: false
     desc:
       ja: "投稿ID"
-      en: "The ID of this post"
+      en: "The ID of this note"
   - name: "createdAt"
     type: "date"
     optional: false
     desc:
       ja: "投稿日時"
-      en: "The posted date of this post"
+      en: "The posted date of this note"
   - name: "viaMobile"
     type: "boolean"
     optional: true
     desc:
       ja: "モバイル端末から投稿したか否か(自己申告であることに留意)"
-      en: "Whether this post sent via a mobile device"
+      en: "Whether this note sent via a mobile device"
   - name: "text"
     type: "string"
     optional: true
     desc:
       ja: "投稿の本文 (ローカルの場合Markdown風のフォーマット)"
-      en: "The text of this post (in Markdown like format if local)"
+      en: "The text of this note (in Markdown like format if local)"
   - name: "textHtml"
     type: "string"
     optional: true
     desc:
       ja: "投稿の本文 (HTML) (投稿時は無視)"
-      en: "The text of this post (in HTML. Ignored when posting.)"
+      en: "The text of this note (in HTML. Ignored when posting.)"
   - name: "mediaIds"
     type: "id(DriveFile)[]"
     optional: true
@@ -52,48 +52,48 @@ props:
     optional: false
     desc:
       ja: "投稿者ID"
-      en: "The ID of author of this post"
+      en: "The ID of author of this note"
   - name: "user"
     type: "entity(User)"
     optional: true
     desc:
       ja: "投稿者"
-      en: "The author of this post"
+      en: "The author of this note"
   - name: "myReaction"
     type: "string"
     optional: true
     desc:
       ja: "この投稿に対する自分の<a href='/docs/api/reactions'>リアクション</a>"
-      en: "The your <a href='/docs/api/reactions'>reaction</a> of this post"
+      en: "The your <a href='/docs/api/reactions'>reaction</a> of this note"
   - name: "reactionCounts"
     type: "object"
     optional: false
     desc:
       ja: "<a href='/docs/api/reactions'>リアクション</a>をキーとし、この投稿に対するそのリアクションの数を値としたオブジェクト"
   - name: "replyId"
-    type: "id(Post)"
+    type: "id(Note)"
     optional: true
     desc:
       ja: "返信した投稿のID"
-      en: "The ID of the replyed post"
+      en: "The ID of the replyed note"
   - name: "reply"
-    type: "entity(Post)"
+    type: "entity(Note)"
     optional: true
     desc:
       ja: "返信した投稿"
-      en: "The replyed post"
-  - name: "repostId"
-    type: "id(Post)"
+      en: "The replyed note"
+  - name: "renoteId"
+    type: "id(Note)"
     optional: true
     desc:
       ja: "引用した投稿のID"
-      en: "The ID of the quoted post"
-  - name: "repost"
-    type: "entity(Post)"
+      en: "The ID of the quoted note"
+  - name: "renote"
+    type: "entity(Note)"
     optional: true
     desc:
       ja: "引用した投稿"
-      en: "The quoted post"
+      en: "The quoted note"
   - name: "poll"
     type: "object"
     optional: true
diff --git a/src/client/docs/api/entities/user.yaml b/src/client/docs/api/entities/user.yaml
index a1fae1482..cccf42f22 100644
--- a/src/client/docs/api/entities/user.yaml
+++ b/src/client/docs/api/entities/user.yaml
@@ -81,24 +81,24 @@ props:
     desc:
       ja: "自分がこのユーザーをミュートしているか"
       en: "Whether you muted this user"
-  - name: "postsCount"
+  - name: "notesCount"
     type: "number"
     optional: false
     desc:
       ja: "投稿の数"
-      en: "The number of the posts of this user"
-  - name: "pinnedPost"
-    type: "entity(Post)"
+      en: "The number of the notes of this user"
+  - name: "pinnedNote"
+    type: "entity(Note)"
     optional: true
     desc:
       ja: "ピン留めされた投稿"
-      en: "The pinned post of this user"
-  - name: "pinnedPostId"
-    type: "id(Post)"
+      en: "The pinned note of this user"
+  - name: "pinnedNoteId"
+    type: "id(Note)"
     optional: true
     desc:
       ja: "ピン留めされた投稿のID"
-      en: "The ID of the pinned post of this user"
+      en: "The ID of the pinned note of this user"
   - name: "driveCapacity"
     type: "number"
     optional: false
diff --git a/src/client/docs/mute.ja.pug b/src/client/docs/mute.ja.pug
index 5e79af5f8..807f7b67a 100644
--- a/src/client/docs/mute.ja.pug
+++ b/src/client/docs/mute.ja.pug
@@ -4,7 +4,7 @@ p ユーザーページから、そのユーザーをミュートすることが
 
 p ユーザーをミュートすると、そのユーザーに関する次のコンテンツがMisskeyに表示されなくなります:
 ul
-	li タイムラインや投稿の検索結果内の、そのユーザーの投稿(およびそれらの投稿に対する返信やRepost)
+	li タイムラインや投稿の検索結果内の、そのユーザーの投稿(およびそれらの投稿に対する返信やRenote)
 	li そのユーザーからの通知
 	li メッセージ履歴一覧内の、そのユーザーとのメッセージ履歴
 
diff --git a/src/client/docs/search.ja.pug b/src/client/docs/search.ja.pug
index e14e8c867..fc62d16ca 100644
--- a/src/client/docs/search.ja.pug
+++ b/src/client/docs/search.ja.pug
@@ -64,19 +64,19 @@ section
 			tr
 				td mute
 				td
-					| mute_all ... ミュートしているユーザーの投稿とその投稿に対する返信やRepostを除外する(デフォルト)
+					| mute_all ... ミュートしているユーザーの投稿とその投稿に対する返信やRenoteを除外する(デフォルト)
 					br
-					| mute_related ... ミュートしているユーザーの投稿に対する返信やRepostだけ除外する
+					| mute_related ... ミュートしているユーザーの投稿に対する返信やRenoteだけ除外する
 					br
 					| mute_direct ... ミュートしているユーザーの投稿だけ除外する
 					br
-					| disabled ... ミュートしているユーザーの投稿とその投稿に対する返信やRepostも含める
+					| disabled ... ミュートしているユーザーの投稿とその投稿に対する返信やRenoteも含める
 					br
 					| direct_only ... ミュートしているユーザーの投稿だけに限定
 					br
-					| related_only ... ミュートしているユーザーの投稿に対する返信やRepostだけに限定
+					| related_only ... ミュートしているユーザーの投稿に対する返信やRenoteだけに限定
 					br
-					| all_only ... ミュートしているユーザーの投稿とその投稿に対する返信やRepostに限定
+					| all_only ... ミュートしているユーザーの投稿とその投稿に対する返信やRenoteに限定
 			tr
 				td reply
 				td
@@ -86,11 +86,11 @@ section
 					br
 					| null ... 特に限定しない(デフォルト)
 			tr
-				td repost
+				td renote
 				td
-					| true ... Repostに限定。
+					| true ... Renoteに限定。
 					br
-					| false ... Repostでない投稿に限定。
+					| false ... Renoteでない投稿に限定。
 					br
 					| null ... 特に限定しない(デフォルト)
 			tr
diff --git a/src/models/favorite.ts b/src/models/favorite.ts
index 2fa00e99c..73f888192 100644
--- a/src/models/favorite.ts
+++ b/src/models/favorite.ts
@@ -8,5 +8,5 @@ export type IFavorite = {
 	_id: mongo.ObjectID;
 	createdAt: Date;
 	userId: mongo.ObjectID;
-	postId: mongo.ObjectID;
+	noteId: mongo.ObjectID;
 };
diff --git a/src/models/post-reaction.ts b/src/models/note-reaction.ts
similarity index 79%
rename from src/models/post-reaction.ts
rename to src/models/note-reaction.ts
index 81be95b8d..d499442de 100644
--- a/src/models/post-reaction.ts
+++ b/src/models/note-reaction.ts
@@ -1,17 +1,17 @@
 import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
 import db from '../db/mongodb';
-import Reaction from './post-reaction';
+import Reaction from './note-reaction';
 import { pack as packUser } from './user';
 
-const PostReaction = db.get<IPostReaction>('postReactions');
-PostReaction.createIndex(['userId', 'postId'], { unique: true });
-export default PostReaction;
+const NoteReaction = db.get<INoteReaction>('noteReactions');
+NoteReaction.createIndex(['userId', 'noteId'], { unique: true });
+export default NoteReaction;
 
-export interface IPostReaction {
+export interface INoteReaction {
 	_id: mongo.ObjectID;
 	createdAt: Date;
-	postId: mongo.ObjectID;
+	noteId: mongo.ObjectID;
 	userId: mongo.ObjectID;
 	reaction: string;
 }
diff --git a/src/models/note-watching.ts b/src/models/note-watching.ts
new file mode 100644
index 000000000..b5ef3b61b
--- /dev/null
+++ b/src/models/note-watching.ts
@@ -0,0 +1,13 @@
+import * as mongo from 'mongodb';
+import db from '../db/mongodb';
+
+const NoteWatching = db.get<INoteWatching>('noteWatching');
+NoteWatching.createIndex(['userId', 'noteId'], { unique: true });
+export default NoteWatching;
+
+export interface INoteWatching {
+	_id: mongo.ObjectID;
+	createdAt: Date;
+	userId: mongo.ObjectID;
+	noteId: mongo.ObjectID;
+}
diff --git a/src/models/post.ts b/src/models/note.ts
similarity index 61%
rename from src/models/post.ts
rename to src/models/note.ts
index ac7890d2e..9b33bb76f 100644
--- a/src/models/post.ts
+++ b/src/models/note.ts
@@ -6,14 +6,14 @@ import { IUser, pack as packUser } from './user';
 import { pack as packApp } from './app';
 import { pack as packChannel } from './channel';
 import Vote from './poll-vote';
-import Reaction from './post-reaction';
+import Reaction from './note-reaction';
 import { pack as packFile } from './drive-file';
 
-const Post = db.get<IPost>('posts');
+const Note = db.get<INote>('notes');
 
-Post.createIndex('uri', { sparse: true, unique: true });
+Note.createIndex('uri', { sparse: true, unique: true });
 
-export default Post;
+export default Note;
 
 export function isValidText(text: string): boolean {
 	return text.length <= 1000 && text.trim() != '';
@@ -23,14 +23,14 @@ export function isValidCw(text: string): boolean {
 	return text.length <= 100 && text.trim() != '';
 }
 
-export type IPost = {
+export type INote = {
 	_id: mongo.ObjectID;
 	channelId: mongo.ObjectID;
 	createdAt: Date;
 	deletedAt: Date;
 	mediaIds: mongo.ObjectID[];
 	replyId: mongo.ObjectID;
-	repostId: mongo.ObjectID;
+	renoteId: mongo.ObjectID;
 	poll: any; // todo
 	text: string;
 	tags: string[];
@@ -39,7 +39,7 @@ export type IPost = {
 	userId: mongo.ObjectID;
 	appId: mongo.ObjectID;
 	viaMobile: boolean;
-	repostCount: number;
+	renoteCount: number;
 	repliesCount: number;
 	reactionCounts: any;
 	mentions: mongo.ObjectID[];
@@ -57,7 +57,7 @@ export type IPost = {
 	_reply?: {
 		userId: mongo.ObjectID;
 	};
-	_repost?: {
+	_renote?: {
 		userId: mongo.ObjectID;
 	};
 	_user: {
@@ -70,15 +70,15 @@ export type IPost = {
 };
 
 /**
- * Pack a post for API response
+ * Pack a note for API response
  *
- * @param post target
+ * @param note target
  * @param me? serializee
  * @param options? serialize options
  * @return response
  */
 export const pack = async (
-	post: string | mongo.ObjectID | IPost,
+	note: string | mongo.ObjectID | INote,
 	me?: string | mongo.ObjectID | IUser,
 	options?: {
 		detail: boolean
@@ -97,58 +97,58 @@ export const pack = async (
 				: (me as IUser)._id
 		: null;
 
-	let _post: any;
+	let _note: any;
 
-	// Populate the post if 'post' is ID
-	if (mongo.ObjectID.prototype.isPrototypeOf(post)) {
-		_post = await Post.findOne({
-			_id: post
+	// Populate the note if 'note' is ID
+	if (mongo.ObjectID.prototype.isPrototypeOf(note)) {
+		_note = await Note.findOne({
+			_id: note
 		});
-	} else if (typeof post === 'string') {
-		_post = await Post.findOne({
-			_id: new mongo.ObjectID(post)
+	} else if (typeof note === 'string') {
+		_note = await Note.findOne({
+			_id: new mongo.ObjectID(note)
 		});
 	} else {
-		_post = deepcopy(post);
+		_note = deepcopy(note);
 	}
 
-	if (!_post) throw 'invalid post arg.';
+	if (!_note) throw 'invalid note arg.';
 
-	const id = _post._id;
+	const id = _note._id;
 
 	// Rename _id to id
-	_post.id = _post._id;
-	delete _post._id;
+	_note.id = _note._id;
+	delete _note._id;
 
-	delete _post.mentions;
-	if (_post.geo) delete _post.geo.type;
+	delete _note.mentions;
+	if (_note.geo) delete _note.geo.type;
 
 	// Populate user
-	_post.user = packUser(_post.userId, meId);
+	_note.user = packUser(_note.userId, meId);
 
 	// Populate app
-	if (_post.appId) {
-		_post.app = packApp(_post.appId);
+	if (_note.appId) {
+		_note.app = packApp(_note.appId);
 	}
 
 	// Populate channel
-	if (_post.channelId) {
-		_post.channel = packChannel(_post.channelId);
+	if (_note.channelId) {
+		_note.channel = packChannel(_note.channelId);
 	}
 
 	// Populate media
-	if (_post.mediaIds) {
-		_post.media = Promise.all(_post.mediaIds.map(fileId =>
+	if (_note.mediaIds) {
+		_note.media = Promise.all(_note.mediaIds.map(fileId =>
 			packFile(fileId)
 		));
 	}
 
-	// When requested a detailed post data
+	// When requested a detailed note data
 	if (opts.detail) {
-		// Get previous post info
-		_post.prev = (async () => {
-			const prev = await Post.findOne({
-				userId: _post.userId,
+		// Get previous note info
+		_note.prev = (async () => {
+			const prev = await Note.findOne({
+				userId: _note.userId,
 				_id: {
 					$lt: id
 				}
@@ -163,10 +163,10 @@ export const pack = async (
 			return prev ? prev._id : null;
 		})();
 
-		// Get next post info
-		_post.next = (async () => {
-			const next = await Post.findOne({
-				userId: _post.userId,
+		// Get next note info
+		_note.next = (async () => {
+			const next = await Note.findOne({
+				userId: _note.userId,
 				_id: {
 					$gt: id
 				}
@@ -181,27 +181,27 @@ export const pack = async (
 			return next ? next._id : null;
 		})();
 
-		if (_post.replyId) {
-			// Populate reply to post
-			_post.reply = pack(_post.replyId, meId, {
+		if (_note.replyId) {
+			// Populate reply to note
+			_note.reply = pack(_note.replyId, meId, {
 				detail: false
 			});
 		}
 
-		if (_post.repostId) {
-			// Populate repost
-			_post.repost = pack(_post.repostId, meId, {
-				detail: _post.text == null
+		if (_note.renoteId) {
+			// Populate renote
+			_note.renote = pack(_note.renoteId, meId, {
+				detail: _note.text == null
 			});
 		}
 
 		// Poll
-		if (meId && _post.poll) {
-			_post.poll = (async (poll) => {
+		if (meId && _note.poll) {
+			_note.poll = (async (poll) => {
 				const vote = await Vote
 					.findOne({
 						userId: meId,
-						postId: id
+						noteId: id
 					});
 
 				if (vote != null) {
@@ -212,16 +212,16 @@ export const pack = async (
 				}
 
 				return poll;
-			})(_post.poll);
+			})(_note.poll);
 		}
 
 		// Fetch my reaction
 		if (meId) {
-			_post.myReaction = (async () => {
+			_note.myReaction = (async () => {
 				const reaction = await Reaction
 					.findOne({
 						userId: meId,
-						postId: id,
+						noteId: id,
 						deletedAt: { $exists: false }
 					});
 
@@ -234,8 +234,8 @@ export const pack = async (
 		}
 	}
 
-	// resolve promises in _post object
-	_post = await rap(_post);
+	// resolve promises in _note object
+	_note = await rap(_note);
 
-	return _post;
+	return _note;
 };
diff --git a/src/models/notification.ts b/src/models/notification.ts
index 078c8d511..17144d7ee 100644
--- a/src/models/notification.ts
+++ b/src/models/notification.ts
@@ -2,7 +2,7 @@ import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
 import db from '../db/mongodb';
 import { IUser, pack as packUser } from './user';
-import { pack as packPost } from './post';
+import { pack as packNote } from './note';
 
 const Notification = db.get<INotification>('notifications');
 export default Notification;
@@ -36,12 +36,12 @@ export interface INotification {
 	 * follow - フォローされた
 	 * mention - 投稿で自分が言及された
 	 * reply - (自分または自分がWatchしている)投稿が返信された
-	 * repost - (自分または自分がWatchしている)投稿がRepostされた
-	 * quote - (自分または自分がWatchしている)投稿が引用Repostされた
+	 * renote - (自分または自分がWatchしている)投稿がRenoteされた
+	 * quote - (自分または自分がWatchしている)投稿が引用Renoteされた
 	 * reaction - (自分または自分がWatchしている)投稿にリアクションされた
 	 * poll_vote - (自分または自分がWatchしている)投稿の投票に投票された
 	 */
-	type: 'follow' | 'mention' | 'reply' | 'repost' | 'quote' | 'reaction' | 'poll_vote';
+	type: 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'poll_vote';
 
 	/**
 	 * 通知が読まれたかどうか
@@ -91,12 +91,12 @@ export const pack = (notification: any) => new Promise<any>(async (resolve, reje
 			break;
 		case 'mention':
 		case 'reply':
-		case 'repost':
+		case 'renote':
 		case 'quote':
 		case 'reaction':
 		case 'poll_vote':
-			// Populate post
-			_notification.post = await packPost(_notification.postId, me);
+			// Populate note
+			_notification.note = await packNote(_notification.noteId, me);
 			break;
 		default:
 			console.error(`Unknown type: ${_notification.type}`);
diff --git a/src/models/poll-vote.ts b/src/models/poll-vote.ts
index cd18ffd5f..4d33b100e 100644
--- a/src/models/poll-vote.ts
+++ b/src/models/poll-vote.ts
@@ -8,6 +8,6 @@ export interface IPollVote {
 	_id: mongo.ObjectID;
 	createdAt: Date;
 	userId: mongo.ObjectID;
-	postId: mongo.ObjectID;
+	noteId: mongo.ObjectID;
 	choice: number;
 }
diff --git a/src/models/post-watching.ts b/src/models/post-watching.ts
deleted file mode 100644
index 032b9d10f..000000000
--- a/src/models/post-watching.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const PostWatching = db.get<IPostWatching>('postWatching');
-PostWatching.createIndex(['userId', 'postId'], { unique: true });
-export default PostWatching;
-
-export interface IPostWatching {
-	_id: mongo.ObjectID;
-	createdAt: Date;
-	userId: mongo.ObjectID;
-	postId: mongo.ObjectID;
-}
diff --git a/src/models/user.ts b/src/models/user.ts
index 92091c687..f86aefe9a 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -2,7 +2,7 @@ import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
 import rap from '@prezzemolo/rap';
 import db from '../db/mongodb';
-import { IPost, pack as packPost } from './post';
+import { INote, pack as packNote } from './note';
 import Following from './following';
 import Mute from './mute';
 import getFriends from '../server/api/common/get-friends';
@@ -22,7 +22,7 @@ type IUserBase = {
 	followersCount: number;
 	followingCount: number;
 	name?: string;
-	postsCount: number;
+	notesCount: number;
 	driveCapacity: number;
 	username: string;
 	usernameLower: string;
@@ -30,8 +30,8 @@ type IUserBase = {
 	bannerId: mongo.ObjectID;
 	data: any;
 	description: string;
-	latestPost: IPost;
-	pinnedPostId: mongo.ObjectID;
+	latestNote: INote;
+	pinnedNoteId: mongo.ObjectID;
 	isSuspended: boolean;
 	keywords: string[];
 	host: string;
@@ -120,7 +120,7 @@ export function init(user): IUser {
 	user._id = new mongo.ObjectID(user._id);
 	user.avatarId = new mongo.ObjectID(user.avatarId);
 	user.bannerId = new mongo.ObjectID(user.bannerId);
-	user.pinnedPostId = new mongo.ObjectID(user.pinnedPostId);
+	user.pinnedNoteId = new mongo.ObjectID(user.pinnedNoteId);
 	return user;
 }
 
@@ -186,7 +186,7 @@ export const pack = (
 	delete _user._id;
 
 	// Remove needless properties
-	delete _user.latestPost;
+	delete _user.latestNote;
 
 	if (!_user.host) {
 		// Remove private properties
@@ -260,9 +260,9 @@ export const pack = (
 	}
 
 	if (opts.detail) {
-		if (_user.pinnedPostId) {
-			// Populate pinned post
-			_user.pinnedPost = packPost(_user.pinnedPostId, meId, {
+		if (_user.pinnedNoteId) {
+			// Populate pinned note
+			_user.pinnedNote = packNote(_user.pinnedNoteId, meId, {
 				detail: true
 			});
 		}
diff --git a/src/othello/ai/back.ts b/src/othello/ai/back.ts
index 4d06ed956..e4d0cfdd3 100644
--- a/src/othello/ai/back.ts
+++ b/src/othello/ai/back.ts
@@ -24,7 +24,7 @@ const id = conf.othello_ai.id;
  */
 const i = conf.othello_ai.i;
 
-let post;
+let note;
 
 process.on('message', async msg => {
 	// 親プロセスからデータをもらう
@@ -51,13 +51,13 @@ process.on('message', async msg => {
 			? `?[${getUserName(user)}](${conf.url}/@${user.username})さんの接待を始めました!`
 			: `対局を?[${getUserName(user)}](${conf.url}/@${user.username})さんと始めました! (強さ${form[0].value})`;
 
-		const res = await request.post(`${conf.api_url}/posts/create`, {
+		const res = await request.post(`${conf.api_url}/notes/create`, {
 			json: { i,
 				text: `${text}\n→[観戦する](${url})`
 			}
 		});
 
-		post = res.createdPost;
+		note = res.createdNote;
 		//#endregion
 	}
 
@@ -83,9 +83,9 @@ process.on('message', async msg => {
 					? `?[${getUserName(user)}](${conf.url}/@${user.username})さんに勝ちました♪`
 					: `?[${getUserName(user)}](${conf.url}/@${user.username})さんに負けました...`;
 
-		await request.post(`${conf.api_url}/posts/create`, {
+		await request.post(`${conf.api_url}/notes/create`, {
 			json: { i,
-				repostId: post.id,
+				renoteId: note.id,
 				text: text
 			}
 		});
diff --git a/src/othello/ai/front.ts b/src/othello/ai/front.ts
index 9d0b5f980..ff74b7216 100644
--- a/src/othello/ai/front.ts
+++ b/src/othello/ai/front.ts
@@ -46,28 +46,28 @@ homeStream.on('message', message => {
 
 	// タイムライン上でなんか言われたまたは返信されたとき
 	if (msg.type == 'mention' || msg.type == 'reply') {
-		const post = msg.body;
+		const note = msg.body;
 
-		if (post.userId == id) return;
+		if (note.userId == id) return;
 
 		// リアクションする
-		request.post(`${conf.api_url}/posts/reactions/create`, {
+		request.post(`${conf.api_url}/notes/reactions/create`, {
 			json: { i,
-				postId: post.id,
+				noteId: note.id,
 				reaction: 'love'
 			}
 		});
 
-		if (post.text) {
-			if (post.text.indexOf('オセロ') > -1) {
-				request.post(`${conf.api_url}/posts/create`, {
+		if (note.text) {
+			if (note.text.indexOf('オセロ') > -1) {
+				request.post(`${conf.api_url}/notes/create`, {
 					json: { i,
-						replyId: post.id,
+						replyId: note.id,
 						text: '良いですよ~'
 					}
 				});
 
-				invite(post.userId);
+				invite(note.userId);
 			}
 		}
 	}
diff --git a/src/publishers/stream.ts b/src/publishers/stream.ts
index 498ff33f3..a6d2c2277 100644
--- a/src/publishers/stream.ts
+++ b/src/publishers/stream.ts
@@ -21,8 +21,8 @@ class MisskeyEvent {
 		this.publish(`drive-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
-	public publishPostStream(postId: ID, type: string, value?: any): void {
-		this.publish(`post-stream:${postId}`, type, typeof value === 'undefined' ? null : value);
+	public publishNoteStream(noteId: ID, type: string, value?: any): void {
+		this.publish(`note-stream:${noteId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
 	public publishMessagingStream(userId: ID, otherpartyId: ID, type: string, value?: any): void {
@@ -60,7 +60,7 @@ export default ev.publishUserStream.bind(ev);
 
 export const publishDriveStream = ev.publishDriveStream.bind(ev);
 
-export const publishPostStream = ev.publishPostStream.bind(ev);
+export const publishNoteStream = ev.publishNoteStream.bind(ev);
 
 export const publishMessagingStream = ev.publishMessagingStream.bind(ev);
 
diff --git a/src/queue/processors/http/report-github-failure.ts b/src/queue/processors/http/report-github-failure.ts
index 1e0b51f89..13f9afadd 100644
--- a/src/queue/processors/http/report-github-failure.ts
+++ b/src/queue/processors/http/report-github-failure.ts
@@ -1,6 +1,6 @@
 import * as request from 'request-promise-native';
 import User from '../../../models/user';
-import createPost from '../../../services/post/create';
+import createNote from '../../../services/note/create';
 
 export default async ({ data }) => {
 	const asyncBot = User.findOne({ _id: data.userId });
@@ -20,5 +20,5 @@ export default async ({ data }) => {
 		`**⚠️BUILD STILL FAILED⚠️**: ?[${data.message}](${data.htmlUrl})` :
 		`**🚨BUILD FAILED🚨**: →→→?[${data.message}](${data.htmlUrl})←←←`;
 
-	createPost(await asyncBot, { text });
+	createNote(await asyncBot, { text });
 };
diff --git a/src/remote/activitypub/act/create/note.ts b/src/remote/activitypub/act/create/note.ts
index 82a620703..572a293ab 100644
--- a/src/remote/activitypub/act/create/note.ts
+++ b/src/remote/activitypub/act/create/note.ts
@@ -2,8 +2,8 @@ import { JSDOM } from 'jsdom';
 import * as debug from 'debug';
 
 import Resolver from '../../resolver';
-import Post, { IPost } from '../../../../models/post';
-import createPost from '../../../../services/post/create';
+import Note, { INote } from '../../../../models/note';
+import post from '../../../../services/note/create';
 import { IRemoteUser } from '../../../../models/user';
 import resolvePerson from '../../resolve-person';
 import createImage from './image';
@@ -14,14 +14,14 @@ const log = debug('misskey:activitypub');
 /**
  * 投稿作成アクティビティを捌きます
  */
-export default async function createNote(resolver: Resolver, actor: IRemoteUser, note, silent = false): Promise<IPost> {
+export default async function createNote(resolver: Resolver, actor: IRemoteUser, note, silent = false): Promise<INote> {
 	if (typeof note.id !== 'string') {
 		log(`invalid note: ${JSON.stringify(note, null, 2)}`);
 		throw new Error('invalid note');
 	}
 
 	// 既に同じURIを持つものが登録されていないかチェックし、登録されていたらそれを返す
-	const exist = await Post.findOne({ uri: note.id });
+	const exist = await Note.findOne({ uri: note.id });
 	if (exist) {
 		return exist;
 	}
@@ -54,12 +54,12 @@ export default async function createNote(resolver: Resolver, actor: IRemoteUser,
 	if ('inReplyTo' in note && note.inReplyTo != null) {
 		// リプライ先の投稿がMisskeyに登録されているか調べる
 		const uri: string = note.inReplyTo.id || note.inReplyTo;
-		const inReplyToPost = uri.startsWith(config.url + '/')
-			? await Post.findOne({ _id: uri.split('/').pop() })
-			: await Post.findOne({ uri });
+		const inReplyToNote = uri.startsWith(config.url + '/')
+			? await Note.findOne({ _id: uri.split('/').pop() })
+			: await Note.findOne({ uri });
 
-		if (inReplyToPost) {
-			reply = inReplyToPost;
+		if (inReplyToNote) {
+			reply = inReplyToNote;
 		} else {
 			// 無かったらフェッチ
 			const inReplyTo = await resolver.resolve(note.inReplyTo) as any;
@@ -75,11 +75,11 @@ export default async function createNote(resolver: Resolver, actor: IRemoteUser,
 
 	const { window } = new JSDOM(note.content);
 
-	return await createPost(actor, {
+	return await post(actor, {
 		createdAt: new Date(note.published),
 		media,
 		reply,
-		repost: undefined,
+		renote: undefined,
 		text: window.document.body.textContent,
 		viaMobile: false,
 		geo: undefined,
diff --git a/src/remote/activitypub/act/delete/index.ts b/src/remote/activitypub/act/delete/index.ts
index e34577b31..6c6faa1ae 100644
--- a/src/remote/activitypub/act/delete/index.ts
+++ b/src/remote/activitypub/act/delete/index.ts
@@ -1,6 +1,6 @@
 import Resolver from '../../resolver';
 import deleteNote from './note';
-import Post from '../../../../models/post';
+import Note from '../../../../models/note';
 import { IRemoteUser } from '../../../../models/user';
 
 /**
@@ -23,8 +23,8 @@ export default async (actor: IRemoteUser, activity): Promise<void> => {
 		break;
 
 	case 'Tombstone':
-		const post = await Post.findOne({ uri });
-		if (post != null) {
+		const note = await Note.findOne({ uri });
+		if (note != null) {
 			deleteNote(actor, uri);
 		}
 		break;
diff --git a/src/remote/activitypub/act/delete/note.ts b/src/remote/activitypub/act/delete/note.ts
index 8e9447b48..64c342d39 100644
--- a/src/remote/activitypub/act/delete/note.ts
+++ b/src/remote/activitypub/act/delete/note.ts
@@ -1,6 +1,6 @@
 import * as debug from 'debug';
 
-import Post from '../../../../models/post';
+import Note from '../../../../models/note';
 import { IRemoteUser } from '../../../../models/user';
 
 const log = debug('misskey:activitypub');
@@ -8,17 +8,17 @@ const log = debug('misskey:activitypub');
 export default async function(actor: IRemoteUser, uri: string): Promise<void> {
 	log(`Deleting the Note: ${uri}`);
 
-	const post = await Post.findOne({ uri });
+	const note = await Note.findOne({ uri });
 
-	if (post == null) {
-		throw new Error('post not found');
+	if (note == null) {
+		throw new Error('note not found');
 	}
 
-	if (!post.userId.equals(actor._id)) {
+	if (!note.userId.equals(actor._id)) {
 		throw new Error('投稿を削除しようとしているユーザーは投稿の作成者ではありません');
 	}
 
-	Post.update({ _id: post._id }, {
+	Note.update({ _id: note._id }, {
 		$set: {
 			deletedAt: new Date(),
 			text: null,
diff --git a/src/remote/activitypub/act/like.ts b/src/remote/activitypub/act/like.ts
index 2f5e3f807..a3243948b 100644
--- a/src/remote/activitypub/act/like.ts
+++ b/src/remote/activitypub/act/like.ts
@@ -1,7 +1,7 @@
-import Post from '../../../models/post';
+import Note from '../../../models/note';
 import { IRemoteUser } from '../../../models/user';
 import { ILike } from '../type';
-import create from '../../../services/post/reaction/create';
+import create from '../../../services/note/reaction/create';
 
 export default async (actor: IRemoteUser, activity: ILike) => {
 	const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
@@ -9,12 +9,12 @@ export default async (actor: IRemoteUser, activity: ILike) => {
 	// Transform:
 	// https://misskey.ex/@syuilo/xxxx to
 	// xxxx
-	const postId = id.split('/').pop();
+	const noteId = id.split('/').pop();
 
-	const post = await Post.findOne({ _id: postId });
-	if (post === null) {
+	const note = await Note.findOne({ _id: noteId });
+	if (note === null) {
 		throw new Error();
 	}
 
-	await create(actor, post, 'pudding');
+	await create(actor, note, 'pudding');
 };
diff --git a/src/remote/activitypub/renderer/like.ts b/src/remote/activitypub/renderer/like.ts
index 903b10789..fe36c7094 100644
--- a/src/remote/activitypub/renderer/like.ts
+++ b/src/remote/activitypub/renderer/like.ts
@@ -1,9 +1,9 @@
 import config from '../../../config';
 
-export default (user, post) => {
+export default (user, note) => {
 	return {
 		type: 'Like',
 		actor: `${config.url}/@${user.username}`,
-		object: post.uri ? post.uri : `${config.url}/posts/${post._id}`
+		object: note.uri ? note.uri : `${config.url}/notes/${note._id}`
 	};
 };
diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts
index bbab63db3..244aecf6a 100644
--- a/src/remote/activitypub/renderer/note.ts
+++ b/src/remote/activitypub/renderer/note.ts
@@ -2,28 +2,28 @@ import renderDocument from './document';
 import renderHashtag from './hashtag';
 import config from '../../../config';
 import DriveFile from '../../../models/drive-file';
-import Post, { IPost } from '../../../models/post';
+import Note, { INote } from '../../../models/note';
 import User, { IUser } from '../../../models/user';
 
-export default async (user: IUser, post: IPost) => {
-	const promisedFiles = post.mediaIds
-		? DriveFile.find({ _id: { $in: post.mediaIds } })
+export default async (user: IUser, note: INote) => {
+	const promisedFiles = note.mediaIds
+		? DriveFile.find({ _id: { $in: note.mediaIds } })
 		: Promise.resolve([]);
 
 	let inReplyTo;
 
-	if (post.replyId) {
-		const inReplyToPost = await Post.findOne({
-			_id: post.replyId,
+	if (note.replyId) {
+		const inReplyToNote = await Note.findOne({
+			_id: note.replyId,
 		});
 
-		if (inReplyToPost !== null) {
+		if (inReplyToNote !== null) {
 			const inReplyToUser = await User.findOne({
-				_id: inReplyToPost.userId,
+				_id: inReplyToNote.userId,
 			});
 
 			if (inReplyToUser !== null) {
-				inReplyTo = inReplyToPost.uri || `${config.url}/posts/${inReplyToPost._id}`;
+				inReplyTo = inReplyToNote.uri || `${config.url}/notes/${inReplyToNote._id}`;
 			}
 		}
 	} else {
@@ -33,15 +33,15 @@ export default async (user: IUser, post: IPost) => {
 	const attributedTo = `${config.url}/@${user.username}`;
 
 	return {
-		id: `${config.url}/posts/${post._id}`,
+		id: `${config.url}/notes/${note._id}`,
 		type: 'Note',
 		attributedTo,
-		content: post.textHtml,
-		published: post.createdAt.toISOString(),
+		content: note.textHtml,
+		published: note.createdAt.toISOString(),
 		to: 'https://www.w3.org/ns/activitystreams#Public',
 		cc: `${attributedTo}/followers`,
 		inReplyTo,
 		attachment: (await promisedFiles).map(renderDocument),
-		tag: (post.tags || []).map(renderHashtag)
+		tag: (note.tags || []).map(renderHashtag)
 	};
 };
diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts
index b3bac3cd3..ac0900307 100644
--- a/src/remote/activitypub/resolve-person.ts
+++ b/src/remote/activitypub/resolve-person.ts
@@ -31,7 +31,7 @@ export default async (value, verifier?: string) => {
 		throw new Error('invalid person');
 	}
 
-	const [followersCount = 0, followingCount = 0, postsCount = 0, finger] = await Promise.all([
+	const [followersCount = 0, followingCount = 0, notesCount = 0, finger] = await Promise.all([
 		resolver.resolve(object.followers).then(
 			resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
 			() => undefined
@@ -59,7 +59,7 @@ export default async (value, verifier?: string) => {
 		description: summaryDOM.textContent,
 		followersCount,
 		followingCount,
-		postsCount,
+		notesCount,
 		name: object.name,
 		driveCapacity: 1024 * 1024 * 8, // 8MiB
 		username: object.preferredUsername,
diff --git a/src/renderers/get-note-summary.ts b/src/renderers/get-note-summary.ts
new file mode 100644
index 000000000..13859439a
--- /dev/null
+++ b/src/renderers/get-note-summary.ts
@@ -0,0 +1,45 @@
+/**
+ * 投稿を表す文字列を取得します。
+ * @param {*} note 投稿
+ */
+const summarize = (note: any): string => {
+	let summary = '';
+
+	// チャンネル
+	summary += note.channel ? `${note.channel.title}:` : '';
+
+	// 本文
+	summary += note.text ? note.text : '';
+
+	// メディアが添付されているとき
+	if (note.media) {
+		summary += ` (${note.media.length}つのメディア)`;
+	}
+
+	// 投票が添付されているとき
+	if (note.poll) {
+		summary += ' (投票)';
+	}
+
+	// 返信のとき
+	if (note.replyId) {
+		if (note.reply) {
+			summary += ` RE: ${summarize(note.reply)}`;
+		} else {
+			summary += ' RE: ...';
+		}
+	}
+
+	// Renoteのとき
+	if (note.renoteId) {
+		if (note.renote) {
+			summary += ` RP: ${summarize(note.renote)}`;
+		} else {
+			summary += ' RP: ...';
+		}
+	}
+
+	return summary.trim();
+};
+
+export default summarize;
diff --git a/src/renderers/get-notification-summary.ts b/src/renderers/get-notification-summary.ts
index f5e38faf9..f9c5a5587 100644
--- a/src/renderers/get-notification-summary.ts
+++ b/src/renderers/get-notification-summary.ts
@@ -1,5 +1,5 @@
 import getUserName from '../renderers/get-user-name';
-import getPostSummary from './get-post-summary';
+import getNoteSummary from './get-note-summary';
 import getReactionEmoji from './get-reaction-emoji';
 
 /**
@@ -11,17 +11,17 @@ export default function(notification: any): string {
 		case 'follow':
 			return `${getUserName(notification.user)}にフォローされました`;
 		case 'mention':
-			return `言及されました:\n${getUserName(notification.user)}「${getPostSummary(notification.post)}」`;
+			return `言及されました:\n${getUserName(notification.user)}「${getNoteSummary(notification.note)}」`;
 		case 'reply':
-			return `返信されました:\n${getUserName(notification.user)}「${getPostSummary(notification.post)}」`;
-		case 'repost':
-			return `Repostされました:\n${getUserName(notification.user)}「${getPostSummary(notification.post)}」`;
+			return `返信されました:\n${getUserName(notification.user)}「${getNoteSummary(notification.note)}」`;
+		case 'renote':
+			return `Renoteされました:\n${getUserName(notification.user)}「${getNoteSummary(notification.note)}」`;
 		case 'quote':
-			return `引用されました:\n${getUserName(notification.user)}「${getPostSummary(notification.post)}」`;
+			return `引用されました:\n${getUserName(notification.user)}「${getNoteSummary(notification.note)}」`;
 		case 'reaction':
-			return `リアクションされました:\n${getUserName(notification.user)} <${getReactionEmoji(notification.reaction)}>「${getPostSummary(notification.post)}」`;
+			return `リアクションされました:\n${getUserName(notification.user)} <${getReactionEmoji(notification.reaction)}>「${getNoteSummary(notification.note)}」`;
 		case 'poll_vote':
-			return `投票されました:\n${getUserName(notification.user)}「${getPostSummary(notification.post)}」`;
+			return `投票されました:\n${getUserName(notification.user)}「${getNoteSummary(notification.note)}」`;
 		default:
 			return `<不明な通知タイプ: ${notification.type}>`;
 	}
diff --git a/src/renderers/get-post-summary.ts b/src/renderers/get-post-summary.ts
deleted file mode 100644
index 8d0033064..000000000
--- a/src/renderers/get-post-summary.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-/**
- * 投稿を表す文字列を取得します。
- * @param {*} post 投稿
- */
-const summarize = (post: any): string => {
-	let summary = '';
-
-	// チャンネル
-	summary += post.channel ? `${post.channel.title}:` : '';
-
-	// 本文
-	summary += post.text ? post.text : '';
-
-	// メディアが添付されているとき
-	if (post.media) {
-		summary += ` (${post.media.length}つのメディア)`;
-	}
-
-	// 投票が添付されているとき
-	if (post.poll) {
-		summary += ' (投票)';
-	}
-
-	// 返信のとき
-	if (post.replyId) {
-		if (post.reply) {
-			summary += ` RE: ${summarize(post.reply)}`;
-		} else {
-			summary += ' RE: ...';
-		}
-	}
-
-	// Repostのとき
-	if (post.repostId) {
-		if (post.repost) {
-			summary += ` RP: ${summarize(post.repost)}`;
-		} else {
-			summary += ' RP: ...';
-		}
-	}
-
-	return summary.trim();
-};
-
-export default summarize;
diff --git a/src/renderers/get-user-summary.ts b/src/renderers/get-user-summary.ts
index d002933b6..52309954d 100644
--- a/src/renderers/get-user-summary.ts
+++ b/src/renderers/get-user-summary.ts
@@ -8,7 +8,7 @@ import getUserName from './get-user-name';
  */
 export default function(user: IUser): string {
 	let string = `${getUserName(user)} (@${getAcct(user)})\n` +
-		`${user.postsCount}投稿、${user.followingCount}フォロー、${user.followersCount}フォロワー\n`;
+		`${user.notesCount}投稿、${user.followingCount}フォロー、${user.followersCount}フォロワー\n`;
 
 	if (isLocalUser(user)) {
 		const account = user.account;
diff --git a/src/server/activitypub/index.ts b/src/server/activitypub/index.ts
index ac7a184f2..042579db9 100644
--- a/src/server/activitypub/index.ts
+++ b/src/server/activitypub/index.ts
@@ -4,7 +4,7 @@ import user from './user';
 import inbox from './inbox';
 import outbox from './outbox';
 import publicKey from './publickey';
-import post from './post';
+import note from './note';
 
 const app = express();
 app.disable('x-powered-by');
@@ -13,6 +13,6 @@ app.use(user);
 app.use(inbox);
 app.use(outbox);
 app.use(publicKey);
-app.use(post);
+app.use(note);
 
 export default app;
diff --git a/src/server/activitypub/post.ts b/src/server/activitypub/note.ts
similarity index 80%
rename from src/server/activitypub/post.ts
rename to src/server/activitypub/note.ts
index 355c60356..cea9be52d 100644
--- a/src/server/activitypub/post.ts
+++ b/src/server/activitypub/note.ts
@@ -2,12 +2,12 @@ import * as express from 'express';
 import context from '../../remote/activitypub/renderer/context';
 import render from '../../remote/activitypub/renderer/note';
 import parseAcct from '../../acct/parse';
-import Post from '../../models/post';
+import Note from '../../models/note';
 import User from '../../models/user';
 
 const app = express.Router();
 
-app.get('/@:user/:post', async (req, res, next) => {
+app.get('/@:user/:note', async (req, res, next) => {
 	const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']);
 	if (!(['application/activity+json', 'application/ld+json'] as any[]).includes(accepted)) {
 		return next();
@@ -26,15 +26,15 @@ app.get('/@:user/:post', async (req, res, next) => {
 		return res.sendStatus(404);
 	}
 
-	const post = await Post.findOne({
-		_id: req.params.post,
+	const note = await Note.findOne({
+		_id: req.params.note,
 		userId: user._id
 	});
-	if (post === null) {
+	if (note === null) {
 		return res.sendStatus(404);
 	}
 
-	const rendered = await render(user, post);
+	const rendered = await render(user, note);
 	rendered['@context'] = context;
 
 	res.json(rendered);
diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts
index 976908d1f..b6f3a3f9d 100644
--- a/src/server/activitypub/outbox.ts
+++ b/src/server/activitypub/outbox.ts
@@ -3,7 +3,7 @@ import context from '../../remote/activitypub/renderer/context';
 import renderNote from '../../remote/activitypub/renderer/note';
 import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
 import config from '../../config';
-import Post from '../../models/post';
+import Note from '../../models/note';
 import withUser from './with-user';
 
 const app = express.Router();
@@ -11,13 +11,13 @@ const app = express.Router();
 app.get('/@:user/outbox', withUser(username => {
 	return `${config.url}/@${username}/inbox`;
 }, async (user, req, res) => {
-	const posts = await Post.find({ userId: user._id }, {
+	const notes = await Note.find({ userId: user._id }, {
 		limit: 20,
 		sort: { _id: -1 }
 	});
 
-	const renderedPosts = await Promise.all(posts.map(post => renderNote(user, post)));
-	const rendered = renderOrderedCollection(`${config.url}/@${user.username}/inbox`, user.postsCount, renderedPosts);
+	const renderedNotes = await Promise.all(notes.map(note => renderNote(user, note)));
+	const rendered = renderOrderedCollection(`${config.url}/@${user.username}/inbox`, user.notesCount, renderedNotes);
 	rendered['@context'] = context;
 
 	res.json(rendered);
diff --git a/src/server/api/bot/core.ts b/src/server/api/bot/core.ts
index a44aa9d7b..1cf052234 100644
--- a/src/server/api/bot/core.ts
+++ b/src/server/api/bot/core.ts
@@ -3,7 +3,7 @@ import * as bcrypt from 'bcryptjs';
 
 import User, { IUser, init as initUser, ILocalUser } from '../../../models/user';
 
-import getPostSummary from '../../../renderers/get-post-summary';
+import getNoteSummary from '../../../renderers/get-note-summary';
 import getUserName from '../../../renderers/get-user-name';
 import getUserSummary from '../../../renderers/get-user-summary';
 import parseAcct from '../../../acct/parse';
@@ -83,7 +83,7 @@ export default class BotCore extends EventEmitter {
 					'me: アカウント情報を見ます\n' +
 					'login, signin: サインインします\n' +
 					'logout, signout: サインアウトします\n' +
-					'post: 投稿します\n' +
+					'note: 投稿します\n' +
 					'tl: タイムラインを見ます\n' +
 					'no: 通知を見ます\n' +
 					'@<ユーザー名>: ユーザーを表示します\n' +
@@ -109,10 +109,10 @@ export default class BotCore extends EventEmitter {
 				this.signout();
 				return 'ご利用ありがとうございました <3';
 
-			case 'post':
+			case 'note':
 			case '投稿':
 				if (this.user == null) return 'まずサインインしてください。';
-				this.setContext(new PostContext(this));
+				this.setContext(new NoteContext(this));
 				return await this.context.greet();
 
 			case 'tl':
@@ -190,7 +190,7 @@ abstract class Context extends EventEmitter {
 
 	public static import(bot: BotCore, data: any) {
 		if (data.type == 'guessing-game') return GuessingGameContext.import(bot, data.content);
-		if (data.type == 'post') return PostContext.import(bot, data.content);
+		if (data.type == 'note') return NoteContext.import(bot, data.content);
 		if (data.type == 'tl') return TlContext.import(bot, data.content);
 		if (data.type == 'notifications') return NotificationsContext.import(bot, data.content);
 		if (data.type == 'signin') return SigninContext.import(bot, data.content);
@@ -254,13 +254,13 @@ class SigninContext extends Context {
 	}
 }
 
-class PostContext extends Context {
+class NoteContext extends Context {
 	public async greet(): Promise<string> {
 		return '内容:';
 	}
 
 	public async q(query: string): Promise<string> {
-		await require('../endpoints/posts/create')({
+		await require('../endpoints/notes/create')({
 			text: query
 		}, this.bot.user);
 		this.bot.clearContext();
@@ -269,12 +269,12 @@ class PostContext extends Context {
 
 	public export() {
 		return {
-			type: 'post'
+			type: 'note'
 		};
 	}
 
 	public static import(bot: BotCore, data: any) {
-		const context = new PostContext(bot);
+		const context = new NoteContext(bot);
 		return context;
 	}
 }
@@ -296,7 +296,7 @@ class TlContext extends Context {
 	}
 
 	private async getTl() {
-		const tl = await require('../endpoints/posts/timeline')({
+		const tl = await require('../endpoints/notes/timeline')({
 			limit: 5,
 			untilId: this.next ? this.next : undefined
 		}, this.bot.user);
@@ -306,7 +306,7 @@ class TlContext extends Context {
 			this.emit('updated');
 
 			const text = tl
-				.map(post => `${getUserName(post.user)}\n「${getPostSummary(post)}」`)
+				.map(note => `${getUserName(note.user)}\n「${getNoteSummary(note)}」`)
 				.join('\n-----\n');
 
 			return text;
diff --git a/src/server/api/bot/interfaces/line.ts b/src/server/api/bot/interfaces/line.ts
index 1191aaf39..b6b0c257e 100644
--- a/src/server/api/bot/interfaces/line.ts
+++ b/src/server/api/bot/interfaces/line.ts
@@ -9,7 +9,7 @@ import _redis from '../../../../db/redis';
 import prominence = require('prominence');
 import getAcct from '../../../../acct/render';
 import parseAcct from '../../../../acct/parse';
-import getPostSummary from '../../../../renderers/get-post-summary';
+import getNoteSummary from '../../../../renderers/get-note-summary';
 import getUserName from '../../../../renderers/get-user-name';
 
 const redis = prominence(_redis);
@@ -80,14 +80,14 @@ class LineBot extends BotCore {
 				}
 				break;
 
-			// postback
-			case 'postback':
-				const data = ev.postback.data;
+			// noteback
+			case 'noteback':
+				const data = ev.noteback.data;
 				const cmd = data.split('|')[0];
 				const arg = data.split('|')[1];
 				switch (cmd) {
 					case 'showtl':
-						this.showUserTimelinePostback(arg);
+						this.showUserTimelineNoteback(arg);
 						break;
 				}
 				break;
@@ -107,7 +107,7 @@ class LineBot extends BotCore {
 		const actions = [];
 
 		actions.push({
-			type: 'postback',
+			type: 'noteback',
 			label: 'タイムラインを見る',
 			data: `showtl|${user.id}`
 		});
@@ -141,14 +141,14 @@ class LineBot extends BotCore {
 		return null;
 	}
 
-	public async showUserTimelinePostback(userId: string) {
-		const tl = await require('../../endpoints/users/posts')({
+	public async showUserTimelineNoteback(userId: string) {
+		const tl = await require('../../endpoints/users/notes')({
 			userId: userId,
 			limit: 5
 		}, this.user);
 
 		const text = `${getUserName(tl[0].user)}さんのタイムラインはこちらです:\n\n` + tl
-			.map(post => getPostSummary(post))
+			.map(note => getNoteSummary(note))
 			.join('\n-----\n');
 
 		this.reply([{
diff --git a/src/server/api/endpoints.ts b/src/server/api/endpoints.ts
index c7100bd03..67f3217fa 100644
--- a/src/server/api/endpoints.ts
+++ b/src/server/api/endpoints.ts
@@ -113,7 +113,7 @@ const endpoints: Endpoint[] = [
 		secure: true
 	},
 	{
-		name: 'aggregation/posts',
+		name: 'aggregation/notes',
 	},
 	{
 		name: 'aggregation/users',
@@ -122,7 +122,7 @@ const endpoints: Endpoint[] = [
 		name: 'aggregation/users/activity',
 	},
 	{
-		name: 'aggregation/users/post',
+		name: 'aggregation/users/note',
 	},
 	{
 		name: 'aggregation/users/followers'
@@ -134,16 +134,16 @@ const endpoints: Endpoint[] = [
 		name: 'aggregation/users/reaction'
 	},
 	{
-		name: 'aggregation/posts/repost'
+		name: 'aggregation/notes/renote'
 	},
 	{
-		name: 'aggregation/posts/reply'
+		name: 'aggregation/notes/reply'
 	},
 	{
-		name: 'aggregation/posts/reaction'
+		name: 'aggregation/notes/reaction'
 	},
 	{
-		name: 'aggregation/posts/reactions'
+		name: 'aggregation/notes/reactions'
 	},
 
 	{
@@ -391,7 +391,7 @@ const endpoints: Endpoint[] = [
 		name: 'users/search_by_username'
 	},
 	{
-		name: 'users/posts'
+		name: 'users/notes'
 	},
 	{
 		name: 'users/following'
@@ -428,35 +428,35 @@ const endpoints: Endpoint[] = [
 	},
 
 	{
-		name: 'posts'
+		name: 'notes'
 	},
 	{
-		name: 'posts/show'
+		name: 'notes/show'
 	},
 	{
-		name: 'posts/replies'
+		name: 'notes/replies'
 	},
 	{
-		name: 'posts/context'
+		name: 'notes/context'
 	},
 	{
-		name: 'posts/create',
+		name: 'notes/create',
 		withCredential: true,
 		limit: {
 			duration: ms('1hour'),
 			max: 120,
 			minInterval: ms('1second')
 		},
-		kind: 'post-write'
+		kind: 'note-write'
 	},
 	{
-		name: 'posts/reposts'
+		name: 'notes/renotes'
 	},
 	{
-		name: 'posts/search'
+		name: 'notes/search'
 	},
 	{
-		name: 'posts/timeline',
+		name: 'notes/timeline',
 		withCredential: true,
 		limit: {
 			duration: ms('10minutes'),
@@ -464,7 +464,7 @@ const endpoints: Endpoint[] = [
 		}
 	},
 	{
-		name: 'posts/mentions',
+		name: 'notes/mentions',
 		withCredential: true,
 		limit: {
 			duration: ms('10minutes'),
@@ -472,19 +472,19 @@ const endpoints: Endpoint[] = [
 		}
 	},
 	{
-		name: 'posts/trend',
+		name: 'notes/trend',
 		withCredential: true
 	},
 	{
-		name: 'posts/categorize',
+		name: 'notes/categorize',
 		withCredential: true
 	},
 	{
-		name: 'posts/reactions',
+		name: 'notes/reactions',
 		withCredential: true
 	},
 	{
-		name: 'posts/reactions/create',
+		name: 'notes/reactions/create',
 		withCredential: true,
 		limit: {
 			duration: ms('1hour'),
@@ -493,7 +493,7 @@ const endpoints: Endpoint[] = [
 		kind: 'reaction-write'
 	},
 	{
-		name: 'posts/reactions/delete',
+		name: 'notes/reactions/delete',
 		withCredential: true,
 		limit: {
 			duration: ms('1hour'),
@@ -502,7 +502,7 @@ const endpoints: Endpoint[] = [
 		kind: 'reaction-write'
 	},
 	{
-		name: 'posts/favorites/create',
+		name: 'notes/favorites/create',
 		withCredential: true,
 		limit: {
 			duration: ms('1hour'),
@@ -511,7 +511,7 @@ const endpoints: Endpoint[] = [
 		kind: 'favorite-write'
 	},
 	{
-		name: 'posts/favorites/delete',
+		name: 'notes/favorites/delete',
 		withCredential: true,
 		limit: {
 			duration: ms('1hour'),
@@ -520,7 +520,7 @@ const endpoints: Endpoint[] = [
 		kind: 'favorite-write'
 	},
 	{
-		name: 'posts/polls/vote',
+		name: 'notes/polls/vote',
 		withCredential: true,
 		limit: {
 			duration: ms('1hour'),
@@ -529,7 +529,7 @@ const endpoints: Endpoint[] = [
 		kind: 'vote-write'
 	},
 	{
-		name: 'posts/polls/recommendation',
+		name: 'notes/polls/recommendation',
 		withCredential: true
 	},
 
@@ -566,7 +566,7 @@ const endpoints: Endpoint[] = [
 		name: 'channels/show'
 	},
 	{
-		name: 'channels/posts'
+		name: 'channels/notes'
 	},
 	{
 		name: 'channels/watch',
diff --git a/src/server/api/endpoints/aggregation/posts/reaction.ts b/src/server/api/endpoints/aggregation/notes/reaction.ts
similarity index 72%
rename from src/server/api/endpoints/aggregation/posts/reaction.ts
rename to src/server/api/endpoints/aggregation/notes/reaction.ts
index e62274533..586e8c2d8 100644
--- a/src/server/api/endpoints/aggregation/posts/reaction.ts
+++ b/src/server/api/endpoints/aggregation/notes/reaction.ts
@@ -2,32 +2,32 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post from '../../../../../models/post';
-import Reaction from '../../../../../models/post-reaction';
+import Note from '../../../../../models/note';
+import Reaction from '../../../../../models/note-reaction';
 
 /**
- * Aggregate reaction of a post
+ * Aggregate reaction of a note
  *
  * @param {any} params
  * @return {Promise<any>}
  */
 module.exports = (params) => new Promise(async (res, rej) => {
-	// Get 'postId' parameter
-	const [postId, postIdErr] = $(params.postId).id().$;
-	if (postIdErr) return rej('invalid postId param');
+	// Get 'noteId' parameter
+	const [noteId, noteIdErr] = $(params.noteId).id().$;
+	if (noteIdErr) return rej('invalid noteId param');
 
-	// Lookup post
-	const post = await Post.findOne({
-		_id: postId
+	// Lookup note
+	const note = await Note.findOne({
+		_id: noteId
 	});
 
-	if (post === null) {
-		return rej('post not found');
+	if (note === null) {
+		return rej('note not found');
 	}
 
 	const datas = await Reaction
 		.aggregate([
-			{ $match: { postId: post._id } },
+			{ $match: { noteId: note._id } },
 			{ $project: {
 				createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
 			}},
diff --git a/src/server/api/endpoints/aggregation/posts/reactions.ts b/src/server/api/endpoints/aggregation/notes/reactions.ts
similarity index 71%
rename from src/server/api/endpoints/aggregation/posts/reactions.ts
rename to src/server/api/endpoints/aggregation/notes/reactions.ts
index 5f23e296f..ff9491292 100644
--- a/src/server/api/endpoints/aggregation/posts/reactions.ts
+++ b/src/server/api/endpoints/aggregation/notes/reactions.ts
@@ -2,34 +2,34 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post from '../../../../../models/post';
-import Reaction from '../../../../../models/post-reaction';
+import Note from '../../../../../models/note';
+import Reaction from '../../../../../models/note-reaction';
 
 /**
- * Aggregate reactions of a post
+ * Aggregate reactions of a note
  *
  * @param {any} params
  * @return {Promise<any>}
  */
 module.exports = (params) => new Promise(async (res, rej) => {
-	// Get 'postId' parameter
-	const [postId, postIdErr] = $(params.postId).id().$;
-	if (postIdErr) return rej('invalid postId param');
+	// Get 'noteId' parameter
+	const [noteId, noteIdErr] = $(params.noteId).id().$;
+	if (noteIdErr) return rej('invalid noteId param');
 
-	// Lookup post
-	const post = await Post.findOne({
-		_id: postId
+	// Lookup note
+	const note = await Note.findOne({
+		_id: noteId
 	});
 
-	if (post === null) {
-		return rej('post not found');
+	if (note === null) {
+		return rej('note not found');
 	}
 
 	const startTime = new Date(new Date().setMonth(new Date().getMonth() - 1));
 
 	const reactions = await Reaction
 		.find({
-			postId: post._id,
+			noteId: note._id,
 			$or: [
 				{ deletedAt: { $exists: false } },
 				{ deletedAt: { $gt: startTime } }
@@ -40,7 +40,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
 			},
 			fields: {
 				_id: false,
-				postId: false
+				noteId: false
 			}
 		});
 
diff --git a/src/server/api/endpoints/aggregation/posts/reply.ts b/src/server/api/endpoints/aggregation/notes/reply.ts
similarity index 74%
rename from src/server/api/endpoints/aggregation/posts/reply.ts
rename to src/server/api/endpoints/aggregation/notes/reply.ts
index c76191e86..42df95a9a 100644
--- a/src/server/api/endpoints/aggregation/posts/reply.ts
+++ b/src/server/api/endpoints/aggregation/notes/reply.ts
@@ -2,31 +2,31 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post from '../../../../../models/post';
+import Note from '../../../../../models/note';
 
 /**
- * Aggregate reply of a post
+ * Aggregate reply of a note
  *
  * @param {any} params
  * @return {Promise<any>}
  */
 module.exports = (params) => new Promise(async (res, rej) => {
-	// Get 'postId' parameter
-	const [postId, postIdErr] = $(params.postId).id().$;
-	if (postIdErr) return rej('invalid postId param');
+	// Get 'noteId' parameter
+	const [noteId, noteIdErr] = $(params.noteId).id().$;
+	if (noteIdErr) return rej('invalid noteId param');
 
-	// Lookup post
-	const post = await Post.findOne({
-		_id: postId
+	// Lookup note
+	const note = await Note.findOne({
+		_id: noteId
 	});
 
-	if (post === null) {
-		return rej('post not found');
+	if (note === null) {
+		return rej('note not found');
 	}
 
-	const datas = await Post
+	const datas = await Note
 		.aggregate([
-			{ $match: { reply: post._id } },
+			{ $match: { reply: note._id } },
 			{ $project: {
 				createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
 			}},
diff --git a/src/server/api/endpoints/aggregation/posts/repost.ts b/src/server/api/endpoints/aggregation/notes/repost.ts
similarity index 74%
rename from src/server/api/endpoints/aggregation/posts/repost.ts
rename to src/server/api/endpoints/aggregation/notes/repost.ts
index a203605eb..feb3348a7 100644
--- a/src/server/api/endpoints/aggregation/posts/repost.ts
+++ b/src/server/api/endpoints/aggregation/notes/repost.ts
@@ -2,31 +2,31 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post from '../../../../../models/post';
+import Note from '../../../../../models/note';
 
 /**
- * Aggregate repost of a post
+ * Aggregate renote of a note
  *
  * @param {any} params
  * @return {Promise<any>}
  */
 module.exports = (params) => new Promise(async (res, rej) => {
-	// Get 'postId' parameter
-	const [postId, postIdErr] = $(params.postId).id().$;
-	if (postIdErr) return rej('invalid postId param');
+	// Get 'noteId' parameter
+	const [noteId, noteIdErr] = $(params.noteId).id().$;
+	if (noteIdErr) return rej('invalid noteId param');
 
-	// Lookup post
-	const post = await Post.findOne({
-		_id: postId
+	// Lookup note
+	const note = await Note.findOne({
+		_id: noteId
 	});
 
-	if (post === null) {
-		return rej('post not found');
+	if (note === null) {
+		return rej('note not found');
 	}
 
-	const datas = await Post
+	const datas = await Note
 		.aggregate([
-			{ $match: { repostId: post._id } },
+			{ $match: { renoteId: note._id } },
 			{ $project: {
 				createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
 			}},
diff --git a/src/server/api/endpoints/aggregation/posts.ts b/src/server/api/endpoints/aggregation/posts.ts
index f4d401eda..cc2a48b53 100644
--- a/src/server/api/endpoints/aggregation/posts.ts
+++ b/src/server/api/endpoints/aggregation/posts.ts
@@ -2,10 +2,10 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post from '../../../../models/post';
+import Note from '../../../../models/note';
 
 /**
- * Aggregate posts
+ * Aggregate notes
  *
  * @param {any} params
  * @return {Promise<any>}
@@ -15,10 +15,10 @@ module.exports = params => new Promise(async (res, rej) => {
 	const [limit = 365, limitErr] = $(params.limit).optional.number().range(1, 365).$;
 	if (limitErr) return rej('invalid limit param');
 
-	const datas = await Post
+	const datas = await Note
 		.aggregate([
 			{ $project: {
-				repostId: '$repostId',
+				renoteId: '$renoteId',
 				replyId: '$replyId',
 				createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
 			}},
@@ -30,13 +30,13 @@ module.exports = params => new Promise(async (res, rej) => {
 				},
 				type: {
 					$cond: {
-						if: { $ne: ['$repostId', null] },
-						then: 'repost',
+						if: { $ne: ['$renoteId', null] },
+						then: 'renote',
 						else: {
 							$cond: {
 								if: { $ne: ['$replyId', null] },
 								then: 'reply',
-								else: 'post'
+								else: 'note'
 							}
 						}
 					}
@@ -59,8 +59,8 @@ module.exports = params => new Promise(async (res, rej) => {
 		data.date = data._id;
 		delete data._id;
 
-		data.posts = (data.data.filter(x => x.type == 'post')[0] || { count: 0 }).count;
-		data.reposts = (data.data.filter(x => x.type == 'repost')[0] || { count: 0 }).count;
+		data.notes = (data.data.filter(x => x.type == 'note')[0] || { count: 0 }).count;
+		data.renotes = (data.data.filter(x => x.type == 'renote')[0] || { count: 0 }).count;
 		data.replies = (data.data.filter(x => x.type == 'reply')[0] || { count: 0 }).count;
 
 		delete data.data;
@@ -79,8 +79,8 @@ module.exports = params => new Promise(async (res, rej) => {
 			graph.push(data);
 		} else {
 			graph.push({
-				posts: 0,
-				reposts: 0,
+				notes: 0,
+				renotes: 0,
 				replies: 0
 			});
 		}
diff --git a/src/server/api/endpoints/aggregation/users/activity.ts b/src/server/api/endpoints/aggregation/users/activity.ts
index cef007229..318cce77a 100644
--- a/src/server/api/endpoints/aggregation/users/activity.ts
+++ b/src/server/api/endpoints/aggregation/users/activity.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import User from '../../../../../models/user';
-import Post from '../../../../../models/post';
+import Note from '../../../../../models/note';
 
 // TODO: likeやfollowも集計
 
@@ -35,11 +35,11 @@ module.exports = (params) => new Promise(async (res, rej) => {
 		return rej('user not found');
 	}
 
-	const datas = await Post
+	const datas = await Note
 		.aggregate([
 			{ $match: { userId: user._id } },
 			{ $project: {
-				repostId: '$repostId',
+				renoteId: '$renoteId',
 				replyId: '$replyId',
 				createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
 			}},
@@ -51,13 +51,13 @@ module.exports = (params) => new Promise(async (res, rej) => {
 				},
 				type: {
 					$cond: {
-						if: { $ne: ['$repostId', null] },
-						then: 'repost',
+						if: { $ne: ['$renoteId', null] },
+						then: 'renote',
 						else: {
 							$cond: {
 								if: { $ne: ['$replyId', null] },
 								then: 'reply',
-								else: 'post'
+								else: 'note'
 							}
 						}
 					}
@@ -80,8 +80,8 @@ module.exports = (params) => new Promise(async (res, rej) => {
 		data.date = data._id;
 		delete data._id;
 
-		data.posts = (data.data.filter(x => x.type == 'post')[0] || { count: 0 }).count;
-		data.reposts = (data.data.filter(x => x.type == 'repost')[0] || { count: 0 }).count;
+		data.notes = (data.data.filter(x => x.type == 'note')[0] || { count: 0 }).count;
+		data.renotes = (data.data.filter(x => x.type == 'renote')[0] || { count: 0 }).count;
 		data.replies = (data.data.filter(x => x.type == 'reply')[0] || { count: 0 }).count;
 
 		delete data.data;
@@ -105,8 +105,8 @@ module.exports = (params) => new Promise(async (res, rej) => {
 					month: day.getMonth() + 1, // In JavaScript, month is zero-based.
 					day: day.getDate()
 				},
-				posts: 0,
-				reposts: 0,
+				notes: 0,
+				renotes: 0,
 				replies: 0
 			});
 		}
diff --git a/src/server/api/endpoints/aggregation/users/post.ts b/src/server/api/endpoints/aggregation/users/post.ts
index 13617cf63..e6170d83e 100644
--- a/src/server/api/endpoints/aggregation/users/post.ts
+++ b/src/server/api/endpoints/aggregation/users/post.ts
@@ -3,10 +3,10 @@
  */
 import $ from 'cafy';
 import User from '../../../../../models/user';
-import Post from '../../../../../models/post';
+import Note from '../../../../../models/note';
 
 /**
- * Aggregate post of a user
+ * Aggregate note of a user
  *
  * @param {any} params
  * @return {Promise<any>}
@@ -29,11 +29,11 @@ module.exports = (params) => new Promise(async (res, rej) => {
 		return rej('user not found');
 	}
 
-	const datas = await Post
+	const datas = await Note
 		.aggregate([
 			{ $match: { userId: user._id } },
 			{ $project: {
-				repostId: '$repostId',
+				renoteId: '$renoteId',
 				replyId: '$replyId',
 				createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
 			}},
@@ -45,13 +45,13 @@ module.exports = (params) => new Promise(async (res, rej) => {
 				},
 				type: {
 					$cond: {
-						if: { $ne: ['$repostId', null] },
-						then: 'repost',
+						if: { $ne: ['$renoteId', null] },
+						then: 'renote',
 						else: {
 							$cond: {
 								if: { $ne: ['$replyId', null] },
 								then: 'reply',
-								else: 'post'
+								else: 'note'
 							}
 						}
 					}
@@ -74,8 +74,8 @@ module.exports = (params) => new Promise(async (res, rej) => {
 		data.date = data._id;
 		delete data._id;
 
-		data.posts = (data.data.filter(x => x.type == 'post')[0] || { count: 0 }).count;
-		data.reposts = (data.data.filter(x => x.type == 'repost')[0] || { count: 0 }).count;
+		data.notes = (data.data.filter(x => x.type == 'note')[0] || { count: 0 }).count;
+		data.renotes = (data.data.filter(x => x.type == 'renote')[0] || { count: 0 }).count;
 		data.replies = (data.data.filter(x => x.type == 'reply')[0] || { count: 0 }).count;
 
 		delete data.data;
@@ -99,8 +99,8 @@ module.exports = (params) => new Promise(async (res, rej) => {
 					month: day.getMonth() + 1, // In JavaScript, month is zero-based.
 					day: day.getDate()
 				},
-				posts: 0,
-				reposts: 0,
+				notes: 0,
+				renotes: 0,
 				replies: 0
 			});
 		}
diff --git a/src/server/api/endpoints/aggregation/users/reaction.ts b/src/server/api/endpoints/aggregation/users/reaction.ts
index 0c42ba336..881c7ea69 100644
--- a/src/server/api/endpoints/aggregation/users/reaction.ts
+++ b/src/server/api/endpoints/aggregation/users/reaction.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import User from '../../../../../models/user';
-import Reaction from '../../../../../models/post-reaction';
+import Reaction from '../../../../../models/note-reaction';
 
 /**
  * Aggregate reaction of a user
diff --git a/src/server/api/endpoints/app/create.ts b/src/server/api/endpoints/app/create.ts
index f2033d33f..4a55d33f2 100644
--- a/src/server/api/endpoints/app/create.ts
+++ b/src/server/api/endpoints/app/create.ts
@@ -8,7 +8,7 @@ import App, { isValidNameId, pack } from '../../../../models/app';
 /**
  * @swagger
  * /app/create:
- *   post:
+ *   note:
  *     summary: Create an application
  *     parameters:
  *       - $ref: "#/parameters/AccessToken"
diff --git a/src/server/api/endpoints/app/name_id/available.ts b/src/server/api/endpoints/app/name_id/available.ts
index 93b33cfa2..ec2d69241 100644
--- a/src/server/api/endpoints/app/name_id/available.ts
+++ b/src/server/api/endpoints/app/name_id/available.ts
@@ -8,7 +8,7 @@ import { isValidNameId } from '../../../../../models/app';
 /**
  * @swagger
  * /app/nameId/available:
- *   post:
+ *   note:
  *     summary: Check available nameId on creation an application
  *     parameters:
  *       -
diff --git a/src/server/api/endpoints/app/show.ts b/src/server/api/endpoints/app/show.ts
index 7c8d2881d..3a3c25f47 100644
--- a/src/server/api/endpoints/app/show.ts
+++ b/src/server/api/endpoints/app/show.ts
@@ -7,7 +7,7 @@ import App, { pack } from '../../../../models/app';
 /**
  * @swagger
  * /app/show:
- *   post:
+ *   note:
  *     summary: Show an application's information
  *     description: Require appId or nameId
  *     parameters:
diff --git a/src/server/api/endpoints/auth/accept.ts b/src/server/api/endpoints/auth/accept.ts
index aeabac2db..b6297d663 100644
--- a/src/server/api/endpoints/auth/accept.ts
+++ b/src/server/api/endpoints/auth/accept.ts
@@ -11,7 +11,7 @@ import AccessToken from '../../../../models/access-token';
 /**
  * @swagger
  * /auth/accept:
- *   post:
+ *   note:
  *     summary: Accept a session
  *     parameters:
  *       - $ref: "#/parameters/NativeToken"
diff --git a/src/server/api/endpoints/auth/session/generate.ts b/src/server/api/endpoints/auth/session/generate.ts
index 9857b31d8..7c475dbe2 100644
--- a/src/server/api/endpoints/auth/session/generate.ts
+++ b/src/server/api/endpoints/auth/session/generate.ts
@@ -10,7 +10,7 @@ import config from '../../../../../config';
 /**
  * @swagger
  * /auth/session/generate:
- *   post:
+ *   note:
  *     summary: Generate a session
  *     parameters:
  *       -
diff --git a/src/server/api/endpoints/auth/session/show.ts b/src/server/api/endpoints/auth/session/show.ts
index f473d7397..f7f0b087b 100644
--- a/src/server/api/endpoints/auth/session/show.ts
+++ b/src/server/api/endpoints/auth/session/show.ts
@@ -7,7 +7,7 @@ import AuthSess, { pack } from '../../../../../models/auth-session';
 /**
  * @swagger
  * /auth/session/show:
- *   post:
+ *   note:
  *     summary: Show a session information
  *     parameters:
  *       -
diff --git a/src/server/api/endpoints/auth/session/userkey.ts b/src/server/api/endpoints/auth/session/userkey.ts
index 7dbb5ea6e..ddb67cb45 100644
--- a/src/server/api/endpoints/auth/session/userkey.ts
+++ b/src/server/api/endpoints/auth/session/userkey.ts
@@ -10,7 +10,7 @@ import { pack } from '../../../../../models/user';
 /**
  * @swagger
  * /auth/session/userkey:
- *   post:
+ *   note:
  *     summary: Get an access token(userkey)
  *     parameters:
  *       -
diff --git a/src/server/api/endpoints/channels/posts.ts b/src/server/api/endpoints/channels/posts.ts
index e48f96da7..d636aa0d1 100644
--- a/src/server/api/endpoints/channels/posts.ts
+++ b/src/server/api/endpoints/channels/posts.ts
@@ -3,10 +3,10 @@
  */
 import $ from 'cafy';
 import { default as Channel, IChannel } from '../../../../models/channel';
-import Post, { pack } from '../../../../models/post';
+import Note, { pack } from '../../../../models/note';
 
 /**
- * Show a posts of a channel
+ * Show a notes of a channel
  *
  * @param {any} params
  * @param {any} user
@@ -65,14 +65,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	//#endregion Construct query
 
 	// Issue query
-	const posts = await Post
+	const notes = await Note
 		.find(query, {
 			limit: limit,
 			sort: sort
 		});
 
 	// Serialize
-	res(await Promise.all(posts.map(async (post) =>
-		await pack(post, user)
+	res(await Promise.all(notes.map(async (note) =>
+		await pack(note, user)
 	)));
 });
diff --git a/src/server/api/endpoints/i/favorites.ts b/src/server/api/endpoints/i/favorites.ts
index 0b594e318..b40f2b388 100644
--- a/src/server/api/endpoints/i/favorites.ts
+++ b/src/server/api/endpoints/i/favorites.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import Favorite from '../../../../models/favorite';
-import { pack } from '../../../../models/post';
+import { pack } from '../../../../models/note';
 
 /**
  * Get followers of a user
@@ -39,6 +39,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Serialize
 	res(await Promise.all(favorites.map(async favorite =>
-		await pack(favorite.postId)
+		await pack(favorite.noteId)
 	)));
 });
diff --git a/src/server/api/endpoints/i/pin.ts b/src/server/api/endpoints/i/pin.ts
index 2a5757977..909a6fdbd 100644
--- a/src/server/api/endpoints/i/pin.ts
+++ b/src/server/api/endpoints/i/pin.ts
@@ -3,34 +3,34 @@
  */
 import $ from 'cafy';
 import User from '../../../../models/user';
-import Post from '../../../../models/post';
+import Note from '../../../../models/note';
 import { pack } from '../../../../models/user';
 
 /**
- * Pin post
+ * Pin note
  *
  * @param {any} params
  * @param {any} user
  * @return {Promise<any>}
  */
 module.exports = async (params, user) => new Promise(async (res, rej) => {
-	// Get 'postId' parameter
-	const [postId, postIdErr] = $(params.postId).id().$;
-	if (postIdErr) return rej('invalid postId param');
+	// Get 'noteId' parameter
+	const [noteId, noteIdErr] = $(params.noteId).id().$;
+	if (noteIdErr) return rej('invalid noteId param');
 
 	// Fetch pinee
-	const post = await Post.findOne({
-		_id: postId,
+	const note = await Note.findOne({
+		_id: noteId,
 		userId: user._id
 	});
 
-	if (post === null) {
-		return rej('post not found');
+	if (note === null) {
+		return rej('note not found');
 	}
 
 	await User.update(user._id, {
 		$set: {
-			pinnedPostId: post._id
+			pinnedNoteId: note._id
 		}
 	});
 
diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts
index 70ae7e99c..8574362fc 100644
--- a/src/server/api/endpoints/meta.ts
+++ b/src/server/api/endpoints/meta.ts
@@ -9,7 +9,7 @@ import Meta from '../../../models/meta';
 /**
  * @swagger
  * /meta:
- *   post:
+ *   note:
  *     summary: Show the misskey's information
  *     responses:
  *       200:
diff --git a/src/server/api/endpoints/posts/context.ts b/src/server/api/endpoints/notes/context.ts
similarity index 60%
rename from src/server/api/endpoints/posts/context.ts
rename to src/server/api/endpoints/notes/context.ts
index 7abb045a4..2caf742d2 100644
--- a/src/server/api/endpoints/posts/context.ts
+++ b/src/server/api/endpoints/notes/context.ts
@@ -2,19 +2,19 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post, { pack } from '../../../../models/post';
+import Note, { pack } from '../../../../models/note';
 
 /**
- * Show a context of a post
+ * Show a context of a note
  *
  * @param {any} params
  * @param {any} user
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'postId' parameter
-	const [postId, postIdErr] = $(params.postId).id().$;
-	if (postIdErr) return rej('invalid postId param');
+	// Get 'noteId' parameter
+	const [noteId, noteIdErr] = $(params.noteId).id().$;
+	if (noteIdErr) return rej('invalid noteId param');
 
 	// Get 'limit' parameter
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
@@ -24,13 +24,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$;
 	if (offsetErr) return rej('invalid offset param');
 
-	// Lookup post
-	const post = await Post.findOne({
-		_id: postId
+	// Lookup note
+	const note = await Note.findOne({
+		_id: noteId
 	});
 
-	if (post === null) {
-		return rej('post not found');
+	if (note === null) {
+		return rej('note not found');
 	}
 
 	const context = [];
@@ -38,7 +38,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	async function get(id) {
 		i++;
-		const p = await Post.findOne({ _id: id });
+		const p = await Note.findOne({ _id: id });
 
 		if (i > offset) {
 			context.push(p);
@@ -53,11 +53,11 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		}
 	}
 
-	if (post.replyId) {
-		await get(post.replyId);
+	if (note.replyId) {
+		await get(note.replyId);
 	}
 
 	// Serialize
-	res(await Promise.all(context.map(async post =>
-		await pack(post, user))));
+	res(await Promise.all(context.map(async note =>
+		await pack(note, user))));
 });
diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts
new file mode 100644
index 000000000..7e79912b1
--- /dev/null
+++ b/src/server/api/endpoints/notes/create.ts
@@ -0,0 +1,251 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import deepEqual = require('deep-equal');
+import Note, { INote, isValidText, isValidCw, pack } from '../../../../models/note';
+import { ILocalUser } from '../../../../models/user';
+import Channel, { IChannel } from '../../../../models/channel';
+import DriveFile from '../../../../models/drive-file';
+import create from '../../../../services/note/create';
+import { IApp } from '../../../../models/app';
+
+/**
+ * Create a note
+ *
+ * @param {any} params
+ * @param {any} user
+ * @param {any} app
+ * @return {Promise<any>}
+ */
+module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res, rej) => {
+	// Get 'visibility' parameter
+	const [visibility = 'public', visibilityErr] = $(params.visibility).optional.string().or(['public', 'unlisted', 'private', 'direct']).$;
+	if (visibilityErr) return rej('invalid visibility');
+
+	// Get 'text' parameter
+	const [text, textErr] = $(params.text).optional.string().pipe(isValidText).$;
+	if (textErr) return rej('invalid text');
+
+	// Get 'cw' parameter
+	const [cw, cwErr] = $(params.cw).optional.string().pipe(isValidCw).$;
+	if (cwErr) return rej('invalid cw');
+
+	// Get 'viaMobile' parameter
+	const [viaMobile = false, viaMobileErr] = $(params.viaMobile).optional.boolean().$;
+	if (viaMobileErr) return rej('invalid viaMobile');
+
+	// Get 'tags' parameter
+	const [tags = [], tagsErr] = $(params.tags).optional.array('string').unique().eachQ(t => t.range(1, 32)).$;
+	if (tagsErr) return rej('invalid tags');
+
+	// Get 'geo' parameter
+	const [geo, geoErr] = $(params.geo).optional.nullable.strict.object()
+		.have('coordinates', $().array().length(2)
+			.item(0, $().number().range(-180, 180))
+			.item(1, $().number().range(-90, 90)))
+		.have('altitude', $().nullable.number())
+		.have('accuracy', $().nullable.number())
+		.have('altitudeAccuracy', $().nullable.number())
+		.have('heading', $().nullable.number().range(0, 360))
+		.have('speed', $().nullable.number())
+		.$;
+	if (geoErr) return rej('invalid geo');
+
+	// Get 'mediaIds' parameter
+	const [mediaIds, mediaIdsErr] = $(params.mediaIds).optional.array('id').unique().range(1, 4).$;
+	if (mediaIdsErr) return rej('invalid mediaIds');
+
+	let files = [];
+	if (mediaIds !== undefined) {
+		// Fetch files
+		// forEach だと途中でエラーなどがあっても return できないので
+		// 敢えて for を使っています。
+		for (const mediaId of mediaIds) {
+			// Fetch file
+			// SELECT _id
+			const entity = await DriveFile.findOne({
+				_id: mediaId,
+				'metadata.userId': user._id
+			});
+
+			if (entity === null) {
+				return rej('file not found');
+			} else {
+				files.push(entity);
+			}
+		}
+	} else {
+		files = null;
+	}
+
+	// Get 'renoteId' parameter
+	const [renoteId, renoteIdErr] = $(params.renoteId).optional.id().$;
+	if (renoteIdErr) return rej('invalid renoteId');
+
+	let renote: INote = null;
+	let isQuote = false;
+	if (renoteId !== undefined) {
+		// Fetch renote to note
+		renote = await Note.findOne({
+			_id: renoteId
+		});
+
+		if (renote == null) {
+			return rej('renoteee is not found');
+		} else if (renote.renoteId && !renote.text && !renote.mediaIds) {
+			return rej('cannot renote to renote');
+		}
+
+		// Fetch recently note
+		const latestNote = await Note.findOne({
+			userId: user._id
+		}, {
+			sort: {
+				_id: -1
+			}
+		});
+
+		isQuote = text != null || files != null;
+
+		// 直近と同じRenote対象かつ引用じゃなかったらエラー
+		if (latestNote &&
+			latestNote.renoteId &&
+			latestNote.renoteId.equals(renote._id) &&
+			!isQuote) {
+			return rej('cannot renote same note that already reposted in your latest note');
+		}
+
+		// 直近がRenote対象かつ引用じゃなかったらエラー
+		if (latestNote &&
+			latestNote._id.equals(renote._id) &&
+			!isQuote) {
+			return rej('cannot renote your latest note');
+		}
+	}
+
+	// Get 'replyId' parameter
+	const [replyId, replyIdErr] = $(params.replyId).optional.id().$;
+	if (replyIdErr) return rej('invalid replyId');
+
+	let reply: INote = null;
+	if (replyId !== undefined) {
+		// Fetch reply
+		reply = await Note.findOne({
+			_id: replyId
+		});
+
+		if (reply === null) {
+			return rej('in reply to note is not found');
+		}
+
+		// 返信対象が引用でないRenoteだったらエラー
+		if (reply.renoteId && !reply.text && !reply.mediaIds) {
+			return rej('cannot reply to renote');
+		}
+	}
+
+	// Get 'channelId' parameter
+	const [channelId, channelIdErr] = $(params.channelId).optional.id().$;
+	if (channelIdErr) return rej('invalid channelId');
+
+	let channel: IChannel = null;
+	if (channelId !== undefined) {
+		// Fetch channel
+		channel = await Channel.findOne({
+			_id: channelId
+		});
+
+		if (channel === null) {
+			return rej('channel not found');
+		}
+
+		// 返信対象の投稿がこのチャンネルじゃなかったらダメ
+		if (reply && !channelId.equals(reply.channelId)) {
+			return rej('チャンネル内部からチャンネル外部の投稿に返信することはできません');
+		}
+
+		// Renote対象の投稿がこのチャンネルじゃなかったらダメ
+		if (renote && !channelId.equals(renote.channelId)) {
+			return rej('チャンネル内部からチャンネル外部の投稿をRenoteすることはできません');
+		}
+
+		// 引用ではないRenoteはダメ
+		if (renote && !isQuote) {
+			return rej('チャンネル内部では引用ではないRenoteをすることはできません');
+		}
+	} else {
+		// 返信対象の投稿がチャンネルへの投稿だったらダメ
+		if (reply && reply.channelId != null) {
+			return rej('チャンネル外部からチャンネル内部の投稿に返信することはできません');
+		}
+
+		// Renote対象の投稿がチャンネルへの投稿だったらダメ
+		if (renote && renote.channelId != null) {
+			return rej('チャンネル外部からチャンネル内部の投稿をRenoteすることはできません');
+		}
+	}
+
+	// Get 'poll' parameter
+	const [poll, pollErr] = $(params.poll).optional.strict.object()
+		.have('choices', $().array('string')
+			.unique()
+			.range(2, 10)
+			.each(c => c.length > 0 && c.length < 50))
+		.$;
+	if (pollErr) return rej('invalid poll');
+
+	if (poll) {
+		(poll as any).choices = (poll as any).choices.map((choice, i) => ({
+			id: i, // IDを付与
+			text: choice.trim(),
+			votes: 0
+		}));
+	}
+
+	// テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー
+	if (text === undefined && files === null && renote === null && poll === undefined) {
+		return rej('text, mediaIds, renoteId or poll is required');
+	}
+
+	// 直近の投稿と重複してたらエラー
+	// TODO: 直近の投稿が一日前くらいなら重複とは見なさない
+	if (user.latestNote) {
+		if (deepEqual({
+			text: user.latestNote.text,
+			reply: user.latestNote.replyId ? user.latestNote.replyId.toString() : null,
+			renote: user.latestNote.renoteId ? user.latestNote.renoteId.toString() : null,
+			mediaIds: (user.latestNote.mediaIds || []).map(id => id.toString())
+		}, {
+			text: text,
+			reply: reply ? reply._id.toString() : null,
+			renote: renote ? renote._id.toString() : null,
+			mediaIds: (files || []).map(file => file._id.toString())
+		})) {
+			return rej('duplicate');
+		}
+	}
+
+	// 投稿を作成
+	const note = await create(user, {
+		createdAt: new Date(),
+		media: files,
+		poll: poll,
+		text: text,
+		reply,
+		renote,
+		cw: cw,
+		tags: tags,
+		app: app,
+		viaMobile: viaMobile,
+		visibility,
+		geo
+	});
+
+	const noteObj = await pack(note, user);
+
+	// Reponse
+	res({
+		createdNote: noteObj
+	});
+});
diff --git a/src/server/api/endpoints/posts/favorites/create.ts b/src/server/api/endpoints/notes/favorites/create.ts
similarity index 62%
rename from src/server/api/endpoints/posts/favorites/create.ts
rename to src/server/api/endpoints/notes/favorites/create.ts
index f537fb7dd..c8e7f5242 100644
--- a/src/server/api/endpoints/posts/favorites/create.ts
+++ b/src/server/api/endpoints/notes/favorites/create.ts
@@ -3,32 +3,32 @@
  */
 import $ from 'cafy';
 import Favorite from '../../../../../models/favorite';
-import Post from '../../../../../models/post';
+import Note from '../../../../../models/note';
 
 /**
- * Favorite a post
+ * Favorite a note
  *
  * @param {any} params
  * @param {any} user
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'postId' parameter
-	const [postId, postIdErr] = $(params.postId).id().$;
-	if (postIdErr) return rej('invalid postId param');
+	// Get 'noteId' parameter
+	const [noteId, noteIdErr] = $(params.noteId).id().$;
+	if (noteIdErr) return rej('invalid noteId param');
 
 	// Get favoritee
-	const post = await Post.findOne({
-		_id: postId
+	const note = await Note.findOne({
+		_id: noteId
 	});
 
-	if (post === null) {
-		return rej('post not found');
+	if (note === null) {
+		return rej('note not found');
 	}
 
 	// if already favorited
 	const exist = await Favorite.findOne({
-		postId: post._id,
+		noteId: note._id,
 		userId: user._id
 	});
 
@@ -39,7 +39,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Create favorite
 	await Favorite.insert({
 		createdAt: new Date(),
-		postId: post._id,
+		noteId: note._id,
 		userId: user._id
 	});
 
diff --git a/src/server/api/endpoints/posts/favorites/delete.ts b/src/server/api/endpoints/notes/favorites/delete.ts
similarity index 62%
rename from src/server/api/endpoints/posts/favorites/delete.ts
rename to src/server/api/endpoints/notes/favorites/delete.ts
index 28930337a..92aceb343 100644
--- a/src/server/api/endpoints/posts/favorites/delete.ts
+++ b/src/server/api/endpoints/notes/favorites/delete.ts
@@ -3,32 +3,32 @@
  */
 import $ from 'cafy';
 import Favorite from '../../../../../models/favorite';
-import Post from '../../../../../models/post';
+import Note from '../../../../../models/note';
 
 /**
- * Unfavorite a post
+ * Unfavorite a note
  *
  * @param {any} params
  * @param {any} user
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'postId' parameter
-	const [postId, postIdErr] = $(params.postId).id().$;
-	if (postIdErr) return rej('invalid postId param');
+	// Get 'noteId' parameter
+	const [noteId, noteIdErr] = $(params.noteId).id().$;
+	if (noteIdErr) return rej('invalid noteId param');
 
 	// Get favoritee
-	const post = await Post.findOne({
-		_id: postId
+	const note = await Note.findOne({
+		_id: noteId
 	});
 
-	if (post === null) {
-		return rej('post not found');
+	if (note === null) {
+		return rej('note not found');
 	}
 
 	// if already favorited
 	const exist = await Favorite.findOne({
-		postId: post._id,
+		noteId: note._id,
 		userId: user._id
 	});
 
diff --git a/src/server/api/endpoints/posts/mentions.ts b/src/server/api/endpoints/notes/mentions.ts
similarity index 92%
rename from src/server/api/endpoints/posts/mentions.ts
rename to src/server/api/endpoints/notes/mentions.ts
index d7302c062..c507acbae 100644
--- a/src/server/api/endpoints/posts/mentions.ts
+++ b/src/server/api/endpoints/notes/mentions.ts
@@ -2,9 +2,9 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post from '../../../../models/post';
+import Note from '../../../../models/note';
 import getFriends from '../../common/get-friends';
-import { pack } from '../../../../models/post';
+import { pack } from '../../../../models/note';
 
 /**
  * Get mentions of myself
@@ -65,7 +65,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}
 
 	// Issue query
-	const mentions = await Post
+	const mentions = await Note
 		.find(query, {
 			limit: limit,
 			sort: sort
diff --git a/src/server/api/endpoints/posts/polls/recommendation.ts b/src/server/api/endpoints/notes/polls/recommendation.ts
similarity index 78%
rename from src/server/api/endpoints/posts/polls/recommendation.ts
rename to src/server/api/endpoints/notes/polls/recommendation.ts
index d70674261..cb530ea2c 100644
--- a/src/server/api/endpoints/posts/polls/recommendation.ts
+++ b/src/server/api/endpoints/notes/polls/recommendation.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import Vote from '../../../../../models/poll-vote';
-import Post, { pack } from '../../../../../models/post';
+import Note, { pack } from '../../../../../models/note';
 
 /**
  * Get recommended polls
@@ -27,13 +27,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}, {
 		fields: {
 			_id: false,
-			postId: true
+			noteId: true
 		}
 	});
 
-	const nin = votes && votes.length != 0 ? votes.map(v => v.postId) : [];
+	const nin = votes && votes.length != 0 ? votes.map(v => v.noteId) : [];
 
-	const posts = await Post
+	const notes = await Note
 		.find({
 			_id: {
 				$nin: nin
@@ -54,6 +54,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		});
 
 	// Serialize
-	res(await Promise.all(posts.map(async post =>
-		await pack(post, user, { detail: true }))));
+	res(await Promise.all(notes.map(async note =>
+		await pack(note, user, { detail: true }))));
 });
diff --git a/src/server/api/endpoints/posts/polls/vote.ts b/src/server/api/endpoints/notes/polls/vote.ts
similarity index 63%
rename from src/server/api/endpoints/posts/polls/vote.ts
rename to src/server/api/endpoints/notes/polls/vote.ts
index c270cd09a..0e27f87ee 100644
--- a/src/server/api/endpoints/posts/polls/vote.ts
+++ b/src/server/api/endpoints/notes/polls/vote.ts
@@ -3,47 +3,47 @@
  */
 import $ from 'cafy';
 import Vote from '../../../../../models/poll-vote';
-import Post from '../../../../../models/post';
-import Watching from '../../../../../models/post-watching';
-import watch from '../../../../../post/watch';
-import { publishPostStream } from '../../../../../publishers/stream';
+import Note from '../../../../../models/note';
+import Watching from '../../../../../models/note-watching';
+import watch from '../../../../../note/watch';
+import { publishNoteStream } from '../../../../../publishers/stream';
 import notify from '../../../../../publishers/notify';
 
 /**
- * Vote poll of a post
+ * Vote poll of a note
  *
  * @param {any} params
  * @param {any} user
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'postId' parameter
-	const [postId, postIdErr] = $(params.postId).id().$;
-	if (postIdErr) return rej('invalid postId param');
+	// Get 'noteId' parameter
+	const [noteId, noteIdErr] = $(params.noteId).id().$;
+	if (noteIdErr) return rej('invalid noteId param');
 
 	// Get votee
-	const post = await Post.findOne({
-		_id: postId
+	const note = await Note.findOne({
+		_id: noteId
 	});
 
-	if (post === null) {
-		return rej('post not found');
+	if (note === null) {
+		return rej('note not found');
 	}
 
-	if (post.poll == null) {
+	if (note.poll == null) {
 		return rej('poll not found');
 	}
 
 	// Get 'choice' parameter
 	const [choice, choiceError] =
 		$(params.choice).number()
-			.pipe(c => post.poll.choices.some(x => x.id == c))
+			.pipe(c => note.poll.choices.some(x => x.id == c))
 			.$;
 	if (choiceError) return rej('invalid choice param');
 
 	// if already voted
 	const exist = await Vote.findOne({
-		postId: post._id,
+		noteId: note._id,
 		userId: user._id
 	});
 
@@ -54,7 +54,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Create vote
 	await Vote.insert({
 		createdAt: new Date(),
-		postId: post._id,
+		noteId: note._id,
 		userId: user._id,
 		choice: choice
 	});
@@ -63,25 +63,25 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	res();
 
 	const inc = {};
-	inc[`poll.choices.${findWithAttr(post.poll.choices, 'id', choice)}.votes`] = 1;
+	inc[`poll.choices.${findWithAttr(note.poll.choices, 'id', choice)}.votes`] = 1;
 
 	// Increment votes count
-	await Post.update({ _id: post._id }, {
+	await Note.update({ _id: note._id }, {
 		$inc: inc
 	});
 
-	publishPostStream(post._id, 'poll_voted');
+	publishNoteStream(note._id, 'poll_voted');
 
 	// Notify
-	notify(post.userId, user._id, 'poll_vote', {
-		postId: post._id,
+	notify(note.userId, user._id, 'poll_vote', {
+		noteId: note._id,
 		choice: choice
 	});
 
 	// Fetch watchers
 	Watching
 		.find({
-			postId: post._id,
+			noteId: note._id,
 			userId: { $ne: user._id },
 			// 削除されたドキュメントは除く
 			deletedAt: { $exists: false }
@@ -93,7 +93,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		.then(watchers => {
 			watchers.forEach(watcher => {
 				notify(watcher.userId, user._id, 'poll_vote', {
-					postId: post._id,
+					noteId: note._id,
 					choice: choice
 				});
 			});
@@ -101,7 +101,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// この投稿をWatchする
 	if (user.account.settings.autoWatch !== false) {
-		watch(user._id, post);
+		watch(user._id, note);
 	}
 });
 
diff --git a/src/server/api/endpoints/posts/reactions.ts b/src/server/api/endpoints/notes/reactions.ts
similarity index 70%
rename from src/server/api/endpoints/posts/reactions.ts
rename to src/server/api/endpoints/notes/reactions.ts
index da733f533..bbff97bb0 100644
--- a/src/server/api/endpoints/posts/reactions.ts
+++ b/src/server/api/endpoints/notes/reactions.ts
@@ -2,20 +2,20 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post from '../../../../models/post';
-import Reaction, { pack } from '../../../../models/post-reaction';
+import Note from '../../../../models/note';
+import Reaction, { pack } from '../../../../models/note-reaction';
 
 /**
- * Show reactions of a post
+ * Show reactions of a note
  *
  * @param {any} params
  * @param {any} user
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'postId' parameter
-	const [postId, postIdErr] = $(params.postId).id().$;
-	if (postIdErr) return rej('invalid postId param');
+	// Get 'noteId' parameter
+	const [noteId, noteIdErr] = $(params.noteId).id().$;
+	if (noteIdErr) return rej('invalid noteId param');
 
 	// Get 'limit' parameter
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
@@ -29,19 +29,19 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [sort = 'desc', sortError] = $(params.sort).optional.string().or('desc asc').$;
 	if (sortError) return rej('invalid sort param');
 
-	// Lookup post
-	const post = await Post.findOne({
-		_id: postId
+	// Lookup note
+	const note = await Note.findOne({
+		_id: noteId
 	});
 
-	if (post === null) {
-		return rej('post not found');
+	if (note === null) {
+		return rej('note not found');
 	}
 
 	// Issue query
 	const reactions = await Reaction
 		.find({
-			postId: post._id,
+			noteId: note._id,
 			deletedAt: { $exists: false }
 		}, {
 			limit: limit,
diff --git a/src/server/api/endpoints/posts/reactions/create.ts b/src/server/api/endpoints/notes/reactions/create.ts
similarity index 50%
rename from src/server/api/endpoints/posts/reactions/create.ts
rename to src/server/api/endpoints/notes/reactions/create.ts
index 71fa6a295..ffb7bcc35 100644
--- a/src/server/api/endpoints/posts/reactions/create.ts
+++ b/src/server/api/endpoints/notes/reactions/create.ts
@@ -2,17 +2,17 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Reaction from '../../../../../models/post-reaction';
-import Post from '../../../../../models/post';
-import create from '../../../../../services/post/reaction/create';
+import Reaction from '../../../../../models/note-reaction';
+import Note from '../../../../../models/note';
+import create from '../../../../../services/note/reaction/create';
 
 /**
- * React to a post
+ * React to a note
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'postId' parameter
-	const [postId, postIdErr] = $(params.postId).id().$;
-	if (postIdErr) return rej('invalid postId param');
+	// Get 'noteId' parameter
+	const [noteId, noteIdErr] = $(params.noteId).id().$;
+	if (noteIdErr) return rej('invalid noteId param');
 
 	// Get 'reaction' parameter
 	const [reaction, reactionErr] = $(params.reaction).string().or([
@@ -29,16 +29,16 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	if (reactionErr) return rej('invalid reaction param');
 
 	// Fetch reactee
-	const post = await Post.findOne({
-		_id: postId
+	const note = await Note.findOne({
+		_id: noteId
 	});
 
-	if (post === null) {
-		return rej('post not found');
+	if (note === null) {
+		return rej('note not found');
 	}
 
 	try {
-		await create(user, post, reaction);
+		await create(user, note, reaction);
 	} catch (e) {
 		rej(e);
 	}
diff --git a/src/server/api/endpoints/posts/reactions/delete.ts b/src/server/api/endpoints/notes/reactions/delete.ts
similarity index 63%
rename from src/server/api/endpoints/posts/reactions/delete.ts
rename to src/server/api/endpoints/notes/reactions/delete.ts
index 3a88bbd7c..b5d738b8f 100644
--- a/src/server/api/endpoints/posts/reactions/delete.ts
+++ b/src/server/api/endpoints/notes/reactions/delete.ts
@@ -2,34 +2,34 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Reaction from '../../../../../models/post-reaction';
-import Post from '../../../../../models/post';
+import Reaction from '../../../../../models/note-reaction';
+import Note from '../../../../../models/note';
 // import event from '../../../publishers/stream';
 
 /**
- * Unreact to a post
+ * Unreact to a note
  *
  * @param {any} params
  * @param {any} user
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'postId' parameter
-	const [postId, postIdErr] = $(params.postId).id().$;
-	if (postIdErr) return rej('invalid postId param');
+	// Get 'noteId' parameter
+	const [noteId, noteIdErr] = $(params.noteId).id().$;
+	if (noteIdErr) return rej('invalid noteId param');
 
 	// Fetch unreactee
-	const post = await Post.findOne({
-		_id: postId
+	const note = await Note.findOne({
+		_id: noteId
 	});
 
-	if (post === null) {
-		return rej('post not found');
+	if (note === null) {
+		return rej('note not found');
 	}
 
 	// if already unreacted
 	const exist = await Reaction.findOne({
-		postId: post._id,
+		noteId: note._id,
 		userId: user._id,
 		deletedAt: { $exists: false }
 	});
@@ -54,7 +54,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	dec[`reactionCounts.${exist.reaction}`] = -1;
 
 	// Decrement reactions count
-	Post.update({ _id: post._id }, {
+	Note.update({ _id: note._id }, {
 		$inc: dec
 	});
 });
diff --git a/src/server/api/endpoints/posts/replies.ts b/src/server/api/endpoints/notes/replies.ts
similarity index 63%
rename from src/server/api/endpoints/posts/replies.ts
rename to src/server/api/endpoints/notes/replies.ts
index dd5a95c17..88d9ff329 100644
--- a/src/server/api/endpoints/posts/replies.ts
+++ b/src/server/api/endpoints/notes/replies.ts
@@ -2,19 +2,19 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post, { pack } from '../../../../models/post';
+import Note, { pack } from '../../../../models/note';
 
 /**
- * Show a replies of a post
+ * Show a replies of a note
  *
  * @param {any} params
  * @param {any} user
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'postId' parameter
-	const [postId, postIdErr] = $(params.postId).id().$;
-	if (postIdErr) return rej('invalid postId param');
+	// Get 'noteId' parameter
+	const [noteId, noteIdErr] = $(params.noteId).id().$;
+	if (noteIdErr) return rej('invalid noteId param');
 
 	// Get 'limit' parameter
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
@@ -28,18 +28,18 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [sort = 'desc', sortError] = $(params.sort).optional.string().or('desc asc').$;
 	if (sortError) return rej('invalid sort param');
 
-	// Lookup post
-	const post = await Post.findOne({
-		_id: postId
+	// Lookup note
+	const note = await Note.findOne({
+		_id: noteId
 	});
 
-	if (post === null) {
-		return rej('post not found');
+	if (note === null) {
+		return rej('note not found');
 	}
 
 	// Issue query
-	const replies = await Post
-		.find({ replyId: post._id }, {
+	const replies = await Note
+		.find({ replyId: note._id }, {
 			limit: limit,
 			skip: offset,
 			sort: {
@@ -48,6 +48,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		});
 
 	// Serialize
-	res(await Promise.all(replies.map(async post =>
-		await pack(post, user))));
+	res(await Promise.all(replies.map(async note =>
+		await pack(note, user))));
 });
diff --git a/src/server/api/endpoints/posts/reposts.ts b/src/server/api/endpoints/notes/reposts.ts
similarity index 70%
rename from src/server/api/endpoints/posts/reposts.ts
rename to src/server/api/endpoints/notes/reposts.ts
index ec6218ca3..9dfc2c3cb 100644
--- a/src/server/api/endpoints/posts/reposts.ts
+++ b/src/server/api/endpoints/notes/reposts.ts
@@ -2,19 +2,19 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post, { pack } from '../../../../models/post';
+import Note, { pack } from '../../../../models/note';
 
 /**
- * Show a reposts of a post
+ * Show a renotes of a note
  *
  * @param {any} params
  * @param {any} user
  * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'postId' parameter
-	const [postId, postIdErr] = $(params.postId).id().$;
-	if (postIdErr) return rej('invalid postId param');
+	// Get 'noteId' parameter
+	const [noteId, noteIdErr] = $(params.noteId).id().$;
+	if (noteIdErr) return rej('invalid noteId param');
 
 	// Get 'limit' parameter
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
@@ -33,13 +33,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		return rej('cannot set sinceId and untilId');
 	}
 
-	// Lookup post
-	const post = await Post.findOne({
-		_id: postId
+	// Lookup note
+	const note = await Note.findOne({
+		_id: noteId
 	});
 
-	if (post === null) {
-		return rej('post not found');
+	if (note === null) {
+		return rej('note not found');
 	}
 
 	// Construct query
@@ -47,7 +47,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		_id: -1
 	};
 	const query = {
-		repostId: post._id
+		renoteId: note._id
 	} as any;
 	if (sinceId) {
 		sort._id = 1;
@@ -61,13 +61,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}
 
 	// Issue query
-	const reposts = await Post
+	const renotes = await Note
 		.find(query, {
 			limit: limit,
 			sort: sort
 		});
 
 	// Serialize
-	res(await Promise.all(reposts.map(async post =>
-		await pack(post, user))));
+	res(await Promise.all(renotes.map(async note =>
+		await pack(note, user))));
 });
diff --git a/src/server/api/endpoints/posts/search.ts b/src/server/api/endpoints/notes/search.ts
similarity index 91%
rename from src/server/api/endpoints/posts/search.ts
rename to src/server/api/endpoints/notes/search.ts
index 21c4e77fd..bfa17b000 100644
--- a/src/server/api/endpoints/posts/search.ts
+++ b/src/server/api/endpoints/notes/search.ts
@@ -3,14 +3,14 @@
  */
 import $ from 'cafy';
 const escapeRegexp = require('escape-regexp');
-import Post from '../../../../models/post';
+import Note from '../../../../models/note';
 import User from '../../../../models/user';
 import Mute from '../../../../models/mute';
 import getFriends from '../../common/get-friends';
-import { pack } from '../../../../models/post';
+import { pack } from '../../../../models/note';
 
 /**
- * Search a post
+ * Search a note
  *
  * @param {any} params
  * @param {any} me
@@ -49,9 +49,9 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	const [reply = null, replyErr] = $(params.reply).optional.nullable.boolean().$;
 	if (replyErr) return rej('invalid reply param');
 
-	// Get 'repost' parameter
-	const [repost = null, repostErr] = $(params.repost).optional.nullable.boolean().$;
-	if (repostErr) return rej('invalid repost param');
+	// Get 'renote' parameter
+	const [renote = null, renoteErr] = $(params.renote).optional.nullable.boolean().$;
+	if (renoteErr) return rej('invalid renote param');
 
 	// Get 'media' parameter
 	const [media = null, mediaErr] = $(params.media).optional.nullable.boolean().$;
@@ -100,12 +100,12 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	}
 
 	search(res, rej, me, text, includeUsers, excludeUsers, following,
-			mute, reply, repost, media, poll, sinceDate, untilDate, offset, limit);
+			mute, reply, renote, media, poll, sinceDate, untilDate, offset, limit);
 });
 
 async function search(
 	res, rej, me, text, includeUserIds, excludeUserIds, following,
-	mute, reply, repost, media, poll, sinceDate, untilDate, offset, max) {
+	mute, reply, renote, media, poll, sinceDate, untilDate, offset, max) {
 
 	let q: any = {
 		$and: []
@@ -182,7 +182,7 @@ async function search(
 					'_reply.userId': {
 						$nin: mutedUserIds
 					},
-					'_repost.userId': {
+					'_renote.userId': {
 						$nin: mutedUserIds
 					}
 				});
@@ -192,7 +192,7 @@ async function search(
 					'_reply.userId': {
 						$nin: mutedUserIds
 					},
-					'_repost.userId': {
+					'_renote.userId': {
 						$nin: mutedUserIds
 					}
 				});
@@ -218,7 +218,7 @@ async function search(
 							$in: mutedUserIds
 						}
 					}, {
-						'_repost.userId': {
+						'_renote.userId': {
 							$in: mutedUserIds
 						}
 					}]
@@ -235,7 +235,7 @@ async function search(
 							$in: mutedUserIds
 						}
 					}, {
-						'_repost.userId': {
+						'_renote.userId': {
 							$in: mutedUserIds
 						}
 					}]
@@ -265,10 +265,10 @@ async function search(
 		}
 	}
 
-	if (repost != null) {
-		if (repost) {
+	if (renote != null) {
+		if (renote) {
 			push({
-				repostId: {
+				renoteId: {
 					$exists: true,
 					$ne: null
 				}
@@ -276,11 +276,11 @@ async function search(
 		} else {
 			push({
 				$or: [{
-					repostId: {
+					renoteId: {
 						$exists: false
 					}
 				}, {
-					repostId: null
+					renoteId: null
 				}]
 			});
 		}
@@ -348,8 +348,8 @@ async function search(
 		q = {};
 	}
 
-	// Search posts
-	const posts = await Post
+	// Search notes
+	const notes = await Note
 		.find(q, {
 			sort: {
 				_id: -1
@@ -359,6 +359,6 @@ async function search(
 		});
 
 	// Serialize
-	res(await Promise.all(posts.map(async post =>
-		await pack(post, me))));
+	res(await Promise.all(notes.map(async note =>
+		await pack(note, me))));
 }
diff --git a/src/server/api/endpoints/notes/show.ts b/src/server/api/endpoints/notes/show.ts
new file mode 100644
index 000000000..67cdc3038
--- /dev/null
+++ b/src/server/api/endpoints/notes/show.ts
@@ -0,0 +1,32 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import Note, { pack } from '../../../../models/note';
+
+/**
+ * Show a note
+ *
+ * @param {any} params
+ * @param {any} user
+ * @return {Promise<any>}
+ */
+module.exports = (params, user) => new Promise(async (res, rej) => {
+	// Get 'noteId' parameter
+	const [noteId, noteIdErr] = $(params.noteId).id().$;
+	if (noteIdErr) return rej('invalid noteId param');
+
+	// Get note
+	const note = await Note.findOne({
+		_id: noteId
+	});
+
+	if (note === null) {
+		return rej('note not found');
+	}
+
+	// Serialize
+	res(await pack(note, user, {
+		detail: true
+	}));
+});
diff --git a/src/server/api/endpoints/posts/timeline.ts b/src/server/api/endpoints/notes/timeline.ts
similarity index 93%
rename from src/server/api/endpoints/posts/timeline.ts
rename to src/server/api/endpoints/notes/timeline.ts
index b58d25fa8..5263cfb2a 100644
--- a/src/server/api/endpoints/posts/timeline.ts
+++ b/src/server/api/endpoints/notes/timeline.ts
@@ -3,11 +3,11 @@
  */
 import $ from 'cafy';
 import rap from '@prezzemolo/rap';
-import Post from '../../../../models/post';
+import Note from '../../../../models/note';
 import Mute from '../../../../models/mute';
 import ChannelWatching from '../../../../models/channel-watching';
 import getFriends from '../../common/get-friends';
-import { pack } from '../../../../models/post';
+import { pack } from '../../../../models/note';
 
 /**
  * Get timeline of myself
@@ -94,7 +94,7 @@ module.exports = async (params, user, app) => {
 		'_reply.userId': {
 			$nin: mutedUserIds
 		},
-		'_repost.userId': {
+		'_renote.userId': {
 			$nin: mutedUserIds
 		},
 	} as any;
@@ -121,12 +121,12 @@ module.exports = async (params, user, app) => {
 	//#endregion
 
 	// Issue query
-	const timeline = await Post
+	const timeline = await Note
 		.find(query, {
 			limit: limit,
 			sort: sort
 		});
 
 	// Serialize
-	return await Promise.all(timeline.map(post => pack(post, user)));
+	return await Promise.all(timeline.map(note => pack(note, user)));
 };
diff --git a/src/server/api/endpoints/posts/trend.ts b/src/server/api/endpoints/notes/trend.ts
similarity index 76%
rename from src/server/api/endpoints/posts/trend.ts
rename to src/server/api/endpoints/notes/trend.ts
index dbee16913..48ecd5b84 100644
--- a/src/server/api/endpoints/posts/trend.ts
+++ b/src/server/api/endpoints/notes/trend.ts
@@ -3,10 +3,10 @@
  */
 const ms = require('ms');
 import $ from 'cafy';
-import Post, { pack } from '../../../../models/post';
+import Note, { pack } from '../../../../models/note';
 
 /**
- * Get trend posts
+ * Get trend notes
  *
  * @param {any} params
  * @param {any} user
@@ -25,9 +25,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [reply, replyErr] = $(params.reply).optional.boolean().$;
 	if (replyErr) return rej('invalid reply param');
 
-	// Get 'repost' parameter
-	const [repost, repostErr] = $(params.repost).optional.boolean().$;
-	if (repostErr) return rej('invalid repost param');
+	// Get 'renote' parameter
+	const [renote, renoteErr] = $(params.renote).optional.boolean().$;
+	if (renoteErr) return rej('invalid renote param');
 
 	// Get 'media' parameter
 	const [media, mediaErr] = $(params.media).optional.boolean().$;
@@ -41,7 +41,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		createdAt: {
 			$gte: new Date(Date.now() - ms('1days'))
 		},
-		repostCount: {
+		renoteCount: {
 			$gt: 0
 		}
 	} as any;
@@ -50,8 +50,8 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		query.replyId = reply ? { $exists: true, $ne: null } : null;
 	}
 
-	if (repost != undefined) {
-		query.repostId = repost ? { $exists: true, $ne: null } : null;
+	if (renote != undefined) {
+		query.renoteId = renote ? { $exists: true, $ne: null } : null;
 	}
 
 	if (media != undefined) {
@@ -63,17 +63,17 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}
 
 	// Issue query
-	const posts = await Post
+	const notes = await Note
 		.find(query, {
 			limit: limit,
 			skip: offset,
 			sort: {
-				repostCount: -1,
+				renoteCount: -1,
 				_id: -1
 			}
 		});
 
 	// Serialize
-	res(await Promise.all(posts.map(async post =>
-		await pack(post, user, { detail: true }))));
+	res(await Promise.all(notes.map(async note =>
+		await pack(note, user, { detail: true }))));
 });
diff --git a/src/server/api/endpoints/posts.ts b/src/server/api/endpoints/posts.ts
index 7af8cff67..3e3b67a66 100644
--- a/src/server/api/endpoints/posts.ts
+++ b/src/server/api/endpoints/posts.ts
@@ -2,10 +2,10 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post, { pack } from '../../../models/post';
+import Note, { pack } from '../../../models/note';
 
 /**
- * Lists all posts
+ * Lists all notes
  *
  * @param {any} params
  * @return {Promise<any>}
@@ -15,9 +15,9 @@ module.exports = (params) => new Promise(async (res, rej) => {
 	const [reply, replyErr] = $(params.reply).optional.boolean().$;
 	if (replyErr) return rej('invalid reply param');
 
-	// Get 'repost' parameter
-	const [repost, repostErr] = $(params.repost).optional.boolean().$;
-	if (repostErr) return rej('invalid repost param');
+	// Get 'renote' parameter
+	const [renote, renoteErr] = $(params.renote).optional.boolean().$;
+	if (renoteErr) return rej('invalid renote param');
 
 	// Get 'media' parameter
 	const [media, mediaErr] = $(params.media).optional.boolean().$;
@@ -68,8 +68,8 @@ module.exports = (params) => new Promise(async (res, rej) => {
 		query.replyId = reply ? { $exists: true, $ne: null } : null;
 	}
 
-	if (repost != undefined) {
-		query.repostId = repost ? { $exists: true, $ne: null } : null;
+	if (renote != undefined) {
+		query.renoteId = renote ? { $exists: true, $ne: null } : null;
 	}
 
 	if (media != undefined) {
@@ -86,12 +86,12 @@ module.exports = (params) => new Promise(async (res, rej) => {
 	//}
 
 	// Issue query
-	const posts = await Post
+	const notes = await Note
 		.find(query, {
 			limit: limit,
 			sort: sort
 		});
 
 	// Serialize
-	res(await Promise.all(posts.map(async post => await pack(post))));
+	res(await Promise.all(notes.map(async note => await pack(note))));
 });
diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts
index 003a892bc..7e79912b1 100644
--- a/src/server/api/endpoints/posts/create.ts
+++ b/src/server/api/endpoints/posts/create.ts
@@ -3,15 +3,15 @@
  */
 import $ from 'cafy';
 import deepEqual = require('deep-equal');
-import Post, { IPost, isValidText, isValidCw, pack } from '../../../../models/post';
+import Note, { INote, isValidText, isValidCw, pack } from '../../../../models/note';
 import { ILocalUser } from '../../../../models/user';
 import Channel, { IChannel } from '../../../../models/channel';
 import DriveFile from '../../../../models/drive-file';
-import create from '../../../../services/post/create';
+import create from '../../../../services/note/create';
 import { IApp } from '../../../../models/app';
 
 /**
- * Create a post
+ * Create a note
  *
  * @param {any} params
  * @param {any} user
@@ -79,26 +79,26 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res
 		files = null;
 	}
 
-	// Get 'repostId' parameter
-	const [repostId, repostIdErr] = $(params.repostId).optional.id().$;
-	if (repostIdErr) return rej('invalid repostId');
+	// Get 'renoteId' parameter
+	const [renoteId, renoteIdErr] = $(params.renoteId).optional.id().$;
+	if (renoteIdErr) return rej('invalid renoteId');
 
-	let repost: IPost = null;
+	let renote: INote = null;
 	let isQuote = false;
-	if (repostId !== undefined) {
-		// Fetch repost to post
-		repost = await Post.findOne({
-			_id: repostId
+	if (renoteId !== undefined) {
+		// Fetch renote to note
+		renote = await Note.findOne({
+			_id: renoteId
 		});
 
-		if (repost == null) {
-			return rej('repostee is not found');
-		} else if (repost.repostId && !repost.text && !repost.mediaIds) {
-			return rej('cannot repost to repost');
+		if (renote == null) {
+			return rej('renoteee is not found');
+		} else if (renote.renoteId && !renote.text && !renote.mediaIds) {
+			return rej('cannot renote to renote');
 		}
 
-		// Fetch recently post
-		const latestPost = await Post.findOne({
+		// Fetch recently note
+		const latestNote = await Note.findOne({
 			userId: user._id
 		}, {
 			sort: {
@@ -108,19 +108,19 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res
 
 		isQuote = text != null || files != null;
 
-		// 直近と同じRepost対象かつ引用じゃなかったらエラー
-		if (latestPost &&
-			latestPost.repostId &&
-			latestPost.repostId.equals(repost._id) &&
+		// 直近と同じRenote対象かつ引用じゃなかったらエラー
+		if (latestNote &&
+			latestNote.renoteId &&
+			latestNote.renoteId.equals(renote._id) &&
 			!isQuote) {
-			return rej('cannot repost same post that already reposted in your latest post');
+			return rej('cannot renote same note that already reposted in your latest note');
 		}
 
-		// 直近がRepost対象かつ引用じゃなかったらエラー
-		if (latestPost &&
-			latestPost._id.equals(repost._id) &&
+		// 直近がRenote対象かつ引用じゃなかったらエラー
+		if (latestNote &&
+			latestNote._id.equals(renote._id) &&
 			!isQuote) {
-			return rej('cannot repost your latest post');
+			return rej('cannot renote your latest note');
 		}
 	}
 
@@ -128,20 +128,20 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res
 	const [replyId, replyIdErr] = $(params.replyId).optional.id().$;
 	if (replyIdErr) return rej('invalid replyId');
 
-	let reply: IPost = null;
+	let reply: INote = null;
 	if (replyId !== undefined) {
 		// Fetch reply
-		reply = await Post.findOne({
+		reply = await Note.findOne({
 			_id: replyId
 		});
 
 		if (reply === null) {
-			return rej('in reply to post is not found');
+			return rej('in reply to note is not found');
 		}
 
-		// 返信対象が引用でないRepostだったらエラー
-		if (reply.repostId && !reply.text && !reply.mediaIds) {
-			return rej('cannot reply to repost');
+		// 返信対象が引用でないRenoteだったらエラー
+		if (reply.renoteId && !reply.text && !reply.mediaIds) {
+			return rej('cannot reply to renote');
 		}
 	}
 
@@ -165,14 +165,14 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res
 			return rej('チャンネル内部からチャンネル外部の投稿に返信することはできません');
 		}
 
-		// Repost対象の投稿がこのチャンネルじゃなかったらダメ
-		if (repost && !channelId.equals(repost.channelId)) {
-			return rej('チャンネル内部からチャンネル外部の投稿をRepostすることはできません');
+		// Renote対象の投稿がこのチャンネルじゃなかったらダメ
+		if (renote && !channelId.equals(renote.channelId)) {
+			return rej('チャンネル内部からチャンネル外部の投稿をRenoteすることはできません');
 		}
 
-		// 引用ではないRepostはダメ
-		if (repost && !isQuote) {
-			return rej('チャンネル内部では引用ではないRepostをすることはできません');
+		// 引用ではないRenoteはダメ
+		if (renote && !isQuote) {
+			return rej('チャンネル内部では引用ではないRenoteをすることはできません');
 		}
 	} else {
 		// 返信対象の投稿がチャンネルへの投稿だったらダメ
@@ -180,9 +180,9 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res
 			return rej('チャンネル外部からチャンネル内部の投稿に返信することはできません');
 		}
 
-		// Repost対象の投稿がチャンネルへの投稿だったらダメ
-		if (repost && repost.channelId != null) {
-			return rej('チャンネル外部からチャンネル内部の投稿をRepostすることはできません');
+		// Renote対象の投稿がチャンネルへの投稿だったらダメ
+		if (renote && renote.channelId != null) {
+			return rej('チャンネル外部からチャンネル内部の投稿をRenoteすることはできません');
 		}
 	}
 
@@ -203,23 +203,23 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res
 		}));
 	}
 
-	// テキストが無いかつ添付ファイルが無いかつRepostも無いかつ投票も無かったらエラー
-	if (text === undefined && files === null && repost === null && poll === undefined) {
-		return rej('text, mediaIds, repostId or poll is required');
+	// テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー
+	if (text === undefined && files === null && renote === null && poll === undefined) {
+		return rej('text, mediaIds, renoteId or poll is required');
 	}
 
 	// 直近の投稿と重複してたらエラー
 	// TODO: 直近の投稿が一日前くらいなら重複とは見なさない
-	if (user.latestPost) {
+	if (user.latestNote) {
 		if (deepEqual({
-			text: user.latestPost.text,
-			reply: user.latestPost.replyId ? user.latestPost.replyId.toString() : null,
-			repost: user.latestPost.repostId ? user.latestPost.repostId.toString() : null,
-			mediaIds: (user.latestPost.mediaIds || []).map(id => id.toString())
+			text: user.latestNote.text,
+			reply: user.latestNote.replyId ? user.latestNote.replyId.toString() : null,
+			renote: user.latestNote.renoteId ? user.latestNote.renoteId.toString() : null,
+			mediaIds: (user.latestNote.mediaIds || []).map(id => id.toString())
 		}, {
 			text: text,
 			reply: reply ? reply._id.toString() : null,
-			repost: repost ? repost._id.toString() : null,
+			renote: renote ? renote._id.toString() : null,
 			mediaIds: (files || []).map(file => file._id.toString())
 		})) {
 			return rej('duplicate');
@@ -227,13 +227,13 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res
 	}
 
 	// 投稿を作成
-	const post = await create(user, {
+	const note = await create(user, {
 		createdAt: new Date(),
 		media: files,
 		poll: poll,
 		text: text,
 		reply,
-		repost,
+		renote,
 		cw: cw,
 		tags: tags,
 		app: app,
@@ -242,10 +242,10 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res
 		geo
 	});
 
-	const postObj = await pack(post, user);
+	const noteObj = await pack(note, user);
 
 	// Reponse
 	res({
-		createdPost: postObj
+		createdNote: noteObj
 	});
 });
diff --git a/src/server/api/endpoints/posts/show.ts b/src/server/api/endpoints/posts/show.ts
deleted file mode 100644
index e1781b545..000000000
--- a/src/server/api/endpoints/posts/show.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * Module dependencies
- */
-import $ from 'cafy';
-import Post, { pack } from '../../../../models/post';
-
-/**
- * Show a post
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
- */
-module.exports = (params, user) => new Promise(async (res, rej) => {
-	// Get 'postId' parameter
-	const [postId, postIdErr] = $(params.postId).id().$;
-	if (postIdErr) return rej('invalid postId param');
-
-	// Get post
-	const post = await Post.findOne({
-		_id: postId
-	});
-
-	if (post === null) {
-		return rej('post not found');
-	}
-
-	// Serialize
-	res(await pack(post, user, {
-		detail: true
-	}));
-});
diff --git a/src/server/api/endpoints/stats.ts b/src/server/api/endpoints/stats.ts
index 0fb0c44b0..52e519548 100644
--- a/src/server/api/endpoints/stats.ts
+++ b/src/server/api/endpoints/stats.ts
@@ -1,13 +1,13 @@
 /**
  * Module dependencies
  */
-import Post from '../../../models/post';
+import Note from '../../../models/note';
 import User from '../../../models/user';
 
 /**
  * @swagger
  * /stats:
- *   post:
+ *   note:
  *     summary: Show the misskey's statistics
  *     responses:
  *       200:
@@ -15,8 +15,8 @@ import User from '../../../models/user';
  *         schema:
  *           type: object
  *           properties:
- *             postsCount:
- *               description: count of all posts of misskey
+ *             notesCount:
+ *               description: count of all notes of misskey
  *               type: number
  *             usersCount:
  *               description: count of all users of misskey
@@ -35,14 +35,14 @@ import User from '../../../models/user';
  * @return {Promise<any>}
  */
 module.exports = params => new Promise(async (res, rej) => {
-	const postsCount = await Post
+	const notesCount = await Note
 		.count();
 
 	const usersCount = await User
 		.count();
 
 	res({
-		postsCount: postsCount,
+		notesCount: notesCount,
 		usersCount: usersCount
 	});
 });
diff --git a/src/server/api/endpoints/users/get_frequently_replied_users.ts b/src/server/api/endpoints/users/get_frequently_replied_users.ts
index 3a116c8e2..7a98f44e9 100644
--- a/src/server/api/endpoints/users/get_frequently_replied_users.ts
+++ b/src/server/api/endpoints/users/get_frequently_replied_users.ts
@@ -2,7 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Post from '../../../../models/post';
+import Note from '../../../../models/note';
 import User, { pack } from '../../../../models/user';
 
 module.exports = (params, me) => new Promise(async (res, rej) => {
@@ -27,8 +27,8 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 		return rej('user not found');
 	}
 
-	// Fetch recent posts
-	const recentPosts = await Post.find({
+	// Fetch recent notes
+	const recentNotes = await Note.find({
 		userId: user._id,
 		replyId: {
 			$exists: true,
@@ -46,13 +46,13 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	});
 
 	// 投稿が少なかったら中断
-	if (recentPosts.length === 0) {
+	if (recentNotes.length === 0) {
 		return res([]);
 	}
 
-	const replyTargetPosts = await Post.find({
+	const replyTargetNotes = await Note.find({
 		_id: {
-			$in: recentPosts.map(p => p.replyId)
+			$in: recentNotes.map(p => p.replyId)
 		},
 		userId: {
 			$ne: user._id
@@ -66,9 +66,9 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	const repliedUsers = {};
 
-	// Extract replies from recent posts
-	replyTargetPosts.forEach(post => {
-		const userId = post.userId.toString();
+	// Extract replies from recent notes
+	replyTargetNotes.forEach(note => {
+		const userId = note.userId.toString();
 		if (repliedUsers[userId]) {
 			repliedUsers[userId]++;
 		} else {
diff --git a/src/server/api/endpoints/users/posts.ts b/src/server/api/endpoints/users/posts.ts
index b6c533fb5..f9f6345e3 100644
--- a/src/server/api/endpoints/users/posts.ts
+++ b/src/server/api/endpoints/users/posts.ts
@@ -3,11 +3,11 @@
  */
 import $ from 'cafy';
 import getHostLower from '../../common/get-host-lower';
-import Post, { pack } from '../../../../models/post';
+import Note, { pack } from '../../../../models/note';
 import User from '../../../../models/user';
 
 /**
- * Get posts of a user
+ * Get notes of a user
  *
  * @param {any} params
  * @param {any} me
@@ -124,14 +124,14 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	//#endregion
 
 	// Issue query
-	const posts = await Post
+	const notes = await Note
 		.find(query, {
 			limit: limit,
 			sort: sort
 		});
 
 	// Serialize
-	res(await Promise.all(posts.map(async (post) =>
-		await pack(post, me)
+	res(await Promise.all(notes.map(async (note) =>
+		await pack(note, me)
 	)));
 });
diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts
index c54d6f1a1..5818ba25c 100644
--- a/src/server/api/private/signup.ts
+++ b/src/server/api/private/signup.ts
@@ -113,7 +113,7 @@ export default async (req: express.Request, res: express.Response) => {
 		followersCount: 0,
 		followingCount: 0,
 		name: null,
-		postsCount: 0,
+		notesCount: 0,
 		driveCapacity: 1024 * 1024 * 128, // 128MiB
 		username: username,
 		usernameLower: username.toLowerCase(),
diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts
index 5fc4a92f5..6a327f1f7 100644
--- a/src/server/api/service/github.ts
+++ b/src/server/api/service/github.ts
@@ -18,7 +18,7 @@ module.exports = async (app: express.Application) => {
 		return;
 	}
 
-	const post = text => require('../endpoints/posts/create')({ text }, bot);
+	const post = text => require('../endpoints/notes/create')({ text }, bot);
 
 	const handler = new EventEmitter();
 
diff --git a/src/server/api/stream/home.ts b/src/server/api/stream/home.ts
index 648bd7c3c..313558851 100644
--- a/src/server/api/stream/home.ts
+++ b/src/server/api/stream/home.ts
@@ -4,7 +4,7 @@ import * as debug from 'debug';
 
 import User from '../../../models/user';
 import Mute from '../../../models/mute';
-import { pack as packPost } from '../../../models/post';
+import { pack as packNote } from '../../../models/note';
 import readNotification from '../common/read-notification';
 
 const log = debug('misskey');
@@ -25,14 +25,14 @@ export default async function(request: websocket.request, connection: websocket.
 				try {
 					const x = JSON.parse(data);
 
-					if (x.type == 'post') {
+					if (x.type == 'note') {
 						if (mutedUserIds.indexOf(x.body.userId) != -1) {
 							return;
 						}
 						if (x.body.reply != null && mutedUserIds.indexOf(x.body.reply.userId) != -1) {
 							return;
 						}
-						if (x.body.repost != null && mutedUserIds.indexOf(x.body.repost.userId) != -1) {
+						if (x.body.renote != null && mutedUserIds.indexOf(x.body.renote.userId) != -1) {
 							return;
 						}
 					} else if (x.type == 'notification') {
@@ -46,16 +46,16 @@ export default async function(request: websocket.request, connection: websocket.
 					connection.send(data);
 				}
 				break;
-			case 'post-stream':
-				const postId = channel.split(':')[2];
-				log(`RECEIVED: ${postId} ${data} by @${user.username}`);
-				const post = await packPost(postId, user, {
+			case 'note-stream':
+				const noteId = channel.split(':')[2];
+				log(`RECEIVED: ${noteId} ${data} by @${user.username}`);
+				const note = await packNote(noteId, user, {
 					detail: true
 				});
 				connection.send(JSON.stringify({
-					type: 'post-updated',
+					type: 'note-updated',
 					body: {
-						post: post
+						note: note
 					}
 				}));
 				break;
@@ -86,9 +86,9 @@ export default async function(request: websocket.request, connection: websocket.
 
 			case 'capture':
 				if (!msg.id) return;
-				const postId = msg.id;
-				log(`CAPTURE: ${postId} by @${user.username}`);
-				subscriber.subscribe(`misskey:post-stream:${postId}`);
+				const noteId = msg.id;
+				log(`CAPTURE: ${noteId} by @${user.username}`);
+				subscriber.subscribe(`misskey:note-stream:${noteId}`);
 				break;
 		}
 	});
diff --git a/src/services/post/create.ts b/src/services/note/create.ts
similarity index 75%
rename from src/services/post/create.ts
rename to src/services/note/create.ts
index 745683b51..8eee8f44a 100644
--- a/src/services/post/create.ts
+++ b/src/services/note/create.ts
@@ -1,4 +1,4 @@
-import Post, { pack, IPost } from '../../models/post';
+import Note, { pack, INote } from '../../models/note';
 import User, { isLocalUser, IUser, isRemoteUser } from '../../models/user';
 import stream from '../../publishers/stream';
 import Following from '../../models/following';
@@ -8,7 +8,7 @@ import renderCreate from '../../remote/activitypub/renderer/create';
 import context from '../../remote/activitypub/renderer/context';
 import { IDriveFile } from '../../models/drive-file';
 import notify from '../../publishers/notify';
-import PostWatching from '../../models/post-watching';
+import NoteWatching from '../../models/note-watching';
 import watch from './watch';
 import Mute from '../../models/mute';
 import pushSw from '../../publishers/push-sw';
@@ -20,8 +20,8 @@ import { IApp } from '../../models/app';
 export default async (user: IUser, data: {
 	createdAt?: Date;
 	text?: string;
-	reply?: IPost;
-	repost?: IPost;
+	reply?: INote;
+	renote?: INote;
 	media?: IDriveFile[];
 	geo?: any;
 	poll?: any;
@@ -31,7 +31,7 @@ export default async (user: IUser, data: {
 	visibility?: string;
 	uri?: string;
 	app?: IApp;
-}, silent = false) => new Promise<IPost>(async (res, rej) => {
+}, silent = false) => new Promise<INote>(async (res, rej) => {
 	if (data.createdAt == null) data.createdAt = new Date();
 	if (data.visibility == null) data.visibility = 'public';
 
@@ -59,7 +59,7 @@ export default async (user: IUser, data: {
 		createdAt: data.createdAt,
 		mediaIds: data.media ? data.media.map(file => file._id) : [],
 		replyId: data.reply ? data.reply._id : null,
-		repostId: data.repost ? data.repost._id : null,
+		renoteId: data.renote ? data.renote._id : null,
 		text: data.text,
 		textHtml: tokens === null ? null : html(tokens),
 		poll: data.poll,
@@ -73,7 +73,7 @@ export default async (user: IUser, data: {
 
 		// 以下非正規化データ
 		_reply: data.reply ? { userId: data.reply.userId } : null,
-		_repost: data.repost ? { userId: data.repost.userId } : null,
+		_renote: data.renote ? { userId: data.renote.userId } : null,
 		_user: {
 			host: user.host,
 			hostLower: user.hostLower,
@@ -86,29 +86,29 @@ export default async (user: IUser, data: {
 	if (data.uri != null) insert.uri = data.uri;
 
 	// 投稿を作成
-	const post = await Post.insert(insert);
+	const note = await Note.insert(insert);
 
-	res(post);
+	res(note);
 
 	User.update({ _id: user._id }, {
-		// Increment posts count
+		// Increment notes count
 		$inc: {
-			postsCount: 1
+			notesCount: 1
 		},
-		// Update latest post
+		// Update latest note
 		$set: {
-			latestPost: post
+			latestNote: note
 		}
 	});
 
 	// Serialize
-	const postObj = await pack(post);
+	const noteObj = await pack(note);
 
 	// タイムラインへの投稿
-	if (post.channelId == null) {
+	if (note.channelId == null) {
 		// Publish event to myself's stream
 		if (isLocalUser(user)) {
-			stream(post.userId, 'post', postObj);
+			stream(note.userId, 'note', noteObj);
 		}
 
 		// Fetch all followers
@@ -121,15 +121,14 @@ export default async (user: IUser, data: {
 			}
 		}, {
 			$match: {
-				followeeId: post.userId
+				followeeId: note.userId
 			}
 		}], {
 			_id: false
 		});
 
 		if (!silent) {
-			const note = await renderNote(user, post);
-			const content = renderCreate(note);
+			const content = renderCreate(await renderNote(user, note));
 			content['@context'] = context;
 
 			// 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送
@@ -142,7 +141,7 @@ export default async (user: IUser, data: {
 
 				if (isLocalUser(follower)) {
 					// Publish event to followers stream
-					stream(follower._id, 'post', postObj);
+					stream(follower._id, 'note', noteObj);
 				} else {
 					// フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信
 					if (isLocalUser(user)) {
@@ -155,33 +154,33 @@ export default async (user: IUser, data: {
 
 	// チャンネルへの投稿
 	/* TODO
-	if (post.channelId) {
+	if (note.channelId) {
 		promises.push(
-			// Increment channel index(posts count)
-			Channel.update({ _id: post.channelId }, {
+			// Increment channel index(notes count)
+			Channel.update({ _id: note.channelId }, {
 				$inc: {
 					index: 1
 				}
 			}),
 
 			// Publish event to channel
-			promisedPostObj.then(postObj => {
-				publishChannelStream(post.channelId, 'post', postObj);
+			promisedNoteObj.then(noteObj => {
+				publishChannelStream(note.channelId, 'note', noteObj);
 			}),
 
 			Promise.all([
-				promisedPostObj,
+				promisedNoteObj,
 
 				// Get channel watchers
 				ChannelWatching.find({
-					channelId: post.channelId,
+					channelId: note.channelId,
 					// 削除されたドキュメントは除く
 					deletedAt: { $exists: false }
 				})
-			]).then(([postObj, watches]) => {
+			]).then(([noteObj, watches]) => {
 				// チャンネルの視聴者(のタイムライン)に配信
 				watches.forEach(w => {
-					stream(w.userId, 'post', postObj);
+					stream(w.userId, 'note', noteObj);
 				});
 			})
 		);
@@ -204,16 +203,16 @@ export default async (user: IUser, data: {
 			});
 			const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString());
 			if (mentioneesMutedUserIds.indexOf(user._id.toString()) == -1) {
-				event(mentionee, reason, postObj);
-				pushSw(mentionee, reason, postObj);
+				event(mentionee, reason, noteObj);
+				pushSw(mentionee, reason, noteObj);
 			}
 		}
 	}
 
-	// If has in reply to post
+	// If has in reply to note
 	if (data.reply) {
 		// Increment replies count
-		Post.update({ _id: data.reply._id }, {
+		Note.update({ _id: data.reply._id }, {
 			$inc: {
 				repliesCount: 1
 			}
@@ -221,12 +220,12 @@ export default async (user: IUser, data: {
 
 		// (自分自身へのリプライでない限りは)通知を作成
 		notify(data.reply.userId, user._id, 'reply', {
-			postId: post._id
+			noteId: note._id
 		});
 
 		// Fetch watchers
-		PostWatching.find({
-			postId: data.reply._id,
+		NoteWatching.find({
+			noteId: data.reply._id,
 			userId: { $ne: user._id },
 			// 削除されたドキュメントは除く
 			deletedAt: { $exists: false }
@@ -237,7 +236,7 @@ export default async (user: IUser, data: {
 		}).then(watchers => {
 			watchers.forEach(watcher => {
 				notify(watcher.userId, user._id, 'reply', {
-					postId: post._id
+					noteId: note._id
 				});
 			});
 		});
@@ -251,17 +250,17 @@ export default async (user: IUser, data: {
 		addMention(data.reply.userId, 'reply');
 	}
 
-	// If it is repost
-	if (data.repost) {
+	// If it is renote
+	if (data.renote) {
 		// Notify
-		const type = data.text ? 'quote' : 'repost';
-		notify(data.repost.userId, user._id, type, {
-			post_id: post._id
+		const type = data.text ? 'quote' : 'renote';
+		notify(data.renote.userId, user._id, type, {
+			note_id: note._id
 		});
 
 		// Fetch watchers
-		PostWatching.find({
-			postId: data.repost._id,
+		NoteWatching.find({
+			noteId: data.renote._id,
 			userId: { $ne: user._id },
 			// 削除されたドキュメントは除く
 			deletedAt: { $exists: false }
@@ -272,41 +271,41 @@ export default async (user: IUser, data: {
 		}).then(watchers => {
 			watchers.forEach(watcher => {
 				notify(watcher.userId, user._id, type, {
-					postId: post._id
+					noteId: note._id
 				});
 			});
 		});
 
 		// この投稿をWatchする
 		if (isLocalUser(user) && user.account.settings.autoWatch !== false) {
-			watch(user._id, data.repost);
+			watch(user._id, data.renote);
 		}
 
-		// If it is quote repost
+		// If it is quote renote
 		if (data.text) {
 			// Add mention
-			addMention(data.repost.userId, 'quote');
+			addMention(data.renote.userId, 'quote');
 		} else {
 			// Publish event
-			if (!user._id.equals(data.repost.userId)) {
-				event(data.repost.userId, 'repost', postObj);
+			if (!user._id.equals(data.renote.userId)) {
+				event(data.renote.userId, 'renote', noteObj);
 			}
 		}
 
-		// 今までで同じ投稿をRepostしているか
-		const existRepost = await Post.findOne({
+		// 今までで同じ投稿をRenoteしているか
+		const existRenote = await Note.findOne({
 			userId: user._id,
-			repostId: data.repost._id,
+			renoteId: data.renote._id,
 			_id: {
-				$ne: post._id
+				$ne: note._id
 			}
 		});
 
-		if (!existRepost) {
-			// Update repostee status
-			Post.update({ _id: data.repost._id }, {
+		if (!existRenote) {
+			// Update renoteee status
+			Note.update({ _id: data.renote._id }, {
 				$inc: {
-					repostCount: 1
+					renoteCount: 1
 				}
 			});
 		}
@@ -333,23 +332,23 @@ export default async (user: IUser, data: {
 			// When mentioned user not found
 			if (mentionee == null) return;
 
-			// 既に言及されたユーザーに対する返信や引用repostの場合も無視
+			// 既に言及されたユーザーに対する返信や引用renoteの場合も無視
 			if (data.reply && data.reply.userId.equals(mentionee._id)) return;
-			if (data.repost && data.repost.userId.equals(mentionee._id)) return;
+			if (data.renote && data.renote.userId.equals(mentionee._id)) return;
 
 			// Add mention
 			addMention(mentionee._id, 'mention');
 
 			// Create notification
 			notify(mentionee._id, user._id, 'mention', {
-				post_id: post._id
+				note_id: note._id
 			});
 		}));
 	}
 
 	// Append mentions data
 	if (mentions.length > 0) {
-		Post.update({ _id: post._id }, {
+		Note.update({ _id: note._id }, {
 			$set: {
 				mentions
 			}
diff --git a/src/services/post/reaction/create.ts b/src/services/note/reaction/create.ts
similarity index 59%
rename from src/services/post/reaction/create.ts
rename to src/services/note/reaction/create.ts
index c26efcfc7..d0ce65ee5 100644
--- a/src/services/post/reaction/create.ts
+++ b/src/services/note/reaction/create.ts
@@ -1,24 +1,24 @@
 import { IUser, pack as packUser, isLocalUser, isRemoteUser } from '../../../models/user';
-import Post, { IPost, pack as packPost } from '../../../models/post';
-import PostReaction from '../../../models/post-reaction';
-import { publishPostStream } from '../../../publishers/stream';
+import Note, { INote, pack as packNote } from '../../../models/note';
+import NoteReaction from '../../../models/note-reaction';
+import { publishNoteStream } from '../../../publishers/stream';
 import notify from '../../../publishers/notify';
 import pushSw from '../../../publishers/push-sw';
-import PostWatching from '../../../models/post-watching';
+import NoteWatching from '../../../models/note-watching';
 import watch from '../watch';
 import renderLike from '../../../remote/activitypub/renderer/like';
 import { deliver } from '../../../queue';
 import context from '../../../remote/activitypub/renderer/context';
 
-export default async (user: IUser, post: IPost, reaction: string) => new Promise(async (res, rej) => {
+export default async (user: IUser, note: INote, reaction: string) => new Promise(async (res, rej) => {
 	// Myself
-	if (post.userId.equals(user._id)) {
-		return rej('cannot react to my post');
+	if (note.userId.equals(user._id)) {
+		return rej('cannot react to my note');
 	}
 
 	// if already reacted
-	const exist = await PostReaction.findOne({
-		postId: post._id,
+	const exist = await NoteReaction.findOne({
+		noteId: note._id,
 		userId: user._id
 	});
 
@@ -27,9 +27,9 @@ export default async (user: IUser, post: IPost, reaction: string) => new Promise
 	}
 
 	// Create reaction
-	await PostReaction.insert({
+	await NoteReaction.insert({
 		createdAt: new Date(),
-		postId: post._id,
+		noteId: note._id,
 		userId: user._id,
 		reaction
 	});
@@ -40,28 +40,28 @@ export default async (user: IUser, post: IPost, reaction: string) => new Promise
 	inc[`reactionCounts.${reaction}`] = 1;
 
 	// Increment reactions count
-	await Post.update({ _id: post._id }, {
+	await Note.update({ _id: note._id }, {
 		$inc: inc
 	});
 
-	publishPostStream(post._id, 'reacted');
+	publishNoteStream(note._id, 'reacted');
 
 	// Notify
-	notify(post.userId, user._id, 'reaction', {
-		postId: post._id,
+	notify(note.userId, user._id, 'reaction', {
+		noteId: note._id,
 		reaction: reaction
 	});
 
-	pushSw(post.userId, 'reaction', {
-		user: await packUser(user, post.userId),
-		post: await packPost(post, post.userId),
+	pushSw(note.userId, 'reaction', {
+		user: await packUser(user, note.userId),
+		note: await packNote(note, note.userId),
 		reaction: reaction
 	});
 
 	// Fetch watchers
-	PostWatching
+	NoteWatching
 		.find({
-			postId: post._id,
+			noteId: note._id,
 			userId: { $ne: user._id }
 		}, {
 			fields: {
@@ -71,7 +71,7 @@ export default async (user: IUser, post: IPost, reaction: string) => new Promise
 		.then(watchers => {
 			watchers.forEach(watcher => {
 				notify(watcher.userId, user._id, 'reaction', {
-					postId: post._id,
+					noteId: note._id,
 					reaction: reaction
 				});
 			});
@@ -79,16 +79,16 @@ export default async (user: IUser, post: IPost, reaction: string) => new Promise
 
 	// ユーザーがローカルユーザーかつ自動ウォッチ設定がオンならばこの投稿をWatchする
 	if (isLocalUser(user) && user.account.settings.autoWatch !== false) {
-		watch(user._id, post);
+		watch(user._id, note);
 	}
 
 	//#region 配信
-	const content = renderLike(user, post);
+	const content = renderLike(user, note);
 	content['@context'] = context;
 
 	// リアクターがローカルユーザーかつリアクション対象がリモートユーザーの投稿なら配送
-	if (isLocalUser(user) && isRemoteUser(post._user)) {
-		deliver(user, content, post._user.account.inbox).save();
+	if (isLocalUser(user) && isRemoteUser(note._user)) {
+		deliver(user, content, note._user.account.inbox).save();
 	}
 	//#endregion
 });
diff --git a/src/services/post/watch.ts b/src/services/note/watch.ts
similarity index 59%
rename from src/services/post/watch.ts
rename to src/services/note/watch.ts
index bbd9976f4..28fb7a32c 100644
--- a/src/services/post/watch.ts
+++ b/src/services/note/watch.ts
@@ -1,15 +1,15 @@
 import * as mongodb from 'mongodb';
-import Watching from '../../models/post-watching';
+import Watching from '../../models/note-watching';
 
-export default async (me: mongodb.ObjectID, post: object) => {
+export default async (me: mongodb.ObjectID, note: object) => {
 	// 自分の投稿はwatchできない
-	if (me.equals((post as any).userId)) {
+	if (me.equals((note as any).userId)) {
 		return;
 	}
 
 	// if watching now
 	const exist = await Watching.findOne({
-		postId: (post as any)._id,
+		noteId: (note as any)._id,
 		userId: me,
 		deletedAt: { $exists: false }
 	});
@@ -20,7 +20,7 @@ export default async (me: mongodb.ObjectID, post: object) => {
 
 	await Watching.insert({
 		createdAt: new Date(),
-		postId: (post as any)._id,
+		noteId: (note as any)._id,
 		userId: me
 	});
 };
diff --git a/tools/migration/nighthike/11.js b/tools/migration/nighthike/11.js
new file mode 100644
index 000000000..979e5bdc5
--- /dev/null
+++ b/tools/migration/nighthike/11.js
@@ -0,0 +1,35 @@
+db.pollVotes.update({}, {
+	$rename: {
+		postId: 'noteId',
+	}
+}, false, true);
+
+db.postReactions.renameCollection('noteReactions');
+db.noteReactions.update({}, {
+	$rename: {
+		postId: 'noteId',
+	}
+}, false, true);
+
+db.postWatching.renameCollection('noteWatching');
+db.noteWatching.update({}, {
+	$rename: {
+		postId: 'noteId',
+	}
+}, false, true);
+
+db.posts.renameCollection('notes');
+db.notes.update({}, {
+	$rename: {
+		_repost: '_renote',
+		repostId: 'renoteId',
+	}
+}, false, true);
+
+db.users.update({}, {
+	$rename: {
+		postsCount: 'notesCount',
+		pinnedPostId: 'pinnedNoteId',
+		latestPost: 'latestNote'
+	}
+}, false, true);

From 9739dc4ac2826f9c2f5d623d04f500bd4042af19 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 03:58:11 +0900
Subject: [PATCH 1154/1250] Some bug fixes

---
 src/client/app/common/define-widget.ts        |   4 +-
 src/client/app/common/mios.ts                 |  10 +-
 .../app/common/scripts/streaming/drive.ts     |   2 +-
 .../app/common/scripts/streaming/home.ts      |   4 +-
 .../scripts/streaming/messaging-index.ts      |   2 +-
 .../app/common/scripts/streaming/messaging.ts |   4 +-
 .../common/scripts/streaming/othello-game.ts  |   2 +-
 .../app/common/scripts/streaming/othello.ts   |   2 +-
 .../app/common/views/components/signin.vue    |   4 +-
 .../views/components/twitter-setting.vue      |  12 +-
 .../app/common/views/components/uploader.vue  |   2 +-
 src/client/app/desktop/api/update-avatar.ts   |   2 +-
 src/client/app/desktop/api/update-banner.ts   |   2 +-
 .../app/desktop/views/components/home.vue     |  25 +-
 .../desktop/views/components/note-detail.vue  |   4 +-
 .../desktop/views/components/notes.note.vue   |  27 +-
 .../desktop/views/components/post-detail.vue  |   4 +-
 .../desktop/views/components/posts.post.vue   |   6 +-
 .../desktop/views/components/settings.2fa.vue |   8 +-
 .../desktop/views/components/settings.api.vue |   2 +-
 .../views/components/settings.profile.vue     |   8 +-
 .../app/desktop/views/components/settings.vue |  12 +-
 .../app/desktop/views/components/timeline.vue |   2 +-
 .../desktop/views/components/ui.header.vue    |   8 +-
 .../views/components/widget-container.vue     |   4 +-
 .../app/desktop/views/components/window.vue   |   4 +-
 .../desktop/views/pages/user/user.header.vue  |   2 +-
 .../desktop/views/pages/user/user.home.vue    |   2 +-
 .../desktop/views/pages/user/user.profile.vue |  10 +-
 .../mobile/views/components/note-detail.vue   |   4 +-
 .../app/mobile/views/components/note.vue      |  32 +-
 .../mobile/views/components/post-detail.vue   | 462 ---------------
 .../app/mobile/views/components/post-form.vue |   2 +-
 .../app/mobile/views/components/post.vue      | 540 ------------------
 .../app/mobile/views/components/ui.header.vue |   4 +-
 src/client/app/mobile/views/pages/home.vue    |  20 +-
 .../mobile/views/pages/profile-setting.vue    |   4 +-
 src/client/app/mobile/views/pages/user.vue    |  10 +-
 .../app/mobile/views/pages/user/home.vue      |   2 +-
 src/client/app/mobile/views/pages/welcome.vue |   4 +-
 src/models/user.ts                            | 106 ++--
 src/queue/processors/http/process-inbox.ts    |   4 +-
 src/remote/activitypub/act/create/image.ts    |   2 +-
 src/remote/activitypub/act/create/index.ts    |   2 +-
 src/remote/activitypub/act/delete/index.ts    |   2 +-
 src/remote/activitypub/act/undo/index.ts      |   2 +-
 src/remote/activitypub/renderer/follow.ts     |   2 +-
 src/remote/activitypub/renderer/key.ts        |   2 +-
 src/remote/request.ts                         |   7 +-
 src/renderers/get-user-summary.ts             |   3 +-
 src/server/api/authenticate.ts                |   2 +-
 src/server/api/bot/core.ts                    |   2 +-
 src/server/api/bot/interfaces/line.ts         |  10 +-
 src/server/api/common/signin.ts               |   2 +-
 src/server/api/endpoints/following/create.ts  |   2 +-
 src/server/api/endpoints/following/delete.ts  |   2 +-
 src/server/api/endpoints/i.ts                 |   8 +-
 src/server/api/endpoints/i/2fa/done.ts        |   4 +-
 src/server/api/endpoints/i/2fa/register.ts    |   2 +-
 src/server/api/endpoints/i/2fa/unregister.ts  |   6 +-
 src/server/api/endpoints/i/change_password.ts |   4 +-
 .../api/endpoints/i/regenerate_token.ts       |   4 +-
 src/server/api/endpoints/i/update.ts          |  14 +-
 .../api/endpoints/i/update_client_setting.ts  |   4 +-
 src/server/api/endpoints/i/update_home.ts     |   6 +-
 .../api/endpoints/i/update_mobile_home.ts     |   6 +-
 src/server/api/endpoints/mute/create.ts       |   2 +-
 src/server/api/endpoints/mute/delete.ts       |   2 +-
 src/server/api/endpoints/notes/polls/vote.ts  |   2 +-
 .../api/endpoints/users/recommendation.ts     |   2 +-
 src/server/api/private/signin.ts              |  10 +-
 src/server/api/private/signup.ts              |  55 +-
 src/server/api/service/twitter.ts             |  10 +-
 src/server/api/stream/home.ts                 |   2 +-
 src/server/api/streaming.ts                   |   2 +-
 src/services/following/create.ts              |   4 +-
 src/services/following/delete.ts              |   2 +-
 src/services/note/create.ts                   |  10 +-
 src/services/note/reaction/create.ts          |   4 +-
 test/api.ts                                   |  16 +-
 tools/migration/nighthike/12.js               |  58 ++
 81 files changed, 337 insertions(+), 1318 deletions(-)
 delete mode 100644 src/client/app/mobile/views/components/post-detail.vue
 delete mode 100644 src/client/app/mobile/views/components/post.vue
 create mode 100644 tools/migration/nighthike/12.js

diff --git a/src/client/app/common/define-widget.ts b/src/client/app/common/define-widget.ts
index 9f8dcfc7e..7b98c0903 100644
--- a/src/client/app/common/define-widget.ts
+++ b/src/client/app/common/define-widget.ts
@@ -56,14 +56,14 @@ export default function<T extends object>(data: {
 						id: this.id,
 						data: newProps
 					}).then(() => {
-						(this as any).os.i.account.clientSettings.mobileHome.find(w => w.id == this.id).data = newProps;
+						(this as any).os.i.clientSettings.mobileHome.find(w => w.id == this.id).data = newProps;
 					});
 				} else {
 					(this as any).api('i/update_home', {
 						id: this.id,
 						data: newProps
 					}).then(() => {
-						(this as any).os.i.account.clientSettings.home.find(w => w.id == this.id).data = newProps;
+						(this as any).os.i.clientSettings.home.find(w => w.id == this.id).data = newProps;
 					});
 				}
 			}, {
diff --git a/src/client/app/common/mios.ts b/src/client/app/common/mios.ts
index 48e830810..fd267bc3f 100644
--- a/src/client/app/common/mios.ts
+++ b/src/client/app/common/mios.ts
@@ -270,7 +270,7 @@ export default class MiOS extends EventEmitter {
 				// Parse response
 				res.json().then(i => {
 					me = i;
-					me.account.token = token;
+					me.token = token;
 					done();
 				});
 			})
@@ -294,12 +294,12 @@ export default class MiOS extends EventEmitter {
 		const fetched = me => {
 			if (me) {
 				// デフォルトの設定をマージ
-				me.account.clientSettings = Object.assign({
+				me.clientSettings = Object.assign({
 					fetchOnScroll: true,
 					showMaps: true,
 					showPostFormOnTopOfTl: false,
 					gradientWindowHeader: false
-				}, me.account.clientSettings);
+				}, me.clientSettings);
 
 				// ローカルストレージにキャッシュ
 				localStorage.setItem('me', JSON.stringify(me));
@@ -329,7 +329,7 @@ export default class MiOS extends EventEmitter {
 			fetched(cachedMe);
 
 			// 後から新鮮なデータをフェッチ
-			fetchme(cachedMe.account.token, freshData => {
+			fetchme(cachedMe.token, freshData => {
 				merge(cachedMe, freshData);
 			});
 		} else {
@@ -437,7 +437,7 @@ export default class MiOS extends EventEmitter {
 		}
 
 		// Append a credential
-		if (this.isSignedIn) (data as any).i = this.i.account.token;
+		if (this.isSignedIn) (data as any).i = this.i.token;
 
 		// TODO
 		//const viaStream = localStorage.getItem('enableExperimental') == 'true';
diff --git a/src/client/app/common/scripts/streaming/drive.ts b/src/client/app/common/scripts/streaming/drive.ts
index f11573685..7ff85b594 100644
--- a/src/client/app/common/scripts/streaming/drive.ts
+++ b/src/client/app/common/scripts/streaming/drive.ts
@@ -8,7 +8,7 @@ import MiOS from '../../mios';
 export class DriveStream extends Stream {
 	constructor(os: MiOS, me) {
 		super(os, 'drive', {
-			i: me.account.token
+			i: me.token
 		});
 	}
 }
diff --git a/src/client/app/common/scripts/streaming/home.ts b/src/client/app/common/scripts/streaming/home.ts
index c19861940..e085801e1 100644
--- a/src/client/app/common/scripts/streaming/home.ts
+++ b/src/client/app/common/scripts/streaming/home.ts
@@ -10,13 +10,13 @@ import MiOS from '../../mios';
 export class HomeStream extends Stream {
 	constructor(os: MiOS, me) {
 		super(os, '', {
-			i: me.account.token
+			i: me.token
 		});
 
 		// 最終利用日時を更新するため定期的にaliveメッセージを送信
 		setInterval(() => {
 			this.send({ type: 'alive' });
-			me.account.lastUsedAt = new Date();
+			me.lastUsedAt = new Date();
 		}, 1000 * 60);
 
 		// 自分の情報が更新されたとき
diff --git a/src/client/app/common/scripts/streaming/messaging-index.ts b/src/client/app/common/scripts/streaming/messaging-index.ts
index 24f0ce0c9..84e2174ec 100644
--- a/src/client/app/common/scripts/streaming/messaging-index.ts
+++ b/src/client/app/common/scripts/streaming/messaging-index.ts
@@ -8,7 +8,7 @@ import MiOS from '../../mios';
 export class MessagingIndexStream extends Stream {
 	constructor(os: MiOS, me) {
 		super(os, 'messaging-index', {
-			i: me.account.token
+			i: me.token
 		});
 	}
 }
diff --git a/src/client/app/common/scripts/streaming/messaging.ts b/src/client/app/common/scripts/streaming/messaging.ts
index 4c593deb3..c1b5875cf 100644
--- a/src/client/app/common/scripts/streaming/messaging.ts
+++ b/src/client/app/common/scripts/streaming/messaging.ts
@@ -7,13 +7,13 @@ import MiOS from '../../mios';
 export class MessagingStream extends Stream {
 	constructor(os: MiOS, me, otherparty) {
 		super(os, 'messaging', {
-			i: me.account.token,
+			i: me.token,
 			otherparty
 		});
 
 		(this as any).on('_connected_', () => {
 			this.send({
-				i: me.account.token
+				i: me.token
 			});
 		});
 	}
diff --git a/src/client/app/common/scripts/streaming/othello-game.ts b/src/client/app/common/scripts/streaming/othello-game.ts
index f34ef3514..b85af8f72 100644
--- a/src/client/app/common/scripts/streaming/othello-game.ts
+++ b/src/client/app/common/scripts/streaming/othello-game.ts
@@ -4,7 +4,7 @@ import MiOS from '../../mios';
 export class OthelloGameStream extends Stream {
 	constructor(os: MiOS, me, game) {
 		super(os, 'othello-game', {
-			i: me ? me.account.token : null,
+			i: me ? me.token : null,
 			game: game.id
 		});
 	}
diff --git a/src/client/app/common/scripts/streaming/othello.ts b/src/client/app/common/scripts/streaming/othello.ts
index 8c6f4b9c3..f5d47431c 100644
--- a/src/client/app/common/scripts/streaming/othello.ts
+++ b/src/client/app/common/scripts/streaming/othello.ts
@@ -5,7 +5,7 @@ import MiOS from '../../mios';
 export class OthelloStream extends Stream {
 	constructor(os: MiOS, me) {
 		super(os, 'othello', {
-			i: me.account.token
+			i: me.token
 		});
 	}
 }
diff --git a/src/client/app/common/views/components/signin.vue b/src/client/app/common/views/components/signin.vue
index 17154e6b3..da7472b8c 100644
--- a/src/client/app/common/views/components/signin.vue
+++ b/src/client/app/common/views/components/signin.vue
@@ -6,7 +6,7 @@
 	<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.account.twoFactorEnabled">
+	<label class="token" v-if="user && user.twoFactorEnabled">
 		<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>
@@ -43,7 +43,7 @@ export default Vue.extend({
 			(this as any).api('signin', {
 				username: this.username,
 				password: this.password,
-				token: this.user && this.user.account.twoFactorEnabled ? this.token : undefined
+				token: this.user && this.user.twoFactorEnabled ? this.token : undefined
 			}).then(() => {
 				location.reload();
 			}).catch(() => {
diff --git a/src/client/app/common/views/components/twitter-setting.vue b/src/client/app/common/views/components/twitter-setting.vue
index 082d2b435..00669cd83 100644
--- a/src/client/app/common/views/components/twitter-setting.vue
+++ b/src/client/app/common/views/components/twitter-setting.vue
@@ -1,13 +1,13 @@
 <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.account.twitter" :title="`Twitter ID: ${os.i.account.twitter.userId}`">%i18n:common.tags.mk-twitter-setting.connected-to%: <a :href="`https://twitter.com/${os.i.account.twitter.screenName}`" target="_blank">@{{ os.i.account.twitter.screenName }}</a></p>
+	<p class="account" v-if="os.i.twitter" :title="`Twitter ID: ${os.i.twitter.userId}`">%i18n:common.tags.mk-twitter-setting.connected-to%: <a :href="`https://twitter.com/${os.i.twitter.screenName}`" target="_blank">@{{ os.i.twitter.screenName }}</a></p>
 	<p>
-		<a :href="`${apiUrl}/connect/twitter`" target="_blank" @click.prevent="connect">{{ os.i.account.twitter ? '%i18n:common.tags.mk-twitter-setting.reconnect%' : '%i18n:common.tags.mk-twitter-setting.connect%' }}</a>
-		<span v-if="os.i.account.twitter"> or </span>
-		<a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="os.i.account.twitter" @click.prevent="disconnect">%i18n:common.tags.mk-twitter-setting.disconnect%</a>
+		<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.account.twitter">Twitter ID: {{ os.i.account.twitter.userId }}</p>
+	<p class="id" v-if="os.i.twitter">Twitter ID: {{ os.i.twitter.userId }}</p>
 </div>
 </template>
 
@@ -25,7 +25,7 @@ export default Vue.extend({
 	},
 	mounted() {
 		this.$watch('os.i', () => {
-			if ((this as any).os.i.account.twitter) {
+			if ((this as any).os.i.twitter) {
 				if (this.form) this.form.close();
 			}
 		}, {
diff --git a/src/client/app/common/views/components/uploader.vue b/src/client/app/common/views/components/uploader.vue
index c74a1edb4..ccad50dc3 100644
--- a/src/client/app/common/views/components/uploader.vue
+++ b/src/client/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 as any).os.i.account.token);
+			data.append('i', (this as any).os.i.token);
 			data.append('file', file);
 
 			if (folder) data.append('folderId', folder);
diff --git a/src/client/app/desktop/api/update-avatar.ts b/src/client/app/desktop/api/update-avatar.ts
index 36a2ffe91..dc89adeb8 100644
--- a/src/client/app/desktop/api/update-avatar.ts
+++ b/src/client/app/desktop/api/update-avatar.ts
@@ -16,7 +16,7 @@ export default (os: OS) => (cb, file = null) => {
 
 		w.$once('cropped', blob => {
 			const data = new FormData();
-			data.append('i', os.i.account.token);
+			data.append('i', os.i.token);
 			data.append('file', blob, file.name + '.cropped.png');
 
 			os.api('drive/folders/find', {
diff --git a/src/client/app/desktop/api/update-banner.ts b/src/client/app/desktop/api/update-banner.ts
index e66dbf016..5f10bf285 100644
--- a/src/client/app/desktop/api/update-banner.ts
+++ b/src/client/app/desktop/api/update-banner.ts
@@ -16,7 +16,7 @@ export default (os: OS) => (cb, file = null) => {
 
 		w.$once('cropped', blob => {
 			const data = new FormData();
-			data.append('i', os.i.account.token);
+			data.append('i', os.i.token);
 			data.append('file', blob, file.name + '.cropped.png');
 
 			os.api('drive/folders/find', {
diff --git a/src/client/app/desktop/views/components/home.vue b/src/client/app/desktop/views/components/home.vue
index 7145ddce0..90e9d1b78 100644
--- a/src/client/app/desktop/views/components/home.vue
+++ b/src/client/app/desktop/views/components/home.vue
@@ -53,7 +53,7 @@
 			<div class="main">
 				<a @click="hint">カスタマイズのヒント</a>
 				<div>
-					<mk-post-form v-if="os.i.account.clientSettings.showPostFormOnTopOfTl"/>
+					<mk-post-form v-if="os.i.clientSettings.showPostFormOnTopOfTl"/>
 					<mk-timeline ref="tl" @loaded="onTlLoaded"/>
 				</div>
 			</div>
@@ -63,7 +63,7 @@
 				<component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" @chosen="warp"/>
 			</div>
 			<div class="main">
-				<mk-post-form v-if="os.i.account.clientSettings.showPostFormOnTopOfTl"/>
+				<mk-post-form v-if="os.i.clientSettings.showPostFormOnTopOfTl"/>
 				<mk-timeline ref="tl" @loaded="onTlLoaded" v-if="mode == 'timeline'"/>
 				<mk-mentions @loaded="onTlLoaded" v-if="mode == 'mentions'"/>
 			</div>
@@ -82,7 +82,10 @@ export default Vue.extend({
 		XDraggable
 	},
 	props: {
-		customize: Boolean,
+		customize: {
+			type: Boolean,
+			default: false
+		},
 		mode: {
 			type: String,
 			default: 'timeline'
@@ -104,16 +107,16 @@ export default Vue.extend({
 		home: {
 			get(): any[] {
 				//#region 互換性のため
-				(this as any).os.i.account.clientSettings.home.forEach(w => {
+				(this as any).os.i.clientSettings.home.forEach(w => {
 					if (w.name == 'rss-reader') w.name = 'rss';
 					if (w.name == 'user-recommendation') w.name = 'users';
 					if (w.name == 'recommended-polls') w.name = 'polls';
 				});
 				//#endregion
-				return (this as any).os.i.account.clientSettings.home;
+				return (this as any).os.i.clientSettings.home;
 			},
 			set(value) {
-				(this as any).os.i.account.clientSettings.home = value;
+				(this as any).os.i.clientSettings.home = value;
 			}
 		},
 		left(): any[] {
@@ -126,7 +129,7 @@ export default Vue.extend({
 	created() {
 		this.widgets.left = this.left;
 		this.widgets.right = this.right;
-		this.$watch('os.i.account.clientSettings', i => {
+		this.$watch('os.i.clientSettings', i => {
 			this.widgets.left = this.left;
 			this.widgets.right = this.right;
 		}, {
@@ -161,17 +164,17 @@ export default Vue.extend({
 		},
 		onHomeUpdated(data) {
 			if (data.home) {
-				(this as any).os.i.account.clientSettings.home = data.home;
+				(this as any).os.i.clientSettings.home = data.home;
 				this.widgets.left = data.home.filter(w => w.place == 'left');
 				this.widgets.right = data.home.filter(w => w.place == 'right');
 			} else {
-				const w = (this as any).os.i.account.clientSettings.home.find(w => w.id == data.id);
+				const w = (this as any).os.i.clientSettings.home.find(w => w.id == data.id);
 				if (w != null) {
 					w.data = data.data;
 					this.$refs[w.id][0].preventSave = true;
 					this.$refs[w.id][0].props = w.data;
-					this.widgets.left = (this as any).os.i.account.clientSettings.home.filter(w => w.place == 'left');
-					this.widgets.right = (this as any).os.i.account.clientSettings.home.filter(w => w.place == 'right');
+					this.widgets.left = (this as any).os.i.clientSettings.home.filter(w => w.place == 'left');
+					this.widgets.right = (this as any).os.i.clientSettings.home.filter(w => w.place == 'right');
 				}
 			}
 		},
diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue
index 790f797ad..df7c33dfa 100644
--- a/src/client/app/desktop/views/components/note-detail.vue
+++ b/src/client/app/desktop/views/components/note-detail.vue
@@ -115,7 +115,7 @@ export default Vue.extend({
 		isRenote(): boolean {
 			return (this.note.renote &&
 				this.note.text == null &&
-				this.note.mediaIds == null &&
+				this.note.mediaIds.length == 0 &&
 				this.note.poll == null);
 		},
 		p(): any {
@@ -168,7 +168,7 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true;
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
 					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue
index 924642c01..4547ac58b 100644
--- a/src/client/app/desktop/views/components/notes.note.vue
+++ b/src/client/app/desktop/views/components/notes.note.vue
@@ -5,25 +5,25 @@
 	</div>
 	<div class="renote" v-if="isRenote">
 		<p>
-			<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="note.userId">
+			<router-link class="avatar-anchor" :to="`/@${getAcct(note.user)}`" v-user-preview="note.userId">
 				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
 			</router-link>
 			%fa:retweet%
 			<span>{{ '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.indexOf('{')) }}</span>
-			<a class="name" :href="`/@${acct}`" v-user-preview="note.userId">{{ getUserName(note.user) }}</a>
+			<a class="name" :href="`/@${getAcct(note.user)}`" v-user-preview="note.userId">{{ getUserName(note.user) }}</a>
 			<span>{{ '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-note.reposted-by%'.indexOf('}') + 1) }}</span>
 		</p>
 		<mk-time :time="note.createdAt"/>
 	</div>
 	<article>
-		<router-link class="avatar-anchor" :to="`/@${acct}`">
+		<router-link class="avatar-anchor" :to="`/@${getAcct(p.user)}`">
 			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
 		</router-link>
 		<div class="main">
 			<header>
-				<router-link class="name" :to="`/@${acct}`" v-user-preview="p.user.id">{{ acct }}</router-link>
-				<span class="is-bot" v-if="p.user.host === null && p.user.account.isBot">bot</span>
-				<span class="username">@{{ acct }}</span>
+				<router-link class="name" :to="`/@${getAcct(p.user)}`" v-user-preview="p.user.id">{{ getUserName(p) }}</router-link>
+				<span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span>
+				<span class="username">@{{ getAcct(p.user) }}</span>
 				<div class="info">
 					<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
 					<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
@@ -117,21 +117,18 @@ export default Vue.extend({
 		return {
 			isDetailOpened: false,
 			connection: null,
-			connectionId: null
+			connectionId: null,
+			getAcct,
+			getUserName
 		};
 	},
 
 	computed: {
-		acct(): string {
-			return getAcct(this.p.user);
-		},
-		name(): string {
-			return getUserName(this.p.user);
-		},
+
 		isRenote(): boolean {
 			return (this.note.renote &&
 				this.note.text == null &&
-				this.note.mediaIds == null &&
+				this.note.mediaIds.length == 0 &&
 				this.note.poll == null);
 		},
 		p(): any {
@@ -178,7 +175,7 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true;
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
 					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
diff --git a/src/client/app/desktop/views/components/post-detail.vue b/src/client/app/desktop/views/components/post-detail.vue
index 790f797ad..df7c33dfa 100644
--- a/src/client/app/desktop/views/components/post-detail.vue
+++ b/src/client/app/desktop/views/components/post-detail.vue
@@ -115,7 +115,7 @@ export default Vue.extend({
 		isRenote(): boolean {
 			return (this.note.renote &&
 				this.note.text == null &&
-				this.note.mediaIds == null &&
+				this.note.mediaIds.length == 0 &&
 				this.note.poll == null);
 		},
 		p(): any {
@@ -168,7 +168,7 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true;
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
 					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
diff --git a/src/client/app/desktop/views/components/posts.post.vue b/src/client/app/desktop/views/components/posts.post.vue
index 924642c01..d7c21dfa0 100644
--- a/src/client/app/desktop/views/components/posts.post.vue
+++ b/src/client/app/desktop/views/components/posts.post.vue
@@ -22,7 +22,7 @@
 		<div class="main">
 			<header>
 				<router-link class="name" :to="`/@${acct}`" v-user-preview="p.user.id">{{ acct }}</router-link>
-				<span class="is-bot" v-if="p.user.host === null && p.user.account.isBot">bot</span>
+				<span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span>
 				<span class="username">@{{ acct }}</span>
 				<div class="info">
 					<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
@@ -131,7 +131,7 @@ export default Vue.extend({
 		isRenote(): boolean {
 			return (this.note.renote &&
 				this.note.text == null &&
-				this.note.mediaIds == null &&
+				this.note.mediaIds.length == 0 &&
 				this.note.poll == null);
 		},
 		p(): any {
@@ -178,7 +178,7 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true;
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
 					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
diff --git a/src/client/app/desktop/views/components/settings.2fa.vue b/src/client/app/desktop/views/components/settings.2fa.vue
index b8dd1dfd9..fb2dc30f2 100644
--- a/src/client/app/desktop/views/components/settings.2fa.vue
+++ b/src/client/app/desktop/views/components/settings.2fa.vue
@@ -2,8 +2,8 @@
 <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 && !os.i.account.twoFactorEnabled"><button @click="register" class="ui primary">%i18n:desktop.tags.mk-2fa-setting.register%</button></p>
-	<template v-if="os.i.account.twoFactorEnabled">
+	<p v-if="!data && !os.i.twoFactorEnabled"><button @click="register" class="ui primary">%i18n:desktop.tags.mk-2fa-setting.register%</button></p>
+	<template v-if="os.i.twoFactorEnabled">
 		<p>%i18n:desktop.tags.mk-2fa-setting.already-registered%</p>
 		<button @click="unregister" class="ui">%i18n:desktop.tags.mk-2fa-setting.unregister%</button>
 	</template>
@@ -54,7 +54,7 @@ export default Vue.extend({
 					password: password
 				}).then(() => {
 					(this as any).apis.notify('%i18n:desktop.tags.mk-2fa-setting.unregistered%');
-					(this as any).os.i.account.twoFactorEnabled = false;
+					(this as any).os.i.twoFactorEnabled = false;
 				});
 			});
 		},
@@ -64,7 +64,7 @@ export default Vue.extend({
 				token: this.token
 			}).then(() => {
 				(this as any).apis.notify('%i18n:desktop.tags.mk-2fa-setting.success%');
-				(this as any).os.i.account.twoFactorEnabled = true;
+				(this as any).os.i.twoFactorEnabled = true;
 			}).catch(() => {
 				(this as any).apis.notify('%i18n:desktop.tags.mk-2fa-setting.failed%');
 			});
diff --git a/src/client/app/desktop/views/components/settings.api.vue b/src/client/app/desktop/views/components/settings.api.vue
index 0d5921ab7..5831f8207 100644
--- a/src/client/app/desktop/views/components/settings.api.vue
+++ b/src/client/app/desktop/views/components/settings.api.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="root api">
-	<p>Token: <code>{{ os.i.account.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>
diff --git a/src/client/app/desktop/views/components/settings.profile.vue b/src/client/app/desktop/views/components/settings.profile.vue
index 28be48e0a..324a939f9 100644
--- a/src/client/app/desktop/views/components/settings.profile.vue
+++ b/src/client/app/desktop/views/components/settings.profile.vue
@@ -24,7 +24,7 @@
 	<button class="ui primary" @click="save">%i18n:desktop.tags.mk-profile-setting.save%</button>
 	<section>
 		<h2>その他</h2>
-		<mk-switch v-model="os.i.account.isBot" @change="onChangeIsBot" text="このアカウントはbotです"/>
+		<mk-switch v-model="os.i.isBot" @change="onChangeIsBot" text="このアカウントはbotです"/>
 	</section>
 </div>
 </template>
@@ -43,9 +43,9 @@ export default Vue.extend({
 	},
 	created() {
 		this.name = (this as any).os.i.name || '';
-		this.location = (this as any).os.i.account.profile.location;
+		this.location = (this as any).os.i.profile.location;
 		this.description = (this as any).os.i.description;
-		this.birthday = (this as any).os.i.account.profile.birthday;
+		this.birthday = (this as any).os.i.profile.birthday;
 	},
 	methods: {
 		updateAvatar() {
@@ -63,7 +63,7 @@ export default Vue.extend({
 		},
 		onChangeIsBot() {
 			(this as any).api('i/update', {
-				isBot: (this as any).os.i.account.isBot
+				isBot: (this as any).os.i.isBot
 			});
 		}
 	}
diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue
index fd82c171c..4184ae82c 100644
--- a/src/client/app/desktop/views/components/settings.vue
+++ b/src/client/app/desktop/views/components/settings.vue
@@ -20,7 +20,7 @@
 
 		<section class="web" v-show="page == 'web'">
 			<h1>動作</h1>
-			<mk-switch v-model="os.i.account.clientSettings.fetchOnScroll" @change="onChangeFetchOnScroll" text="スクロールで自動読み込み">
+			<mk-switch v-model="os.i.clientSettings.fetchOnScroll" @change="onChangeFetchOnScroll" text="スクロールで自動読み込み">
 				<span>ページを下までスクロールしたときに自動で追加のコンテンツを読み込みます。</span>
 			</mk-switch>
 			<mk-switch v-model="autoPopout" text="ウィンドウの自動ポップアウト">
@@ -33,11 +33,11 @@
 			<div class="div">
 				<button class="ui button" @click="customizeHome">ホームをカスタマイズ</button>
 			</div>
-			<mk-switch v-model="os.i.account.clientSettings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="タイムライン上部に投稿フォームを表示する"/>
-			<mk-switch v-model="os.i.account.clientSettings.showMaps" @change="onChangeShowMaps" text="マップの自動展開">
+			<mk-switch v-model="os.i.clientSettings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="タイムライン上部に投稿フォームを表示する"/>
+			<mk-switch v-model="os.i.clientSettings.showMaps" @change="onChangeShowMaps" text="マップの自動展開">
 				<span>位置情報が添付された投稿のマップを自動的に展開します。</span>
 			</mk-switch>
-			<mk-switch v-model="os.i.account.clientSettings.gradientWindowHeader" @change="onChangeGradientWindowHeader" text="ウィンドウのタイトルバーにグラデーションを使用"/>
+			<mk-switch v-model="os.i.clientSettings.gradientWindowHeader" @change="onChangeGradientWindowHeader" text="ウィンドウのタイトルバーにグラデーションを使用"/>
 		</section>
 
 		<section class="web" v-show="page == 'web'">
@@ -57,7 +57,7 @@
 
 		<section class="web" v-show="page == 'web'">
 			<h1>モバイル</h1>
-			<mk-switch v-model="os.i.account.clientSettings.disableViaMobile" @change="onChangeDisableViaMobile" text="「モバイルからの投稿」フラグを付けない"/>
+			<mk-switch v-model="os.i.clientSettings.disableViaMobile" @change="onChangeDisableViaMobile" text="「モバイルからの投稿」フラグを付けない"/>
 		</section>
 
 		<section class="web" v-show="page == 'web'">
@@ -86,7 +86,7 @@
 
 		<section class="notification" v-show="page == 'notification'">
 			<h1>通知</h1>
-			<mk-switch v-model="os.i.account.settings.autoWatch" @change="onChangeAutoWatch" text="投稿の自動ウォッチ">
+			<mk-switch v-model="os.i.settings.autoWatch" @change="onChangeAutoWatch" text="投稿の自動ウォッチ">
 				<span>リアクションしたり返信したりした投稿に関する通知を自動的に受け取るようにします。</span>
 			</mk-switch>
 		</section>
diff --git a/src/client/app/desktop/views/components/timeline.vue b/src/client/app/desktop/views/components/timeline.vue
index 6d049eee9..ea8f0053b 100644
--- a/src/client/app/desktop/views/components/timeline.vue
+++ b/src/client/app/desktop/views/components/timeline.vue
@@ -107,7 +107,7 @@ export default Vue.extend({
 			this.fetch();
 		},
 		onScroll() {
-			if ((this as any).os.i.account.clientSettings.fetchOnScroll !== false) {
+			if ((this as any).os.i.clientSettings.fetchOnScroll !== false) {
 				const current = window.scrollY + window.innerHeight;
 				if (current > document.body.offsetHeight - 8) this.more();
 			}
diff --git a/src/client/app/desktop/views/components/ui.header.vue b/src/client/app/desktop/views/components/ui.header.vue
index 7d93847fa..527d10843 100644
--- a/src/client/app/desktop/views/components/ui.header.vue
+++ b/src/client/app/desktop/views/components/ui.header.vue
@@ -37,8 +37,8 @@ import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	computed: {
-		name() {
-			return getUserName(this.os.i);
+		name(): string {
+			return getUserName((this as any).os.i);
 		}
 	},
 	components: {
@@ -51,9 +51,9 @@ export default Vue.extend({
 	},
 	mounted() {
 		if ((this as any).os.isSignedIn) {
-			const ago = (new Date().getTime() - new Date((this as any).os.i.account.lastUsedAt).getTime()) / 1000
+			const ago = (new Date().getTime() - new Date((this as any).os.i.lastUsedAt).getTime()) / 1000
 			const isHisasiburi = ago >= 3600;
-			(this as any).os.i.account.lastUsedAt = new Date();
+			(this as any).os.i.lastUsedAt = new Date();
 			if (isHisasiburi) {
 				(this.$refs.welcomeback as any).style.display = 'block';
 				(this.$refs.main as any).style.overflow = 'hidden';
diff --git a/src/client/app/desktop/views/components/widget-container.vue b/src/client/app/desktop/views/components/widget-container.vue
index 68c5bcb8d..188a67313 100644
--- a/src/client/app/desktop/views/components/widget-container.vue
+++ b/src/client/app/desktop/views/components/widget-container.vue
@@ -24,8 +24,8 @@ export default Vue.extend({
 	computed: {
 		withGradient(): boolean {
 			return (this as any).os.isSignedIn
-				? (this as any).os.i.account.clientSettings.gradientWindowHeader != null
-					? (this as any).os.i.account.clientSettings.gradientWindowHeader
+				? (this as any).os.i.clientSettings.gradientWindowHeader != null
+					? (this as any).os.i.clientSettings.gradientWindowHeader
 					: false
 				: false;
 		}
diff --git a/src/client/app/desktop/views/components/window.vue b/src/client/app/desktop/views/components/window.vue
index 48dc46feb..e2cab2179 100644
--- a/src/client/app/desktop/views/components/window.vue
+++ b/src/client/app/desktop/views/components/window.vue
@@ -92,8 +92,8 @@ export default Vue.extend({
 		},
 		withGradient(): boolean {
 			return (this as any).os.isSignedIn
-				? (this as any).os.i.account.clientSettings.gradientWindowHeader != null
-					? (this as any).os.i.account.clientSettings.gradientWindowHeader
+				? (this as any).os.i.clientSettings.gradientWindowHeader != null
+					? (this as any).os.i.clientSettings.gradientWindowHeader
 					: false
 				: false;
 		}
diff --git a/src/client/app/desktop/views/pages/user/user.header.vue b/src/client/app/desktop/views/pages/user/user.header.vue
index 5c6746d5d..ceeb78431 100644
--- a/src/client/app/desktop/views/pages/user/user.header.vue
+++ b/src/client/app/desktop/views/pages/user/user.header.vue
@@ -9,7 +9,7 @@
 		<div class="title">
 			<p class="name">{{ name }}</p>
 			<p class="username">@{{ acct }}</p>
-			<p class="location" v-if="user.host === null && user.account.profile.location">%fa:map-marker%{{ user.account.profile.location }}</p>
+			<p class="location" v-if="user.host === null && user.profile.location">%fa:map-marker%{{ user.profile.location }}</p>
 		</div>
 		<footer>
 			<router-link :to="`/@${acct}`" :data-active="$parent.page == 'home'">%fa:home%概要</router-link>
diff --git a/src/client/app/desktop/views/pages/user/user.home.vue b/src/client/app/desktop/views/pages/user/user.home.vue
index ed3b1c710..c254a320c 100644
--- a/src/client/app/desktop/views/pages/user/user.home.vue
+++ b/src/client/app/desktop/views/pages/user/user.home.vue
@@ -5,7 +5,7 @@
 			<x-profile :user="user"/>
 			<x-photos :user="user"/>
 			<x-followers-you-know v-if="os.isSignedIn && os.i.id != user.id" :user="user"/>
-			<p v-if="user.host === null">%i18n:desktop.tags.mk-user.last-used-at%: <b><mk-time :time="user.account.lastUsedAt"/></b></p>
+			<p v-if="user.host === null">%i18n:desktop.tags.mk-user.last-used-at%: <b><mk-time :time="user.lastUsedAt"/></b></p>
 		</div>
 	</div>
 	<main>
diff --git a/src/client/app/desktop/views/pages/user/user.profile.vue b/src/client/app/desktop/views/pages/user/user.profile.vue
index 2a82ba786..44f20c2bc 100644
--- a/src/client/app/desktop/views/pages/user/user.profile.vue
+++ b/src/client/app/desktop/views/pages/user/user.profile.vue
@@ -7,11 +7,11 @@
 		<p v-if="!user.isMuted"><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.host === null && user.account.profile.birthday">
-		<p>%fa:birthday-cake%{{ user.account.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳)</p>
+	<div class="birthday" v-if="user.host === null && user.profile.birthday">
+		<p>%fa:birthday-cake%{{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳)</p>
 	</div>
-	<div class="twitter" v-if="user.host === null && user.account.twitter">
-		<p>%fa:B twitter%<a :href="`https://twitter.com/${user.account.twitter.screenName}`" target="_blank">@{{ user.account.twitter.screenName }}</a></p>
+	<div class="twitter" v-if="user.host === null && user.twitter">
+		<p>%fa:B twitter%<a :href="`https://twitter.com/${user.twitter.screenName}`" target="_blank">@{{ user.twitter.screenName }}</a></p>
 	</div>
 	<div class="status">
 		<p class="notes-count">%fa:angle-right%<a>{{ user.notesCount }}</a><b>投稿</b></p>
@@ -31,7 +31,7 @@ export default Vue.extend({
 	props: ['user'],
 	computed: {
 		age(): number {
-			return age(this.user.account.profile.birthday);
+			return age(this.user.profile.birthday);
 		}
 	},
 	methods: {
diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue
index e1682e58e..483f5aaf3 100644
--- a/src/client/app/mobile/views/components/note-detail.vue
+++ b/src/client/app/mobile/views/components/note-detail.vue
@@ -127,7 +127,7 @@ export default Vue.extend({
 		isRenote(): boolean {
 			return (this.note.renote &&
 				this.note.text == null &&
-				this.note.mediaIds == null &&
+				this.note.mediaIds.length == 0 &&
 				this.note.poll == null);
 		},
 		p(): any {
@@ -165,7 +165,7 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true;
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
 					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue
index 4b33c6f07..295fe4d6a 100644
--- a/src/client/app/mobile/views/components/note.vue
+++ b/src/client/app/mobile/views/components/note.vue
@@ -5,25 +5,25 @@
 	</div>
 	<div class="renote" v-if="isRenote">
 		<p>
-			<router-link class="avatar-anchor" :to="`/@${acct}`">
+			<router-link class="avatar-anchor" :to="`/@${getAcct(note.user)}`">
 				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 			</router-link>
 			%fa:retweet%
 			<span>{{ '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.substr(0, '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.indexOf('{')) }}</span>
-			<router-link class="name" :to="`/@${acct}`">{{ name }}</router-link>
+			<router-link class="name" :to="`/@${getAcct(note.user)}`">{{ getUserName(note.user) }}</router-link>
 			<span>{{ '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.substr('%i18n:mobile.tags.mk-timeline-note.reposted-by%'.indexOf('}') + 1) }}</span>
 		</p>
 		<mk-time :time="note.createdAt"/>
 	</div>
 	<article>
-		<router-link class="avatar-anchor" :to="`/@${pAcct}`">
+		<router-link class="avatar-anchor" :to="`/@${getAcct(p.user)}`">
 			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
 		</router-link>
 		<div class="main">
 			<header>
-				<router-link class="name" :to="`/@${pAcct}`">{{ pName }}</router-link>
-				<span class="is-bot" v-if="p.user.host === null && p.user.account.isBot">bot</span>
-				<span class="username">@{{ pAcct }}</span>
+				<router-link class="name" :to="`/@${getAcct(p.user)}`">{{ getUserName(p.user) }}</router-link>
+				<span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span>
+				<span class="username">@{{ getAcct(p.user) }}</span>
 				<div class="info">
 					<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
 					<router-link class="created-at" :to="url">
@@ -95,27 +95,17 @@ export default Vue.extend({
 	data() {
 		return {
 			connection: null,
-			connectionId: null
+			connectionId: null,
+			getAcct,
+			getUserName
 		};
 	},
 
 	computed: {
-		acct(): string {
-			return getAcct(this.note.user);
-		},
-		name(): string {
-			return getUserName(this.note.user);
-		},
-		pAcct(): string {
-			return getAcct(this.p.user);
-		},
-		pName(): string {
-			return getUserName(this.p.user);
-		},
 		isRenote(): boolean {
 			return (this.note.renote &&
 				this.note.text == null &&
-				this.note.mediaIds == null &&
+				this.note.mediaIds.length == 0 &&
 				this.note.poll == null);
 		},
 		p(): any {
@@ -159,7 +149,7 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true;
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
 					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
diff --git a/src/client/app/mobile/views/components/post-detail.vue b/src/client/app/mobile/views/components/post-detail.vue
deleted file mode 100644
index e1682e58e..000000000
--- a/src/client/app/mobile/views/components/post-detail.vue
+++ /dev/null
@@ -1,462 +0,0 @@
-<template>
-<div class="mk-note-detail">
-	<button
-		class="more"
-		v-if="p.reply && p.reply.replyId && 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">
-		<x-sub v-for="note in context" :key="note.id" :note="note"/>
-	</div>
-	<div class="reply-to" v-if="p.reply">
-		<x-sub :note="p.reply"/>
-	</div>
-	<div class="renote" v-if="isRenote">
-		<p>
-			<router-link class="avatar-anchor" :to="`/@${acct}`">
-				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
-			</router-link>
-			%fa:retweet%
-			<router-link class="name" :to="`/@${acct}`">
-				{{ name }}
-			</router-link>
-			がRenote
-		</p>
-	</div>
-	<article>
-		<header>
-			<router-link class="avatar-anchor" :to="`/@${pAcct}`">
-				<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-			</router-link>
-			<div>
-				<router-link class="name" :to="`/@${pAcct}`">{{ pName }}</router-link>
-				<span class="username">@{{ pAcct }}</span>
-			</div>
-		</header>
-		<div class="body">
-			<mk-note-html v-if="p.text" :ast="p.text" :i="os.i" :class="$style.text"/>
-			<div class="tags" v-if="p.tags && p.tags.length > 0">
-				<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
-			</div>
-			<div class="media" v-if="p.media.length > 0">
-				<mk-media-list :media-list="p.media"/>
-			</div>
-			<mk-poll v-if="p.poll" :note="p"/>
-			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
-			<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
-			<div class="map" v-if="p.geo" ref="map"></div>
-			<div class="renote" v-if="p.renote">
-				<mk-note-preview :note="p.renote"/>
-			</div>
-		</div>
-		<router-link class="time" :to="`/@${pAcct}/${p.id}`">
-			<mk-time :time="p.createdAt" mode="detail"/>
-		</router-link>
-		<footer>
-			<mk-reactions-viewer :note="p"/>
-			<button @click="reply" title="%i18n:mobile.tags.mk-note-detail.reply%">
-				%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
-			</button>
-			<button @click="renote" title="Renote">
-				%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
-			</button>
-			<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:mobile.tags.mk-note-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">
-		<x-sub v-for="note in replies" :key="note.id" :note="note"/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
-import parse from '../../../../../text/parse';
-
-import MkNoteMenu from '../../../common/views/components/note-menu.vue';
-import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
-import XSub from './note-detail.sub.vue';
-
-export default Vue.extend({
-	components: {
-		XSub
-	},
-
-	props: {
-		note: {
-			type: Object,
-			required: true
-		},
-		compact: {
-			default: false
-		}
-	},
-
-	data() {
-		return {
-			context: [],
-			contextFetching: false,
-			replies: []
-		};
-	},
-
-	computed: {
-		acct(): string {
-			return getAcct(this.note.user);
-		},
-		name(): string {
-			return getUserName(this.note.user);
-		},
-		pAcct(): string {
-			return getAcct(this.p.user);
-		},
-		pName(): string {
-			return getUserName(this.p.user);
-		},
-		isRenote(): boolean {
-			return (this.note.renote &&
-				this.note.text == null &&
-				this.note.mediaIds == null &&
-				this.note.poll == null);
-		},
-		p(): any {
-			return this.isRenote ? this.note.renote : this.note;
-		},
-		reactionsCount(): number {
-			return this.p.reactionCounts
-				? Object.keys(this.p.reactionCounts)
-					.map(key => this.p.reactionCounts[key])
-					.reduce((a, b) => a + b)
-				: 0;
-		},
-		urls(): string[] {
-			if (this.p.text) {
-				const ast = parse(this.p.text);
-				return 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 as any).api('notes/replies', {
-				noteId: this.p.id,
-				limit: 8
-			}).then(replies => {
-				this.replies = replies;
-			});
-		}
-
-		// Draw map
-		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true;
-			if (shouldShowMap) {
-				(this as any).os.getGoogleMaps().then(maps => {
-					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
-					const map = new maps.Map(this.$refs.map, {
-						center: uluru,
-						zoom: 15
-					});
-					new maps.Marker({
-						position: uluru,
-						map: map
-					});
-				});
-			}
-		}
-	},
-
-	methods: {
-		fetchContext() {
-			this.contextFetching = true;
-
-			// Fetch context
-			(this as any).api('notes/context', {
-				noteId: this.p.replyId
-			}).then(context => {
-				this.contextFetching = false;
-				this.context = context.reverse();
-			});
-		},
-		reply() {
-			(this as any).apis.post({
-				reply: this.p
-			});
-		},
-		renote() {
-			(this as any).apis.post({
-				renote: this.p
-			});
-		},
-		react() {
-			(this as any).os.new(MkReactionPicker, {
-				source: this.$refs.reactButton,
-				note: this.p,
-				compact: true
-			});
-		},
-		menu() {
-			(this as any).os.new(MkNoteMenu, {
-				source: this.$refs.menuButton,
-				note: this.p,
-				compact: true
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-@import '~const.styl'
-
-.mk-note-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
-
-	> .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
-
-	> .renote
-		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
-
-			> .renote
-				margin 8px 0
-
-				> .mk-note-preview
-					padding 16px
-					border dashed 1px #c0dac6
-					border-radius 8px
-
-			> .location
-				margin 4px 0
-				font-size 12px
-				color #ccc
-
-			> .map
-				width 100%
-				height 200px
-
-				&:empty
-					display none
-
-			> .mk-url-preview
-				margin-top 8px
-
-			> .media
-				> img
-					display block
-					max-width 100%
-
-			> .tags
-				margin 4px 0 0 0
-
-				> *
-					display inline-block
-					margin 0 8px 0 0
-					padding 2px 8px 2px 16px
-					font-size 90%
-					color #8d969e
-					background #edf0f3
-					border-radius 4px
-
-					&:before
-						content ""
-						display block
-						position absolute
-						top 0
-						bottom 0
-						left 4px
-						width 8px
-						height 8px
-						margin auto 0
-						background #fff
-						border-radius 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>
-
-<style lang="stylus" module>
-.text
-	display block
-	margin 0
-	padding 0
-	overflow-wrap break-word
-	font-size 16px
-	color #717171
-
-	@media (min-width 500px)
-		font-size 24px
-
-</style>
diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue
index 6fcbbb47e..70be6db7b 100644
--- a/src/client/app/mobile/views/components/post-form.vue
+++ b/src/client/app/mobile/views/components/post-form.vue
@@ -111,7 +111,7 @@ export default Vue.extend({
 		},
 		post() {
 			this.posting = true;
-			const viaMobile = (this as any).os.i.account.clientSettings.disableViaMobile !== true;
+			const viaMobile = (this as any).os.i.clientSettings.disableViaMobile !== true;
 			(this as any).api('notes/create', {
 				text: this.text == '' ? undefined : this.text,
 				mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
diff --git a/src/client/app/mobile/views/components/post.vue b/src/client/app/mobile/views/components/post.vue
deleted file mode 100644
index 4b33c6f07..000000000
--- a/src/client/app/mobile/views/components/post.vue
+++ /dev/null
@@ -1,540 +0,0 @@
-<template>
-<div class="note" :class="{ renote: isRenote }">
-	<div class="reply-to" v-if="p.reply">
-		<x-sub :note="p.reply"/>
-	</div>
-	<div class="renote" v-if="isRenote">
-		<p>
-			<router-link class="avatar-anchor" :to="`/@${acct}`">
-				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-			</router-link>
-			%fa:retweet%
-			<span>{{ '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.substr(0, '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.indexOf('{')) }}</span>
-			<router-link class="name" :to="`/@${acct}`">{{ name }}</router-link>
-			<span>{{ '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.substr('%i18n:mobile.tags.mk-timeline-note.reposted-by%'.indexOf('}') + 1) }}</span>
-		</p>
-		<mk-time :time="note.createdAt"/>
-	</div>
-	<article>
-		<router-link class="avatar-anchor" :to="`/@${pAcct}`">
-			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
-		</router-link>
-		<div class="main">
-			<header>
-				<router-link class="name" :to="`/@${pAcct}`">{{ pName }}</router-link>
-				<span class="is-bot" v-if="p.user.host === null && p.user.account.isBot">bot</span>
-				<span class="username">@{{ pAcct }}</span>
-				<div class="info">
-					<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
-					<router-link class="created-at" :to="url">
-						<mk-time :time="p.createdAt"/>
-					</router-link>
-				</div>
-			</header>
-			<div class="body">
-				<p class="channel" v-if="p.channel != null"><a target="_blank">{{ p.channel.title }}</a>:</p>
-				<div class="text">
-					<a class="reply" v-if="p.reply">
-						%fa:reply%
-					</a>
-					<mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/>
-					<a class="rp" v-if="p.renote != null">RP:</a>
-				</div>
-				<div class="media" v-if="p.media.length > 0">
-					<mk-media-list :media-list="p.media"/>
-				</div>
-				<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
-				<div class="tags" v-if="p.tags && p.tags.length > 0">
-					<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
-				</div>
-				<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
-				<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
-				<div class="map" v-if="p.geo" ref="map"></div>
-				<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
-				<div class="renote" v-if="p.renote">
-					<mk-note-preview :note="p.renote"/>
-				</div>
-			</div>
-			<footer>
-				<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
-				<button @click="reply">
-					%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
-				</button>
-				<button @click="renote" title="Renote">
-					%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
-				</button>
-				<button :class="{ reacted: p.myReaction != 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 getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
-import parse from '../../../../../text/parse';
-
-import MkNoteMenu from '../../../common/views/components/note-menu.vue';
-import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
-import XSub from './note.sub.vue';
-
-export default Vue.extend({
-	components: {
-		XSub
-	},
-
-	props: ['note'],
-
-	data() {
-		return {
-			connection: null,
-			connectionId: null
-		};
-	},
-
-	computed: {
-		acct(): string {
-			return getAcct(this.note.user);
-		},
-		name(): string {
-			return getUserName(this.note.user);
-		},
-		pAcct(): string {
-			return getAcct(this.p.user);
-		},
-		pName(): string {
-			return getUserName(this.p.user);
-		},
-		isRenote(): boolean {
-			return (this.note.renote &&
-				this.note.text == null &&
-				this.note.mediaIds == null &&
-				this.note.poll == null);
-		},
-		p(): any {
-			return this.isRenote ? this.note.renote : this.note;
-		},
-		reactionsCount(): number {
-			return this.p.reactionCounts
-				? Object.keys(this.p.reactionCounts)
-					.map(key => this.p.reactionCounts[key])
-					.reduce((a, b) => a + b)
-				: 0;
-		},
-		url(): string {
-			return `/@${this.pAcct}/${this.p.id}`;
-		},
-		urls(): string[] {
-			if (this.p.text) {
-				const ast = parse(this.p.text);
-				return ast
-					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
-					.map(t => t.url);
-			} else {
-				return null;
-			}
-		}
-	},
-
-	created() {
-		if ((this as any).os.isSignedIn) {
-			this.connection = (this as any).os.stream.getConnection();
-			this.connectionId = (this as any).os.stream.use();
-		}
-	},
-
-	mounted() {
-		this.capture(true);
-
-		if ((this as any).os.isSignedIn) {
-			this.connection.on('_connected_', this.onStreamConnected);
-		}
-
-		// Draw map
-		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.account.clientSettings.showMaps : true;
-			if (shouldShowMap) {
-				(this as any).os.getGoogleMaps().then(maps => {
-					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
-					const map = new maps.Map(this.$refs.map, {
-						center: uluru,
-						zoom: 15
-					});
-					new maps.Marker({
-						position: uluru,
-						map: map
-					});
-				});
-			}
-		}
-	},
-
-	beforeDestroy() {
-		this.decapture(true);
-
-		if ((this as any).os.isSignedIn) {
-			this.connection.off('_connected_', this.onStreamConnected);
-			(this as any).os.stream.dispose(this.connectionId);
-		}
-	},
-
-	methods: {
-		capture(withHandler = false) {
-			if ((this as any).os.isSignedIn) {
-				this.connection.send({
-					type: 'capture',
-					id: this.p.id
-				});
-				if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
-			}
-		},
-		decapture(withHandler = false) {
-			if ((this as any).os.isSignedIn) {
-				this.connection.send({
-					type: 'decapture',
-					id: this.p.id
-				});
-				if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated);
-			}
-		},
-		onStreamConnected() {
-			this.capture();
-		},
-		onStreamNoteUpdated(data) {
-			const note = data.note;
-			if (note.id == this.note.id) {
-				this.$emit('update:note', note);
-			} else if (note.id == this.note.renoteId) {
-				this.note.renote = note;
-			}
-		},
-		reply() {
-			(this as any).apis.post({
-				reply: this.p
-			});
-		},
-		renote() {
-			(this as any).apis.post({
-				renote: this.p
-			});
-		},
-		react() {
-			(this as any).os.new(MkReactionPicker, {
-				source: this.$refs.reactButton,
-				note: this.p,
-				compact: true
-			});
-		},
-		menu() {
-			(this as any).os.new(MkNoteMenu, {
-				source: this.$refs.menuButton,
-				note: this.p,
-				compact: true
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-@import '~const.styl'
-
-.note
-	font-size 12px
-	border-bottom solid 1px #eaeaea
-
-	&:first-child
-		border-radius 8px 8px 0 0
-
-		> .renote
-			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
-
-	> .renote
-		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-note-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
-				align-items center
-				white-space nowrap
-
-				@media (min-width 500px)
-					margin-bottom 2px
-
-				> .name
-					display block
-					margin 0 0.5em 0 0
-					padding 0
-					overflow hidden
-					color #627079
-					font-size 1em
-					font-weight bold
-					text-decoration none
-					text-overflow ellipsis
-
-					&:hover
-						text-decoration underline
-
-				> .is-bot
-					margin 0 0.5em 0 0
-					padding 1px 6px
-					font-size 12px
-					color #aaa
-					border solid 1px #ddd
-					border-radius 3px
-
-				> .username
-					margin 0 0.5em 0 0
-					color #ccc
-
-				> .info
-					margin-left auto
-					font-size 0.9em
-
-					> .mobile
-						margin-right 6px
-						color #c0c0c0
-
-					> .created-at
-						color #c0c0c0
-
-			> .body
-
-				> .text
-					display block
-					margin 0
-					padding 0
-					overflow-wrap break-word
-					font-size 1.1em
-					color #717171
-
-					>>> .quote
-						margin 8px
-						padding 6px 12px
-						color #aaa
-						border-left solid 3px #eee
-
-					> .reply
-						margin-right 8px
-						color #717171
-
-					> .rp
-						margin-left 4px
-						font-style oblique
-						color #a0bf46
-
-					[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-url-preview
-					margin-top 8px
-
-				> .channel
-					margin 0
-
-				> .tags
-					margin 4px 0 0 0
-
-					> *
-						display inline-block
-						margin 0 8px 0 0
-						padding 2px 8px 2px 16px
-						font-size 90%
-						color #8d969e
-						background #edf0f3
-						border-radius 4px
-
-						&:before
-							content ""
-							display block
-							position absolute
-							top 0
-							bottom 0
-							left 4px
-							width 8px
-							height 8px
-							margin auto 0
-							background #fff
-							border-radius 100%
-
-				> .media
-					> img
-						display block
-						max-width 100%
-
-				> .location
-					margin 4px 0
-					font-size 12px
-					color #ccc
-
-				> .map
-					width 100%
-					height 200px
-
-					&:empty
-						display none
-
-				> .app
-					font-size 12px
-					color #ccc
-
-				> .mk-poll
-					font-size 80%
-
-				> .renote
-					margin 8px 0
-
-					> .mk-note-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>
-
-<style lang="stylus" module>
-.text
-	code
-		padding 4px 8px
-		margin 0 0.5em
-		font-size 80%
-		color #525252
-		background #f8f8f8
-		border-radius 2px
-
-	pre > code
-		padding 16px
-		margin 0
-</style>
diff --git a/src/client/app/mobile/views/components/ui.header.vue b/src/client/app/mobile/views/components/ui.header.vue
index fd4f31fd9..f664341cd 100644
--- a/src/client/app/mobile/views/components/ui.header.vue
+++ b/src/client/app/mobile/views/components/ui.header.vue
@@ -63,9 +63,9 @@ export default Vue.extend({
 				}
 			});
 
-			const ago = (new Date().getTime() - new Date((this as any).os.i.account.lastUsedAt).getTime()) / 1000
+			const ago = (new Date().getTime() - new Date((this as any).os.i.lastUsedAt).getTime()) / 1000
 			const isHisasiburi = ago >= 3600;
-			(this as any).os.i.account.lastUsedAt = new Date();
+			(this as any).os.i.lastUsedAt = new Date();
 			if (isHisasiburi) {
 				(this.$refs.welcomeback as any).style.display = 'block';
 				(this.$refs.main as any).style.overflow = 'hidden';
diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue
index ab61166cf..3de2ba1c3 100644
--- a/src/client/app/mobile/views/pages/home.vue
+++ b/src/client/app/mobile/views/pages/home.vue
@@ -82,8 +82,8 @@ export default Vue.extend({
 		};
 	},
 	created() {
-		if ((this as any).os.i.account.clientSettings.mobileHome == null) {
-			Vue.set((this as any).os.i.account.clientSettings, 'mobileHome', [{
+		if ((this as any).os.i.clientSettings.mobileHome == null) {
+			Vue.set((this as any).os.i.clientSettings, 'mobileHome', [{
 				name: 'calendar',
 				id: 'a', data: {}
 			}, {
@@ -105,14 +105,14 @@ export default Vue.extend({
 				name: 'version',
 				id: 'g', data: {}
 			}]);
-			this.widgets = (this as any).os.i.account.clientSettings.mobileHome;
+			this.widgets = (this as any).os.i.clientSettings.mobileHome;
 			this.saveHome();
 		} else {
-			this.widgets = (this as any).os.i.account.clientSettings.mobileHome;
+			this.widgets = (this as any).os.i.clientSettings.mobileHome;
 		}
 
-		this.$watch('os.i.account.clientSettings', i => {
-			this.widgets = (this as any).os.i.account.clientSettings.mobileHome;
+		this.$watch('os.i.clientSettings', i => {
+			this.widgets = (this as any).os.i.clientSettings.mobileHome;
 		}, {
 			deep: true
 		});
@@ -157,15 +157,15 @@ export default Vue.extend({
 		},
 		onHomeUpdated(data) {
 			if (data.home) {
-				(this as any).os.i.account.clientSettings.mobileHome = data.home;
+				(this as any).os.i.clientSettings.mobileHome = data.home;
 				this.widgets = data.home;
 			} else {
-				const w = (this as any).os.i.account.clientSettings.mobileHome.find(w => w.id == data.id);
+				const w = (this as any).os.i.clientSettings.mobileHome.find(w => w.id == data.id);
 				if (w != null) {
 					w.data = data.data;
 					this.$refs[w.id][0].preventSave = true;
 					this.$refs[w.id][0].props = w.data;
-					this.widgets = (this as any).os.i.account.clientSettings.mobileHome;
+					this.widgets = (this as any).os.i.clientSettings.mobileHome;
 				}
 			}
 		},
@@ -194,7 +194,7 @@ export default Vue.extend({
 			this.saveHome();
 		},
 		saveHome() {
-			(this as any).os.i.account.clientSettings.mobileHome = this.widgets;
+			(this as any).os.i.clientSettings.mobileHome = this.widgets;
 			(this as any).api('i/update_mobile_home', {
 				home: this.widgets
 			});
diff --git a/src/client/app/mobile/views/pages/profile-setting.vue b/src/client/app/mobile/views/pages/profile-setting.vue
index 4a560c027..7f0ff5aad 100644
--- a/src/client/app/mobile/views/pages/profile-setting.vue
+++ b/src/client/app/mobile/views/pages/profile-setting.vue
@@ -53,9 +53,9 @@ export default Vue.extend({
 	},
 	created() {
 		this.name = (this as any).os.i.name || '';
-		this.location = (this as any).os.i.account.profile.location;
+		this.location = (this as any).os.i.profile.location;
 		this.description = (this as any).os.i.description;
-		this.birthday = (this as any).os.i.account.profile.birthday;
+		this.birthday = (this as any).os.i.profile.birthday;
 	},
 	mounted() {
 		document.title = 'Misskey | %i18n:mobile.tags.mk-profile-setting-page.title%';
diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue
index 08fa4e277..f33f209db 100644
--- a/src/client/app/mobile/views/pages/user.vue
+++ b/src/client/app/mobile/views/pages/user.vue
@@ -18,11 +18,11 @@
 				</div>
 				<div class="description">{{ user.description }}</div>
 				<div class="info">
-					<p class="location" v-if="user.host === null && user.account.profile.location">
-						%fa:map-marker%{{ user.account.profile.location }}
+					<p class="location" v-if="user.host === null && user.profile.location">
+						%fa:map-marker%{{ user.profile.location }}
 					</p>
-					<p class="birthday" v-if="user.host === null && user.account.profile.birthday">
-						%fa:birthday-cake%{{ user.account.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳)
+					<p class="birthday" v-if="user.host === null && user.profile.birthday">
+						%fa:birthday-cake%{{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳)
 					</p>
 				</div>
 				<div class="status">
@@ -81,7 +81,7 @@ export default Vue.extend({
 			return this.getAcct(this.user);
 		},
 		age(): number {
-			return age(this.user.account.profile.birthday);
+			return age(this.user.profile.birthday);
 		},
 		name() {
 			return getUserName(this.user);
diff --git a/src/client/app/mobile/views/pages/user/home.vue b/src/client/app/mobile/views/pages/user/home.vue
index 255408496..c0cd9b8da 100644
--- a/src/client/app/mobile/views/pages/user/home.vue
+++ b/src/client/app/mobile/views/pages/user/home.vue
@@ -31,7 +31,7 @@
 			<x-followers-you-know :user="user"/>
 		</div>
 	</section>
-	<p v-if="user.host === null">%i18n:mobile.tags.mk-user-overview.last-used-at%: <b><mk-time :time="user.account.lastUsedAt"/></b></p>
+	<p v-if="user.host === null">%i18n:mobile.tags.mk-user-overview.last-used-at%: <b><mk-time :time="user.lastUsedAt"/></b></p>
 </div>
 </template>
 
diff --git a/src/client/app/mobile/views/pages/welcome.vue b/src/client/app/mobile/views/pages/welcome.vue
index 17cdf9306..27baf8bee 100644
--- a/src/client/app/mobile/views/pages/welcome.vue
+++ b/src/client/app/mobile/views/pages/welcome.vue
@@ -8,7 +8,7 @@
 			<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.account.twoFactorEnabled" v-model="token" type="number" placeholder="トークン" required/>
+				<input v-if="user && user.twoFactorEnabled" v-model="token" type="number" placeholder="トークン" required/>
 				<button type="submit" :disabled="signing">{{ signing ? 'ログインしています' : 'ログイン' }}</button>
 			</form>
 			<div>
@@ -70,7 +70,7 @@ export default Vue.extend({
 			(this as any).api('signin', {
 				username: this.username,
 				password: this.password,
-				token: this.user && this.user.account.twoFactorEnabled ? this.token : undefined
+				token: this.user && this.user.twoFactorEnabled ? this.token : undefined
 			}).then(() => {
 				location.reload();
 			}).catch(() => {
diff --git a/src/models/user.ts b/src/models/user.ts
index f86aefe9a..906bcb533 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -11,7 +11,7 @@ import config from '../config';
 const User = db.get<IUser>('users');
 
 User.createIndex('username');
-User.createIndex('account.token');
+User.createIndex('token');
 
 export default User;
 
@@ -40,45 +40,41 @@ type IUserBase = {
 
 export interface ILocalUser extends IUserBase {
 	host: null;
-	account: {
-		keypair: string;
-		email: string;
-		links: string[];
-		password: string;
-		token: string;
-		twitter: {
-			accessToken: string;
-			accessTokenSecret: string;
-			userId: string;
-			screenName: string;
-		};
-		line: {
-			userId: string;
-		};
-		profile: {
-			location: string;
-			birthday: string; // 'YYYY-MM-DD'
-			tags: string[];
-		};
-		lastUsedAt: Date;
-		isBot: boolean;
-		isPro: boolean;
-		twoFactorSecret: string;
-		twoFactorEnabled: boolean;
-		twoFactorTempSecret: string;
-		clientSettings: any;
-		settings: any;
+	keypair: string;
+	email: string;
+	links: string[];
+	password: string;
+	token: string;
+	twitter: {
+		accessToken: string;
+		accessTokenSecret: string;
+		userId: string;
+		screenName: string;
 	};
+	line: {
+		userId: string;
+	};
+	profile: {
+		location: string;
+		birthday: string; // 'YYYY-MM-DD'
+		tags: string[];
+	};
+	lastUsedAt: Date;
+	isBot: boolean;
+	isPro: boolean;
+	twoFactorSecret: string;
+	twoFactorEnabled: boolean;
+	twoFactorTempSecret: string;
+	clientSettings: any;
+	settings: any;
 }
 
 export interface IRemoteUser extends IUserBase {
-	account: {
-		inbox: string;
-		uri: string;
-		publicKey: {
-			id: string;
-			publicKeyPem: string;
-		};
+	inbox: string;
+	uri: string;
+	publicKey: {
+		id: string;
+		publicKeyPem: string;
 	};
 }
 
@@ -150,11 +146,11 @@ export const pack = (
 
 	const fields = opts.detail ? {
 	} : {
-		'account.settings': false,
-		'account.clientSettings': false,
-		'account.profile': false,
-		'account.keywords': false,
-		'account.domains': false
+		settings: false,
+		clientSettings: false,
+		profile: false,
+		keywords: false,
+		domains: false
 	};
 
 	// Populate the user if 'user' is ID
@@ -188,29 +184,29 @@ export const pack = (
 	// Remove needless properties
 	delete _user.latestNote;
 
-	if (!_user.host) {
+	if (_user.host == null) {
 		// Remove private properties
-		delete _user.account.keypair;
-		delete _user.account.password;
-		delete _user.account.token;
-		delete _user.account.twoFactorTempSecret;
-		delete _user.account.twoFactorSecret;
+		delete _user.keypair;
+		delete _user.password;
+		delete _user.token;
+		delete _user.twoFactorTempSecret;
+		delete _user.twoFactorSecret;
 		delete _user.usernameLower;
-		if (_user.account.twitter) {
-			delete _user.account.twitter.accessToken;
-			delete _user.account.twitter.accessTokenSecret;
+		if (_user.twitter) {
+			delete _user.twitter.accessToken;
+			delete _user.twitter.accessTokenSecret;
 		}
-		delete _user.account.line;
+		delete _user.line;
 
 		// Visible via only the official client
 		if (!opts.includeSecrets) {
-			delete _user.account.email;
-			delete _user.account.settings;
-			delete _user.account.clientSettings;
+			delete _user.email;
+			delete _user.settings;
+			delete _user.clientSettings;
 		}
 
 		if (!opts.detail) {
-			delete _user.account.twoFactorEnabled;
+			delete _user.twoFactorEnabled;
 		}
 	}
 
diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts
index eb4b62d37..6608907a7 100644
--- a/src/queue/processors/http/process-inbox.ts
+++ b/src/queue/processors/http/process-inbox.ts
@@ -36,7 +36,7 @@ export default async (job: kue.Job, done): Promise<void> => {
 	} else {
 		user = await User.findOne({
 			host: { $ne: null },
-			'account.publicKey.id': signature.keyId
+			'publicKey.id': signature.keyId
 		}) as IRemoteUser;
 
 		// アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する
@@ -50,7 +50,7 @@ export default async (job: kue.Job, done): Promise<void> => {
 		return;
 	}
 
-	if (!verifySignature(signature, user.account.publicKey.publicKeyPem)) {
+	if (!verifySignature(signature, user.publicKey.publicKeyPem)) {
 		console.warn('signature verification failed');
 		done();
 		return;
diff --git a/src/remote/activitypub/act/create/image.ts b/src/remote/activitypub/act/create/image.ts
index 30a75e737..c87423c5f 100644
--- a/src/remote/activitypub/act/create/image.ts
+++ b/src/remote/activitypub/act/create/image.ts
@@ -7,7 +7,7 @@ import { IDriveFile } from '../../../../models/drive-file';
 const log = debug('misskey:activitypub');
 
 export default async function(actor: IRemoteUser, image): Promise<IDriveFile> {
-	if ('attributedTo' in image && actor.account.uri !== image.attributedTo) {
+	if ('attributedTo' in image && actor.uri !== image.attributedTo) {
 		log(`invalid image: ${JSON.stringify(image, null, 2)}`);
 		throw new Error('invalid image');
 	}
diff --git a/src/remote/activitypub/act/create/index.ts b/src/remote/activitypub/act/create/index.ts
index dd0b11214..7cb9b0844 100644
--- a/src/remote/activitypub/act/create/index.ts
+++ b/src/remote/activitypub/act/create/index.ts
@@ -9,7 +9,7 @@ import { ICreate } from '../../type';
 const log = debug('misskey:activitypub');
 
 export default async (actor: IRemoteUser, activity: ICreate): Promise<void> => {
-	if ('actor' in activity && actor.account.uri !== activity.actor) {
+	if ('actor' in activity && actor.uri !== activity.actor) {
 		throw new Error('invalid actor');
 	}
 
diff --git a/src/remote/activitypub/act/delete/index.ts b/src/remote/activitypub/act/delete/index.ts
index 6c6faa1ae..10b47dc4c 100644
--- a/src/remote/activitypub/act/delete/index.ts
+++ b/src/remote/activitypub/act/delete/index.ts
@@ -7,7 +7,7 @@ import { IRemoteUser } from '../../../../models/user';
  * 削除アクティビティを捌きます
  */
 export default async (actor: IRemoteUser, activity): Promise<void> => {
-	if ('actor' in activity && actor.account.uri !== activity.actor) {
+	if ('actor' in activity && actor.uri !== activity.actor) {
 		throw new Error('invalid actor');
 	}
 
diff --git a/src/remote/activitypub/act/undo/index.ts b/src/remote/activitypub/act/undo/index.ts
index 3ede9fcfb..71f547aeb 100644
--- a/src/remote/activitypub/act/undo/index.ts
+++ b/src/remote/activitypub/act/undo/index.ts
@@ -8,7 +8,7 @@ import Resolver from '../../resolver';
 const log = debug('misskey:activitypub');
 
 export default async (actor: IRemoteUser, activity: IUndo): Promise<void> => {
-	if ('actor' in activity && actor.account.uri !== activity.actor) {
+	if ('actor' in activity && actor.uri !== activity.actor) {
 		throw new Error('invalid actor');
 	}
 
diff --git a/src/remote/activitypub/renderer/follow.ts b/src/remote/activitypub/renderer/follow.ts
index 6d1ded9a9..0a1ae1a4b 100644
--- a/src/remote/activitypub/renderer/follow.ts
+++ b/src/remote/activitypub/renderer/follow.ts
@@ -4,5 +4,5 @@ import { IRemoteUser } from '../../../models/user';
 export default ({ username }, followee: IRemoteUser) => ({
 	type: 'Follow',
 	actor: `${config.url}/@${username}`,
-	object: followee.account.uri
+	object: followee.uri
 });
diff --git a/src/remote/activitypub/renderer/key.ts b/src/remote/activitypub/renderer/key.ts
index 85be7b136..76e2f13bc 100644
--- a/src/remote/activitypub/renderer/key.ts
+++ b/src/remote/activitypub/renderer/key.ts
@@ -6,5 +6,5 @@ export default (user: ILocalUser) => ({
 	id: `${config.url}/@${user.username}/publickey`,
 	type: 'Key',
 	owner: `${config.url}/@${user.username}`,
-	publicKeyPem: extractPublic(user.account.keypair)
+	publicKeyPem: extractPublic(user.keypair)
 });
diff --git a/src/remote/request.ts b/src/remote/request.ts
index a375aebfb..a0c69cf4e 100644
--- a/src/remote/request.ts
+++ b/src/remote/request.ts
@@ -4,10 +4,11 @@ import { URL } from 'url';
 import * as debug from 'debug';
 
 import config from '../config';
+import { ILocalUser } from '../models/user';
 
 const log = debug('misskey:activitypub:deliver');
 
-export default ({ account, username }, url, object) => new Promise((resolve, reject) => {
+export default (user: ILocalUser, url, object) => new Promise((resolve, reject) => {
 	log(`--> ${url}`);
 
 	const { protocol, hostname, port, pathname, search } = new URL(url);
@@ -35,8 +36,8 @@ export default ({ account, username }, url, object) => new Promise((resolve, rej
 
 	sign(req, {
 		authorizationHeaderName: 'Signature',
-		key: account.keypair,
-		keyId: `acct:${username}@${config.host}`
+		key: user.keypair,
+		keyId: `acct:${user.username}@${config.host}`
 	});
 
 	req.end(JSON.stringify(object));
diff --git a/src/renderers/get-user-summary.ts b/src/renderers/get-user-summary.ts
index 52309954d..1bd9a7fb4 100644
--- a/src/renderers/get-user-summary.ts
+++ b/src/renderers/get-user-summary.ts
@@ -11,8 +11,7 @@ export default function(user: IUser): string {
 		`${user.notesCount}投稿、${user.followingCount}フォロー、${user.followersCount}フォロワー\n`;
 
 	if (isLocalUser(user)) {
-		const account = user.account;
-		string += `場所: ${account.profile.location}、誕生日: ${account.profile.birthday}\n`;
+		string += `場所: ${user.profile.location}、誕生日: ${user.profile.birthday}\n`;
 	}
 
 	return string + `「${user.description}」`;
diff --git a/src/server/api/authenticate.ts b/src/server/api/authenticate.ts
index 856674483..adbeeb3b3 100644
--- a/src/server/api/authenticate.ts
+++ b/src/server/api/authenticate.ts
@@ -34,7 +34,7 @@ export default (req: express.Request) => new Promise<IAuthContext>(async (resolv
 
 	if (isNativeToken(token)) {
 		const user: IUser = await User
-			.findOne({ 'account.token': token });
+			.findOne({ 'token': token });
 
 		if (user === null) {
 			return reject('user not found');
diff --git a/src/server/api/bot/core.ts b/src/server/api/bot/core.ts
index 1cf052234..d41af4805 100644
--- a/src/server/api/bot/core.ts
+++ b/src/server/api/bot/core.ts
@@ -226,7 +226,7 @@ class SigninContext extends Context {
 			}
 		} else {
 			// Compare password
-			const same = await bcrypt.compare(query, this.temporaryUser.account.password);
+			const same = await bcrypt.compare(query, this.temporaryUser.password);
 
 			if (same) {
 				this.bot.signin(this.temporaryUser);
diff --git a/src/server/api/bot/interfaces/line.ts b/src/server/api/bot/interfaces/line.ts
index b6b0c257e..be3bfe33d 100644
--- a/src/server/api/bot/interfaces/line.ts
+++ b/src/server/api/bot/interfaces/line.ts
@@ -112,11 +112,11 @@ class LineBot extends BotCore {
 			data: `showtl|${user.id}`
 		});
 
-		if (user.account.twitter) {
+		if (user.twitter) {
 			actions.push({
 				type: 'uri',
 				label: 'Twitterアカウントを見る',
-				uri: `https://twitter.com/${user.account.twitter.screenName}`
+				uri: `https://twitter.com/${user.twitter.screenName}`
 			});
 		}
 
@@ -174,7 +174,7 @@ module.exports = async (app: express.Application) => {
 		if (session == null) {
 			const user = await User.findOne({
 				host: null,
-				'account.line': {
+				'line': {
 					userId: sourceId
 				}
 			});
@@ -184,7 +184,7 @@ module.exports = async (app: express.Application) => {
 			bot.on('signin', user => {
 				User.update(user._id, {
 					$set: {
-						'account.line': {
+						'line': {
 							userId: sourceId
 						}
 					}
@@ -194,7 +194,7 @@ module.exports = async (app: express.Application) => {
 			bot.on('signout', user => {
 				User.update(user._id, {
 					$set: {
-						'account.line': {
+						'line': {
 							userId: null
 						}
 					}
diff --git a/src/server/api/common/signin.ts b/src/server/api/common/signin.ts
index f9688790c..8bb327694 100644
--- a/src/server/api/common/signin.ts
+++ b/src/server/api/common/signin.ts
@@ -2,7 +2,7 @@ import config from '../../../config';
 
 export default function(res, user, redirect: boolean) {
 	const expires = 1000 * 60 * 60 * 24 * 365; // One Year
-	res.cookie('i', user.account.token, {
+	res.cookie('i', user.token, {
 		path: '/',
 		domain: `.${config.hostname}`,
 		secure: config.url.substr(0, 5) === 'https',
diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts
index 0ccac8d83..d3cc549ca 100644
--- a/src/server/api/endpoints/following/create.ts
+++ b/src/server/api/endpoints/following/create.ts
@@ -31,7 +31,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}, {
 		fields: {
 			data: false,
-			'account.profile': false
+			'profile': false
 		}
 	});
 
diff --git a/src/server/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts
index 0684b8750..0d0a6c713 100644
--- a/src/server/api/endpoints/following/delete.ts
+++ b/src/server/api/endpoints/following/delete.ts
@@ -31,7 +31,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}, {
 		fields: {
 			data: false,
-			'account.profile': false
+			'profile': false
 		}
 	});
 
diff --git a/src/server/api/endpoints/i.ts b/src/server/api/endpoints/i.ts
index 44de71d16..0be30500c 100644
--- a/src/server/api/endpoints/i.ts
+++ b/src/server/api/endpoints/i.ts
@@ -5,12 +5,6 @@ import User, { pack } from '../../../models/user';
 
 /**
  * Show myself
- *
- * @param {any} params
- * @param {any} user
- * @param {any} app
- * @param {Boolean} isSecure
- * @return {Promise<any>}
  */
 module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) => {
 	// Serialize
@@ -22,7 +16,7 @@ module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) =>
 	// Update lastUsedAt
 	User.update({ _id: user._id }, {
 		$set: {
-			'account.lastUsedAt': new Date()
+			lastUsedAt: new Date()
 		}
 	});
 });
diff --git a/src/server/api/endpoints/i/2fa/done.ts b/src/server/api/endpoints/i/2fa/done.ts
index 0b2e32c13..3e824feff 100644
--- a/src/server/api/endpoints/i/2fa/done.ts
+++ b/src/server/api/endpoints/i/2fa/done.ts
@@ -28,8 +28,8 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 	await User.update(user._id, {
 		$set: {
-			'account.twoFactorSecret': user.twoFactorTempSecret,
-			'account.twoFactorEnabled': true
+			'twoFactorSecret': user.twoFactorTempSecret,
+			'twoFactorEnabled': true
 		}
 	});
 
diff --git a/src/server/api/endpoints/i/2fa/register.ts b/src/server/api/endpoints/i/2fa/register.ts
index dc7fb959b..bed64a254 100644
--- a/src/server/api/endpoints/i/2fa/register.ts
+++ b/src/server/api/endpoints/i/2fa/register.ts
@@ -14,7 +14,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	if (passwordErr) return rej('invalid password param');
 
 	// Compare password
-	const same = await bcrypt.compare(password, user.account.password);
+	const same = await bcrypt.compare(password, user.password);
 
 	if (!same) {
 		return rej('incorrect password');
diff --git a/src/server/api/endpoints/i/2fa/unregister.ts b/src/server/api/endpoints/i/2fa/unregister.ts
index ff2a435fe..f9d7a25f5 100644
--- a/src/server/api/endpoints/i/2fa/unregister.ts
+++ b/src/server/api/endpoints/i/2fa/unregister.ts
@@ -11,7 +11,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	if (passwordErr) return rej('invalid password param');
 
 	// Compare password
-	const same = await bcrypt.compare(password, user.account.password);
+	const same = await bcrypt.compare(password, user.password);
 
 	if (!same) {
 		return rej('incorrect password');
@@ -19,8 +19,8 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 	await User.update(user._id, {
 		$set: {
-			'account.twoFactorSecret': null,
-			'account.twoFactorEnabled': false
+			'twoFactorSecret': null,
+			'twoFactorEnabled': false
 		}
 	});
 
diff --git a/src/server/api/endpoints/i/change_password.ts b/src/server/api/endpoints/i/change_password.ts
index a38b56a21..57415083f 100644
--- a/src/server/api/endpoints/i/change_password.ts
+++ b/src/server/api/endpoints/i/change_password.ts
@@ -22,7 +22,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	if (newPasswordErr) return rej('invalid newPassword param');
 
 	// Compare password
-	const same = await bcrypt.compare(currentPassword, user.account.password);
+	const same = await bcrypt.compare(currentPassword, user.password);
 
 	if (!same) {
 		return rej('incorrect password');
@@ -34,7 +34,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 	await User.update(user._id, {
 		$set: {
-			'account.password': hash
+			'password': hash
 		}
 	});
 
diff --git a/src/server/api/endpoints/i/regenerate_token.ts b/src/server/api/endpoints/i/regenerate_token.ts
index 9aa6725f8..f9e92c179 100644
--- a/src/server/api/endpoints/i/regenerate_token.ts
+++ b/src/server/api/endpoints/i/regenerate_token.ts
@@ -20,7 +20,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	if (passwordErr) return rej('invalid password param');
 
 	// Compare password
-	const same = await bcrypt.compare(password, user.account.password);
+	const same = await bcrypt.compare(password, user.password);
 
 	if (!same) {
 		return rej('incorrect password');
@@ -31,7 +31,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 	await User.update(user._id, {
 		$set: {
-			'account.token': secret
+			'token': secret
 		}
 	});
 
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index 279b062f5..a8caa0ebc 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -29,12 +29,12 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re
 	// Get 'location' parameter
 	const [location, locationErr] = $(params.location).optional.nullable.string().pipe(isValidLocation).$;
 	if (locationErr) return rej('invalid location param');
-	if (location !== undefined) user.account.profile.location = location;
+	if (location !== undefined) user.profile.location = location;
 
 	// Get 'birthday' parameter
 	const [birthday, birthdayErr] = $(params.birthday).optional.nullable.string().pipe(isValidBirthday).$;
 	if (birthdayErr) return rej('invalid birthday param');
-	if (birthday !== undefined) user.account.profile.birthday = birthday;
+	if (birthday !== undefined) user.profile.birthday = birthday;
 
 	// Get 'avatarId' parameter
 	const [avatarId, avatarIdErr] = $(params.avatarId).optional.id().$;
@@ -49,12 +49,12 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re
 	// Get 'isBot' parameter
 	const [isBot, isBotErr] = $(params.isBot).optional.boolean().$;
 	if (isBotErr) return rej('invalid isBot param');
-	if (isBot != null) user.account.isBot = isBot;
+	if (isBot != null) user.isBot = isBot;
 
 	// Get 'autoWatch' parameter
 	const [autoWatch, autoWatchErr] = $(params.autoWatch).optional.boolean().$;
 	if (autoWatchErr) return rej('invalid autoWatch param');
-	if (autoWatch != null) user.account.settings.autoWatch = autoWatch;
+	if (autoWatch != null) user.settings.autoWatch = autoWatch;
 
 	await User.update(user._id, {
 		$set: {
@@ -62,9 +62,9 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re
 			description: user.description,
 			avatarId: user.avatarId,
 			bannerId: user.bannerId,
-			'account.profile': user.account.profile,
-			'account.isBot': user.account.isBot,
-			'account.settings': user.account.settings
+			'profile': user.profile,
+			'isBot': user.isBot,
+			'settings': user.settings
 		}
 	});
 
diff --git a/src/server/api/endpoints/i/update_client_setting.ts b/src/server/api/endpoints/i/update_client_setting.ts
index 10741aceb..b0d5db5ec 100644
--- a/src/server/api/endpoints/i/update_client_setting.ts
+++ b/src/server/api/endpoints/i/update_client_setting.ts
@@ -22,14 +22,14 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	if (valueErr) return rej('invalid value param');
 
 	const x = {};
-	x[`account.clientSettings.${name}`] = value;
+	x[`clientSettings.${name}`] = value;
 
 	await User.update(user._id, {
 		$set: x
 	});
 
 	// Serialize
-	user.account.clientSettings[name] = value;
+	user.clientSettings[name] = value;
 	const iObj = await pack(user, user, {
 		detail: true,
 		includeSecrets: true
diff --git a/src/server/api/endpoints/i/update_home.ts b/src/server/api/endpoints/i/update_home.ts
index 91be0714d..ce7661ede 100644
--- a/src/server/api/endpoints/i/update_home.ts
+++ b/src/server/api/endpoints/i/update_home.ts
@@ -26,7 +26,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	if (home) {
 		await User.update(user._id, {
 			$set: {
-				'account.clientSettings.home': home
+				'clientSettings.home': home
 			}
 		});
 
@@ -38,7 +38,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	} else {
 		if (id == null && data == null) return rej('you need to set id and data params if home param unset');
 
-		const _home = user.account.clientSettings.home;
+		const _home = user.clientSettings.home;
 		const widget = _home.find(w => w.id == id);
 
 		if (widget == null) return rej('widget not found');
@@ -47,7 +47,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 		await User.update(user._id, {
 			$set: {
-				'account.clientSettings.home': _home
+				'clientSettings.home': _home
 			}
 		});
 
diff --git a/src/server/api/endpoints/i/update_mobile_home.ts b/src/server/api/endpoints/i/update_mobile_home.ts
index 1efda120d..b710e2f33 100644
--- a/src/server/api/endpoints/i/update_mobile_home.ts
+++ b/src/server/api/endpoints/i/update_mobile_home.ts
@@ -25,7 +25,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	if (home) {
 		await User.update(user._id, {
 			$set: {
-				'account.clientSettings.mobileHome': home
+				'clientSettings.mobileHome': home
 			}
 		});
 
@@ -37,7 +37,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	} else {
 		if (id == null && data == null) return rej('you need to set id and data params if home param unset');
 
-		const _home = user.account.clientSettings.mobileHome || [];
+		const _home = user.clientSettings.mobileHome || [];
 		const widget = _home.find(w => w.id == id);
 
 		if (widget == null) return rej('widget not found');
@@ -46,7 +46,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 		await User.update(user._id, {
 			$set: {
-				'account.clientSettings.mobileHome': _home
+				'clientSettings.mobileHome': _home
 			}
 		});
 
diff --git a/src/server/api/endpoints/mute/create.ts b/src/server/api/endpoints/mute/create.ts
index a7fa5f7b4..19894d07a 100644
--- a/src/server/api/endpoints/mute/create.ts
+++ b/src/server/api/endpoints/mute/create.ts
@@ -30,7 +30,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}, {
 		fields: {
 			data: false,
-			'account.profile': false
+			'profile': false
 		}
 	});
 
diff --git a/src/server/api/endpoints/mute/delete.ts b/src/server/api/endpoints/mute/delete.ts
index 687f01033..10096352b 100644
--- a/src/server/api/endpoints/mute/delete.ts
+++ b/src/server/api/endpoints/mute/delete.ts
@@ -30,7 +30,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}, {
 		fields: {
 			data: false,
-			'account.profile': false
+			'profile': false
 		}
 	});
 
diff --git a/src/server/api/endpoints/notes/polls/vote.ts b/src/server/api/endpoints/notes/polls/vote.ts
index 0e27f87ee..fd4412ad3 100644
--- a/src/server/api/endpoints/notes/polls/vote.ts
+++ b/src/server/api/endpoints/notes/polls/vote.ts
@@ -100,7 +100,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		});
 
 	// この投稿をWatchする
-	if (user.account.settings.autoWatch !== false) {
+	if (user.settings.autoWatch !== false) {
 		watch(user._id, note);
 	}
 });
diff --git a/src/server/api/endpoints/users/recommendation.ts b/src/server/api/endpoints/users/recommendation.ts
index 60483936f..2de22da13 100644
--- a/src/server/api/endpoints/users/recommendation.ts
+++ b/src/server/api/endpoints/users/recommendation.ts
@@ -32,7 +32,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 			},
 			$or: [
 				{
-					'account.lastUsedAt': {
+					'lastUsedAt': {
 						$gte: new Date(Date.now() - ms('7days'))
 					}
 				}, {
diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts
index e0bd67d1c..d7c4832c9 100644
--- a/src/server/api/private/signin.ts
+++ b/src/server/api/private/signin.ts
@@ -37,7 +37,7 @@ export default async (req: express.Request, res: express.Response) => {
 	}, {
 		fields: {
 			data: false,
-			'account.profile': false
+			'profile': false
 		}
 	}) as ILocalUser;
 
@@ -48,15 +48,13 @@ export default async (req: express.Request, res: express.Response) => {
 		return;
 	}
 
-	const account = user.account;
-
 	// Compare password
-	const same = await bcrypt.compare(password, account.password);
+	const same = await bcrypt.compare(password, password);
 
 	if (same) {
-		if (account.twoFactorEnabled) {
+		if (user.twoFactorEnabled) {
 			const verified = (speakeasy as any).totp.verify({
-				secret: account.twoFactorSecret,
+				secret: user.twoFactorSecret,
 				encoding: 'base32',
 				token: token
 			});
diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts
index 5818ba25c..f441e1b75 100644
--- a/src/server/api/private/signup.ts
+++ b/src/server/api/private/signup.ts
@@ -119,44 +119,29 @@ export default async (req: express.Request, res: express.Response) => {
 		usernameLower: username.toLowerCase(),
 		host: null,
 		hostLower: null,
-		account: {
-			keypair: generateKeypair(),
-			token: secret,
-			email: null,
-			links: null,
-			password: hash,
-			profile: {
-				bio: null,
-				birthday: null,
-				blood: null,
-				gender: null,
-				handedness: null,
-				height: null,
-				location: null,
-				weight: null
-			},
-			settings: {
-				autoWatch: true
-			},
-			clientSettings: {
-				home: homeData
-			}
+		keypair: generateKeypair(),
+		token: secret,
+		email: null,
+		links: null,
+		password: hash,
+		profile: {
+			bio: null,
+			birthday: null,
+			blood: null,
+			gender: null,
+			handedness: null,
+			height: null,
+			location: null,
+			weight: null
+		},
+		settings: {
+			autoWatch: true
+		},
+		clientSettings: {
+			home: homeData
 		}
 	});
 
 	// Response
 	res.send(await pack(account));
-
-	// Create search index
-	if (config.elasticsearch.enable) {
-		const es = require('../../db/elasticsearch');
-		es.index({
-			index: 'misskey',
-			type: 'user',
-			id: account._id.toString(),
-			body: {
-				username: username
-			}
-		});
-	}
 };
diff --git a/src/server/api/service/twitter.ts b/src/server/api/service/twitter.ts
index 77b932b13..da48e30a8 100644
--- a/src/server/api/service/twitter.ts
+++ b/src/server/api/service/twitter.ts
@@ -40,10 +40,10 @@ module.exports = (app: express.Application) => {
 
 		const user = await User.findOneAndUpdate({
 			host: null,
-			'account.token': userToken
+			'token': userToken
 		}, {
 			$set: {
-				'account.twitter': null
+				'twitter': null
 			}
 		});
 
@@ -128,7 +128,7 @@ module.exports = (app: express.Application) => {
 
 				const user = await User.findOne({
 					host: null,
-					'account.twitter.userId': result.userId
+					'twitter.userId': result.userId
 				});
 
 				if (user == null) {
@@ -151,10 +151,10 @@ module.exports = (app: express.Application) => {
 
 				const user = await User.findOneAndUpdate({
 					host: null,
-					'account.token': userToken
+					'token': userToken
 				}, {
 					$set: {
-						'account.twitter': {
+						'twitter': {
 							accessToken: result.accessToken,
 							accessTokenSecret: result.accessTokenSecret,
 							userId: result.userId,
diff --git a/src/server/api/stream/home.ts b/src/server/api/stream/home.ts
index 313558851..359ef74af 100644
--- a/src/server/api/stream/home.ts
+++ b/src/server/api/stream/home.ts
@@ -74,7 +74,7 @@ export default async function(request: websocket.request, connection: websocket.
 				// Update lastUsedAt
 				User.update({ _id: user._id }, {
 					$set: {
-						'account.lastUsedAt': new Date()
+						'lastUsedAt': new Date()
 					}
 				});
 				break;
diff --git a/src/server/api/streaming.ts b/src/server/api/streaming.ts
index edcf505d2..26946b524 100644
--- a/src/server/api/streaming.ts
+++ b/src/server/api/streaming.ts
@@ -97,7 +97,7 @@ function authenticate(token: string): Promise<IUser> {
 			const user: IUser = await User
 				.findOne({
 					host: null,
-					'account.token': token
+					'token': token
 				});
 
 			resolve(user);
diff --git a/src/services/following/create.ts b/src/services/following/create.ts
index d919f4487..31e3be19e 100644
--- a/src/services/following/create.ts
+++ b/src/services/following/create.ts
@@ -60,13 +60,13 @@ export default async function(follower: IUser, followee: IUser, activity?) {
 		const content = renderFollow(follower, followee);
 		content['@context'] = context;
 
-		deliver(follower, content, followee.account.inbox).save();
+		deliver(follower, content, followee.inbox).save();
 	}
 
 	if (isRemoteUser(follower) && isLocalUser(followee)) {
 		const content = renderAccept(activity);
 		content['@context'] = context;
 
-		deliver(followee, content, follower.account.inbox).save();
+		deliver(followee, content, follower.inbox).save();
 	}
 }
diff --git a/src/services/following/delete.ts b/src/services/following/delete.ts
index 364a4803b..d79bf64f5 100644
--- a/src/services/following/delete.ts
+++ b/src/services/following/delete.ts
@@ -59,6 +59,6 @@ export default async function(follower: IUser, followee: IUser, activity?) {
 		const content = renderUndo(renderFollow(follower, followee));
 		content['@context'] = context;
 
-		deliver(follower, content, followee.account.inbox).save();
+		deliver(follower, content, followee.inbox).save();
 	}
 }
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index 8eee8f44a..551d61856 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -78,7 +78,7 @@ export default async (user: IUser, data: {
 			host: user.host,
 			hostLower: user.hostLower,
 			account: isLocalUser(user) ? {} : {
-				inbox: user.account.inbox
+				inbox: user.inbox
 			}
 		}
 	};
@@ -133,7 +133,7 @@ export default async (user: IUser, data: {
 
 			// 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送
 			if (data.reply && isLocalUser(user) && isRemoteUser(data.reply._user)) {
-				deliver(user, content, data.reply._user.account.inbox).save();
+				deliver(user, content, data.reply._user.inbox).save();
 			}
 
 			Promise.all(followers.map(follower => {
@@ -145,7 +145,7 @@ export default async (user: IUser, data: {
 				} else {
 					// フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信
 					if (isLocalUser(user)) {
-						deliver(user, content, follower.account.inbox).save();
+						deliver(user, content, follower.inbox).save();
 					}
 				}
 			}));
@@ -242,7 +242,7 @@ export default async (user: IUser, data: {
 		});
 
 		// この投稿をWatchする
-		if (isLocalUser(user) && user.account.settings.autoWatch !== false) {
+		if (isLocalUser(user) && user.settings.autoWatch !== false) {
 			watch(user._id, data.reply);
 		}
 
@@ -277,7 +277,7 @@ export default async (user: IUser, data: {
 		});
 
 		// この投稿をWatchする
-		if (isLocalUser(user) && user.account.settings.autoWatch !== false) {
+		if (isLocalUser(user) && user.settings.autoWatch !== false) {
 			watch(user._id, data.renote);
 		}
 
diff --git a/src/services/note/reaction/create.ts b/src/services/note/reaction/create.ts
index d0ce65ee5..ea51b205d 100644
--- a/src/services/note/reaction/create.ts
+++ b/src/services/note/reaction/create.ts
@@ -78,7 +78,7 @@ export default async (user: IUser, note: INote, reaction: string) => new Promise
 		});
 
 	// ユーザーがローカルユーザーかつ自動ウォッチ設定がオンならばこの投稿をWatchする
-	if (isLocalUser(user) && user.account.settings.autoWatch !== false) {
+	if (isLocalUser(user) && user.settings.autoWatch !== false) {
 		watch(user._id, note);
 	}
 
@@ -88,7 +88,7 @@ export default async (user: IUser, note: INote, reaction: string) => new Promise
 
 	// リアクターがローカルユーザーかつリアクション対象がリモートユーザーの投稿なら配送
 	if (isLocalUser(user) && isRemoteUser(note._user)) {
-		deliver(user, content, note._user.account.inbox).save();
+		deliver(user, content, note._user.inbox).save();
 	}
 	//#endregion
 });
diff --git a/test/api.ts b/test/api.ts
index 953c5aea0..87bbb8ee1 100644
--- a/test/api.ts
+++ b/test/api.ts
@@ -32,7 +32,7 @@ const async = fn => (done) => {
 
 const request = (endpoint, params, me?) => new Promise<any>((ok, ng) => {
 	const auth = me ? {
-		i: me.account.token
+		i: me.token
 	} : {};
 
 	_chai.request(server)
@@ -157,10 +157,10 @@ describe('API', () => {
 			res.should.have.status(200);
 			res.body.should.be.a('object');
 			res.body.should.have.property('name').eql(myName);
-			res.body.should.have.nested.property('account.profile').a('object');
-			res.body.should.have.nested.property('account.profile.location').eql(myLocation);
-			res.body.should.have.nested.property('account.profile.birthday').eql(myBirthday);
-			res.body.should.have.nested.property('account.profile.gender').eql('female');
+			res.body.should.have.nested.property('profile').a('object');
+			res.body.should.have.nested.property('profile.location').eql(myLocation);
+			res.body.should.have.nested.property('profile.birthday').eql(myBirthday);
+			res.body.should.have.nested.property('profile.gender').eql('female');
 		}));
 
 		it('名前を空白にできない', async(async () => {
@@ -180,8 +180,8 @@ describe('API', () => {
 			}, me);
 			res.should.have.status(200);
 			res.body.should.be.a('object');
-			res.body.should.have.nested.property('account.profile').a('object');
-			res.body.should.have.nested.property('account.profile.birthday').eql(null);
+			res.body.should.have.nested.property('profile').a('object');
+			res.body.should.have.nested.property('profile.birthday').eql(null);
 		}));
 
 		it('不正な誕生日の形式で怒られる', async(async () => {
@@ -736,7 +736,7 @@ describe('API', () => {
 			const me = await insertSakurako();
 			const res = await _chai.request(server)
 				.post('/drive/files/create')
-				.field('i', me.account.token)
+				.field('i', me.token)
 				.attach('file', fs.readFileSync(__dirname + '/resources/Lenna.png'), 'Lenna.png');
 			res.should.have.status(200);
 			res.body.should.be.a('object');
diff --git a/tools/migration/nighthike/12.js b/tools/migration/nighthike/12.js
new file mode 100644
index 000000000..f4b61e2ee
--- /dev/null
+++ b/tools/migration/nighthike/12.js
@@ -0,0 +1,58 @@
+// for Node.js interpret
+
+const { default: User } = require('../../../built/models/user');
+const { generate } = require('../../../built/crypto_key');
+const { default: zip } = require('@prezzemolo/zip')
+
+const migrate = async (user) => {
+	const result = await User.update(user._id, {
+		$unset: {
+			account: ''
+		},
+		$set: {
+			host: null,
+			hostLower: null,
+			email: user.account.email,
+			links: user.account.links,
+			password: user.account.password,
+			token: user.account.token,
+			twitter: user.account.twitter,
+			line: user.account.line,
+			profile: user.account.profile,
+			lastUsedAt: user.account.lastUsedAt,
+			isBot: user.account.isBot,
+			isPro: user.account.isPro,
+			twoFactorSecret: user.account.twoFactorSecret,
+			twoFactorEnabled: user.account.twoFactorEnabled,
+			clientSettings: user.account.clientSettings,
+			settings: user.account.settings,
+			keypair: user.account.keypair
+		}
+	});
+	return result.ok === 1;
+}
+
+async function main() {
+	const count = await User.count({});
+
+	const dop = Number.parseInt(process.argv[2]) || 5
+	const idop = ((count - (count % dop)) / dop) + 1
+
+	return zip(
+		1,
+		async (time) => {
+			console.log(`${time} / ${idop}`)
+			const doc = await User.find({}, {
+				limit: dop, skip: time * dop
+			})
+			return Promise.all(doc.map(migrate))
+		},
+		idop
+	).then(a => {
+		const rv = []
+		a.forEach(e => rv.push(...e))
+		return rv
+	})
+}
+
+main().then(console.dir).catch(console.error)

From f482c9329fdede2b88f05116e6d09a57ab1d3dbc Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 04:02:12 +0900
Subject: [PATCH 1155/1250] Some bug fixes and clean ups

---
 .../endpoints/aggregation/notes/reaction.ts   | 76 -------------------
 .../endpoints/aggregation/notes/reactions.ts  | 72 ------------------
 .../api/endpoints/aggregation/notes/reply.ts  | 75 ------------------
 .../api/endpoints/aggregation/notes/repost.ts | 75 ------------------
 .../api/endpoints/drive/files/create.ts       |  2 +-
 .../endpoints/drive/files/upload_from_url.ts  |  2 +-
 src/server/api/endpoints/notes/polls/vote.ts  |  2 +-
 .../api/endpoints/notes/reactions/create.ts   |  1 -
 8 files changed, 3 insertions(+), 302 deletions(-)
 delete mode 100644 src/server/api/endpoints/aggregation/notes/reaction.ts
 delete mode 100644 src/server/api/endpoints/aggregation/notes/reactions.ts
 delete mode 100644 src/server/api/endpoints/aggregation/notes/reply.ts
 delete mode 100644 src/server/api/endpoints/aggregation/notes/repost.ts

diff --git a/src/server/api/endpoints/aggregation/notes/reaction.ts b/src/server/api/endpoints/aggregation/notes/reaction.ts
deleted file mode 100644
index 586e8c2d8..000000000
--- a/src/server/api/endpoints/aggregation/notes/reaction.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-/**
- * Module dependencies
- */
-import $ from 'cafy';
-import Note from '../../../../../models/note';
-import Reaction from '../../../../../models/note-reaction';
-
-/**
- * Aggregate reaction of a note
- *
- * @param {any} params
- * @return {Promise<any>}
- */
-module.exports = (params) => new Promise(async (res, rej) => {
-	// Get 'noteId' parameter
-	const [noteId, noteIdErr] = $(params.noteId).id().$;
-	if (noteIdErr) return rej('invalid noteId param');
-
-	// Lookup note
-	const note = await Note.findOne({
-		_id: noteId
-	});
-
-	if (note === null) {
-		return rej('note not found');
-	}
-
-	const datas = await Reaction
-		.aggregate([
-			{ $match: { noteId: note._id } },
-			{ $project: {
-				createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
-			}},
-			{ $project: {
-				date: {
-					year: { $year: '$createdAt' },
-					month: { $month: '$createdAt' },
-					day: { $dayOfMonth: '$createdAt' }
-				}
-			}},
-			{ $group: {
-				_id: '$date',
-				count: { $sum: 1 }
-			}}
-		]);
-
-	datas.forEach(data => {
-		data.date = data._id;
-		delete data._id;
-	});
-
-	const graph = [];
-
-	for (let i = 0; i < 30; i++) {
-		const day = new Date(new Date().setDate(new Date().getDate() - i));
-
-		const data = datas.filter(d =>
-			d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate()
-		)[0];
-
-		if (data) {
-			graph.push(data);
-		} else {
-			graph.push({
-				date: {
-					year: day.getFullYear(),
-					month: day.getMonth() + 1, // In JavaScript, month is zero-based.
-					day: day.getDate()
-				},
-				count: 0
-			});
-		}
-	}
-
-	res(graph);
-});
diff --git a/src/server/api/endpoints/aggregation/notes/reactions.ts b/src/server/api/endpoints/aggregation/notes/reactions.ts
deleted file mode 100644
index ff9491292..000000000
--- a/src/server/api/endpoints/aggregation/notes/reactions.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-/**
- * Module dependencies
- */
-import $ from 'cafy';
-import Note from '../../../../../models/note';
-import Reaction from '../../../../../models/note-reaction';
-
-/**
- * Aggregate reactions of a note
- *
- * @param {any} params
- * @return {Promise<any>}
- */
-module.exports = (params) => new Promise(async (res, rej) => {
-	// Get 'noteId' parameter
-	const [noteId, noteIdErr] = $(params.noteId).id().$;
-	if (noteIdErr) return rej('invalid noteId param');
-
-	// Lookup note
-	const note = await Note.findOne({
-		_id: noteId
-	});
-
-	if (note === null) {
-		return rej('note not found');
-	}
-
-	const startTime = new Date(new Date().setMonth(new Date().getMonth() - 1));
-
-	const reactions = await Reaction
-		.find({
-			noteId: note._id,
-			$or: [
-				{ deletedAt: { $exists: false } },
-				{ deletedAt: { $gt: startTime } }
-			]
-		}, {
-			sort: {
-				_id: -1
-			},
-			fields: {
-				_id: false,
-				noteId: false
-			}
-		});
-
-	const graph = [];
-
-	for (let i = 0; i < 30; i++) {
-		let day = new Date(new Date().setDate(new Date().getDate() - i));
-		day = new Date(day.setMilliseconds(999));
-		day = new Date(day.setSeconds(59));
-		day = new Date(day.setMinutes(59));
-		day = new Date(day.setHours(23));
-		// day = day.getTime();
-
-		const count = reactions.filter(r =>
-			r.createdAt < day && (r.deletedAt == null || r.deletedAt > day)
-		).length;
-
-		graph.push({
-			date: {
-				year: day.getFullYear(),
-				month: day.getMonth() + 1, // In JavaScript, month is zero-based.
-				day: day.getDate()
-			},
-			count: count
-		});
-	}
-
-	res(graph);
-});
diff --git a/src/server/api/endpoints/aggregation/notes/reply.ts b/src/server/api/endpoints/aggregation/notes/reply.ts
deleted file mode 100644
index 42df95a9a..000000000
--- a/src/server/api/endpoints/aggregation/notes/reply.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-/**
- * Module dependencies
- */
-import $ from 'cafy';
-import Note from '../../../../../models/note';
-
-/**
- * Aggregate reply of a note
- *
- * @param {any} params
- * @return {Promise<any>}
- */
-module.exports = (params) => new Promise(async (res, rej) => {
-	// Get 'noteId' parameter
-	const [noteId, noteIdErr] = $(params.noteId).id().$;
-	if (noteIdErr) return rej('invalid noteId param');
-
-	// Lookup note
-	const note = await Note.findOne({
-		_id: noteId
-	});
-
-	if (note === null) {
-		return rej('note not found');
-	}
-
-	const datas = await Note
-		.aggregate([
-			{ $match: { reply: note._id } },
-			{ $project: {
-				createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
-			}},
-			{ $project: {
-				date: {
-					year: { $year: '$createdAt' },
-					month: { $month: '$createdAt' },
-					day: { $dayOfMonth: '$createdAt' }
-				}
-			}},
-			{ $group: {
-				_id: '$date',
-				count: { $sum: 1 }
-			}}
-		]);
-
-	datas.forEach(data => {
-		data.date = data._id;
-		delete data._id;
-	});
-
-	const graph = [];
-
-	for (let i = 0; i < 30; i++) {
-		const day = new Date(new Date().setDate(new Date().getDate() - i));
-
-		const data = datas.filter(d =>
-			d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate()
-		)[0];
-
-		if (data) {
-			graph.push(data);
-		} else {
-			graph.push({
-				date: {
-					year: day.getFullYear(),
-					month: day.getMonth() + 1, // In JavaScript, month is zero-based.
-					day: day.getDate()
-				},
-				count: 0
-			});
-		}
-	}
-
-	res(graph);
-});
diff --git a/src/server/api/endpoints/aggregation/notes/repost.ts b/src/server/api/endpoints/aggregation/notes/repost.ts
deleted file mode 100644
index feb3348a7..000000000
--- a/src/server/api/endpoints/aggregation/notes/repost.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-/**
- * Module dependencies
- */
-import $ from 'cafy';
-import Note from '../../../../../models/note';
-
-/**
- * Aggregate renote of a note
- *
- * @param {any} params
- * @return {Promise<any>}
- */
-module.exports = (params) => new Promise(async (res, rej) => {
-	// Get 'noteId' parameter
-	const [noteId, noteIdErr] = $(params.noteId).id().$;
-	if (noteIdErr) return rej('invalid noteId param');
-
-	// Lookup note
-	const note = await Note.findOne({
-		_id: noteId
-	});
-
-	if (note === null) {
-		return rej('note not found');
-	}
-
-	const datas = await Note
-		.aggregate([
-			{ $match: { renoteId: note._id } },
-			{ $project: {
-				createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
-			}},
-			{ $project: {
-				date: {
-					year: { $year: '$createdAt' },
-					month: { $month: '$createdAt' },
-					day: { $dayOfMonth: '$createdAt' }
-				}
-			}},
-			{ $group: {
-				_id: '$date',
-				count: { $sum: 1 }
-			}}
-		]);
-
-	datas.forEach(data => {
-		data.date = data._id;
-		delete data._id;
-	});
-
-	const graph = [];
-
-	for (let i = 0; i < 30; i++) {
-		const day = new Date(new Date().setDate(new Date().getDate() - i));
-
-		const data = datas.filter(d =>
-			d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate()
-		)[0];
-
-		if (data) {
-			graph.push(data);
-		} else {
-			graph.push({
-				date: {
-					year: day.getFullYear(),
-					month: day.getMonth() + 1, // In JavaScript, month is zero-based.
-					day: day.getDate()
-				},
-				count: 0
-			});
-		}
-	}
-
-	res(graph);
-});
diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts
index 10ced1d8b..df0bd0a0d 100644
--- a/src/server/api/endpoints/drive/files/create.ts
+++ b/src/server/api/endpoints/drive/files/create.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import { validateFileName, pack } from '../../../../../models/drive-file';
-import create from '../../../../../drive/add-file';
+import create from '../../../../../services/drive/add-file';
 
 /**
  * Create a file
diff --git a/src/server/api/endpoints/drive/files/upload_from_url.ts b/src/server/api/endpoints/drive/files/upload_from_url.ts
index acb67b2e0..9ebc8b823 100644
--- a/src/server/api/endpoints/drive/files/upload_from_url.ts
+++ b/src/server/api/endpoints/drive/files/upload_from_url.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy';
 import { pack } from '../../../../../models/drive-file';
-import uploadFromUrl from '../../../../../drive/upload-from-url';
+import uploadFromUrl from '../../../../../services/drive/upload-from-url';
 
 /**
  * Create a file from a URL
diff --git a/src/server/api/endpoints/notes/polls/vote.ts b/src/server/api/endpoints/notes/polls/vote.ts
index fd4412ad3..03d94da60 100644
--- a/src/server/api/endpoints/notes/polls/vote.ts
+++ b/src/server/api/endpoints/notes/polls/vote.ts
@@ -5,7 +5,7 @@ import $ from 'cafy';
 import Vote from '../../../../../models/poll-vote';
 import Note from '../../../../../models/note';
 import Watching from '../../../../../models/note-watching';
-import watch from '../../../../../note/watch';
+import watch from '../../../../../services/note/watch';
 import { publishNoteStream } from '../../../../../publishers/stream';
 import notify from '../../../../../publishers/notify';
 
diff --git a/src/server/api/endpoints/notes/reactions/create.ts b/src/server/api/endpoints/notes/reactions/create.ts
index ffb7bcc35..c80c5416b 100644
--- a/src/server/api/endpoints/notes/reactions/create.ts
+++ b/src/server/api/endpoints/notes/reactions/create.ts
@@ -2,7 +2,6 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Reaction from '../../../../../models/note-reaction';
 import Note from '../../../../../models/note';
 import create from '../../../../../services/note/reaction/create';
 

From 96c7153188a7bbdb6d40e8a4fe45a4cd4e5c4884 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 04:44:59 +0900
Subject: [PATCH 1156/1250] Fix bugs

---
 package.json                     | 2 +-
 src/client/app/common/mios.ts    | 7 ++++++-
 src/server/api/private/signin.ts | 2 +-
 3 files changed, 8 insertions(+), 3 deletions(-)

diff --git a/package.json b/package.json
index 78b812e96..08119fe32 100644
--- a/package.json
+++ b/package.json
@@ -203,7 +203,7 @@
 		"vue-cropperjs": "2.2.0",
 		"vue-js-modal": "1.3.12",
 		"vue-json-tree-view": "2.1.3",
-		"vue-loader": "15.0.0-rc.1",
+		"vue-loader": "14.2.2",
 		"vue-router": "3.0.1",
 		"vue-template-compiler": "2.5.16",
 		"vuedraggable": "2.16.0",
diff --git a/src/client/app/common/mios.ts b/src/client/app/common/mios.ts
index fd267bc3f..7baf974ad 100644
--- a/src/client/app/common/mios.ts
+++ b/src/client/app/common/mios.ts
@@ -220,7 +220,7 @@ export default class MiOS extends EventEmitter {
 
 	public signout() {
 		localStorage.removeItem('me');
-		document.cookie = `i=; domain=.${hostname}; expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
+		document.cookie = `i=; domain=${hostname}; expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
 		location.href = '/';
 	}
 
@@ -325,6 +325,11 @@ export default class MiOS extends EventEmitter {
 
 		// キャッシュがあったとき
 		if (cachedMe) {
+			if (cachedMe.token == null) {
+				this.signout();
+				return;
+			}
+
 			// とりあえずキャッシュされたデータでお茶を濁して(?)おいて、
 			fetched(cachedMe);
 
diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts
index d7c4832c9..665ee21eb 100644
--- a/src/server/api/private/signin.ts
+++ b/src/server/api/private/signin.ts
@@ -49,7 +49,7 @@ export default async (req: express.Request, res: express.Response) => {
 	}
 
 	// Compare password
-	const same = await bcrypt.compare(password, password);
+	const same = await bcrypt.compare(password, user.password);
 
 	if (same) {
 		if (user.twoFactorEnabled) {

From 45d443373aeecb0026228644070969a37ac3c90d Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sat, 7 Apr 2018 19:51:58 +0000
Subject: [PATCH 1157/1250] fix(package): update html-minifier to version
 3.5.14

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 08119fe32..10517b0ce 100644
--- a/package.json
+++ b/package.json
@@ -134,7 +134,7 @@
 		"gulp-util": "3.0.8",
 		"hard-source-webpack-plugin": "0.6.4",
 		"highlight.js": "9.12.0",
-		"html-minifier": "3.5.13",
+		"html-minifier": "3.5.14",
 		"http-signature": "^1.2.0",
 		"inquirer": "5.2.0",
 		"is-root": "2.0.0",

From b288304ac3eb155923f0af90eb111be6eb712797 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 05:02:50 +0900
Subject: [PATCH 1158/1250] Fix bugs

---
 src/remote/activitypub/renderer/follow.ts     |  6 ++---
 src/remote/activitypub/resolve-person.ts      | 12 ++++------
 src/remote/request.ts                         |  2 +-
 src/server/api/endpoints/following/create.ts  |  2 +-
 src/server/api/endpoints/i/update.ts          | 24 ++++---------------
 .../endpoints/users/{posts.ts => notes.ts}    |  0
 6 files changed, 14 insertions(+), 32 deletions(-)
 rename src/server/api/endpoints/users/{posts.ts => notes.ts} (100%)

diff --git a/src/remote/activitypub/renderer/follow.ts b/src/remote/activitypub/renderer/follow.ts
index 0a1ae1a4b..89993d945 100644
--- a/src/remote/activitypub/renderer/follow.ts
+++ b/src/remote/activitypub/renderer/follow.ts
@@ -1,8 +1,8 @@
 import config from '../../../config';
-import { IRemoteUser } from '../../../models/user';
+import { IRemoteUser, ILocalUser } from '../../../models/user';
 
-export default ({ username }, followee: IRemoteUser) => ({
+export default (follower: ILocalUser, followee: IRemoteUser) => ({
 	type: 'Follow',
-	actor: `${config.url}/@${username}`,
+	actor: `${config.url}/@${follower.username}`,
 	object: followee.uri
 });
diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts
index ac0900307..ddb8d6871 100644
--- a/src/remote/activitypub/resolve-person.ts
+++ b/src/remote/activitypub/resolve-person.ts
@@ -66,14 +66,12 @@ export default async (value, verifier?: string) => {
 		usernameLower: object.preferredUsername.toLowerCase(),
 		host,
 		hostLower,
-		account: {
-			publicKey: {
-				id: object.publicKey.id,
-				publicKeyPem: object.publicKey.publicKeyPem
-			},
-			inbox: object.inbox,
-			uri: id,
+		publicKey: {
+			id: object.publicKey.id,
+			publicKeyPem: object.publicKey.publicKeyPem
 		},
+		inbox: object.inbox,
+		uri: id
 	});
 
 	const [avatarId, bannerId] = (await Promise.all([
diff --git a/src/remote/request.ts b/src/remote/request.ts
index a0c69cf4e..81e7c05c7 100644
--- a/src/remote/request.ts
+++ b/src/remote/request.ts
@@ -8,7 +8,7 @@ import { ILocalUser } from '../models/user';
 
 const log = debug('misskey:activitypub:deliver');
 
-export default (user: ILocalUser, url, object) => new Promise((resolve, reject) => {
+export default (user: ILocalUser, url: string, object) => new Promise((resolve, reject) => {
 	log(`--> ${url}`);
 
 	const { protocol, hostname, port, pathname, search } = new URL(url);
diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts
index d3cc549ca..0a642f50b 100644
--- a/src/server/api/endpoints/following/create.ts
+++ b/src/server/api/endpoints/following/create.ts
@@ -31,7 +31,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}, {
 		fields: {
 			data: false,
-			'profile': false
+			profile: false
 		}
 	});
 
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index a8caa0ebc..36be2774f 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -4,7 +4,6 @@
 import $ from 'cafy';
 import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../../../models/user';
 import event from '../../../../publishers/stream';
-import config from '../../../../config';
 
 /**
  * Update myself
@@ -17,7 +16,7 @@ import config from '../../../../config';
  */
 module.exports = async (params, user, _, isSecure) => new Promise(async (res, rej) => {
 	// Get 'name' parameter
-	const [name, nameErr] = $(params.name).optional.string().pipe(isValidName).$;
+	const [name, nameErr] = $(params.name).optional.nullable.string().pipe(isValidName).$;
 	if (nameErr) return rej('invalid name param');
 	if (name) user.name = name;
 
@@ -62,9 +61,9 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re
 			description: user.description,
 			avatarId: user.avatarId,
 			bannerId: user.bannerId,
-			'profile': user.profile,
-			'isBot': user.isBot,
-			'settings': user.settings
+			profile: user.profile,
+			isBot: user.isBot,
+			settings: user.settings
 		}
 	});
 
@@ -79,19 +78,4 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re
 
 	// Publish i updated event
 	event(user._id, 'i_updated', iObj);
-
-	// Update search index
-	if (config.elasticsearch.enable) {
-		const es = require('../../../db/elasticsearch');
-
-		es.index({
-			index: 'misskey',
-			type: 'user',
-			id: user._id.toString(),
-			body: {
-				name: user.name,
-				bio: user.bio
-			}
-		});
-	}
 });
diff --git a/src/server/api/endpoints/users/posts.ts b/src/server/api/endpoints/users/notes.ts
similarity index 100%
rename from src/server/api/endpoints/users/posts.ts
rename to src/server/api/endpoints/users/notes.ts

From 5e5c46545cb0e153d94d645080d02ead7a503514 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 05:07:17 +0900
Subject: [PATCH 1159/1250] Fix bug

---
 src/remote/activitypub/act/follow.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/remote/activitypub/act/follow.ts b/src/remote/activitypub/act/follow.ts
index 3dd029af5..236886dc6 100644
--- a/src/remote/activitypub/act/follow.ts
+++ b/src/remote/activitypub/act/follow.ts
@@ -6,7 +6,7 @@ import { IFollow } from '../type';
 
 export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
 	const prefix = config.url + '/@';
-	const id = typeof activity == 'string' ? activity : activity.id;
+	const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
 
 	if (!id.startsWith(prefix)) {
 		return null;

From d482ca7f9f609713ea3b806bd71dfea393ac26ac Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 06:55:26 +0900
Subject: [PATCH 1160/1250] Implement announce

And bug fixes
---
 src/models/note.ts                           |   2 +-
 src/models/notification.ts                   |   3 -
 src/remote/activitypub/act/announce/index.ts |  39 +++
 src/remote/activitypub/act/announce/note.ts  |  52 ++++
 src/remote/activitypub/act/index.ts          |   5 +
 src/remote/activitypub/act/like.ts           |   2 +-
 src/remote/activitypub/renderer/announce.ts  |   4 +
 src/remote/activitypub/renderer/like.ts      |   3 +-
 src/remote/activitypub/renderer/note.ts      |   8 +-
 src/remote/activitypub/resolve-person.ts     |  10 +-
 src/remote/activitypub/resolver.ts           |   7 +
 src/remote/activitypub/type.ts               |  15 +-
 src/server/activitypub/note.ts               |  23 +-
 src/server/activitypub/outbox.ts             |   2 +-
 src/server/api/endpoints/posts/create.ts     | 251 -------------------
 src/services/note/create.ts                  |  32 ++-
 src/services/note/reaction/create.ts         |   6 +-
 17 files changed, 164 insertions(+), 300 deletions(-)
 create mode 100644 src/remote/activitypub/act/announce/index.ts
 create mode 100644 src/remote/activitypub/act/announce/note.ts
 create mode 100644 src/remote/activitypub/renderer/announce.ts
 delete mode 100644 src/server/api/endpoints/posts/create.ts

diff --git a/src/models/note.ts b/src/models/note.ts
index 9b33bb76f..f509fa66c 100644
--- a/src/models/note.ts
+++ b/src/models/note.ts
@@ -112,7 +112,7 @@ export const pack = async (
 		_note = deepcopy(note);
 	}
 
-	if (!_note) throw 'invalid note arg.';
+	if (!_note) throw `invalid note arg ${note}`;
 
 	const id = _note._id;
 
diff --git a/src/models/notification.ts b/src/models/notification.ts
index 17144d7ee..d5ca7135b 100644
--- a/src/models/notification.ts
+++ b/src/models/notification.ts
@@ -51,9 +51,6 @@ export interface INotification {
 
 /**
  * Pack a notification for API response
- *
- * @param {any} notification
- * @return {Promise<any>}
  */
 export const pack = (notification: any) => new Promise<any>(async (resolve, reject) => {
 	let _notification: any;
diff --git a/src/remote/activitypub/act/announce/index.ts b/src/remote/activitypub/act/announce/index.ts
new file mode 100644
index 000000000..c3ac06607
--- /dev/null
+++ b/src/remote/activitypub/act/announce/index.ts
@@ -0,0 +1,39 @@
+import * as debug from 'debug';
+
+import Resolver from '../../resolver';
+import { IRemoteUser } from '../../../../models/user';
+import announceNote from './note';
+import { IAnnounce } from '../../type';
+
+const log = debug('misskey:activitypub');
+
+export default async (actor: IRemoteUser, activity: IAnnounce): Promise<void> => {
+	if ('actor' in activity && actor.uri !== activity.actor) {
+		throw new Error('invalid actor');
+	}
+
+	const uri = activity.id || activity;
+
+	log(`Announce: ${uri}`);
+
+	const resolver = new Resolver();
+
+	let object;
+
+	try {
+		object = await resolver.resolve(activity.object);
+	} catch (e) {
+		log(`Resolution failed: ${e}`);
+		throw e;
+	}
+
+	switch (object.type) {
+	case 'Note':
+		announceNote(resolver, actor, activity, object);
+		break;
+
+	default:
+		console.warn(`Unknown announce type: ${object.type}`);
+		break;
+	}
+};
diff --git a/src/remote/activitypub/act/announce/note.ts b/src/remote/activitypub/act/announce/note.ts
new file mode 100644
index 000000000..24d159f18
--- /dev/null
+++ b/src/remote/activitypub/act/announce/note.ts
@@ -0,0 +1,52 @@
+import * as debug from 'debug';
+
+import Resolver from '../../resolver';
+import Note from '../../../../models/note';
+import post from '../../../../services/note/create';
+import { IRemoteUser, isRemoteUser } from '../../../../models/user';
+import { IAnnounce, INote } from '../../type';
+import createNote from '../create/note';
+import resolvePerson from '../../resolve-person';
+
+const log = debug('misskey:activitypub');
+
+/**
+ * アナウンスアクティビティを捌きます
+ */
+export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, note: INote): Promise<void> {
+	const uri = activity.id || activity;
+
+	if (typeof uri !== 'string') {
+		throw new Error('invalid announce');
+	}
+
+	// 既に同じURIを持つものが登録されていないかチェック
+	const exist = await Note.findOne({ uri });
+	if (exist) {
+		return;
+	}
+
+	// アナウンス元の投稿の投稿者をフェッチ
+	const announcee = await resolvePerson(note.attributedTo);
+
+	const renote = isRemoteUser(announcee)
+		? await createNote(resolver, announcee, note, true)
+		: await Note.findOne({ _id: note.id.split('/').pop() });
+
+	log(`Creating the (Re)Note: ${uri}`);
+
+	//#region Visibility
+	let visibility = 'public';
+	if (!activity.to.includes('https://www.w3.org/ns/activitystreams#Public')) visibility = 'unlisted';
+	if (activity.cc.length == 0) visibility = 'private';
+	// TODO
+	if (visibility != 'public') throw new Error('unspported visibility');
+	//#endergion
+
+	await post(actor, {
+		createdAt: new Date(activity.published),
+		renote,
+		visibility,
+		uri
+	});
+}
diff --git a/src/remote/activitypub/act/index.ts b/src/remote/activitypub/act/index.ts
index 45d7bd16a..15ea9494a 100644
--- a/src/remote/activitypub/act/index.ts
+++ b/src/remote/activitypub/act/index.ts
@@ -5,6 +5,7 @@ import performDeleteActivity from './delete';
 import follow from './follow';
 import undo from './undo';
 import like from './like';
+import announce from './announce';
 
 const self = async (actor: IRemoteUser, activity: Object): Promise<void> => {
 	switch (activity.type) {
@@ -24,6 +25,10 @@ const self = async (actor: IRemoteUser, activity: Object): Promise<void> => {
 		// noop
 		break;
 
+	case 'Announce':
+		await announce(actor, activity);
+		break;
+
 	case 'Like':
 		await like(actor, activity);
 		break;
diff --git a/src/remote/activitypub/act/like.ts b/src/remote/activitypub/act/like.ts
index a3243948b..494160858 100644
--- a/src/remote/activitypub/act/like.ts
+++ b/src/remote/activitypub/act/like.ts
@@ -7,7 +7,7 @@ export default async (actor: IRemoteUser, activity: ILike) => {
 	const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
 
 	// Transform:
-	// https://misskey.ex/@syuilo/xxxx to
+	// https://misskey.ex/notes/xxxx to
 	// xxxx
 	const noteId = id.split('/').pop();
 
diff --git a/src/remote/activitypub/renderer/announce.ts b/src/remote/activitypub/renderer/announce.ts
new file mode 100644
index 000000000..8e4b3d26a
--- /dev/null
+++ b/src/remote/activitypub/renderer/announce.ts
@@ -0,0 +1,4 @@
+export default object => ({
+	type: 'Announce',
+	object
+});
diff --git a/src/remote/activitypub/renderer/like.ts b/src/remote/activitypub/renderer/like.ts
index fe36c7094..744896cc4 100644
--- a/src/remote/activitypub/renderer/like.ts
+++ b/src/remote/activitypub/renderer/like.ts
@@ -1,6 +1,7 @@
 import config from '../../../config';
+import { ILocalUser } from '../../../models/user';
 
-export default (user, note) => {
+export default (user: ILocalUser, note) => {
 	return {
 		type: 'Like',
 		actor: `${config.url}/@${user.username}`,
diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts
index 244aecf6a..48799af08 100644
--- a/src/remote/activitypub/renderer/note.ts
+++ b/src/remote/activitypub/renderer/note.ts
@@ -3,9 +3,9 @@ import renderHashtag from './hashtag';
 import config from '../../../config';
 import DriveFile from '../../../models/drive-file';
 import Note, { INote } from '../../../models/note';
-import User, { IUser } from '../../../models/user';
+import User from '../../../models/user';
 
-export default async (user: IUser, note: INote) => {
+export default async (note: INote) => {
 	const promisedFiles = note.mediaIds
 		? DriveFile.find({ _id: { $in: note.mediaIds } })
 		: Promise.resolve([]);
@@ -30,6 +30,10 @@ export default async (user: IUser, note: INote) => {
 		inReplyTo = null;
 	}
 
+	const user = await User.findOne({
+		_id: note.userId
+	});
+
 	const attributedTo = `${config.url}/@${user.username}`;
 
 	return {
diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts
index ddb8d6871..7d7989a01 100644
--- a/src/remote/activitypub/resolve-person.ts
+++ b/src/remote/activitypub/resolve-person.ts
@@ -2,18 +2,18 @@ import { JSDOM } from 'jsdom';
 import { toUnicode } from 'punycode';
 import parseAcct from '../../acct/parse';
 import config from '../../config';
-import User, { validateUsername, isValidName, isValidDescription } from '../../models/user';
+import User, { validateUsername, isValidName, isValidDescription, IUser } from '../../models/user';
 import webFinger from '../webfinger';
 import Resolver from './resolver';
 import uploadFromUrl from '../../services/drive/upload-from-url';
-import { isCollectionOrOrderedCollection } from './type';
+import { isCollectionOrOrderedCollection, IObject } from './type';
 
-export default async (value, verifier?: string) => {
-	const id = value.id || value;
+export default async (value: string | IObject, verifier?: string): Promise<IUser> => {
+	const id = typeof value == 'string' ? value : value.id;
 	const localPrefix = config.url + '/@';
 
 	if (id.startsWith(localPrefix)) {
-		return User.findOne(parseAcct(id.slice(localPrefix)));
+		return await User.findOne(parseAcct(id.substr(localPrefix.length)));
 	}
 
 	const resolver = new Resolver();
diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts
index 4a97e2ef6..1466139c4 100644
--- a/src/remote/activitypub/resolver.ts
+++ b/src/remote/activitypub/resolver.ts
@@ -1,6 +1,7 @@
 import * as request from 'request-promise-native';
 import * as debug from 'debug';
 import { IObject } from './type';
+//import config from '../../config';
 
 const log = debug('misskey:activitypub:resolver');
 
@@ -47,6 +48,11 @@ export default class Resolver {
 
 		this.history.add(value);
 
+		//#region resolve local objects
+		// TODO
+		//if (value.startsWith(`${config.url}/@`)) {
+		//#endregion
+
 		const object = await request({
 			url: value,
 			headers: {
@@ -60,6 +66,7 @@ export default class Resolver {
 				!object['@context'].includes('https://www.w3.org/ns/activitystreams') :
 				object['@context'] !== 'https://www.w3.org/ns/activitystreams'
 		)) {
+			log(`invalid response: ${JSON.stringify(object, null, 2)}`);
 			throw new Error('invalid response');
 		}
 
diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts
index 450d5906d..233551764 100644
--- a/src/remote/activitypub/type.ts
+++ b/src/remote/activitypub/type.ts
@@ -5,6 +5,10 @@ export interface IObject {
 	type: string;
 	id?: string;
 	summary?: string;
+	published?: string;
+	cc?: string[];
+	to?: string[];
+	attributedTo: string;
 }
 
 export interface IActivity extends IObject {
@@ -26,6 +30,10 @@ export interface IOrderedCollection extends IObject {
 	orderedItems: IObject | string | IObject[] | string[];
 }
 
+export interface INote extends IObject {
+	type: 'Note';
+}
+
 export const isCollection = (object: IObject): object is ICollection =>
 	object.type === 'Collection';
 
@@ -59,6 +67,10 @@ export interface ILike extends IActivity {
 	type: 'Like';
 }
 
+export interface IAnnounce extends IActivity {
+	type: 'Announce';
+}
+
 export type Object =
 	ICollection |
 	IOrderedCollection |
@@ -67,4 +79,5 @@ export type Object =
 	IUndo |
 	IFollow |
 	IAccept |
-	ILike;
+	ILike |
+	IAnnounce;
diff --git a/src/server/activitypub/note.ts b/src/server/activitypub/note.ts
index cea9be52d..1c2e695b8 100644
--- a/src/server/activitypub/note.ts
+++ b/src/server/activitypub/note.ts
@@ -1,40 +1,25 @@
 import * as express from 'express';
 import context from '../../remote/activitypub/renderer/context';
 import render from '../../remote/activitypub/renderer/note';
-import parseAcct from '../../acct/parse';
 import Note from '../../models/note';
-import User from '../../models/user';
 
 const app = express.Router();
 
-app.get('/@:user/:note', async (req, res, next) => {
+app.get('/notes/:note', async (req, res, next) => {
 	const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']);
 	if (!(['application/activity+json', 'application/ld+json'] as any[]).includes(accepted)) {
 		return next();
 	}
 
-	const { username, host } = parseAcct(req.params.user);
-	if (host !== null) {
-		return res.sendStatus(422);
-	}
-
-	const user = await User.findOne({
-		usernameLower: username.toLowerCase(),
-		host: null
-	});
-	if (user === null) {
-		return res.sendStatus(404);
-	}
-
 	const note = await Note.findOne({
-		_id: req.params.note,
-		userId: user._id
+		_id: req.params.note
 	});
+
 	if (note === null) {
 		return res.sendStatus(404);
 	}
 
-	const rendered = await render(user, note);
+	const rendered = await render(note);
 	rendered['@context'] = context;
 
 	res.json(rendered);
diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts
index b6f3a3f9d..4557871bc 100644
--- a/src/server/activitypub/outbox.ts
+++ b/src/server/activitypub/outbox.ts
@@ -16,7 +16,7 @@ app.get('/@:user/outbox', withUser(username => {
 		sort: { _id: -1 }
 	});
 
-	const renderedNotes = await Promise.all(notes.map(note => renderNote(user, note)));
+	const renderedNotes = await Promise.all(notes.map(note => renderNote(note)));
 	const rendered = renderOrderedCollection(`${config.url}/@${user.username}/inbox`, user.notesCount, renderedNotes);
 	rendered['@context'] = context;
 
diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts
deleted file mode 100644
index 7e79912b1..000000000
--- a/src/server/api/endpoints/posts/create.ts
+++ /dev/null
@@ -1,251 +0,0 @@
-/**
- * Module dependencies
- */
-import $ from 'cafy';
-import deepEqual = require('deep-equal');
-import Note, { INote, isValidText, isValidCw, pack } from '../../../../models/note';
-import { ILocalUser } from '../../../../models/user';
-import Channel, { IChannel } from '../../../../models/channel';
-import DriveFile from '../../../../models/drive-file';
-import create from '../../../../services/note/create';
-import { IApp } from '../../../../models/app';
-
-/**
- * Create a note
- *
- * @param {any} params
- * @param {any} user
- * @param {any} app
- * @return {Promise<any>}
- */
-module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res, rej) => {
-	// Get 'visibility' parameter
-	const [visibility = 'public', visibilityErr] = $(params.visibility).optional.string().or(['public', 'unlisted', 'private', 'direct']).$;
-	if (visibilityErr) return rej('invalid visibility');
-
-	// Get 'text' parameter
-	const [text, textErr] = $(params.text).optional.string().pipe(isValidText).$;
-	if (textErr) return rej('invalid text');
-
-	// Get 'cw' parameter
-	const [cw, cwErr] = $(params.cw).optional.string().pipe(isValidCw).$;
-	if (cwErr) return rej('invalid cw');
-
-	// Get 'viaMobile' parameter
-	const [viaMobile = false, viaMobileErr] = $(params.viaMobile).optional.boolean().$;
-	if (viaMobileErr) return rej('invalid viaMobile');
-
-	// Get 'tags' parameter
-	const [tags = [], tagsErr] = $(params.tags).optional.array('string').unique().eachQ(t => t.range(1, 32)).$;
-	if (tagsErr) return rej('invalid tags');
-
-	// Get 'geo' parameter
-	const [geo, geoErr] = $(params.geo).optional.nullable.strict.object()
-		.have('coordinates', $().array().length(2)
-			.item(0, $().number().range(-180, 180))
-			.item(1, $().number().range(-90, 90)))
-		.have('altitude', $().nullable.number())
-		.have('accuracy', $().nullable.number())
-		.have('altitudeAccuracy', $().nullable.number())
-		.have('heading', $().nullable.number().range(0, 360))
-		.have('speed', $().nullable.number())
-		.$;
-	if (geoErr) return rej('invalid geo');
-
-	// Get 'mediaIds' parameter
-	const [mediaIds, mediaIdsErr] = $(params.mediaIds).optional.array('id').unique().range(1, 4).$;
-	if (mediaIdsErr) return rej('invalid mediaIds');
-
-	let files = [];
-	if (mediaIds !== undefined) {
-		// Fetch files
-		// forEach だと途中でエラーなどがあっても return できないので
-		// 敢えて for を使っています。
-		for (const mediaId of mediaIds) {
-			// Fetch file
-			// SELECT _id
-			const entity = await DriveFile.findOne({
-				_id: mediaId,
-				'metadata.userId': user._id
-			});
-
-			if (entity === null) {
-				return rej('file not found');
-			} else {
-				files.push(entity);
-			}
-		}
-	} else {
-		files = null;
-	}
-
-	// Get 'renoteId' parameter
-	const [renoteId, renoteIdErr] = $(params.renoteId).optional.id().$;
-	if (renoteIdErr) return rej('invalid renoteId');
-
-	let renote: INote = null;
-	let isQuote = false;
-	if (renoteId !== undefined) {
-		// Fetch renote to note
-		renote = await Note.findOne({
-			_id: renoteId
-		});
-
-		if (renote == null) {
-			return rej('renoteee is not found');
-		} else if (renote.renoteId && !renote.text && !renote.mediaIds) {
-			return rej('cannot renote to renote');
-		}
-
-		// Fetch recently note
-		const latestNote = await Note.findOne({
-			userId: user._id
-		}, {
-			sort: {
-				_id: -1
-			}
-		});
-
-		isQuote = text != null || files != null;
-
-		// 直近と同じRenote対象かつ引用じゃなかったらエラー
-		if (latestNote &&
-			latestNote.renoteId &&
-			latestNote.renoteId.equals(renote._id) &&
-			!isQuote) {
-			return rej('cannot renote same note that already reposted in your latest note');
-		}
-
-		// 直近がRenote対象かつ引用じゃなかったらエラー
-		if (latestNote &&
-			latestNote._id.equals(renote._id) &&
-			!isQuote) {
-			return rej('cannot renote your latest note');
-		}
-	}
-
-	// Get 'replyId' parameter
-	const [replyId, replyIdErr] = $(params.replyId).optional.id().$;
-	if (replyIdErr) return rej('invalid replyId');
-
-	let reply: INote = null;
-	if (replyId !== undefined) {
-		// Fetch reply
-		reply = await Note.findOne({
-			_id: replyId
-		});
-
-		if (reply === null) {
-			return rej('in reply to note is not found');
-		}
-
-		// 返信対象が引用でないRenoteだったらエラー
-		if (reply.renoteId && !reply.text && !reply.mediaIds) {
-			return rej('cannot reply to renote');
-		}
-	}
-
-	// Get 'channelId' parameter
-	const [channelId, channelIdErr] = $(params.channelId).optional.id().$;
-	if (channelIdErr) return rej('invalid channelId');
-
-	let channel: IChannel = null;
-	if (channelId !== undefined) {
-		// Fetch channel
-		channel = await Channel.findOne({
-			_id: channelId
-		});
-
-		if (channel === null) {
-			return rej('channel not found');
-		}
-
-		// 返信対象の投稿がこのチャンネルじゃなかったらダメ
-		if (reply && !channelId.equals(reply.channelId)) {
-			return rej('チャンネル内部からチャンネル外部の投稿に返信することはできません');
-		}
-
-		// Renote対象の投稿がこのチャンネルじゃなかったらダメ
-		if (renote && !channelId.equals(renote.channelId)) {
-			return rej('チャンネル内部からチャンネル外部の投稿をRenoteすることはできません');
-		}
-
-		// 引用ではないRenoteはダメ
-		if (renote && !isQuote) {
-			return rej('チャンネル内部では引用ではないRenoteをすることはできません');
-		}
-	} else {
-		// 返信対象の投稿がチャンネルへの投稿だったらダメ
-		if (reply && reply.channelId != null) {
-			return rej('チャンネル外部からチャンネル内部の投稿に返信することはできません');
-		}
-
-		// Renote対象の投稿がチャンネルへの投稿だったらダメ
-		if (renote && renote.channelId != null) {
-			return rej('チャンネル外部からチャンネル内部の投稿をRenoteすることはできません');
-		}
-	}
-
-	// Get 'poll' parameter
-	const [poll, pollErr] = $(params.poll).optional.strict.object()
-		.have('choices', $().array('string')
-			.unique()
-			.range(2, 10)
-			.each(c => c.length > 0 && c.length < 50))
-		.$;
-	if (pollErr) return rej('invalid poll');
-
-	if (poll) {
-		(poll as any).choices = (poll as any).choices.map((choice, i) => ({
-			id: i, // IDを付与
-			text: choice.trim(),
-			votes: 0
-		}));
-	}
-
-	// テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー
-	if (text === undefined && files === null && renote === null && poll === undefined) {
-		return rej('text, mediaIds, renoteId or poll is required');
-	}
-
-	// 直近の投稿と重複してたらエラー
-	// TODO: 直近の投稿が一日前くらいなら重複とは見なさない
-	if (user.latestNote) {
-		if (deepEqual({
-			text: user.latestNote.text,
-			reply: user.latestNote.replyId ? user.latestNote.replyId.toString() : null,
-			renote: user.latestNote.renoteId ? user.latestNote.renoteId.toString() : null,
-			mediaIds: (user.latestNote.mediaIds || []).map(id => id.toString())
-		}, {
-			text: text,
-			reply: reply ? reply._id.toString() : null,
-			renote: renote ? renote._id.toString() : null,
-			mediaIds: (files || []).map(file => file._id.toString())
-		})) {
-			return rej('duplicate');
-		}
-	}
-
-	// 投稿を作成
-	const note = await create(user, {
-		createdAt: new Date(),
-		media: files,
-		poll: poll,
-		text: text,
-		reply,
-		renote,
-		cw: cw,
-		tags: tags,
-		app: app,
-		viaMobile: viaMobile,
-		visibility,
-		geo
-	});
-
-	const noteObj = await pack(note, user);
-
-	// Reponse
-	res({
-		createdNote: noteObj
-	});
-});
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index 551d61856..aac207cc1 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -5,6 +5,7 @@ import Following from '../../models/following';
 import { deliver } from '../../queue';
 import renderNote from '../../remote/activitypub/renderer/note';
 import renderCreate from '../../remote/activitypub/renderer/create';
+import renderAnnounce from '../../remote/activitypub/renderer/announce';
 import context from '../../remote/activitypub/renderer/context';
 import { IDriveFile } from '../../models/drive-file';
 import notify from '../../publishers/notify';
@@ -34,6 +35,7 @@ export default async (user: IUser, data: {
 }, silent = false) => new Promise<INote>(async (res, rej) => {
 	if (data.createdAt == null) data.createdAt = new Date();
 	if (data.visibility == null) data.visibility = 'public';
+	if (data.viaMobile == null) data.viaMobile = false;
 
 	const tags = data.tags || [];
 
@@ -77,9 +79,7 @@ export default async (user: IUser, data: {
 		_user: {
 			host: user.host,
 			hostLower: user.hostLower,
-			account: isLocalUser(user) ? {} : {
-				inbox: user.inbox
-			}
+			inbox: isRemoteUser(user) ? user.inbox : undefined
 		}
 	};
 
@@ -128,15 +128,25 @@ export default async (user: IUser, data: {
 		});
 
 		if (!silent) {
-			const content = renderCreate(await renderNote(user, note));
-			content['@context'] = context;
+			const render = async () => {
+				const content = data.renote && data.text == null
+					? renderAnnounce(data.renote.uri ? data.renote.uri : await renderNote(data.renote))
+					: renderCreate(await renderNote(note));
+				content['@context'] = context;
+				return content;
+			};
 
 			// 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送
 			if (data.reply && isLocalUser(user) && isRemoteUser(data.reply._user)) {
-				deliver(user, content, data.reply._user.inbox).save();
+				deliver(user, await render(), data.reply._user.inbox).save();
 			}
 
-			Promise.all(followers.map(follower => {
+			// 投稿がRenoteかつ投稿者がローカルユーザーかつRenote元の投稿の投稿者がリモートユーザーなら配送
+			if (data.renote && isLocalUser(user) && isRemoteUser(data.renote._user)) {
+				deliver(user, await render(), data.renote._user.inbox).save();
+			}
+
+			Promise.all(followers.map(async follower => {
 				follower = follower.user[0];
 
 				if (isLocalUser(follower)) {
@@ -145,7 +155,7 @@ export default async (user: IUser, data: {
 				} else {
 					// フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信
 					if (isLocalUser(user)) {
-						deliver(user, content, follower.inbox).save();
+						deliver(user, await render(), follower.inbox).save();
 					}
 				}
 			}));
@@ -255,15 +265,13 @@ export default async (user: IUser, data: {
 		// Notify
 		const type = data.text ? 'quote' : 'renote';
 		notify(data.renote.userId, user._id, type, {
-			note_id: note._id
+			noteId: note._id
 		});
 
 		// Fetch watchers
 		NoteWatching.find({
 			noteId: data.renote._id,
-			userId: { $ne: user._id },
-			// 削除されたドキュメントは除く
-			deletedAt: { $exists: false }
+			userId: { $ne: user._id }
 		}, {
 			fields: {
 				userId: true
diff --git a/src/services/note/reaction/create.ts b/src/services/note/reaction/create.ts
index ea51b205d..88158034f 100644
--- a/src/services/note/reaction/create.ts
+++ b/src/services/note/reaction/create.ts
@@ -83,11 +83,11 @@ export default async (user: IUser, note: INote, reaction: string) => new Promise
 	}
 
 	//#region 配信
-	const content = renderLike(user, note);
-	content['@context'] = context;
-
 	// リアクターがローカルユーザーかつリアクション対象がリモートユーザーの投稿なら配送
 	if (isLocalUser(user) && isRemoteUser(note._user)) {
+		const content = renderLike(user, note);
+		content['@context'] = context;
+
 		deliver(user, content, note._user.inbox).save();
 	}
 	//#endregion

From 37afa07c8de2099a3b5713dca56abd39fe070285 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 07:11:29 +0900
Subject: [PATCH 1161/1250] Fix bug

---
 src/remote/activitypub/act/create/image.ts             |  2 +-
 src/remote/activitypub/act/create/note.ts              | 10 ++++------
 .../api/endpoints/drive/files/upload_from_url.ts       |  4 ----
 3 files changed, 5 insertions(+), 11 deletions(-)

diff --git a/src/remote/activitypub/act/create/image.ts b/src/remote/activitypub/act/create/image.ts
index c87423c5f..f1462f4ee 100644
--- a/src/remote/activitypub/act/create/image.ts
+++ b/src/remote/activitypub/act/create/image.ts
@@ -12,7 +12,7 @@ export default async function(actor: IRemoteUser, image): Promise<IDriveFile> {
 		throw new Error('invalid image');
 	}
 
-	log(`Creating the Image: ${image.id}`);
+	log(`Creating the Image: ${image.url}`);
 
 	return await uploadFromUrl(image.url, actor);
 }
diff --git a/src/remote/activitypub/act/create/note.ts b/src/remote/activitypub/act/create/note.ts
index 572a293ab..599bc10aa 100644
--- a/src/remote/activitypub/act/create/note.ts
+++ b/src/remote/activitypub/act/create/note.ts
@@ -37,15 +37,13 @@ export default async function createNote(resolver: Resolver, actor: IRemoteUser,
 	//#endergion
 
 	//#region 添付メディア
-	const media = [];
+	let media = [];
 	if ('attachment' in note && note.attachment != null) {
 		// TODO: attachmentは必ずしもImageではない
 		// TODO: attachmentは必ずしも配列ではない
-		// TODO: ループの中でawaitはすべきでない
-		note.attachment.forEach(async media => {
-			const created = await createImage(note.actor, media);
-			media.push(created);
-		});
+		media = await Promise.all(note.attachment.map(x => {
+			return createImage(actor, x);
+		}));
 	}
 	//#endregion
 
diff --git a/src/server/api/endpoints/drive/files/upload_from_url.ts b/src/server/api/endpoints/drive/files/upload_from_url.ts
index 9ebc8b823..8a426c0ef 100644
--- a/src/server/api/endpoints/drive/files/upload_from_url.ts
+++ b/src/server/api/endpoints/drive/files/upload_from_url.ts
@@ -7,10 +7,6 @@ import uploadFromUrl from '../../../../../services/drive/upload-from-url';
 
 /**
  * Create a file from a URL
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
  */
 module.exports = async (params, user): Promise<any> => {
 	// Get 'url' parameter

From 3c6eef86438691d3d54755b26b94430bb805c2be Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 15:15:22 +0900
Subject: [PATCH 1162/1250] Use id in uri instead of username

---
 src/remote/activitypub/act/follow.ts      | 16 +++++++---------
 src/remote/activitypub/act/undo/follow.ts | 18 ++++++++----------
 src/remote/activitypub/renderer/follow.ts |  2 +-
 src/remote/activitypub/renderer/key.ts    |  4 ++--
 src/remote/activitypub/renderer/like.ts   | 12 +++++-------
 src/remote/activitypub/renderer/note.ts   |  2 +-
 src/remote/activitypub/renderer/person.ts |  2 +-
 src/remote/activitypub/resolve-person.ts  |  6 ++----
 src/remote/activitypub/resolver.ts        |  2 +-
 src/server/activitypub/inbox.ts           |  2 +-
 src/server/activitypub/outbox.ts          | 14 ++++++++------
 src/server/activitypub/publickey.ts       | 13 +++++++------
 src/server/activitypub/user.ts            | 23 ++++++++---------------
 src/server/activitypub/with-user.ts       | 23 -----------------------
 14 files changed, 52 insertions(+), 87 deletions(-)
 delete mode 100644 src/server/activitypub/with-user.ts

diff --git a/src/remote/activitypub/act/follow.ts b/src/remote/activitypub/act/follow.ts
index 236886dc6..6a8b5a1be 100644
--- a/src/remote/activitypub/act/follow.ts
+++ b/src/remote/activitypub/act/follow.ts
@@ -1,25 +1,23 @@
-import parseAcct from '../../../acct/parse';
 import User, { IRemoteUser } from '../../../models/user';
 import config from '../../../config';
 import follow from '../../../services/following/create';
 import { IFollow } from '../type';
 
 export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
-	const prefix = config.url + '/@';
 	const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
 
-	if (!id.startsWith(prefix)) {
+	if (!id.startsWith(config.url + '/')) {
 		return null;
 	}
 
-	const { username, host } = parseAcct(id.slice(prefix.length));
-	if (host !== null) {
-		throw new Error();
+	const followee = await User.findOne({ _id: id.split('/').pop() });
+
+	if (followee === null) {
+		throw new Error('followee not found');
 	}
 
-	const followee = await User.findOne({ username, host });
-	if (followee === null) {
-		throw new Error();
+	if (followee.host != null) {
+		throw new Error('フォローしようとしているユーザーはローカルユーザーではありません');
 	}
 
 	await follow(actor, followee, activity);
diff --git a/src/remote/activitypub/act/undo/follow.ts b/src/remote/activitypub/act/undo/follow.ts
index fcf27c950..a85cb0305 100644
--- a/src/remote/activitypub/act/undo/follow.ts
+++ b/src/remote/activitypub/act/undo/follow.ts
@@ -1,25 +1,23 @@
-import parseAcct from '../../../../acct/parse';
 import User, { IRemoteUser } from '../../../../models/user';
 import config from '../../../../config';
 import unfollow from '../../../../services/following/delete';
 import { IFollow } from '../../type';
 
 export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
-	const prefix = config.url + '/@';
-	const id = typeof activity == 'string' ? activity : activity.id;
+	const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
 
-	if (!id.startsWith(prefix)) {
+	if (!id.startsWith(config.url + '/')) {
 		return null;
 	}
 
-	const { username, host } = parseAcct(id.slice(prefix.length));
-	if (host !== null) {
-		throw new Error();
+	const followee = await User.findOne({ _id: id.split('/').pop() });
+
+	if (followee === null) {
+		throw new Error('followee not found');
 	}
 
-	const followee = await User.findOne({ username, host });
-	if (followee === null) {
-		throw new Error();
+	if (followee.host != null) {
+		throw new Error('フォロー解除しようとしているユーザーはローカルユーザーではありません');
 	}
 
 	await unfollow(actor, followee, activity);
diff --git a/src/remote/activitypub/renderer/follow.ts b/src/remote/activitypub/renderer/follow.ts
index 89993d945..bf8eeff06 100644
--- a/src/remote/activitypub/renderer/follow.ts
+++ b/src/remote/activitypub/renderer/follow.ts
@@ -3,6 +3,6 @@ import { IRemoteUser, ILocalUser } from '../../../models/user';
 
 export default (follower: ILocalUser, followee: IRemoteUser) => ({
 	type: 'Follow',
-	actor: `${config.url}/@${follower.username}`,
+	actor: `${config.url}/users/${follower._id}`,
 	object: followee.uri
 });
diff --git a/src/remote/activitypub/renderer/key.ts b/src/remote/activitypub/renderer/key.ts
index 76e2f13bc..0d5e52557 100644
--- a/src/remote/activitypub/renderer/key.ts
+++ b/src/remote/activitypub/renderer/key.ts
@@ -3,8 +3,8 @@ import { extractPublic } from '../../../crypto_key';
 import { ILocalUser } from '../../../models/user';
 
 export default (user: ILocalUser) => ({
-	id: `${config.url}/@${user.username}/publickey`,
+	id: `${config.url}/users/${user._id}/publickey`,
 	type: 'Key',
-	owner: `${config.url}/@${user.username}`,
+	owner: `${config.url}/users/${user._id}`,
 	publicKeyPem: extractPublic(user.keypair)
 });
diff --git a/src/remote/activitypub/renderer/like.ts b/src/remote/activitypub/renderer/like.ts
index 744896cc4..061a10ba8 100644
--- a/src/remote/activitypub/renderer/like.ts
+++ b/src/remote/activitypub/renderer/like.ts
@@ -1,10 +1,8 @@
 import config from '../../../config';
 import { ILocalUser } from '../../../models/user';
 
-export default (user: ILocalUser, note) => {
-	return {
-		type: 'Like',
-		actor: `${config.url}/@${user.username}`,
-		object: note.uri ? note.uri : `${config.url}/notes/${note._id}`
-	};
-};
+export default (user: ILocalUser, note) => ({
+	type: 'Like',
+	actor: `${config.url}/users/${user._id}`,
+	object: note.uri ? note.uri : `${config.url}/notes/${note._id}`
+});
diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts
index 48799af08..7cc388dc3 100644
--- a/src/remote/activitypub/renderer/note.ts
+++ b/src/remote/activitypub/renderer/note.ts
@@ -34,7 +34,7 @@ export default async (note: INote) => {
 		_id: note.userId
 	});
 
-	const attributedTo = `${config.url}/@${user.username}`;
+	const attributedTo = `${config.url}/users/${user._id}`;
 
 	return {
 		id: `${config.url}/notes/${note._id}`,
diff --git a/src/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts
index 7ea6f532f..82e261029 100644
--- a/src/remote/activitypub/renderer/person.ts
+++ b/src/remote/activitypub/renderer/person.ts
@@ -3,7 +3,7 @@ import renderKey from './key';
 import config from '../../../config';
 
 export default user => {
-	const id = `${config.url}/@${user.username}`;
+	const id = `${config.url}/users/${user._id}`;
 
 	return {
 		type: 'Person',
diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts
index 7d7989a01..0140811f0 100644
--- a/src/remote/activitypub/resolve-person.ts
+++ b/src/remote/activitypub/resolve-person.ts
@@ -1,6 +1,5 @@
 import { JSDOM } from 'jsdom';
 import { toUnicode } from 'punycode';
-import parseAcct from '../../acct/parse';
 import config from '../../config';
 import User, { validateUsername, isValidName, isValidDescription, IUser } from '../../models/user';
 import webFinger from '../webfinger';
@@ -10,10 +9,9 @@ import { isCollectionOrOrderedCollection, IObject } from './type';
 
 export default async (value: string | IObject, verifier?: string): Promise<IUser> => {
 	const id = typeof value == 'string' ? value : value.id;
-	const localPrefix = config.url + '/@';
 
-	if (id.startsWith(localPrefix)) {
-		return await User.findOne(parseAcct(id.substr(localPrefix.length)));
+	if (id.startsWith(config.url + '/')) {
+		return await User.findOne({ _id: id.split('/').pop() });
 	}
 
 	const resolver = new Resolver();
diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts
index 1466139c4..7d45783b4 100644
--- a/src/remote/activitypub/resolver.ts
+++ b/src/remote/activitypub/resolver.ts
@@ -50,7 +50,7 @@ export default class Resolver {
 
 		//#region resolve local objects
 		// TODO
-		//if (value.startsWith(`${config.url}/@`)) {
+		//if (value.startsWith(`${config.url}/`)) {
 		//#endregion
 
 		const object = await request({
diff --git a/src/server/activitypub/inbox.ts b/src/server/activitypub/inbox.ts
index 1b6cc0c00..643d2945b 100644
--- a/src/server/activitypub/inbox.ts
+++ b/src/server/activitypub/inbox.ts
@@ -5,7 +5,7 @@ import { createHttp } from '../../queue';
 
 const app = express.Router();
 
-app.post('/@:user/inbox', bodyParser.json({
+app.post('/users/:user/inbox', bodyParser.json({
 	type() {
 		return true;
 	}
diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts
index 4557871bc..1c97c17a2 100644
--- a/src/server/activitypub/outbox.ts
+++ b/src/server/activitypub/outbox.ts
@@ -4,23 +4,25 @@ import renderNote from '../../remote/activitypub/renderer/note';
 import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
 import config from '../../config';
 import Note from '../../models/note';
-import withUser from './with-user';
+import User from '../../models/user';
 
 const app = express.Router();
 
-app.get('/@:user/outbox', withUser(username => {
-	return `${config.url}/@${username}/inbox`;
-}, async (user, req, res) => {
+app.get('/users/:user/outbox', async (req, res) => {
+	const userId = req.params.user;
+
+	const user = await User.findOne({ _id: userId });
+
 	const notes = await Note.find({ userId: user._id }, {
 		limit: 20,
 		sort: { _id: -1 }
 	});
 
 	const renderedNotes = await Promise.all(notes.map(note => renderNote(note)));
-	const rendered = renderOrderedCollection(`${config.url}/@${user.username}/inbox`, user.notesCount, renderedNotes);
+	const rendered = renderOrderedCollection(`${config.url}/users/${userId}/inbox`, user.notesCount, renderedNotes);
 	rendered['@context'] = context;
 
 	res.json(rendered);
-}));
+});
 
 export default app;
diff --git a/src/server/activitypub/publickey.ts b/src/server/activitypub/publickey.ts
index b48504927..aa0c4271b 100644
--- a/src/server/activitypub/publickey.ts
+++ b/src/server/activitypub/publickey.ts
@@ -1,18 +1,19 @@
 import * as express from 'express';
 import context from '../../remote/activitypub/renderer/context';
 import render from '../../remote/activitypub/renderer/key';
-import config from '../../config';
-import withUser from './with-user';
+import User from '../../models/user';
 
 const app = express.Router();
 
-app.get('/@:user/publickey', withUser(username => {
-	return `${config.url}/@${username}/publickey`;
-}, (user, req, res) => {
+app.get('/users/:user/publickey', async (req, res) => {
+	const userId = req.params.user;
+
+	const user = await User.findOne({ _id: userId });
+
 	const rendered = render(user);
 	rendered['@context'] = context;
 
 	res.json(rendered);
-}));
+});
 
 export default app;
diff --git a/src/server/activitypub/user.ts b/src/server/activitypub/user.ts
index f05497451..9e98e92b6 100644
--- a/src/server/activitypub/user.ts
+++ b/src/server/activitypub/user.ts
@@ -1,26 +1,19 @@
 import * as express from 'express';
-import config from '../../config';
 import context from '../../remote/activitypub/renderer/context';
 import render from '../../remote/activitypub/renderer/person';
-import withUser from './with-user';
+import User from '../../models/user';
+
+const app = express.Router();
+
+app.get('/users/:user', async (req, res) => {
+	const userId = req.params.user;
+
+	const user = await User.findOne({ _id: userId });
 
-const respond = withUser(username => `${config.url}/@${username}`, (user, req, res) => {
 	const rendered = render(user);
 	rendered['@context'] = context;
 
 	res.json(rendered);
 });
 
-const app = express.Router();
-
-app.get('/@:user', (req, res, next) => {
-	const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']);
-
-	if ((['application/activity+json', 'application/ld+json'] as any[]).includes(accepted)) {
-		respond(req, res, next);
-	} else {
-		next();
-	}
-});
-
 export default app;
diff --git a/src/server/activitypub/with-user.ts b/src/server/activitypub/with-user.ts
deleted file mode 100644
index bdbbefb42..000000000
--- a/src/server/activitypub/with-user.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import parseAcct from '../../acct/parse';
-import User from '../../models/user';
-
-export default (redirect, respond) => async (req, res, next) => {
-	const { username, host } = parseAcct(req.params.user);
-	if (host !== null) {
-		return res.sendStatus(422);
-	}
-
-	const user = await User.findOne({
-		usernameLower: username.toLowerCase(),
-		host: null
-	});
-	if (user === null) {
-		return res.sendStatus(404);
-	}
-
-	if (username !== user.username) {
-		return res.redirect(redirect(user.username));
-	}
-
-	return respond(user, req, res, next);
-};

From 1078dd6d65ecc78c67e26d55870ee629e172db8a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 15:25:17 +0900
Subject: [PATCH 1163/1250] :v:

---
 src/remote/resolve-user.ts             | 5 +++++
 src/server/api/endpoints/users/show.ts | 4 ----
 src/server/webfinger.ts                | 9 ++++++---
 3 files changed, 11 insertions(+), 7 deletions(-)

diff --git a/src/remote/resolve-user.ts b/src/remote/resolve-user.ts
index 9e1ae5195..0e7edd8e1 100644
--- a/src/remote/resolve-user.ts
+++ b/src/remote/resolve-user.ts
@@ -2,12 +2,17 @@ import { toUnicode, toASCII } from 'punycode';
 import User from '../models/user';
 import resolvePerson from './activitypub/resolve-person';
 import webFinger from './webfinger';
+import config from '../config';
 
 export default async (username, host, option) => {
 	const usernameLower = username.toLowerCase();
 	const hostLowerAscii = toASCII(host).toLowerCase();
 	const hostLower = toUnicode(hostLowerAscii);
 
+	if (config.host == hostLower) {
+		return await User.findOne({ usernameLower });
+	}
+
 	let user = await User.findOne({ usernameLower, hostLower }, option);
 
 	if (user === null) {
diff --git a/src/server/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts
index d272ce463..7e7f5dc48 100644
--- a/src/server/api/endpoints/users/show.ts
+++ b/src/server/api/endpoints/users/show.ts
@@ -9,10 +9,6 @@ const cursorOption = { fields: { data: false } };
 
 /**
  * Show a user
- *
- * @param {any} params
- * @param {any} me
- * @return {Promise<any>}
  */
 module.exports = (params, me) => new Promise(async (res, rej) => {
 	let user;
diff --git a/src/server/webfinger.ts b/src/server/webfinger.ts
index fd7ebc3fb..dbf0999f3 100644
--- a/src/server/webfinger.ts
+++ b/src/server/webfinger.ts
@@ -4,9 +4,9 @@ import config from '../config';
 import parseAcct from '../acct/parse';
 import User from '../models/user';
 
-const app = express();
+const app = express.Router();
 
-app.get('/.well-known/webfinger', async (req: express.Request, res: express.Response) => {
+app.get('/.well-known/webfinger', async (req, res) => {
 	if (typeof req.query.resource !== 'string') {
 		return res.sendStatus(400);
 	}
@@ -38,11 +38,14 @@ app.get('/.well-known/webfinger', async (req: express.Request, res: express.Resp
 		links: [{
 			rel: 'self',
 			type: 'application/activity+json',
-			href: `${config.url}/@${user.username}`
+			href: `${config.url}/users/${user._id}`
 		}, {
 			rel: 'http://webfinger.net/rel/profile-page',
 			type: 'text/html',
 			href: `${config.url}/@${user.username}`
+		}, {
+			rel: 'http://ostatus.org/schema/1.0/subscribe',
+			template: `${config.url}/authorize-follow?acct={uri}`
 		}]
 	});
 });

From 784760263e01146e617009952bd20b9373b4503b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 15:37:31 +0900
Subject: [PATCH 1164/1250] v4679

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 10517b0ce..29b9fd416 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4224",
+	"version": "0.0.4679",
 	"codename": "nighthike",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",

From 237eae215445a2a6cd15c7ad34f3e2566b7497d9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 15:51:32 +0900
Subject: [PATCH 1165/1250] Check whether is local user

---
 src/server/activitypub/publickey.ts | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/src/server/activitypub/publickey.ts b/src/server/activitypub/publickey.ts
index aa0c4271b..e874b8272 100644
--- a/src/server/activitypub/publickey.ts
+++ b/src/server/activitypub/publickey.ts
@@ -1,7 +1,7 @@
 import * as express from 'express';
 import context from '../../remote/activitypub/renderer/context';
 import render from '../../remote/activitypub/renderer/key';
-import User from '../../models/user';
+import User, { isLocalUser } from '../../models/user';
 
 const app = express.Router();
 
@@ -10,10 +10,14 @@ app.get('/users/:user/publickey', async (req, res) => {
 
 	const user = await User.findOne({ _id: userId });
 
-	const rendered = render(user);
-	rendered['@context'] = context;
+	if (isLocalUser(user)) {
+		const rendered = render(user);
+		rendered['@context'] = context;
 
-	res.json(rendered);
+		res.json(rendered);
+	} else {
+		res.sendStatus(400);
+	}
 });
 
 export default app;

From 6ea6f0258c297fd6c4c1e71f4de1ad556a56302b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 15:57:23 +0900
Subject: [PATCH 1166/1250] oops

---
 tools/migration/nighthike/2.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tools/migration/nighthike/2.js b/tools/migration/nighthike/2.js
index e8f622902..bb516db5b 100644
--- a/tools/migration/nighthike/2.js
+++ b/tools/migration/nighthike/2.js
@@ -4,7 +4,7 @@ const { default: App } = require('../../../built/models/app');
 const { default: zip } = require('@prezzemolo/zip')
 
 const migrate = async (app) => {
-	const result = await User.update(app._id, {
+	const result = await App.update(app._id, {
 		$set: {
 			'name_id': app.name_id.replace(/\-/g, '_'),
 			'name_id_lower': app.name_id_lower.replace(/\-/g, '_')

From c283fe22d2847ea22ec809e6dcefc946b7cf501f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 16:13:25 +0900
Subject: [PATCH 1167/1250] oops

---
 tools/migration/nighthike/4.js | 41 ++++++++++++++++++++++++----------
 1 file changed, 29 insertions(+), 12 deletions(-)

diff --git a/tools/migration/nighthike/4.js b/tools/migration/nighthike/4.js
index f308341f0..d77c91ca8 100644
--- a/tools/migration/nighthike/4.js
+++ b/tools/migration/nighthike/4.js
@@ -178,8 +178,22 @@ db.posts.update({}, {
 		via_mobile: 'viaMobile',
 		reaction_counts: 'reactionCounts',
 		replies_count: 'repliesCount',
-		repost_count: 'repostCount',
+		repost_count: 'repostCount'
+	}
+}, false, true);
+
+db.posts.update({
+	_reply: { $ne: null }
+}, {
+	$rename: {
 		'_reply.user_id': '_reply.userId',
+	}
+}, false, true);
+
+db.posts.update({
+	_repost: { $ne: null }
+}, {
+	$rename: {
 		'_repost.user_id': '_repost.userId',
 	}
 }, false, true);
@@ -198,6 +212,20 @@ db.swSubscriptions.update({}, {
 	}
 }, false, true);
 
+db.users.update({}, {
+	$unset: {
+		likes_count: '',
+		liked_count: '',
+		latest_post: '',
+		'account.twitter.access_token': '',
+		'account.twitter.access_token_secret': '',
+		'account.twitter.user_id': '',
+		'account.twitter.screen_name': '',
+		'account.line.user_id': '',
+		'account.client_settings.mobile_home': ''
+	}
+}, false, true);
+
 db.users.update({}, {
 	$rename: {
 		created_at: 'createdAt',
@@ -218,16 +246,5 @@ db.users.update({}, {
 		'account.two_factor_secret': 'account.twoFactorSecret',
 		'account.two_factor_enabled': 'account.twoFactorEnabled',
 		'account.client_settings': 'account.clientSettings'
-	},
-	$unset: {
-		likes_count: '',
-		liked_count: '',
-		latest_post: '',
-		'account.twitter.access_token': '',
-		'account.twitter.access_token_secret': '',
-		'account.twitter.user_id': '',
-		'account.twitter.screen_name': '',
-		'account.line.user_id': '',
-		'account.client_settings.mobile_home': ''
 	}
 }, false, true);

From 7f37b461de0c1c9fedcdfd1a96dd482b82facf19 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 16:14:08 +0900
Subject: [PATCH 1168/1250] oops

---
 tools/migration/nighthike/5.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tools/migration/nighthike/5.js b/tools/migration/nighthike/5.js
index 97f507733..2221b667a 100644
--- a/tools/migration/nighthike/5.js
+++ b/tools/migration/nighthike/5.js
@@ -1,6 +1,6 @@
 // for Node.js interpret
 
-const { default: Post } = require('../../../built/models/post');
+const { default: Post } = require('../../../built/models/note');
 const { default: zip } = require('@prezzemolo/zip')
 
 const migrate = async (post) => {

From e4b42b4b25bdcfc024a1446537a92497779aaa7f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 16:16:13 +0900
Subject: [PATCH 1169/1250] oops

---
 tools/migration/nighthike/5.js | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/tools/migration/nighthike/5.js b/tools/migration/nighthike/5.js
index 2221b667a..3989ea630 100644
--- a/tools/migration/nighthike/5.js
+++ b/tools/migration/nighthike/5.js
@@ -1,6 +1,8 @@
 // for Node.js interpret
 
-const { default: Post } = require('../../../built/models/note');
+const mongodb = require("../../../built/db/mongodb");
+const Post = mongodb.default.get('posts');
+
 const { default: zip } = require('@prezzemolo/zip')
 
 const migrate = async (post) => {

From 85ed6ea036b0601dc7e5275fcb11397391a16119 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 16:17:30 +0900
Subject: [PATCH 1170/1250] oops

---
 tools/migration/nighthike/7.js | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/tools/migration/nighthike/7.js b/tools/migration/nighthike/7.js
index f1102d13e..ed5f1e6b9 100644
--- a/tools/migration/nighthike/7.js
+++ b/tools/migration/nighthike/7.js
@@ -1,6 +1,7 @@
 // for Node.js interpret
 
-const { default: Post } = require('../../../built/models/post');
+const mongodb = require("../../../built/db/mongodb");
+const Post = mongodb.default.get('posts');
 const { default: zip } = require('@prezzemolo/zip')
 const html = require('../../../built/text/html').default;
 const parse = require('../../../built/text/parse').default;

From b6c5644cd4079d1ba7b85eeb7d0fae364eb5739e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 16:36:05 +0900
Subject: [PATCH 1171/1250] oops

---
 tools/migration/nighthike/11.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/tools/migration/nighthike/11.js b/tools/migration/nighthike/11.js
index 979e5bdc5..2a4e8630d 100644
--- a/tools/migration/nighthike/11.js
+++ b/tools/migration/nighthike/11.js
@@ -23,6 +23,7 @@ db.notes.update({}, {
 	$rename: {
 		_repost: '_renote',
 		repostId: 'renoteId',
+		repostCount: 'renoteCount'
 	}
 }, false, true);
 

From fd91ad7df7c5210073d68d7715aaa784c37f4a40 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 17:15:07 +0900
Subject: [PATCH 1172/1250] oops

---
 .../app/desktop/views/components/timeline.vue      |  2 +-
 tools/migration/nighthike/4.js                     | 14 +++++++++++++-
 2 files changed, 14 insertions(+), 2 deletions(-)

diff --git a/src/client/app/desktop/views/components/timeline.vue b/src/client/app/desktop/views/components/timeline.vue
index ea8f0053b..e1f88b62f 100644
--- a/src/client/app/desktop/views/components/timeline.vue
+++ b/src/client/app/desktop/views/components/timeline.vue
@@ -96,7 +96,7 @@ export default Vue.extend({
 		onNote(note) {
 			// サウンドを再生する
 			if ((this as any).os.isEnableSounds) {
-				const sound = new Audio(`${url}/assets/note.mp3`);
+				const sound = new Audio(`${url}/assets/post.mp3`);
 				sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 1;
 				sound.play();
 			}
diff --git a/tools/migration/nighthike/4.js b/tools/migration/nighthike/4.js
index d77c91ca8..5e12e6405 100644
--- a/tools/migration/nighthike/4.js
+++ b/tools/migration/nighthike/4.js
@@ -50,8 +50,20 @@ db.drive_files.files.renameCollection('driveFiles.files');
 db.drive_files.chunks.renameCollection('driveFiles.chunks');
 db.driveFiles.files.update({}, {
 	$rename: {
-		'metadata.user_id': 'metadata.userId',
+		'metadata.user_id': 'metadata.userId'
+	}
+}, false, true);
+db.driveFiles.files.update({
+	'metadata.folder_id': { $ne: null }
+}, {
+	$rename: {
 		'metadata.folder_id': 'metadata.folderId',
+	}
+}, false, true);
+db.driveFiles.files.update({
+	'metadata.properties.average_color': { $ne: null }
+}, {
+	$rename: {
 		'metadata.properties.average_color': 'metadata.properties.avgColor'
 	}
 }, false, true);

From d2c02b307e32546816d1622057f9f5acd9afc79c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 17:16:11 +0900
Subject: [PATCH 1173/1250] oops

---
 src/server/api/endpoints/channels/{posts.ts => notes.ts} | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename src/server/api/endpoints/channels/{posts.ts => notes.ts} (100%)

diff --git a/src/server/api/endpoints/channels/posts.ts b/src/server/api/endpoints/channels/notes.ts
similarity index 100%
rename from src/server/api/endpoints/channels/posts.ts
rename to src/server/api/endpoints/channels/notes.ts

From 8fa89e044307dd8c38511f0b75402c7f8dcc46b3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 17:20:46 +0900
Subject: [PATCH 1174/1250] oops

---
 .../desktop/views/components/post-detail.vue  | 448 -------------
 .../desktop/views/components/posts.post.vue   | 596 ------------------
 2 files changed, 1044 deletions(-)
 delete mode 100644 src/client/app/desktop/views/components/post-detail.vue
 delete mode 100644 src/client/app/desktop/views/components/posts.post.vue

diff --git a/src/client/app/desktop/views/components/post-detail.vue b/src/client/app/desktop/views/components/post-detail.vue
deleted file mode 100644
index df7c33dfa..000000000
--- a/src/client/app/desktop/views/components/post-detail.vue
+++ /dev/null
@@ -1,448 +0,0 @@
-<template>
-<div class="mk-note-detail" :title="title">
-	<button
-		class="read-more"
-		v-if="p.reply && p.reply.replyId && context == null"
-		title="会話をもっと読み込む"
-		@click="fetchContext"
-		:disabled="contextFetching"
-	>
-		<template v-if="!contextFetching">%fa:ellipsis-v%</template>
-		<template v-if="contextFetching">%fa:spinner .pulse%</template>
-	</button>
-	<div class="context">
-		<x-sub v-for="note in context" :key="note.id" :note="note"/>
-	</div>
-	<div class="reply-to" v-if="p.reply">
-		<x-sub :note="p.reply"/>
-	</div>
-	<div class="renote" v-if="isRenote">
-		<p>
-			<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="note.userId">
-				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
-			</router-link>
-			%fa:retweet%
-			<router-link class="name" :href="`/@${acct}`">{{ getUserName(note.user) }}</router-link>
-			がRenote
-		</p>
-	</div>
-	<article>
-		<router-link class="avatar-anchor" :to="`/@${pAcct}`">
-			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
-		</router-link>
-		<header>
-			<router-link class="name" :to="`/@${pAcct}`" v-user-preview="p.user.id">{{ getUserName(p.user) }}</router-link>
-			<span class="username">@{{ pAcct }}</span>
-			<router-link class="time" :to="`/@${pAcct}/${p.id}`">
-				<mk-time :time="p.createdAt"/>
-			</router-link>
-		</header>
-		<div class="body">
-			<mk-note-html :class="$style.text" v-if="p.text" :text="p.text" :i="os.i"/>
-			<div class="media" v-if="p.media.length > 0">
-				<mk-media-list :media-list="p.media"/>
-			</div>
-			<mk-poll v-if="p.poll" :note="p"/>
-			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
-			<div class="tags" v-if="p.tags && p.tags.length > 0">
-				<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
-			</div>
-			<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
-			<div class="map" v-if="p.geo" ref="map"></div>
-			<div class="renote" v-if="p.renote">
-				<mk-note-preview :note="p.renote"/>
-			</div>
-		</div>
-		<footer>
-			<mk-reactions-viewer :note="p"/>
-			<button @click="reply" title="返信">
-				%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
-			</button>
-			<button @click="renote" title="Renote">
-				%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
-			</button>
-			<button :class="{ reacted: p.myReaction != 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">
-		<x-sub v-for="note in replies" :key="note.id" :note="note"/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import dateStringify from '../../../common/scripts/date-stringify';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
-import parse from '../../../../../text/parse';
-
-import MkPostFormWindow from './post-form-window.vue';
-import MkRenoteFormWindow from './renote-form-window.vue';
-import MkNoteMenu from '../../../common/views/components/note-menu.vue';
-import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
-import XSub from './note-detail.sub.vue';
-
-export default Vue.extend({
-	components: {
-		XSub
-	},
-
-	props: {
-		note: {
-			type: Object,
-			required: true
-		},
-		compact: {
-			default: false
-		}
-	},
-
-	data() {
-		return {
-			context: [],
-			contextFetching: false,
-			replies: []
-		};
-	},
-
-	computed: {
-		isRenote(): boolean {
-			return (this.note.renote &&
-				this.note.text == null &&
-				this.note.mediaIds.length == 0 &&
-				this.note.poll == null);
-		},
-		p(): any {
-			return this.isRenote ? this.note.renote : this.note;
-		},
-		reactionsCount(): number {
-			return this.p.reactionCounts
-				? Object.keys(this.p.reactionCounts)
-					.map(key => this.p.reactionCounts[key])
-					.reduce((a, b) => a + b)
-				: 0;
-		},
-		title(): string {
-			return dateStringify(this.p.createdAt);
-		},
-		acct(): string {
-			return getAcct(this.note.user);
-		},
-		name(): string {
-			return getUserName(this.note.user);
-		},
-		pAcct(): string {
-			return getAcct(this.p.user);
-		},
-		pName(): string {
-			return getUserName(this.p.user);
-		},
-		urls(): string[] {
-			if (this.p.text) {
-				const ast = parse(this.p.text);
-				return 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 as any).api('notes/replies', {
-				noteId: this.p.id,
-				limit: 8
-			}).then(replies => {
-				this.replies = replies;
-			});
-		}
-
-		// Draw map
-		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
-			if (shouldShowMap) {
-				(this as any).os.getGoogleMaps().then(maps => {
-					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
-					const map = new maps.Map(this.$refs.map, {
-						center: uluru,
-						zoom: 15
-					});
-					new maps.Marker({
-						position: uluru,
-						map: map
-					});
-				});
-			}
-		}
-	},
-
-	methods: {
-		fetchContext() {
-			this.contextFetching = true;
-
-			// Fetch context
-			(this as any).api('notes/context', {
-				noteId: this.p.replyId
-			}).then(context => {
-				this.contextFetching = false;
-				this.context = context.reverse();
-			});
-		},
-		reply() {
-			(this as any).os.new(MkPostFormWindow, {
-				reply: this.p
-			});
-		},
-		renote() {
-			(this as any).os.new(MkRenoteFormWindow, {
-				note: this.p
-			});
-		},
-		react() {
-			(this as any).os.new(MkReactionPicker, {
-				source: this.$refs.reactButton,
-				note: this.p
-			});
-		},
-		menu() {
-			(this as any).os.new(MkNoteMenu, {
-				source: this.$refs.menuButton,
-				note: this.p
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-@import '~const.styl'
-
-.mk-note-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
-
-	> .renote
-		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
-
-			> .renote
-				margin 8px 0
-
-				> .mk-note-preview
-					padding 16px
-					border dashed 1px #c0dac6
-					border-radius 8px
-
-			> .location
-				margin 4px 0
-				font-size 12px
-				color #ccc
-
-			> .map
-				width 100%
-				height 300px
-
-				&:empty
-					display none
-
-			> .mk-url-preview
-				margin-top 8px
-
-			> .tags
-				margin 4px 0 0 0
-
-				> *
-					display inline-block
-					margin 0 8px 0 0
-					padding 2px 8px 2px 16px
-					font-size 90%
-					color #8d969e
-					background #edf0f3
-					border-radius 4px
-
-					&:before
-						content ""
-						display block
-						position absolute
-						top 0
-						bottom 0
-						left 4px
-						width 8px
-						height 8px
-						margin auto 0
-						background #fff
-						border-radius 100%
-
-					&:hover
-						text-decoration none
-						background #e2e7ec
-
-		> 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>
-
-<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/client/app/desktop/views/components/posts.post.vue b/src/client/app/desktop/views/components/posts.post.vue
deleted file mode 100644
index d7c21dfa0..000000000
--- a/src/client/app/desktop/views/components/posts.post.vue
+++ /dev/null
@@ -1,596 +0,0 @@
-<template>
-<div class="note" tabindex="-1" :title="title" @keydown="onKeydown">
-	<div class="reply-to" v-if="p.reply">
-		<x-sub :note="p.reply"/>
-	</div>
-	<div class="renote" v-if="isRenote">
-		<p>
-			<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="note.userId">
-				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
-			</router-link>
-			%fa:retweet%
-			<span>{{ '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.indexOf('{')) }}</span>
-			<a class="name" :href="`/@${acct}`" v-user-preview="note.userId">{{ getUserName(note.user) }}</a>
-			<span>{{ '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-note.reposted-by%'.indexOf('}') + 1) }}</span>
-		</p>
-		<mk-time :time="note.createdAt"/>
-	</div>
-	<article>
-		<router-link class="avatar-anchor" :to="`/@${acct}`">
-			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
-		</router-link>
-		<div class="main">
-			<header>
-				<router-link class="name" :to="`/@${acct}`" v-user-preview="p.user.id">{{ acct }}</router-link>
-				<span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span>
-				<span class="username">@{{ acct }}</span>
-				<div class="info">
-					<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
-					<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
-					<router-link class="created-at" :to="url">
-						<mk-time :time="p.createdAt"/>
-					</router-link>
-				</div>
-			</header>
-			<div class="body">
-				<p class="channel" v-if="p.channel">
-					<a :href="`${_CH_URL_}/${p.channel.id}`" target="_blank">{{ p.channel.title }}</a>:
-				</p>
-				<div class="text">
-					<a class="reply" v-if="p.reply">%fa:reply%</a>
-					<mk-note-html v-if="p.textHtml" :text="p.text" :i="os.i" :class="$style.text"/>
-					<a class="rp" v-if="p.renote">RP:</a>
-				</div>
-				<div class="media" v-if="p.media.length > 0">
-					<mk-media-list :media-list="p.media"/>
-				</div>
-				<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
-				<div class="tags" v-if="p.tags && p.tags.length > 0">
-					<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
-				</div>
-				<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
-				<div class="map" v-if="p.geo" ref="map"></div>
-				<div class="renote" v-if="p.renote">
-					<mk-note-preview :note="p.renote"/>
-				</div>
-				<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
-			</div>
-			<footer>
-				<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
-				<button @click="reply" title="%i18n:desktop.tags.mk-timeline-note.reply%">
-					%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
-				</button>
-				<button @click="renote" title="%i18n:desktop.tags.mk-timeline-note.renote%">
-					%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
-				</button>
-				<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:desktop.tags.mk-timeline-note.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 title="%i18n:desktop.tags.mk-timeline-note.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-note-status-graph width="462" height="130" :note="p"/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import dateStringify from '../../../common/scripts/date-stringify';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
-import parse from '../../../../../text/parse';
-
-import MkPostFormWindow from './post-form-window.vue';
-import MkRenoteFormWindow from './renote-form-window.vue';
-import MkNoteMenu from '../../../common/views/components/note-menu.vue';
-import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
-import XSub from './notes.note.sub.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({
-	components: {
-		XSub
-	},
-
-	props: ['note'],
-
-	data() {
-		return {
-			isDetailOpened: false,
-			connection: null,
-			connectionId: null
-		};
-	},
-
-	computed: {
-		acct(): string {
-			return getAcct(this.p.user);
-		},
-		name(): string {
-			return getUserName(this.p.user);
-		},
-		isRenote(): boolean {
-			return (this.note.renote &&
-				this.note.text == null &&
-				this.note.mediaIds.length == 0 &&
-				this.note.poll == null);
-		},
-		p(): any {
-			return this.isRenote ? this.note.renote : this.note;
-		},
-		reactionsCount(): number {
-			return this.p.reactionCounts
-				? Object.keys(this.p.reactionCounts)
-					.map(key => this.p.reactionCounts[key])
-					.reduce((a, b) => a + b)
-				: 0;
-		},
-		title(): string {
-			return dateStringify(this.p.createdAt);
-		},
-		url(): string {
-			return `/@${this.acct}/${this.p.id}`;
-		},
-		urls(): string[] {
-			if (this.p.text) {
-				const ast = parse(this.p.text);
-				return ast
-					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
-					.map(t => t.url);
-			} else {
-				return null;
-			}
-		}
-	},
-
-	created() {
-		if ((this as any).os.isSignedIn) {
-			this.connection = (this as any).os.stream.getConnection();
-			this.connectionId = (this as any).os.stream.use();
-		}
-	},
-
-	mounted() {
-		this.capture(true);
-
-		if ((this as any).os.isSignedIn) {
-			this.connection.on('_connected_', this.onStreamConnected);
-		}
-
-		// Draw map
-		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
-			if (shouldShowMap) {
-				(this as any).os.getGoogleMaps().then(maps => {
-					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
-					const map = new maps.Map(this.$refs.map, {
-						center: uluru,
-						zoom: 15
-					});
-					new maps.Marker({
-						position: uluru,
-						map: map
-					});
-				});
-			}
-		}
-	},
-
-	beforeDestroy() {
-		this.decapture(true);
-
-		if ((this as any).os.isSignedIn) {
-			this.connection.off('_connected_', this.onStreamConnected);
-			(this as any).os.stream.dispose(this.connectionId);
-		}
-	},
-
-	methods: {
-		capture(withHandler = false) {
-			if ((this as any).os.isSignedIn) {
-				this.connection.send({
-					type: 'capture',
-					id: this.p.id
-				});
-				if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
-			}
-		},
-		decapture(withHandler = false) {
-			if ((this as any).os.isSignedIn) {
-				this.connection.send({
-					type: 'decapture',
-					id: this.p.id
-				});
-				if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated);
-			}
-		},
-		onStreamConnected() {
-			this.capture();
-		},
-		onStreamNoteUpdated(data) {
-			const note = data.note;
-			if (note.id == this.note.id) {
-				this.$emit('update:note', note);
-			} else if (note.id == this.note.renoteId) {
-				this.note.renote = note;
-			}
-		},
-		reply() {
-			(this as any).os.new(MkPostFormWindow, {
-				reply: this.p
-			});
-		},
-		renote() {
-			(this as any).os.new(MkRenoteFormWindow, {
-				note: this.p
-			});
-		},
-		react() {
-			(this as any).os.new(MkReactionPicker, {
-				source: this.$refs.reactButton,
-				note: this.p
-			});
-		},
-		menu() {
-			(this as any).os.new(MkNoteMenu, {
-				source: this.$refs.menuButton,
-				note: this.p
-			});
-		},
-		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.renote();
-					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>
-
-<style lang="stylus" scoped>
-@import '~const.styl'
-
-.note
-	margin 0
-	padding 0
-	background #fff
-	border-bottom solid 1px #eaeaea
-
-	&:first-child
-		border-top-left-radius 6px
-		border-top-right-radius 6px
-
-		> .renote
-			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
-
-	> .renote
-		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-note-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
-				align-items center
-				margin-bottom 4px
-				white-space nowrap
-
-				> .name
-					display block
-					margin 0 .5em 0 0
-					padding 0
-					overflow hidden
-					color #627079
-					font-size 1em
-					font-weight bold
-					text-decoration none
-					text-overflow ellipsis
-
-					&:hover
-						text-decoration underline
-
-				> .is-bot
-					margin 0 .5em 0 0
-					padding 1px 6px
-					font-size 12px
-					color #aaa
-					border solid 1px #ddd
-					border-radius 3px
-
-				> .username
-					margin 0 .5em 0 0
-					color #ccc
-
-				> .info
-					margin-left auto
-					font-size 0.9em
-
-					> .mobile
-						margin-right 8px
-						color #ccc
-
-					> .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
-
-					>>> .quote
-						margin 8px
-						padding 6px 12px
-						color #aaa
-						border-left solid 3px #eee
-
-					> .reply
-						margin-right 8px
-						color #717171
-
-					> .rp
-						margin-left 4px
-						font-style oblique
-						color #a0bf46
-
-				> .location
-					margin 4px 0
-					font-size 12px
-					color #ccc
-
-				> .map
-					width 100%
-					height 300px
-
-					&:empty
-						display none
-
-				> .tags
-					margin 4px 0 0 0
-
-					> *
-						display inline-block
-						margin 0 8px 0 0
-						padding 2px 8px 2px 16px
-						font-size 90%
-						color #8d969e
-						background #edf0f3
-						border-radius 4px
-
-						&:before
-							content ""
-							display block
-							position absolute
-							top 0
-							bottom 0
-							left 4px
-							width 8px
-							height 8px
-							margin auto 0
-							background #fff
-							border-radius 100%
-
-						&:hover
-							text-decoration none
-							background #e2e7ec
-
-				.mk-url-preview
-					margin-top 8px
-
-				> .channel
-					margin 0
-
-				> .mk-poll
-					font-size 80%
-
-				> .renote
-					margin 8px 0
-
-					> .mk-note-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>
-
-<style lang="stylus" module>
-.text
-
-	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
-</style>

From 358e8f162fe34799773cdb1ff21a2991ec91f820 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 17:23:06 +0900
Subject: [PATCH 1175/1250] :v:

---
 src/server/index.ts | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/src/server/index.ts b/src/server/index.ts
index 61a8739b4..abb8992da 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -50,6 +50,14 @@ app.use((req, res, next) => {
 	}
 });
 
+// 互換性のため
+app.post('/meta', (req, res) => {
+	res.header('Access-Control-Allow-Origin', '*');
+	res.json({
+		version: 'nighthike'
+	});
+});
+
 /**
  * Register modules
  */

From e540a622cba2053bf02dd1cb36a76cc4094255b3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 17:23:32 +0900
Subject: [PATCH 1176/1250] oops

---
 src/server/api/endpoints/{posts.ts => notes.ts} | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename src/server/api/endpoints/{posts.ts => notes.ts} (100%)

diff --git a/src/server/api/endpoints/posts.ts b/src/server/api/endpoints/notes.ts
similarity index 100%
rename from src/server/api/endpoints/posts.ts
rename to src/server/api/endpoints/notes.ts

From 3bb128ff1251b028ae1b501f69f732a0a3b2cdd7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 17:24:07 +0900
Subject: [PATCH 1177/1250] v4692

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 29b9fd416..089c80d5e 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4679",
+	"version": "0.0.4692",
 	"codename": "nighthike",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",

From 094c5d26800a2ccb586705554d3744fae3359764 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 17:37:16 +0900
Subject: [PATCH 1178/1250] Fix bug

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

diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue
index 4547ac58b..0712069e5 100644
--- a/src/client/app/desktop/views/components/notes.note.vue
+++ b/src/client/app/desktop/views/components/notes.note.vue
@@ -21,7 +21,7 @@
 		</router-link>
 		<div class="main">
 			<header>
-				<router-link class="name" :to="`/@${getAcct(p.user)}`" v-user-preview="p.user.id">{{ getUserName(p) }}</router-link>
+				<router-link class="name" :to="`/@${getAcct(p.user)}`" v-user-preview="p.user.id">{{ getUserName(p.user) }}</router-link>
 				<span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span>
 				<span class="username">@{{ getAcct(p.user) }}</span>
 				<div class="info">

From 4ea03d811176ecb5af60fd05b0c2aa9aa44c5867 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 17:37:25 +0900
Subject: [PATCH 1179/1250] v4694

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 089c80d5e..e08c03b20 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4692",
+	"version": "0.0.4694",
 	"codename": "nighthike",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",

From d451c3b4515906eafeac7cb29a590a8dd99d97af Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 17:40:03 +0900
Subject: [PATCH 1180/1250] Fix bug

---
 src/renderers/get-note-summary.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/renderers/get-note-summary.ts b/src/renderers/get-note-summary.ts
index 13859439a..fc7482ca1 100644
--- a/src/renderers/get-note-summary.ts
+++ b/src/renderers/get-note-summary.ts
@@ -12,7 +12,7 @@ const summarize = (note: any): string => {
 	summary += note.text ? note.text : '';
 
 	// メディアが添付されているとき
-	if (note.media) {
+	if (note.media.length != 0) {
 		summary += ` (${note.media.length}つのメディア)`;
 	}
 

From f449918e82a1e07365199d3c6a8e7ce58c704730 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 17:40:41 +0900
Subject: [PATCH 1181/1250] v4696

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index e08c03b20..723701dd9 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4694",
+	"version": "0.0.4696",
 	"codename": "nighthike",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",

From 70214c45ced4a951584d0b90202dd991f6fc8bee Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 18:21:02 +0900
Subject: [PATCH 1182/1250] Provide url property

---
 src/remote/activitypub/renderer/person.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts
index 82e261029..f1c8056a7 100644
--- a/src/remote/activitypub/renderer/person.ts
+++ b/src/remote/activitypub/renderer/person.ts
@@ -10,6 +10,7 @@ export default user => {
 		id,
 		inbox: `${id}/inbox`,
 		outbox: `${id}/outbox`,
+		url: `${config.url}/@${user.username}`,
 		preferredUsername: user.username,
 		name: user.name,
 		summary: user.description,

From d32ca4005ccfaa45ed380b9a56c297bf7250aa3e Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sun, 8 Apr 2018 09:21:54 +0000
Subject: [PATCH 1183/1250] fix(package): update gulp-pug to version 4.0.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 723701dd9..2dd878dc7 100644
--- a/package.json
+++ b/package.json
@@ -123,7 +123,7 @@
 		"gulp-htmlmin": "4.0.0",
 		"gulp-imagemin": "4.1.0",
 		"gulp-mocha": "5.0.0",
-		"gulp-pug": "3.3.0",
+		"gulp-pug": "4.0.0",
 		"gulp-rename": "1.2.2",
 		"gulp-replace": "0.6.1",
 		"gulp-sourcemaps": "2.6.4",

From 47607c7da62eb5a23047e23ec41499e2ccc1024a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 18:52:42 +0900
Subject: [PATCH 1184/1250] Fix bug

---
 src/services/note/create.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index aac207cc1..8f0b84bcc 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -349,7 +349,7 @@ export default async (user: IUser, data: {
 
 			// Create notification
 			notify(mentionee._id, user._id, 'mention', {
-				note_id: note._id
+				noteId: note._id
 			});
 		}));
 	}

From 01349dd36d941666aac23c6f4ead17fdac95654c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 19:14:55 +0900
Subject: [PATCH 1185/1250] Clean up

---
 src/server/api/endpoints/i/notifications.ts | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts
index 5de087a9b..3b4899682 100644
--- a/src/server/api/endpoints/i/notifications.ts
+++ b/src/server/api/endpoints/i/notifications.ts
@@ -10,10 +10,6 @@ import read from '../../common/read-notification';
 
 /**
  * Get notifications
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Get 'following' parameter

From 1884d79c55cf3ae9c8d5378ce80a652f3f0ef059 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 19:16:48 +0900
Subject: [PATCH 1186/1250] Fix bug

---
 .../app/mobile/views/components/note.sub.vue  | 20 +++++++++----------
 1 file changed, 9 insertions(+), 11 deletions(-)

diff --git a/src/client/app/mobile/views/components/note.sub.vue b/src/client/app/mobile/views/components/note.sub.vue
index a37d0dea0..96f8265cc 100644
--- a/src/client/app/mobile/views/components/note.sub.vue
+++ b/src/client/app/mobile/views/components/note.sub.vue
@@ -1,13 +1,13 @@
 <template>
 <div class="sub">
-	<router-link class="avatar-anchor" :to="`/@${acct}`">
+	<router-link class="avatar-anchor" :to="`/@${getAcct(note.user)}`">
 		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`">{{ getUserName(note.user) }}</router-link>
-			<span class="username">@{{ acct }}</span>
-			<router-link class="created-at" :to="`/@${acct}/${note.id}`">
+			<router-link class="name" :to="`/@${getAcct(note.user)}`">{{ getUserName(note.user) }}</router-link>
+			<span class="username">@{{ getAcct(note.user) }}</span>
+			<router-link class="created-at" :to="`/@${getAcct(note.user)}/${note.id}`">
 				<mk-time :time="note.createdAt"/>
 			</router-link>
 		</header>
@@ -25,13 +25,11 @@ import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['note'],
-	computed: {
-		acct() {
-			return getAcct(this.note.user);
-		},
-		name() {
-			return getUserName(this.note.user);
-		}
+	data() {
+		return {
+			getAcct,
+			getUserName
+		};
 	}
 });
 </script>

From 4f29253b95f04b0a07e2ea0c825fca896f3bfa55 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 19:27:53 +0900
Subject: [PATCH 1187/1250] Fix bugs

---
 .../views/components/notification-preview.vue | 25 +++++-------
 .../mobile/views/components/notification.vue  | 39 +++++++------------
 src/client/app/mobile/views/pages/user.vue    | 21 +++++-----
 3 files changed, 33 insertions(+), 52 deletions(-)

diff --git a/src/client/app/mobile/views/components/notification-preview.vue b/src/client/app/mobile/views/components/notification-preview.vue
index 79ca3321e..f0921f91d 100644
--- a/src/client/app/mobile/views/components/notification-preview.vue
+++ b/src/client/app/mobile/views/components/notification-preview.vue
@@ -3,7 +3,7 @@
 	<template v-if="notification.type == 'reaction'">
 		<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p><mk-reaction-icon :reaction="notification.reaction"/>{{ name }}</p>
+			<p><mk-reaction-icon :reaction="notification.reaction"/>{{ getUserName(notification.user) }}</p>
 			<p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%</p>
 		</div>
 	</template>
@@ -11,7 +11,7 @@
 	<template v-if="notification.type == 'renote'">
 		<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:retweet%{{ noteerName }}</p>
+			<p>%fa:retweet%{{ getUserName(notification.note.user) }}</p>
 			<p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right%</p>
 		</div>
 	</template>
@@ -19,7 +19,7 @@
 	<template v-if="notification.type == 'quote'">
 		<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:quote-left%{{ noteerName }}</p>
+			<p>%fa:quote-left%{{ getUserName(notification.note.user) }}</p>
 			<p class="note-preview">{{ getNoteSummary(notification.note) }}</p>
 		</div>
 	</template>
@@ -27,14 +27,14 @@
 	<template v-if="notification.type == 'follow'">
 		<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:user-plus%{{ name }}</p>
+			<p>%fa:user-plus%{{ getUserName(notification.user) }}</p>
 		</div>
 	</template>
 
 	<template v-if="notification.type == 'reply'">
 		<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:reply%{{ noteerName }}</p>
+			<p>%fa:reply%{{ getUserName(notification.note.user) }}</p>
 			<p class="note-preview">{{ getNoteSummary(notification.note) }}</p>
 		</div>
 	</template>
@@ -42,7 +42,7 @@
 	<template v-if="notification.type == 'mention'">
 		<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:at%{{ noteerName }}</p>
+			<p>%fa:at%{{ getUserName(notification.note.user) }}</p>
 			<p class="note-preview">{{ getNoteSummary(notification.note) }}</p>
 		</div>
 	</template>
@@ -50,7 +50,7 @@
 	<template v-if="notification.type == 'poll_vote'">
 		<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:chart-pie%{{ name }}</p>
+			<p>%fa:chart-pie%{{ getUserName(notification.user) }}</p>
 			<p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%</p>
 		</div>
 	</template>
@@ -64,17 +64,10 @@ import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['notification'],
-	computed: {
-		name() {
-			return getUserName(this.notification.user);
-		},
-		noteerName() {
-			return getUserName(this.notification.note.user);
-		}
-	},
 	data() {
 		return {
-			getNoteSummary
+			getNoteSummary,
+			getUserName
 		};
 	}
 });
diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue
index e1294a877..4c98e1990 100644
--- a/src/client/app/mobile/views/components/notification.vue
+++ b/src/client/app/mobile/views/components/notification.vue
@@ -2,15 +2,15 @@
 <div class="mk-notification">
 	<div class="notification reaction" v-if="notification.type == 'reaction'">
 		<mk-time :time="notification.createdAt"/>
-		<router-link class="avatar-anchor" :to="`/@${acct}`">
+		<router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`">
 			<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="text">
 			<p>
 				<mk-reaction-icon :reaction="notification.reaction"/>
-				<router-link :to="`/@${acct}`">{{ getUserName(notification.user) }}</router-link>
+				<router-link :to="`/@${getAcct(notification.user)}`">{{ getUserName(notification.user) }}</router-link>
 			</p>
-			<router-link class="note-ref" :to="`/@${acct}/${notification.note.id}`">
+			<router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">
 				%fa:quote-left%{{ getNoteSummary(notification.note) }}
 				%fa:quote-right%
 			</router-link>
@@ -19,15 +19,15 @@
 
 	<div class="notification renote" v-if="notification.type == 'renote'">
 		<mk-time :time="notification.createdAt"/>
-		<router-link class="avatar-anchor" :to="`/@${acct}`">
-			<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+		<router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`">
+			<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="text">
 			<p>
 				%fa:retweet%
-				<router-link :to="`/@${acct}`">{{ getUserName(notification.note.user) }}</router-link>
+				<router-link :to="`/@${getAcct(notification.user)}`">{{ getUserName(notification.user) }}</router-link>
 			</p>
-			<router-link class="note-ref" :to="`/@${acct}/${notification.note.id}`">
+			<router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">
 				%fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right%
 			</router-link>
 		</div>
@@ -39,13 +39,13 @@
 
 	<div class="notification follow" v-if="notification.type == 'follow'">
 		<mk-time :time="notification.createdAt"/>
-		<router-link class="avatar-anchor" :to="`/@${acct}`">
+		<router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`">
 			<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="text">
 			<p>
 				%fa:user-plus%
-				<router-link :to="`/@${acct}`">{{ getUserName(notification.user) }}</router-link>
+				<router-link :to="`/@${getAcct(notification.user)}`">{{ getUserName(notification.user) }}</router-link>
 			</p>
 		</div>
 	</div>
@@ -60,15 +60,15 @@
 
 	<div class="notification poll_vote" v-if="notification.type == 'poll_vote'">
 		<mk-time :time="notification.createdAt"/>
-		<router-link class="avatar-anchor" :to="`/@${acct}`">
+		<router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`">
 			<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="text">
 			<p>
 				%fa:chart-pie%
-				<router-link :to="`/@${acct}`">{{ getUserName(notification.user) }}</router-link>
+				<router-link :to="`/@${getAcct(notification.user)}`">{{ getUserName(notification.user) }}</router-link>
 			</p>
-			<router-link class="note-ref" :to="`/@${acct}/${notification.note.id}`">
+			<router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">
 				%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%
 			</router-link>
 		</div>
@@ -84,20 +84,11 @@ import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['notification'],
-	computed: {
-		acct() {
-			return getAcct(this.notification.user);
-		},
-		name() {
-			return getUserName(this.notification.user);
-		},
-		noteerName() {
- 			return getUserName(this.notification.note.user);
-		}
-	},
 	data() {
 		return {
-			getNoteSummary
+			getNoteSummary,
+			getAcct,
+			getUserName
 		};
 	}
 });
diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue
index f33f209db..aac8b628b 100644
--- a/src/client/app/mobile/views/pages/user.vue
+++ b/src/client/app/mobile/views/pages/user.vue
@@ -12,8 +12,8 @@
 					<mk-follow-button v-if="os.isSignedIn && os.i.id != user.id" :user="user"/>
 				</div>
 				<div class="title">
-					<h1>{{ user }}</h1>
-					<span class="username">@{{ acct }}</span>
+					<h1>{{ getUserName(user) }}</h1>
+					<span class="username">@{{ getAcct(user) }}</span>
 					<span class="followed" v-if="user.isFollowed">%i18n:mobile.tags.mk-user.follows-you%</span>
 				</div>
 				<div class="description">{{ user.description }}</div>
@@ -30,11 +30,11 @@
 						<b>{{ user.notesCount | number }}</b>
 						<i>%i18n:mobile.tags.mk-user.notes%</i>
 					</a>
-					<a :href="`@${acct}/following`">
+					<a :href="`@${getAcct(user)}/following`">
 						<b>{{ user.followingCount | number }}</b>
 						<i>%i18n:mobile.tags.mk-user.following%</i>
 					</a>
-					<a :href="`@${acct}/followers`">
+					<a :href="`@${getAcct(user)}/followers`">
 						<b>{{ user.followersCount | number }}</b>
 						<i>%i18n:mobile.tags.mk-user.followers%</i>
 					</a>
@@ -60,6 +60,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import * as age from 's-age';
+import parseAcct from '../../../../../acct/parse';
 import getAcct from '../../../../../acct/render';
 import getUserName from '../../../../../renderers/get-user-name';
 import Progress from '../../../common/scripts/loading';
@@ -73,18 +74,14 @@ export default Vue.extend({
 		return {
 			fetching: true,
 			user: null,
-			page: 'home'
+			page: 'home',
+			getAcct,
+			getUserName
 		};
 	},
 	computed: {
-		acct() {
-			return this.getAcct(this.user);
-		},
 		age(): number {
 			return age(this.user.profile.birthday);
-		},
-		name() {
-			return getUserName(this.user);
 		}
 	},
 	watch: {
@@ -105,7 +102,7 @@ export default Vue.extend({
 				this.fetching = false;
 
 				Progress.done();
-				document.title = this.name + ' | Misskey';
+				document.title = this.getUserName(this.user) + ' | Misskey';
 			});
 		}
 	}

From 0c2f47989ea81b55b61a339978d1aff3765c3424 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 19:30:19 +0900
Subject: [PATCH 1188/1250] Fix bug

---
 .../desktop/views/components/user-preview.vue  | 18 +++++++-----------
 1 file changed, 7 insertions(+), 11 deletions(-)

diff --git a/src/client/app/desktop/views/components/user-preview.vue b/src/client/app/desktop/views/components/user-preview.vue
index 3cbaa2816..1cc53743a 100644
--- a/src/client/app/desktop/views/components/user-preview.vue
+++ b/src/client/app/desktop/views/components/user-preview.vue
@@ -2,12 +2,12 @@
 <div class="mk-user-preview">
 	<template v-if="u != null">
 		<div class="banner" :style="u.bannerUrl ? `background-image: url(${u.bannerUrl}?thumbnail&size=512)` : ''"></div>
-		<router-link class="avatar" :to="`/@${acct}`">
+		<router-link class="avatar" :to="`/@${getAcct(u)}`">
 			<img :src="`${u.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="title">
-			<router-link class="name" :to="`/@${acct}`">{{ u.name }}</router-link>
-			<p class="username">@{{ acct }}</p>
+			<router-link class="name" :to="`/@${getAcct(u)}`">{{ u.name }}</router-link>
+			<p class="username">@{{ getAcct(u) }}</p>
 		</div>
 		<div class="description">{{ u.description }}</div>
 		<div class="status">
@@ -39,14 +39,10 @@ export default Vue.extend({
 			required: true
 		}
 	},
-	computed: {
-		acct() {
-			return getAcct(this.u);
-		}
-	},
 	data() {
 		return {
-			u: null
+			u: null,
+			getAcct
 		};
 	},
 	mounted() {
@@ -57,8 +53,8 @@ export default Vue.extend({
 			});
 		} else {
 			const query = this.user[0] == '@' ?
-				parseAcct(this.user[0].substr(1)) :
-				{ userId: this.user[0] };
+				parseAcct(this.user.substr(1)) :
+				{ userId: this.user };
 
 			(this as any).api('users/show', query).then(user => {
 				this.u = user;

From 45b5743a3e5c17deb2c67c87b1b11a744e101ded Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 19:30:56 +0900
Subject: [PATCH 1189/1250] v4703

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 723701dd9..caf9433f3 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4696",
+	"version": "0.0.4703",
 	"codename": "nighthike",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",

From 6af2cd7a2940fe315e60170c9b86d6b3c3e715be Mon Sep 17 00:00:00 2001
From: unarist <m.unarist@gmail.com>
Date: Sun, 8 Apr 2018 20:48:01 +0900
Subject: [PATCH 1190/1250] Fix model method definitions on profile page
 components

---
 .../app/desktop/views/pages/user/user.followers-you-know.vue    | 2 +-
 src/client/app/desktop/views/pages/user/user.friends.vue        | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
index 16625b689..351b1264f 100644
--- a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
+++ b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
@@ -24,7 +24,7 @@ export default Vue.extend({
 			fetching: true
 		};
 	},
-	method() {
+	methods: {
 		getAcct,
 		getUserName
 	},
diff --git a/src/client/app/desktop/views/pages/user/user.friends.vue b/src/client/app/desktop/views/pages/user/user.friends.vue
index a726e2565..c9213cb50 100644
--- a/src/client/app/desktop/views/pages/user/user.friends.vue
+++ b/src/client/app/desktop/views/pages/user/user.friends.vue
@@ -30,7 +30,7 @@ export default Vue.extend({
 			fetching: true
 		};
 	},
-	method() {
+	methods: {
 		getAcct
 	},
 	mounted() {

From 858e6893892e28f7f3e15d1bbe12daf8f0bf160b Mon Sep 17 00:00:00 2001
From: unarist <m.unarist@gmail.com>
Date: Sun, 8 Apr 2018 20:53:48 +0900
Subject: [PATCH 1191/1250] Fix errors related to getUserName on NoteDetail
 component

---
 src/client/app/desktop/views/components/note-detail.sub.vue | 2 +-
 src/client/app/desktop/views/components/note-detail.vue     | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/client/app/desktop/views/components/note-detail.sub.vue b/src/client/app/desktop/views/components/note-detail.sub.vue
index 0159d1ad4..79f5de1f8 100644
--- a/src/client/app/desktop/views/components/note-detail.sub.vue
+++ b/src/client/app/desktop/views/components/note-detail.sub.vue
@@ -6,7 +6,7 @@
 	<div class="main">
 		<header>
 			<div class="left">
-				<router-link class="name" :to="`/@${acct}`" v-user-preview="note.userId">{{ getUserName(note.user) }}</router-link>
+				<router-link class="name" :to="`/@${acct}`" v-user-preview="note.userId">{{ name }}</router-link>
 				<span class="username">@{{ acct }}</span>
 			</div>
 			<div class="right">
diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue
index df7c33dfa..eead82dd0 100644
--- a/src/client/app/desktop/views/components/note-detail.vue
+++ b/src/client/app/desktop/views/components/note-detail.vue
@@ -22,7 +22,7 @@
 				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
 			</router-link>
 			%fa:retweet%
-			<router-link class="name" :href="`/@${acct}`">{{ getUserName(note.user) }}</router-link>
+			<router-link class="name" :href="`/@${acct}`">{{ name }}</router-link>
 			がRenote
 		</p>
 	</div>
@@ -31,7 +31,7 @@
 			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
 		</router-link>
 		<header>
-			<router-link class="name" :to="`/@${pAcct}`" v-user-preview="p.user.id">{{ getUserName(p.user) }}</router-link>
+			<router-link class="name" :to="`/@${pAcct}`" v-user-preview="p.user.id">{{ name }}</router-link>
 			<span class="username">@{{ pAcct }}</span>
 			<router-link class="time" :to="`/@${pAcct}/${p.id}`">
 				<mk-time :time="p.createdAt"/>

From 10ab391b71d52920008920437f37d00938a452d6 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 22:42:07 +0900
Subject: [PATCH 1192/1250] Update DONORS.md

---
 DONORS.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/DONORS.md b/DONORS.md
index 8c764a5e0..6b56b13e0 100644
--- a/DONORS.md
+++ b/DONORS.md
@@ -11,6 +11,7 @@ The list of people who have sent donation for Misskey.
 * 藍
 * 音船 https://otofune.me/
 * aqz https://misskey.xyz/aqz
+* kotodu "虚無創作中"
 
 :heart: Thanks for donating, guys!
 

From 15ea610f951b145e114e14ee6a599d6069714411 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Apr 2018 23:29:27 +0900
Subject: [PATCH 1193/1250] Fix bug

---
 src/models/user.ts                       | 1 +
 src/remote/activitypub/resolve-person.ts | 8 ++++++++
 2 files changed, 9 insertions(+)

diff --git a/src/models/user.ts b/src/models/user.ts
index 906bcb533..ea59730e4 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -12,6 +12,7 @@ const User = db.get<IUser>('users');
 
 User.createIndex('username');
 User.createIndex('token');
+User.createIndex('uri', { sparse: true, unique: true });
 
 export default User;
 
diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts
index 0140811f0..50e7873cb 100644
--- a/src/remote/activitypub/resolve-person.ts
+++ b/src/remote/activitypub/resolve-person.ts
@@ -12,6 +12,14 @@ export default async (value: string | IObject, verifier?: string): Promise<IUser
 
 	if (id.startsWith(config.url + '/')) {
 		return await User.findOne({ _id: id.split('/').pop() });
+	} else {
+		const exist = await User.findOne({
+			uri: id
+		});
+
+		if (exist) {
+			return exist;
+		}
 	}
 
 	const resolver = new Resolver();

From 8835d23b69cadca8623bb7f5f5caa5eae56ae0ab Mon Sep 17 00:00:00 2001
From: unarist <m.unarist@gmail.com>
Date: Mon, 9 Apr 2018 00:33:56 +0900
Subject: [PATCH 1194/1250] Fix banner selection from user page and drives

Previously, updateBanner takes a callback as a first argument of the function, but some calls passes a file object.

I've refactored it with Promise to avoid those misuses (although it should have type definitions :/)
---
 src/client/app/desktop/api/update-banner.ts   | 52 +++++++++++--------
 .../desktop/views/pages/user/user.header.vue  |  2 +-
 2 files changed, 30 insertions(+), 24 deletions(-)

diff --git a/src/client/app/desktop/api/update-banner.ts b/src/client/app/desktop/api/update-banner.ts
index 5f10bf285..bc3f783e3 100644
--- a/src/client/app/desktop/api/update-banner.ts
+++ b/src/client/app/desktop/api/update-banner.ts
@@ -3,9 +3,9 @@ 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 => {
+export default (os: OS) => {
 
+	const cropImage = file => new Promise((resolve, reject) => {
 		const w = new CropWindow({
 			propsData: {
 				image: file,
@@ -26,22 +26,24 @@ export default (os: OS) => (cb, file = null) => {
 					os.api('drive/folders/create', {
 						name: 'バナー'
 					}).then(iconFolder => {
-						upload(data, iconFolder);
+						resolve(upload(data, iconFolder));
 					});
 				} else {
-					upload(data, bannerFolder[0]);
+					resolve(upload(data, bannerFolder[0]));
 				}
 			});
 		});
 
 		w.$once('skipped', () => {
-			set(file);
+			resolve(file);
 		});
 
-		document.body.appendChild(w.$el);
-	};
+		w.$once('cancelled', reject);
 
-	const upload = (data, folder) => {
+		document.body.appendChild(w.$el);
+	});
+
+	const upload = (data, folder) => new Promise((resolve, reject) => {
 		const dialog = new ProgressDialog({
 			propsData: {
 				title: '新しいバナーをアップロードしています'
@@ -56,18 +58,19 @@ export default (os: OS) => (cb, file = null) => {
 		xhr.onload = e => {
 			const file = JSON.parse((e.target as any).response);
 			(dialog as any).close();
-			set(file);
+			resolve(file);
 		};
+		xhr.onerror = reject;
 
 		xhr.upload.onprogress = e => {
 			if (e.lengthComputable) (dialog as any).update(e.loaded, e.total);
 		};
 
 		xhr.send(data);
-	};
+	});
 
-	const set = file => {
-		os.api('i/update', {
+	const setBanner = file => {
+		return os.api('i/update', {
 			bannerId: file.id
 		}).then(i => {
 			os.i.bannerId = i.bannerId;
@@ -81,18 +84,21 @@ export default (os: OS) => (cb, file = null) => {
 				}]
 			});
 
-			if (cb) cb(i);
+			return i;
 		});
 	};
 
-	if (file) {
-		fileSelected(file);
-	} else {
-		os.apis.chooseDriveFile({
-			multiple: false,
-			title: '%fa:image%バナーにする画像を選択'
-		}).then(file => {
-			fileSelected(file);
-		});
-	}
+	return (file = null) => {
+		const selectedFile = file
+			? Promise.resolve(file)
+			: os.apis.chooseDriveFile({
+				multiple: false,
+				title: '%fa:image%バナーにする画像を選択'
+			});
+		
+		return selectedFile
+			.then(cropImage)
+			.then(setBanner)
+			.catch(err => err && console.warn(err));
+	};
 };
diff --git a/src/client/app/desktop/views/pages/user/user.header.vue b/src/client/app/desktop/views/pages/user/user.header.vue
index ceeb78431..f67bf5da2 100644
--- a/src/client/app/desktop/views/pages/user/user.header.vue
+++ b/src/client/app/desktop/views/pages/user/user.header.vue
@@ -62,7 +62,7 @@ export default Vue.extend({
 		onBannerClick() {
 			if (!(this as any).os.isSignedIn || (this as any).os.i.id != this.user.id) return;
 
-			(this as any).apis.updateBanner((this as any).os.i, i => {
+			(this as any).apis.updateBanner().then(i => {
 				this.user.bannerUrl = i.bannerUrl;
 			});
 		}

From 9b58a541b10f0aa1dd5617ece2f223f760f1315e Mon Sep 17 00:00:00 2001
From: unarist <m.unarist@gmail.com>
Date: Mon, 9 Apr 2018 01:10:04 +0900
Subject: [PATCH 1195/1250] Fix username/mention regexes

* Allow underscore instead of hypen
* Fix domain part handling
* Add tests for remote mention
---
 src/client/app/common/views/components/signup.vue |  2 +-
 src/client/app/dev/views/new-app.vue              |  2 +-
 src/models/app.ts                                 |  2 +-
 src/models/user.ts                                |  2 +-
 src/text/parse/elements/mention.ts                |  2 +-
 test/text.ts                                      | 14 ++++++++++++--
 6 files changed, 17 insertions(+), 7 deletions(-)

diff --git a/src/client/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue
index e77d849ad..8d0b16cab 100644
--- a/src/client/app/common/views/components/signup.vue
+++ b/src/client/app/common/views/components/signup.vue
@@ -76,7 +76,7 @@ export default Vue.extend({
 			}
 
 			const err =
-				!this.username.match(/^[a-zA-Z0-9\-]+$/) ? 'invalid-format' :
+				!this.username.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' :
 				this.username.length < 3 ? 'min-range' :
 				this.username.length > 20 ? 'max-range' :
 				null;
diff --git a/src/client/app/dev/views/new-app.vue b/src/client/app/dev/views/new-app.vue
index c9d597139..2317f24d4 100644
--- a/src/client/app/dev/views/new-app.vue
+++ b/src/client/app/dev/views/new-app.vue
@@ -65,7 +65,7 @@ export default Vue.extend({
 			}
 
 			const err =
-				!this.nid.match(/^[a-zA-Z0-9\-]+$/) ? 'invalid-format' :
+				!this.nid.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' :
 				this.nid.length < 3                 ? 'min-range' :
 				this.nid.length > 30                ? 'max-range' :
 				null;
diff --git a/src/models/app.ts b/src/models/app.ts
index 83162a6b9..703f4ef8f 100644
--- a/src/models/app.ts
+++ b/src/models/app.ts
@@ -24,7 +24,7 @@ export type IApp = {
 };
 
 export function isValidNameId(nameId: string): boolean {
-	return typeof nameId == 'string' && /^[a-zA-Z0-9\-]{3,30}$/.test(nameId);
+	return typeof nameId == 'string' && /^[a-zA-Z0-9_]{3,30}$/.test(nameId);
 }
 
 /**
diff --git a/src/models/user.ts b/src/models/user.ts
index ea59730e4..36c63a56d 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -89,7 +89,7 @@ export const isRemoteUser = (user: any): user is IRemoteUser =>
 
 //#region Validators
 export function validateUsername(username: string): boolean {
-	return typeof username == 'string' && /^[a-zA-Z0-9\-]{3,20}$/.test(username);
+	return typeof username == 'string' && /^[a-zA-Z0-9_]{3,20}$/.test(username);
 }
 
 export function validatePassword(password: string): boolean {
diff --git a/src/text/parse/elements/mention.ts b/src/text/parse/elements/mention.ts
index 4f2997b39..2ad278830 100644
--- a/src/text/parse/elements/mention.ts
+++ b/src/text/parse/elements/mention.ts
@@ -4,7 +4,7 @@
 import parseAcct from '../../../acct/parse';
 
 module.exports = text => {
-	const match = text.match(/^(?:@[a-zA-Z0-9\-]+){1,2}/);
+	const match = text.match(/^@[a-z0-9_]+(?:@[a-z0-9\.\-]+[a-z0-9])?/i);
 	if (!match) return null;
 	const mention = match[0];
 	const { username, host } = parseAcct(mention.substr(1));
diff --git a/test/text.ts b/test/text.ts
index 711aad816..8ce55cd1b 100644
--- a/test/text.ts
+++ b/test/text.ts
@@ -9,9 +9,11 @@ const syntaxhighlighter = require('../built/text/parse/core/syntax-highlighter')
 
 describe('Text', () => {
 	it('can be analyzed', () => {
-		const tokens = analyze('@himawari お腹ペコい :cat: #yryr');
+		const tokens = analyze('@himawari @hima_sub@namori.net お腹ペコい :cat: #yryr');
 		assert.deepEqual([
 			{ type: 'mention', content: '@himawari', username: 'himawari', host: null },
+			{ type: 'text', content: ' '},
+			{ type: 'mention', content: '@hima_sub@namori.net', username: 'hima_sub', host: 'namori.net' },
 			{ type: 'text', content: ' お腹ペコい ' },
 			{ type: 'emoji', content: ':cat:', emoji: 'cat'},
 			{ type: 'text', content: ' '},
@@ -20,7 +22,7 @@ describe('Text', () => {
 	});
 
 	it('can be inverted', () => {
-		const text = '@himawari お腹ペコい :cat: #yryr';
+		const text = '@himawari @hima_sub@namori.net お腹ペコい :cat: #yryr';
 		assert.equal(analyze(text).map(x => x.content).join(''), text);
 	});
 
@@ -41,6 +43,14 @@ describe('Text', () => {
 			], tokens);
 		});
 
+		it('remote mention', () => {
+			const tokens = analyze('@hima_sub@namori.net お腹ペコい');
+			assert.deepEqual([
+				{ type: 'mention', content: '@hima_sub@namori.net', username: 'hima_sub', host: 'namori.net' },
+				{ type: 'text', content: ' お腹ペコい' }
+			], tokens);
+		});
+
 		it('hashtag', () => {
 			const tokens = analyze('Strawberry Pasta #alice');
 			assert.deepEqual([

From 8089200d6938e9c0c6e79473fc143a2ea640cba6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Apr 2018 04:08:56 +0900
Subject: [PATCH 1196/1250] wip

---
 src/queue/processors/http/process-inbox.ts    |   6 +-
 src/remote/activitypub/act/create/image.ts    |  18 ---
 src/remote/activitypub/act/create/note.ts     |  87 -----------
 src/remote/activitypub/objects/image.ts       |  29 ++++
 src/remote/activitypub/objects/note.ts        | 110 ++++++++++++++
 src/remote/activitypub/objects/person.ts      | 142 ++++++++++++++++++
 .../{act => perform}/announce/index.ts        |   0
 .../{act => perform}/announce/note.ts         |  15 +-
 .../activitypub/perform/create/image.ts       |   6 +
 .../{act => perform}/create/index.ts          |   0
 src/remote/activitypub/perform/create/note.ts |  13 ++
 .../{act => perform}/delete/index.ts          |   0
 .../{act => perform}/delete/note.ts           |   0
 .../activitypub/{act => perform}/follow.ts    |   0
 .../activitypub/{act => perform}/index.ts     |   0
 .../activitypub/{act => perform}/like.ts      |   0
 .../{act => perform}/undo/follow.ts           |   0
 .../{act => perform}/undo/index.ts            |   0
 src/remote/activitypub/resolve-person.ts      |  98 ------------
 src/remote/activitypub/type.ts                |  16 ++
 src/remote/resolve-user.ts                    |   6 +-
 src/remote/webfinger.ts                       |  27 +---
 22 files changed, 332 insertions(+), 241 deletions(-)
 delete mode 100644 src/remote/activitypub/act/create/image.ts
 delete mode 100644 src/remote/activitypub/act/create/note.ts
 create mode 100644 src/remote/activitypub/objects/image.ts
 create mode 100644 src/remote/activitypub/objects/note.ts
 create mode 100644 src/remote/activitypub/objects/person.ts
 rename src/remote/activitypub/{act => perform}/announce/index.ts (100%)
 rename src/remote/activitypub/{act => perform}/announce/note.ts (67%)
 create mode 100644 src/remote/activitypub/perform/create/image.ts
 rename src/remote/activitypub/{act => perform}/create/index.ts (100%)
 create mode 100644 src/remote/activitypub/perform/create/note.ts
 rename src/remote/activitypub/{act => perform}/delete/index.ts (100%)
 rename src/remote/activitypub/{act => perform}/delete/note.ts (100%)
 rename src/remote/activitypub/{act => perform}/follow.ts (100%)
 rename src/remote/activitypub/{act => perform}/index.ts (100%)
 rename src/remote/activitypub/{act => perform}/like.ts (100%)
 rename src/remote/activitypub/{act => perform}/undo/follow.ts (100%)
 rename src/remote/activitypub/{act => perform}/undo/index.ts (100%)
 delete mode 100644 src/remote/activitypub/resolve-person.ts

diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts
index 6608907a7..ce5b7d5a8 100644
--- a/src/queue/processors/http/process-inbox.ts
+++ b/src/queue/processors/http/process-inbox.ts
@@ -4,8 +4,8 @@ import * as debug from 'debug';
 import { verifySignature } from 'http-signature';
 import parseAcct from '../../../acct/parse';
 import User, { IRemoteUser } from '../../../models/user';
-import act from '../../../remote/activitypub/act';
-import resolvePerson from '../../../remote/activitypub/resolve-person';
+import perform from '../../../remote/activitypub/perform';
+import { resolvePerson } from '../../../remote/activitypub/objects/person';
 
 const log = debug('misskey:queue:inbox');
 
@@ -58,7 +58,7 @@ export default async (job: kue.Job, done): Promise<void> => {
 
 	// アクティビティを処理
 	try {
-		await act(user, activity);
+		await perform(user, activity);
 		done();
 	} catch (e) {
 		done(e);
diff --git a/src/remote/activitypub/act/create/image.ts b/src/remote/activitypub/act/create/image.ts
deleted file mode 100644
index f1462f4ee..000000000
--- a/src/remote/activitypub/act/create/image.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import * as debug from 'debug';
-
-import uploadFromUrl from '../../../../services/drive/upload-from-url';
-import { IRemoteUser } from '../../../../models/user';
-import { IDriveFile } from '../../../../models/drive-file';
-
-const log = debug('misskey:activitypub');
-
-export default async function(actor: IRemoteUser, image): Promise<IDriveFile> {
-	if ('attributedTo' in image && actor.uri !== image.attributedTo) {
-		log(`invalid image: ${JSON.stringify(image, null, 2)}`);
-		throw new Error('invalid image');
-	}
-
-	log(`Creating the Image: ${image.url}`);
-
-	return await uploadFromUrl(image.url, actor);
-}
diff --git a/src/remote/activitypub/act/create/note.ts b/src/remote/activitypub/act/create/note.ts
deleted file mode 100644
index 599bc10aa..000000000
--- a/src/remote/activitypub/act/create/note.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import { JSDOM } from 'jsdom';
-import * as debug from 'debug';
-
-import Resolver from '../../resolver';
-import Note, { INote } from '../../../../models/note';
-import post from '../../../../services/note/create';
-import { IRemoteUser } from '../../../../models/user';
-import resolvePerson from '../../resolve-person';
-import createImage from './image';
-import config from '../../../../config';
-
-const log = debug('misskey:activitypub');
-
-/**
- * 投稿作成アクティビティを捌きます
- */
-export default async function createNote(resolver: Resolver, actor: IRemoteUser, note, silent = false): Promise<INote> {
-	if (typeof note.id !== 'string') {
-		log(`invalid note: ${JSON.stringify(note, null, 2)}`);
-		throw new Error('invalid note');
-	}
-
-	// 既に同じURIを持つものが登録されていないかチェックし、登録されていたらそれを返す
-	const exist = await Note.findOne({ uri: note.id });
-	if (exist) {
-		return exist;
-	}
-
-	log(`Creating the Note: ${note.id}`);
-
-	//#region Visibility
-	let visibility = 'public';
-	if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) visibility = 'unlisted';
-	if (note.cc.length == 0) visibility = 'private';
-	// TODO
-	if (visibility != 'public') throw new Error('unspported visibility');
-	//#endergion
-
-	//#region 添付メディア
-	let media = [];
-	if ('attachment' in note && note.attachment != null) {
-		// TODO: attachmentは必ずしもImageではない
-		// TODO: attachmentは必ずしも配列ではない
-		media = await Promise.all(note.attachment.map(x => {
-			return createImage(actor, x);
-		}));
-	}
-	//#endregion
-
-	//#region リプライ
-	let reply = null;
-	if ('inReplyTo' in note && note.inReplyTo != null) {
-		// リプライ先の投稿がMisskeyに登録されているか調べる
-		const uri: string = note.inReplyTo.id || note.inReplyTo;
-		const inReplyToNote = uri.startsWith(config.url + '/')
-			? await Note.findOne({ _id: uri.split('/').pop() })
-			: await Note.findOne({ uri });
-
-		if (inReplyToNote) {
-			reply = inReplyToNote;
-		} else {
-			// 無かったらフェッチ
-			const inReplyTo = await resolver.resolve(note.inReplyTo) as any;
-
-			// リプライ先の投稿の投稿者をフェッチ
-			const actor = await resolvePerson(inReplyTo.attributedTo) as IRemoteUser;
-
-			// TODO: silentを常にtrueにしてはならない
-			reply = await createNote(resolver, actor, inReplyTo);
-		}
-	}
-	//#endregion
-
-	const { window } = new JSDOM(note.content);
-
-	return await post(actor, {
-		createdAt: new Date(note.published),
-		media,
-		reply,
-		renote: undefined,
-		text: window.document.body.textContent,
-		viaMobile: false,
-		geo: undefined,
-		visibility,
-		uri: note.id
-	});
-}
diff --git a/src/remote/activitypub/objects/image.ts b/src/remote/activitypub/objects/image.ts
new file mode 100644
index 000000000..7f79fc5c0
--- /dev/null
+++ b/src/remote/activitypub/objects/image.ts
@@ -0,0 +1,29 @@
+import * as debug from 'debug';
+
+import uploadFromUrl from '../../../services/drive/upload-from-url';
+import { IRemoteUser } from '../../../models/user';
+import { IDriveFile } from '../../../models/drive-file';
+
+const log = debug('misskey:activitypub');
+
+/**
+ * Imageを作成します。
+ */
+export async function createImage(actor: IRemoteUser, image): Promise<IDriveFile> {
+	log(`Creating the Image: ${image.url}`);
+
+	return await uploadFromUrl(image.url, actor);
+}
+
+/**
+ * Imageを解決します。
+ *
+ * Misskeyに対象のImageが登録されていればそれを返し、そうでなければ
+ * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
+ */
+export async function resolveImage(actor: IRemoteUser, value: any): Promise<IDriveFile> {
+	// TODO
+
+	// リモートサーバーからフェッチしてきて登録
+	return await createImage(actor, value);
+}
diff --git a/src/remote/activitypub/objects/note.ts b/src/remote/activitypub/objects/note.ts
new file mode 100644
index 000000000..3edcb8c63
--- /dev/null
+++ b/src/remote/activitypub/objects/note.ts
@@ -0,0 +1,110 @@
+import { JSDOM } from 'jsdom';
+import * as debug from 'debug';
+
+import config from '../../../config';
+import Resolver from '../resolver';
+import Note, { INote } from '../../../models/note';
+import post from '../../../services/note/create';
+import { INote as INoteActivityStreamsObject, IObject } from '../type';
+import { resolvePerson } from './person';
+import { resolveImage } from './image';
+import { IRemoteUser } from '../../../models/user';
+
+const log = debug('misskey:activitypub');
+
+/**
+ * Noteをフェッチします。
+ *
+ * Misskeyに対象のNoteが登録されていればそれを返します。
+ */
+export async function fetchNote(value: string | IObject, resolver?: Resolver): Promise<INote> {
+	const uri = typeof value == 'string' ? value : value.id;
+
+	// URIがこのサーバーを指しているならデータベースからフェッチ
+	if (uri.startsWith(config.url + '/')) {
+		return await Note.findOne({ _id: uri.split('/').pop() });
+	}
+
+	//#region このサーバーに既に登録されていたらそれを返す
+	const exist = await Note.findOne({ uri });
+
+	if (exist) {
+		return exist;
+	}
+	//#endregion
+
+	return null;
+}
+
+/**
+ * Noteを作成します。
+ */
+export async function createNote(value: any, resolver?: Resolver, silent = false): Promise<INote> {
+	if (resolver == null) resolver = new Resolver();
+
+	const object = await resolver.resolve(value) as any;
+
+	if (object == null || object.type !== 'Note') {
+		throw new Error('invalid note');
+	}
+
+	const note: INoteActivityStreamsObject = object;
+
+	log(`Creating the Note: ${note.id}`);
+
+	// 投稿者をフェッチ
+	const actor = await resolvePerson(note.attributedTo) as IRemoteUser;
+
+	//#region Visibility
+	let visibility = 'public';
+	if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) visibility = 'unlisted';
+	if (note.cc.length == 0) visibility = 'private';
+	// TODO
+	if (visibility != 'public') throw new Error('unspported visibility');
+	//#endergion
+
+	// 添付メディア
+	// TODO: attachmentは必ずしもImageではない
+	// TODO: attachmentは必ずしも配列ではない
+	const media = note.attachment
+		? await Promise.all(note.attachment.map(x => resolveImage(actor, x)))
+		: [];
+
+	// リプライ
+	const reply = note.inReplyTo ? await resolveNote(note.inReplyTo, resolver) : null;
+
+	const { window } = new JSDOM(note.content);
+
+	return await post(actor, {
+		createdAt: new Date(note.published),
+		media,
+		reply,
+		renote: undefined,
+		text: window.document.body.textContent,
+		viaMobile: false,
+		geo: undefined,
+		visibility,
+		uri: note.id
+	}, silent);
+}
+
+/**
+ * Noteを解決します。
+ *
+ * Misskeyに対象のNoteが登録されていればそれを返し、そうでなければ
+ * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
+ */
+export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise<INote> {
+	const uri = typeof value == 'string' ? value : value.id;
+
+	//#region このサーバーに既に登録されていたらそれを返す
+	const exist = await fetchNote(uri);
+
+	if (exist) {
+		return exist;
+	}
+	//#endregion
+
+	// リモートサーバーからフェッチしてきて登録
+	return await createNote(value, resolver);
+}
diff --git a/src/remote/activitypub/objects/person.ts b/src/remote/activitypub/objects/person.ts
new file mode 100644
index 000000000..b1e8c9ee0
--- /dev/null
+++ b/src/remote/activitypub/objects/person.ts
@@ -0,0 +1,142 @@
+import { JSDOM } from 'jsdom';
+import { toUnicode } from 'punycode';
+import * as debug from 'debug';
+
+import config from '../../../config';
+import User, { validateUsername, isValidName, isValidDescription, IUser, IRemoteUser } from '../../../models/user';
+import webFinger from '../../webfinger';
+import Resolver from '../resolver';
+import { resolveImage } from './image';
+import { isCollectionOrOrderedCollection, IObject, IPerson } from '../type';
+
+const log = debug('misskey:activitypub');
+
+/**
+ * Personをフェッチします。
+ *
+ * Misskeyに対象のPersonが登録されていればそれを返します。
+ */
+export async function fetchPerson(value: string | IObject, resolver?: Resolver): Promise<IUser> {
+	const uri = typeof value == 'string' ? value : value.id;
+
+	// URIがこのサーバーを指しているならデータベースからフェッチ
+	if (uri.startsWith(config.url + '/')) {
+		return await User.findOne({ _id: uri.split('/').pop() });
+	}
+
+	//#region このサーバーに既に登録されていたらそれを返す
+	const exist = await User.findOne({ uri });
+
+	if (exist) {
+		return exist;
+	}
+	//#endregion
+
+	return null;
+}
+
+/**
+ * Personを作成します。
+ */
+export async function createPerson(value: any, resolver?: Resolver): Promise<IUser> {
+	if (resolver == null) resolver = new Resolver();
+
+	const object = await resolver.resolve(value) as any;
+
+	if (
+		object == null ||
+		object.type !== 'Person' ||
+		typeof object.preferredUsername !== 'string' ||
+		!validateUsername(object.preferredUsername) ||
+		!isValidName(object.name == '' ? null : object.name) ||
+		!isValidDescription(object.summary)
+	) {
+		throw new Error('invalid person');
+	}
+
+	const person: IPerson = object;
+
+	log(`Creating the Person: ${person.id}`);
+
+	const [followersCount = 0, followingCount = 0, notesCount = 0, finger] = await Promise.all([
+		resolver.resolve(person.followers).then(
+			resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
+			() => undefined
+		),
+		resolver.resolve(person.following).then(
+			resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
+			() => undefined
+		),
+		resolver.resolve(person.outbox).then(
+			resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
+			() => undefined
+		),
+		webFinger(person.id)
+	]);
+
+	const host = toUnicode(finger.subject.replace(/^.*?@/, ''));
+	const hostLower = host.replace(/[A-Z]+/, matched => matched.toLowerCase());
+	const summaryDOM = JSDOM.fragment(person.summary);
+
+	// Create user
+	const user = await User.insert({
+		avatarId: null,
+		bannerId: null,
+		createdAt: Date.parse(person.published) || null,
+		description: summaryDOM.textContent,
+		followersCount,
+		followingCount,
+		notesCount,
+		name: person.name,
+		driveCapacity: 1024 * 1024 * 8, // 8MiB
+		username: person.preferredUsername,
+		usernameLower: person.preferredUsername.toLowerCase(),
+		host,
+		hostLower,
+		publicKey: {
+			id: person.publicKey.id,
+			publicKeyPem: person.publicKey.publicKeyPem
+		},
+		inbox: person.inbox,
+		uri: person.id
+	}) as IRemoteUser;
+
+	//#region アイコンとヘッダー画像をフェッチ
+	const [avatarId, bannerId] = (await Promise.all([
+		person.icon,
+		person.image
+	].map(img =>
+		img == null
+			? Promise.resolve(null)
+			: resolveImage(user, img.url)
+	))).map(file => file != null ? file._id : null);
+
+	User.update({ _id: user._id }, { $set: { avatarId, bannerId } });
+
+	user.avatarId = avatarId;
+	user.bannerId = bannerId;
+	//#endregion
+
+	return user;
+}
+
+/**
+ * Personを解決します。
+ *
+ * Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ
+ * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
+ */
+export async function resolvePerson(value: string | IObject, verifier?: string): Promise<IUser> {
+	const uri = typeof value == 'string' ? value : value.id;
+
+	//#region このサーバーに既に登録されていたらそれを返す
+	const exist = await fetchPerson(uri);
+
+	if (exist) {
+		return exist;
+	}
+	//#endregion
+
+	// リモートサーバーからフェッチしてきて登録
+	return await createPerson(value);
+}
diff --git a/src/remote/activitypub/act/announce/index.ts b/src/remote/activitypub/perform/announce/index.ts
similarity index 100%
rename from src/remote/activitypub/act/announce/index.ts
rename to src/remote/activitypub/perform/announce/index.ts
diff --git a/src/remote/activitypub/act/announce/note.ts b/src/remote/activitypub/perform/announce/note.ts
similarity index 67%
rename from src/remote/activitypub/act/announce/note.ts
rename to src/remote/activitypub/perform/announce/note.ts
index 24d159f18..68fb23c97 100644
--- a/src/remote/activitypub/act/announce/note.ts
+++ b/src/remote/activitypub/perform/announce/note.ts
@@ -1,12 +1,10 @@
 import * as debug from 'debug';
 
 import Resolver from '../../resolver';
-import Note from '../../../../models/note';
 import post from '../../../../services/note/create';
-import { IRemoteUser, isRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/user';
 import { IAnnounce, INote } from '../../type';
-import createNote from '../create/note';
-import resolvePerson from '../../resolve-person';
+import { fetchNote, resolveNote } from '../../objects/note';
 
 const log = debug('misskey:activitypub');
 
@@ -21,17 +19,12 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
 	}
 
 	// 既に同じURIを持つものが登録されていないかチェック
-	const exist = await Note.findOne({ uri });
+	const exist = await fetchNote(uri);
 	if (exist) {
 		return;
 	}
 
-	// アナウンス元の投稿の投稿者をフェッチ
-	const announcee = await resolvePerson(note.attributedTo);
-
-	const renote = isRemoteUser(announcee)
-		? await createNote(resolver, announcee, note, true)
-		: await Note.findOne({ _id: note.id.split('/').pop() });
+	const renote = await resolveNote(note);
 
 	log(`Creating the (Re)Note: ${uri}`);
 
diff --git a/src/remote/activitypub/perform/create/image.ts b/src/remote/activitypub/perform/create/image.ts
new file mode 100644
index 000000000..ea36545f0
--- /dev/null
+++ b/src/remote/activitypub/perform/create/image.ts
@@ -0,0 +1,6 @@
+import { IRemoteUser } from '../../../../models/user';
+import { createImage } from '../../objects/image';
+
+export default async function(actor: IRemoteUser, image): Promise<void> {
+	await createImage(image.url, actor);
+}
diff --git a/src/remote/activitypub/act/create/index.ts b/src/remote/activitypub/perform/create/index.ts
similarity index 100%
rename from src/remote/activitypub/act/create/index.ts
rename to src/remote/activitypub/perform/create/index.ts
diff --git a/src/remote/activitypub/perform/create/note.ts b/src/remote/activitypub/perform/create/note.ts
new file mode 100644
index 000000000..530cf6483
--- /dev/null
+++ b/src/remote/activitypub/perform/create/note.ts
@@ -0,0 +1,13 @@
+import Resolver from '../../resolver';
+import { IRemoteUser } from '../../../../models/user';
+import { createNote, fetchNote } from '../../objects/note';
+
+/**
+ * 投稿作成アクティビティを捌きます
+ */
+export default async function(resolver: Resolver, actor: IRemoteUser, note, silent = false): Promise<void> {
+	const exist = await fetchNote(note);
+	if (exist == null) {
+		await createNote(note);
+	}
+}
diff --git a/src/remote/activitypub/act/delete/index.ts b/src/remote/activitypub/perform/delete/index.ts
similarity index 100%
rename from src/remote/activitypub/act/delete/index.ts
rename to src/remote/activitypub/perform/delete/index.ts
diff --git a/src/remote/activitypub/act/delete/note.ts b/src/remote/activitypub/perform/delete/note.ts
similarity index 100%
rename from src/remote/activitypub/act/delete/note.ts
rename to src/remote/activitypub/perform/delete/note.ts
diff --git a/src/remote/activitypub/act/follow.ts b/src/remote/activitypub/perform/follow.ts
similarity index 100%
rename from src/remote/activitypub/act/follow.ts
rename to src/remote/activitypub/perform/follow.ts
diff --git a/src/remote/activitypub/act/index.ts b/src/remote/activitypub/perform/index.ts
similarity index 100%
rename from src/remote/activitypub/act/index.ts
rename to src/remote/activitypub/perform/index.ts
diff --git a/src/remote/activitypub/act/like.ts b/src/remote/activitypub/perform/like.ts
similarity index 100%
rename from src/remote/activitypub/act/like.ts
rename to src/remote/activitypub/perform/like.ts
diff --git a/src/remote/activitypub/act/undo/follow.ts b/src/remote/activitypub/perform/undo/follow.ts
similarity index 100%
rename from src/remote/activitypub/act/undo/follow.ts
rename to src/remote/activitypub/perform/undo/follow.ts
diff --git a/src/remote/activitypub/act/undo/index.ts b/src/remote/activitypub/perform/undo/index.ts
similarity index 100%
rename from src/remote/activitypub/act/undo/index.ts
rename to src/remote/activitypub/perform/undo/index.ts
diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts
deleted file mode 100644
index 50e7873cb..000000000
--- a/src/remote/activitypub/resolve-person.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-import { JSDOM } from 'jsdom';
-import { toUnicode } from 'punycode';
-import config from '../../config';
-import User, { validateUsername, isValidName, isValidDescription, IUser } from '../../models/user';
-import webFinger from '../webfinger';
-import Resolver from './resolver';
-import uploadFromUrl from '../../services/drive/upload-from-url';
-import { isCollectionOrOrderedCollection, IObject } from './type';
-
-export default async (value: string | IObject, verifier?: string): Promise<IUser> => {
-	const id = typeof value == 'string' ? value : value.id;
-
-	if (id.startsWith(config.url + '/')) {
-		return await User.findOne({ _id: id.split('/').pop() });
-	} else {
-		const exist = await User.findOne({
-			uri: id
-		});
-
-		if (exist) {
-			return exist;
-		}
-	}
-
-	const resolver = new Resolver();
-
-	const object = await resolver.resolve(value) as any;
-
-	if (
-		object == null ||
-		object.type !== 'Person' ||
-		typeof object.preferredUsername !== 'string' ||
-		!validateUsername(object.preferredUsername) ||
-		!isValidName(object.name == '' ? null : object.name) ||
-		!isValidDescription(object.summary)
-	) {
-		throw new Error('invalid person');
-	}
-
-	const [followersCount = 0, followingCount = 0, notesCount = 0, finger] = await Promise.all([
-		resolver.resolve(object.followers).then(
-			resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
-			() => undefined
-		),
-		resolver.resolve(object.following).then(
-			resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
-			() => undefined
-		),
-		resolver.resolve(object.outbox).then(
-			resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
-			() => undefined
-		),
-		webFinger(id, verifier)
-	]);
-
-	const host = toUnicode(finger.subject.replace(/^.*?@/, ''));
-	const hostLower = host.replace(/[A-Z]+/, matched => matched.toLowerCase());
-	const summaryDOM = JSDOM.fragment(object.summary);
-
-	// Create user
-	const user = await User.insert({
-		avatarId: null,
-		bannerId: null,
-		createdAt: Date.parse(object.published) || null,
-		description: summaryDOM.textContent,
-		followersCount,
-		followingCount,
-		notesCount,
-		name: object.name,
-		driveCapacity: 1024 * 1024 * 8, // 8MiB
-		username: object.preferredUsername,
-		usernameLower: object.preferredUsername.toLowerCase(),
-		host,
-		hostLower,
-		publicKey: {
-			id: object.publicKey.id,
-			publicKeyPem: object.publicKey.publicKeyPem
-		},
-		inbox: object.inbox,
-		uri: id
-	});
-
-	const [avatarId, bannerId] = (await Promise.all([
-		object.icon,
-		object.image
-	].map(img =>
-		img == null
-			? Promise.resolve(null)
-			: uploadFromUrl(img.url, user)
-	))).map(file => file != null ? file._id : null);
-
-	User.update({ _id: user._id }, { $set: { avatarId, bannerId } });
-
-	user.avatarId = avatarId;
-	user.bannerId = bannerId;
-
-	return user;
-};
diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts
index 233551764..983eb621f 100644
--- a/src/remote/activitypub/type.ts
+++ b/src/remote/activitypub/type.ts
@@ -9,6 +9,11 @@ export interface IObject {
 	cc?: string[];
 	to?: string[];
 	attributedTo: string;
+	attachment?: any[];
+	inReplyTo?: any;
+	content: string;
+	icon?: any;
+	image?: any;
 }
 
 export interface IActivity extends IObject {
@@ -34,6 +39,17 @@ export interface INote extends IObject {
 	type: 'Note';
 }
 
+export interface IPerson extends IObject {
+	type: 'Person';
+	name: string;
+	preferredUsername: string;
+	inbox: string;
+	publicKey: any;
+	followers: any;
+	following: any;
+	outbox: any;
+}
+
 export const isCollection = (object: IObject): object is ICollection =>
 	object.type === 'Collection';
 
diff --git a/src/remote/resolve-user.ts b/src/remote/resolve-user.ts
index 0e7edd8e1..346e134c9 100644
--- a/src/remote/resolve-user.ts
+++ b/src/remote/resolve-user.ts
@@ -1,8 +1,8 @@
 import { toUnicode, toASCII } from 'punycode';
 import User from '../models/user';
-import resolvePerson from './activitypub/resolve-person';
 import webFinger from './webfinger';
 import config from '../config';
+import { createPerson } from './activitypub/objects/person';
 
 export default async (username, host, option) => {
 	const usernameLower = username.toLowerCase();
@@ -18,13 +18,13 @@ export default async (username, host, option) => {
 	if (user === null) {
 		const acctLower = `${usernameLower}@${hostLowerAscii}`;
 
-		const finger = await webFinger(acctLower, acctLower);
+		const finger = await webFinger(acctLower);
 		const self = finger.links.find(link => link.rel && link.rel.toLowerCase() === 'self');
 		if (!self) {
 			throw new Error('self link not found');
 		}
 
-		user = await resolvePerson(self.href, acctLower);
+		user = await createPerson(self.href);
 	}
 
 	return user;
diff --git a/src/remote/webfinger.ts b/src/remote/webfinger.ts
index bfca8d1c8..4f1ff231c 100644
--- a/src/remote/webfinger.ts
+++ b/src/remote/webfinger.ts
@@ -3,36 +3,21 @@ const WebFinger = require('webfinger.js');
 const webFinger = new WebFinger({ });
 
 type ILink = {
-  href: string;
-  rel: string;
+	href: string;
+	rel: string;
 };
 
 type IWebFinger = {
-  links: ILink[];
-  subject: string;
+	links: ILink[];
+	subject: string;
 };
 
-export default async function resolve(query, verifier?: string): Promise<IWebFinger> {
-	const finger = await new Promise((res, rej) => webFinger.lookup(query, (error, result) => {
+export default async function resolve(query): Promise<IWebFinger> {
+	return await new Promise((res, rej) => webFinger.lookup(query, (error, result) => {
 		if (error) {
 			return rej(error);
 		}
 
 		res(result.object);
 	})) as IWebFinger;
-	const subject = finger.subject.toLowerCase().replace(/^acct:/, '');
-
-	if (typeof verifier === 'string') {
-		if (subject !== verifier) {
-			throw new Error();
-		}
-
-		return finger;
-	}
-
-	if (typeof subject === 'string') {
-		return resolve(subject, subject);
-	}
-
-	throw new Error();
 }

From d760f41da9e6c039738245d5362658aa3e3edcc2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Apr 2018 05:02:52 +0900
Subject: [PATCH 1197/1250] wip

---
 .../activitypub/{perform => kernel}/announce/index.ts      | 0
 .../activitypub/{perform => kernel}/announce/note.ts       | 0
 src/remote/activitypub/{perform => kernel}/create/image.ts | 0
 src/remote/activitypub/{perform => kernel}/create/index.ts | 0
 src/remote/activitypub/{perform => kernel}/create/note.ts  | 0
 src/remote/activitypub/{perform => kernel}/delete/index.ts | 0
 src/remote/activitypub/{perform => kernel}/delete/note.ts  | 0
 src/remote/activitypub/{perform => kernel}/follow.ts       | 0
 src/remote/activitypub/{perform => kernel}/index.ts        | 0
 src/remote/activitypub/{perform => kernel}/like.ts         | 0
 src/remote/activitypub/{perform => kernel}/undo/follow.ts  | 0
 src/remote/activitypub/{perform => kernel}/undo/index.ts   | 0
 src/remote/activitypub/perform.ts                          | 7 +++++++
 13 files changed, 7 insertions(+)
 rename src/remote/activitypub/{perform => kernel}/announce/index.ts (100%)
 rename src/remote/activitypub/{perform => kernel}/announce/note.ts (100%)
 rename src/remote/activitypub/{perform => kernel}/create/image.ts (100%)
 rename src/remote/activitypub/{perform => kernel}/create/index.ts (100%)
 rename src/remote/activitypub/{perform => kernel}/create/note.ts (100%)
 rename src/remote/activitypub/{perform => kernel}/delete/index.ts (100%)
 rename src/remote/activitypub/{perform => kernel}/delete/note.ts (100%)
 rename src/remote/activitypub/{perform => kernel}/follow.ts (100%)
 rename src/remote/activitypub/{perform => kernel}/index.ts (100%)
 rename src/remote/activitypub/{perform => kernel}/like.ts (100%)
 rename src/remote/activitypub/{perform => kernel}/undo/follow.ts (100%)
 rename src/remote/activitypub/{perform => kernel}/undo/index.ts (100%)
 create mode 100644 src/remote/activitypub/perform.ts

diff --git a/src/remote/activitypub/perform/announce/index.ts b/src/remote/activitypub/kernel/announce/index.ts
similarity index 100%
rename from src/remote/activitypub/perform/announce/index.ts
rename to src/remote/activitypub/kernel/announce/index.ts
diff --git a/src/remote/activitypub/perform/announce/note.ts b/src/remote/activitypub/kernel/announce/note.ts
similarity index 100%
rename from src/remote/activitypub/perform/announce/note.ts
rename to src/remote/activitypub/kernel/announce/note.ts
diff --git a/src/remote/activitypub/perform/create/image.ts b/src/remote/activitypub/kernel/create/image.ts
similarity index 100%
rename from src/remote/activitypub/perform/create/image.ts
rename to src/remote/activitypub/kernel/create/image.ts
diff --git a/src/remote/activitypub/perform/create/index.ts b/src/remote/activitypub/kernel/create/index.ts
similarity index 100%
rename from src/remote/activitypub/perform/create/index.ts
rename to src/remote/activitypub/kernel/create/index.ts
diff --git a/src/remote/activitypub/perform/create/note.ts b/src/remote/activitypub/kernel/create/note.ts
similarity index 100%
rename from src/remote/activitypub/perform/create/note.ts
rename to src/remote/activitypub/kernel/create/note.ts
diff --git a/src/remote/activitypub/perform/delete/index.ts b/src/remote/activitypub/kernel/delete/index.ts
similarity index 100%
rename from src/remote/activitypub/perform/delete/index.ts
rename to src/remote/activitypub/kernel/delete/index.ts
diff --git a/src/remote/activitypub/perform/delete/note.ts b/src/remote/activitypub/kernel/delete/note.ts
similarity index 100%
rename from src/remote/activitypub/perform/delete/note.ts
rename to src/remote/activitypub/kernel/delete/note.ts
diff --git a/src/remote/activitypub/perform/follow.ts b/src/remote/activitypub/kernel/follow.ts
similarity index 100%
rename from src/remote/activitypub/perform/follow.ts
rename to src/remote/activitypub/kernel/follow.ts
diff --git a/src/remote/activitypub/perform/index.ts b/src/remote/activitypub/kernel/index.ts
similarity index 100%
rename from src/remote/activitypub/perform/index.ts
rename to src/remote/activitypub/kernel/index.ts
diff --git a/src/remote/activitypub/perform/like.ts b/src/remote/activitypub/kernel/like.ts
similarity index 100%
rename from src/remote/activitypub/perform/like.ts
rename to src/remote/activitypub/kernel/like.ts
diff --git a/src/remote/activitypub/perform/undo/follow.ts b/src/remote/activitypub/kernel/undo/follow.ts
similarity index 100%
rename from src/remote/activitypub/perform/undo/follow.ts
rename to src/remote/activitypub/kernel/undo/follow.ts
diff --git a/src/remote/activitypub/perform/undo/index.ts b/src/remote/activitypub/kernel/undo/index.ts
similarity index 100%
rename from src/remote/activitypub/perform/undo/index.ts
rename to src/remote/activitypub/kernel/undo/index.ts
diff --git a/src/remote/activitypub/perform.ts b/src/remote/activitypub/perform.ts
new file mode 100644
index 000000000..2e4f53adf
--- /dev/null
+++ b/src/remote/activitypub/perform.ts
@@ -0,0 +1,7 @@
+import { Object } from './type';
+import { IRemoteUser } from '../../models/user';
+import kernel from './kernel';
+
+export default async (actor: IRemoteUser, activity: Object): Promise<void> => {
+	await kernel(actor, activity);
+};

From d45e2661aa73e0379b782eb3a672c65f9ada7f4d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Apr 2018 05:14:47 +0900
Subject: [PATCH 1198/1250] Fix bug

---
 src/remote/activitypub/objects/image.ts | 9 ++++++++-
 src/remote/activitypub/type.ts          | 1 +
 2 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/src/remote/activitypub/objects/image.ts b/src/remote/activitypub/objects/image.ts
index 7f79fc5c0..d7bc5aff2 100644
--- a/src/remote/activitypub/objects/image.ts
+++ b/src/remote/activitypub/objects/image.ts
@@ -3,13 +3,20 @@ import * as debug from 'debug';
 import uploadFromUrl from '../../../services/drive/upload-from-url';
 import { IRemoteUser } from '../../../models/user';
 import { IDriveFile } from '../../../models/drive-file';
+import Resolver from '../resolver';
 
 const log = debug('misskey:activitypub');
 
 /**
  * Imageを作成します。
  */
-export async function createImage(actor: IRemoteUser, image): Promise<IDriveFile> {
+export async function createImage(actor: IRemoteUser, value): Promise<IDriveFile> {
+	const image = await new Resolver().resolve(value);
+
+	if (image.url == null) {
+		throw new Error('invalid image: url not privided');
+	}
+
 	log(`Creating the Image: ${image.url}`);
 
 	return await uploadFromUrl(image.url, actor);
diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts
index 983eb621f..08e5493dd 100644
--- a/src/remote/activitypub/type.ts
+++ b/src/remote/activitypub/type.ts
@@ -14,6 +14,7 @@ export interface IObject {
 	content: string;
 	icon?: any;
 	image?: any;
+	url?: string;
 }
 
 export interface IActivity extends IObject {

From ce81f4953bf9d39a28d1fe144f2bf03a850efe87 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Apr 2018 05:23:35 +0900
Subject: [PATCH 1199/1250] :v:

---
 src/remote/activitypub/kernel/create/index.ts | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/src/remote/activitypub/kernel/create/index.ts b/src/remote/activitypub/kernel/create/index.ts
index 7cb9b0844..e11bcac81 100644
--- a/src/remote/activitypub/kernel/create/index.ts
+++ b/src/remote/activitypub/kernel/create/index.ts
@@ -9,10 +9,6 @@ import { ICreate } from '../../type';
 const log = debug('misskey:activitypub');
 
 export default async (actor: IRemoteUser, activity: ICreate): Promise<void> => {
-	if ('actor' in activity && actor.uri !== activity.actor) {
-		throw new Error('invalid actor');
-	}
-
 	const uri = activity.id || activity;
 
 	log(`Create: ${uri}`);

From 0b4b9a2ef48ae4265543d13548dec8d612f513d3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Apr 2018 05:27:38 +0900
Subject: [PATCH 1200/1250] :v:

---
 src/remote/activitypub/resolver.ts | 9 +--------
 1 file changed, 1 insertion(+), 8 deletions(-)

diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts
index 7d45783b4..f405ff10c 100644
--- a/src/remote/activitypub/resolver.ts
+++ b/src/remote/activitypub/resolver.ts
@@ -48,11 +48,6 @@ export default class Resolver {
 
 		this.history.add(value);
 
-		//#region resolve local objects
-		// TODO
-		//if (value.startsWith(`${config.url}/`)) {
-		//#endregion
-
 		const object = await request({
 			url: value,
 			headers: {
@@ -66,12 +61,10 @@ export default class Resolver {
 				!object['@context'].includes('https://www.w3.org/ns/activitystreams') :
 				object['@context'] !== 'https://www.w3.org/ns/activitystreams'
 		)) {
-			log(`invalid response: ${JSON.stringify(object, null, 2)}`);
+			log(`invalid response: ${value}`);
 			throw new Error('invalid response');
 		}
 
-		log(`resolved: ${JSON.stringify(object, null, 2)}`);
-
 		return object;
 	}
 }

From 8e6a772701f1ee8c5caafcd049ede271dd1301bf Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Apr 2018 05:30:17 +0900
Subject: [PATCH 1201/1250] :v:

---
 src/remote/activitypub/objects/person.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/remote/activitypub/objects/person.ts b/src/remote/activitypub/objects/person.ts
index b1e8c9ee0..151b4a7a7 100644
--- a/src/remote/activitypub/objects/person.ts
+++ b/src/remote/activitypub/objects/person.ts
@@ -51,6 +51,7 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs
 		!isValidName(object.name == '' ? null : object.name) ||
 		!isValidDescription(object.summary)
 	) {
+		log(`invalid person: ${JSON.stringify(object, null, 2)}`);
 		throw new Error('invalid person');
 	}
 

From 0831159853af27b493a7eb8aa2ee1825ed77b5f9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Apr 2018 05:34:33 +0900
Subject: [PATCH 1202/1250] :v:

---
 src/remote/activitypub/kernel/announce/index.ts | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/src/remote/activitypub/kernel/announce/index.ts b/src/remote/activitypub/kernel/announce/index.ts
index c3ac06607..a2cf2d576 100644
--- a/src/remote/activitypub/kernel/announce/index.ts
+++ b/src/remote/activitypub/kernel/announce/index.ts
@@ -8,10 +8,6 @@ import { IAnnounce } from '../../type';
 const log = debug('misskey:activitypub');
 
 export default async (actor: IRemoteUser, activity: IAnnounce): Promise<void> => {
-	if ('actor' in activity && actor.uri !== activity.actor) {
-		throw new Error('invalid actor');
-	}
-
 	const uri = activity.id || activity;
 
 	log(`Announce: ${uri}`);

From 4de3c26e56b0edf842170e4b1feb8616a1eb0943 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Apr 2018 05:37:50 +0900
Subject: [PATCH 1203/1250] :v:

---
 src/remote/activitypub/objects/person.ts | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/remote/activitypub/objects/person.ts b/src/remote/activitypub/objects/person.ts
index 151b4a7a7..c5119fb85 100644
--- a/src/remote/activitypub/objects/person.ts
+++ b/src/remote/activitypub/objects/person.ts
@@ -48,8 +48,7 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs
 		object.type !== 'Person' ||
 		typeof object.preferredUsername !== 'string' ||
 		!validateUsername(object.preferredUsername) ||
-		!isValidName(object.name == '' ? null : object.name) ||
-		!isValidDescription(object.summary)
+		!isValidName(object.name == '' ? null : object.name)
 	) {
 		log(`invalid person: ${JSON.stringify(object, null, 2)}`);
 		throw new Error('invalid person');

From 40f482cc6bc199817d80dde9e79bec5a2f982448 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Apr 2018 05:39:18 +0900
Subject: [PATCH 1204/1250] oops

---
 src/remote/activitypub/objects/person.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/remote/activitypub/objects/person.ts b/src/remote/activitypub/objects/person.ts
index c5119fb85..a78538c4b 100644
--- a/src/remote/activitypub/objects/person.ts
+++ b/src/remote/activitypub/objects/person.ts
@@ -3,7 +3,7 @@ import { toUnicode } from 'punycode';
 import * as debug from 'debug';
 
 import config from '../../../config';
-import User, { validateUsername, isValidName, isValidDescription, IUser, IRemoteUser } from '../../../models/user';
+import User, { validateUsername, isValidName, IUser, IRemoteUser } from '../../../models/user';
 import webFinger from '../../webfinger';
 import Resolver from '../resolver';
 import { resolveImage } from './image';

From c44b26f2963418701af8641630386ee2e6a65dcf Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Apr 2018 05:44:35 +0900
Subject: [PATCH 1205/1250] Fix bug

---
 src/remote/activitypub/objects/person.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/remote/activitypub/objects/person.ts b/src/remote/activitypub/objects/person.ts
index a78538c4b..f7ec064cd 100644
--- a/src/remote/activitypub/objects/person.ts
+++ b/src/remote/activitypub/objects/person.ts
@@ -108,7 +108,7 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs
 	].map(img =>
 		img == null
 			? Promise.resolve(null)
-			: resolveImage(user, img.url)
+			: resolveImage(user, img)
 	))).map(file => file != null ? file._id : null);
 
 	User.update({ _id: user._id }, { $set: { avatarId, bannerId } });

From 34f77cb8a042d634a6dc42e0cda14e7ab1645508 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Apr 2018 17:09:57 +0900
Subject: [PATCH 1206/1250] =?UTF-8?q?=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC?=
 =?UTF-8?q?=E5=90=8D=E3=81=AE=E5=B0=91=E3=81=AA=E3=81=8F=E3=81=A8=E3=82=82?=
 =?UTF-8?q?3=E6=96=87=E5=AD=97=E4=BB=A5=E4=B8=8A=E3=81=A8=E3=81=84?=
 =?UTF-8?q?=E3=81=86=E5=88=B6=E9=99=90=E3=82=92=E6=92=A4=E5=BB=83?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 locales/ja.yml                                    | 2 +-
 src/client/app/common/views/components/signup.vue | 2 +-
 src/client/app/dev/views/new-app.vue              | 4 ++--
 src/models/app.ts                                 | 2 +-
 src/models/user.ts                                | 2 +-
 5 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/locales/ja.yml b/locales/ja.yml
index 84694e3c7..4d4c85362 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -148,7 +148,7 @@ common:
       unavailable: "既に利用されています"
       error: "通信エラー"
       invalid-format: "a~z、A~Z、0~9、_が使えます"
-      too-short: "3文字以上でお願いします!"
+      too-short: "1文字以上でお願いします!"
       too-long: "20文字以内でお願いします"
       password: "パスワード"
       password-placeholder: "8文字以上を推奨します"
diff --git a/src/client/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue
index 8d0b16cab..30fe7b7ad 100644
--- a/src/client/app/common/views/components/signup.vue
+++ b/src/client/app/common/views/components/signup.vue
@@ -2,7 +2,7 @@
 <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 @input="onChangeUsername"/>
+		<input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,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>
diff --git a/src/client/app/dev/views/new-app.vue b/src/client/app/dev/views/new-app.vue
index 2317f24d4..a3d7af4b9 100644
--- a/src/client/app/dev/views/new-app.vue
+++ b/src/client/app/dev/views/new-app.vue
@@ -6,13 +6,13 @@
 				<b-form-input v-model="name" type="text" placeholder="ex) Misskey for iOS" autocomplete="off" required/>
 			</b-form-group>
 			<b-form-group label="ID" description="あなたのアプリのID。">
-				<b-input v-model="nid" type="text" pattern="^[a-zA-Z0-9_]{3,30}$" placeholder="ex) misskey-for-ios" autocomplete="off" required/>
+				<b-input v-model="nid" type="text" pattern="^[a-zA-Z0-9_]{1,30}$" placeholder="ex) misskey-for-ios" autocomplete="off" required/>
 				<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 == 'min-range'" style="color:#FF1161">%fa:fw exclamation-triangle%1文字以上でお願いします!</p>
 				<p class="info" v-if="nidState == 'max-range'" style="color:#FF1161">%fa:fw exclamation-triangle%30文字以内でお願いします</p>
 			</b-form-group>
 			<b-form-group label="アプリの概要" description="あなたのアプリの簡単な説明や紹介。">
diff --git a/src/models/app.ts b/src/models/app.ts
index 703f4ef8f..446f0c62f 100644
--- a/src/models/app.ts
+++ b/src/models/app.ts
@@ -24,7 +24,7 @@ export type IApp = {
 };
 
 export function isValidNameId(nameId: string): boolean {
-	return typeof nameId == 'string' && /^[a-zA-Z0-9_]{3,30}$/.test(nameId);
+	return typeof nameId == 'string' && /^[a-zA-Z0-9_]{1,30}$/.test(nameId);
 }
 
 /**
diff --git a/src/models/user.ts b/src/models/user.ts
index 36c63a56d..b05bb8812 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -89,7 +89,7 @@ export const isRemoteUser = (user: any): user is IRemoteUser =>
 
 //#region Validators
 export function validateUsername(username: string): boolean {
-	return typeof username == 'string' && /^[a-zA-Z0-9_]{3,20}$/.test(username);
+	return typeof username == 'string' && /^[a-zA-Z0-9_]{1,20}$/.test(username);
 }
 
 export function validatePassword(password: string): boolean {

From 73f946fd4a035112c64ab096be2fc29862c044db Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Apr 2018 17:33:52 +0900
Subject: [PATCH 1207/1250] Fix bug

---
 package.json                      | 2 +-
 src/server/api/service/twitter.ts | 3 +--
 2 files changed, 2 insertions(+), 3 deletions(-)

diff --git a/package.json b/package.json
index 49a7d04e3..eb237a3d2 100644
--- a/package.json
+++ b/package.json
@@ -87,7 +87,7 @@
 		"accesses": "2.5.0",
 		"animejs": "2.2.0",
 		"autosize": "4.0.1",
-		"autwh": "0.0.1",
+		"autwh": "0.1.0",
 		"bcryptjs": "2.4.3",
 		"body-parser": "1.18.2",
 		"bootstrap-vue": "2.0.0-rc.6",
diff --git a/src/server/api/service/twitter.ts b/src/server/api/service/twitter.ts
index da48e30a8..e51ce92ba 100644
--- a/src/server/api/service/twitter.ts
+++ b/src/server/api/service/twitter.ts
@@ -70,8 +70,7 @@ module.exports = (app: express.Application) => {
 
 	const twAuth = autwh({
 		consumerKey: config.twitter.consumer_key,
-		consumerSecret: config.twitter.consumer_secret,
-		callbackUrl: `${config.api_url}/tw/cb`
+		consumerSecret: config.twitter.consumer_secret
 	});
 
 	app.get('/connect/twitter', async (req, res): Promise<any> => {

From 4fd328336608029b4288e53e799803d71efeedc1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Apr 2018 17:38:22 +0900
Subject: [PATCH 1208/1250] Fix bug

---
 src/queue/processors/http/index.ts            |  2 -
 .../processors/http/report-github-failure.ts  | 24 ----------
 src/server/api/service/github.ts              | 46 ++++++++++++-------
 3 files changed, 30 insertions(+), 42 deletions(-)
 delete mode 100644 src/queue/processors/http/report-github-failure.ts

diff --git a/src/queue/processors/http/index.ts b/src/queue/processors/http/index.ts
index 3dc259537..6f8d1dbc2 100644
--- a/src/queue/processors/http/index.ts
+++ b/src/queue/processors/http/index.ts
@@ -1,11 +1,9 @@
 import deliver from './deliver';
 import processInbox from './process-inbox';
-import reportGitHubFailure from './report-github-failure';
 
 const handlers = {
 	deliver,
 	processInbox,
-	reportGitHubFailure
 };
 
 export default (job, done) => {
diff --git a/src/queue/processors/http/report-github-failure.ts b/src/queue/processors/http/report-github-failure.ts
deleted file mode 100644
index 13f9afadd..000000000
--- a/src/queue/processors/http/report-github-failure.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import * as request from 'request-promise-native';
-import User from '../../../models/user';
-import createNote from '../../../services/note/create';
-
-export default async ({ data }) => {
-	const asyncBot = User.findOne({ _id: data.userId });
-
-	// Fetch parent status
-	const parentStatuses = await request({
-		url: `${data.parentUrl}/statuses`,
-		headers: {
-			'User-Agent': 'misskey'
-		},
-		json: true
-	});
-
-	const parentState = parentStatuses[0].state;
-	const stillFailed = parentState == 'failure' || parentState == 'error';
-	const text = stillFailed ?
-		`**⚠️BUILD STILL FAILED⚠️**: ?[${data.message}](${data.htmlUrl})` :
-		`**🚨BUILD FAILED🚨**: →→→?[${data.message}](${data.htmlUrl})←←←`;
-
-	createNote(await asyncBot, { text });
-};
diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts
index 6a327f1f7..6b1d5d25b 100644
--- a/src/server/api/service/github.ts
+++ b/src/server/api/service/github.ts
@@ -1,16 +1,17 @@
 import * as EventEmitter from 'events';
 import * as express from 'express';
-//const crypto = require('crypto');
+import * as request from 'request';
+const crypto = require('crypto');
+
 import User from '../../../models/user';
+import createNote from '../../../services/note/create';
 import config from '../../../config';
-import { createHttp } from '../../../queue';
 
 module.exports = async (app: express.Application) => {
 	if (config.github_bot == null) return;
 
 	const bot = await User.findOne({
-		usernameLower: config.github_bot.username.toLowerCase(),
-		host: null
+		username_lower: config.github_bot.username.toLowerCase()
 	});
 
 	if (bot == null) {
@@ -18,7 +19,7 @@ module.exports = async (app: express.Application) => {
 		return;
 	}
 
-	const post = text => require('../endpoints/notes/create')({ text }, bot);
+	const post = text => createNote(bot, { text });
 
 	const handler = new EventEmitter();
 
@@ -26,12 +27,12 @@ module.exports = async (app: express.Application) => {
 		// req.headers['x-hub-signature'] および
 		// req.headers['x-github-event'] は常に string ですが、型定義の都合上
 		// string | string[] になっているので string を明示しています
-//		if ((new Buffer(req.headers['x-hub-signature'] as string)).equals(new Buffer(`sha1=${crypto.createHmac('sha1', config.github_bot.hook_secret).update(JSON.stringify(req.body)).digest('hex')}`))) {
+		if ((new Buffer(req.headers['x-hub-signature'] as string)).equals(new Buffer(`sha1=${crypto.createHmac('sha1', config.github_bot.hook_secret).update(JSON.stringify(req.body)).digest('hex')}`))) {
 			handler.emit(req.headers['x-github-event'] as string, req.body);
 			res.sendStatus(200);
-//		} else {
-//			res.sendStatus(400);
-//		}
+		} else {
+			res.sendStatus(400);
+		}
 	});
 
 	handler.on('status', event => {
@@ -42,13 +43,26 @@ module.exports = async (app: express.Application) => {
 				const commit = event.commit;
 				const parent = commit.parents[0];
 
-				createHttp({
-					type: 'gitHubFailureReport',
-					userId: bot._id,
-					parentUrl: parent.url,
-					htmlUrl: commit.html_url,
-					message: commit.commit.message,
-				}).save();
+				// Fetch parent status
+				request({
+					url: `${parent.url}/statuses`,
+					headers: {
+						'User-Agent': 'misskey'
+					}
+				}, (err, res, body) => {
+					if (err) {
+						console.error(err);
+						return;
+					}
+					const parentStatuses = JSON.parse(body);
+					const parentState = parentStatuses[0].state;
+					const stillFailed = parentState == 'failure' || parentState == 'error';
+					if (stillFailed) {
+						post(`**⚠️BUILD STILL FAILED⚠️**: ?[${commit.commit.message}](${commit.html_url})`);
+					} else {
+						post(`**🚨BUILD FAILED🚨**: →→→?[${commit.commit.message}](${commit.html_url})←←←`);
+					}
+				});
 				break;
 		}
 	});

From 37a99f0f021d31c4560e06757842b61d8199f574 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Apr 2018 18:52:29 +0900
Subject: [PATCH 1209/1250] Refactor

---
 .../common/views/components/autocomplete.vue  |  11 +-
 .../app/common/views/components/messaging.vue |  12 +-
 .../views/components/welcome-timeline.vue     |  10 +-
 src/client/app/common/views/filters/index.ts  |   2 +
 src/client/app/common/views/filters/note.ts   |   5 +
 src/client/app/common/views/filters/user.ts   |  15 +
 .../views/components/followers-window.vue     |  10 +-
 .../views/components/following-window.vue     |  10 +-
 .../views/components/friends-maker.vue        |  10 +-
 .../components/messaging-room-window.vue      |   6 +-
 .../views/components/note-detail.sub.vue      |  16 +-
 .../desktop/views/components/note-detail.vue  |  26 +-
 .../desktop/views/components/note-preview.vue |  16 +-
 .../views/components/notes.note.sub.vue       |  16 +-
 .../desktop/views/components/notes.note.vue   |  21 +-
 .../views/components/notifications.vue        |  32 +-
 .../views/components/post-detail.sub.vue      | 122 ++++
 .../desktop/views/components/post-detail.vue  | 434 +++++++++++++
 .../desktop/views/components/post-preview.vue |  99 +++
 .../views/components/posts.post.sub.vue       | 108 ++++
 .../desktop/views/components/posts.post.vue   | 585 ++++++++++++++++++
 .../views/components/settings.mute.vue        |   8 +-
 .../desktop/views/components/ui.header.vue    |   9 +-
 .../desktop/views/components/user-preview.vue |   6 +-
 .../views/components/users-list.item.vue      |  18 +-
 .../pages/user/user.followers-you-know.vue    |  10 +-
 .../desktop/views/pages/user/user.friends.vue |   6 +-
 .../desktop/views/pages/user/user.header.vue  |  14 +-
 .../app/desktop/views/pages/user/user.vue     |   2 +-
 .../app/desktop/views/pages/welcome.vue       |   2 +-
 .../views/widgets/channel.channel.note.vue    |  14 +-
 .../views/widgets/channel.channel.post.vue    |  65 ++
 .../app/desktop/views/widgets/profile.vue     |  10 +-
 .../app/desktop/views/widgets/users.vue       |  10 +-
 .../app/mobile/views/components/note-card.vue |  12 +-
 .../views/components/note-detail.sub.vue      |  20 +-
 .../mobile/views/components/note-detail.vue   |  28 +-
 .../mobile/views/components/note-preview.vue  |  20 +-
 .../app/mobile/views/components/note.sub.vue  |  18 +-
 .../app/mobile/views/components/note.vue      |  21 +-
 .../views/components/notification-preview.vue |  18 +-
 .../mobile/views/components/notification.vue  |  22 +-
 .../app/mobile/views/components/post-card.vue |  85 +++
 .../views/components/post-detail.sub.vue      | 103 +++
 .../mobile/views/components/post-detail.vue   | 444 +++++++++++++
 .../mobile/views/components/post-preview.vue  | 100 +++
 .../app/mobile/views/components/post.vue      | 523 ++++++++++++++++
 .../app/mobile/views/components/ui.header.vue |   8 +-
 .../app/mobile/views/components/ui.nav.vue    |  13 +-
 .../app/mobile/views/components/user-card.vue |  18 +-
 .../mobile/views/components/user-preview.vue  |  18 +-
 .../app/mobile/views/pages/following.vue      |   5 +-
 .../app/mobile/views/pages/messaging-room.vue |  10 +-
 .../app/mobile/views/pages/settings.vue       |   5 +-
 src/client/app/mobile/views/pages/user.vue    |  14 +-
 .../pages/user/home.followers-you-know.vue    |  14 +-
 .../app/mobile/views/widgets/profile.vue      |   9 +-
 57 files changed, 2846 insertions(+), 422 deletions(-)
 create mode 100644 src/client/app/common/views/filters/note.ts
 create mode 100644 src/client/app/common/views/filters/user.ts
 create mode 100644 src/client/app/desktop/views/components/post-detail.sub.vue
 create mode 100644 src/client/app/desktop/views/components/post-detail.vue
 create mode 100644 src/client/app/desktop/views/components/post-preview.vue
 create mode 100644 src/client/app/desktop/views/components/posts.post.sub.vue
 create mode 100644 src/client/app/desktop/views/components/posts.post.vue
 create mode 100644 src/client/app/desktop/views/widgets/channel.channel.post.vue
 create mode 100644 src/client/app/mobile/views/components/post-card.vue
 create mode 100644 src/client/app/mobile/views/components/post-detail.sub.vue
 create mode 100644 src/client/app/mobile/views/components/post-detail.vue
 create mode 100644 src/client/app/mobile/views/components/post-preview.vue
 create mode 100644 src/client/app/mobile/views/components/post.vue

diff --git a/src/client/app/common/views/components/autocomplete.vue b/src/client/app/common/views/components/autocomplete.vue
index 8837fde6b..5c8f61a2a 100644
--- a/src/client/app/common/views/components/autocomplete.vue
+++ b/src/client/app/common/views/components/autocomplete.vue
@@ -3,8 +3,8 @@
 	<ol class="users" ref="suggests" v-if="users.length > 0">
 		<li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1">
 			<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=32`" alt=""/>
-			<span class="name">{{ getUserName(user) }}</span>
-			<span class="username">@{{ getAcct(user) }}</span>
+			<span class="name">{{ user | userName }}</span>
+			<span class="username">@{{ user | acct }}</span>
 		</li>
 	</ol>
 	<ol class="emojis" ref="suggests" v-if="emojis.length > 0">
@@ -21,17 +21,17 @@
 import Vue from 'vue';
 import * as emojilib from 'emojilib';
 import contains from '../../../common/scripts/contains';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 const lib = Object.entries(emojilib.lib).filter((x: any) => {
 	return x[1].category != 'flags';
 });
+
 const emjdb = lib.map((x: any) => ({
 	emoji: x[1].char,
 	name: x[0],
 	alias: null
 }));
+
 lib.forEach((x: any) => {
 	if (x[1].keywords) {
 		x[1].keywords.forEach(k => {
@@ -43,6 +43,7 @@ lib.forEach((x: any) => {
 		});
 	}
 });
+
 emjdb.sort((a, b) => a.name.length - b.name.length);
 
 export default Vue.extend({
@@ -107,8 +108,6 @@ export default Vue.extend({
 		});
 	},
 	methods: {
-		getAcct,
-		getUserName,
 		exec() {
 			this.select = -1;
 			if (this.$refs.suggests) {
diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue
index 9b1449daa..751e4de50 100644
--- a/src/client/app/common/views/components/messaging.vue
+++ b/src/client/app/common/views/components/messaging.vue
@@ -14,8 +14,8 @@
 					tabindex="-1"
 				>
 					<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=32`" alt=""/>
-					<span class="name">{{ getUserName(user) }}</span>
-					<span class="username">@{{ getAcct(user) }}</span>
+					<span class="name">{{ user | userName }}</span>
+					<span class="username">@{{ user | acct }}</span>
 				</li>
 			</ol>
 		</div>
@@ -33,8 +33,8 @@
 				<div>
 					<img class="avatar" :src="`${isMe(message) ? message.recipient.avatarUrl : message.user.avatarUrl}?thumbnail&size=64`" alt=""/>
 					<header>
-						<span class="name">{{ getUserName(isMe(message) ? message.recipient : message.user) }}</span>
-						<span class="username">@{{ getAcct(isMe(message) ? message.recipient : message.user) }}</span>
+						<span class="name">{{ isMe(message) ? message.recipient : message.use | userName }}</span>
+						<span class="username">@{{ isMe(message) ? message.recipient : message.user | acct }}</span>
 						<mk-time :time="message.createdAt"/>
 					</header>
 					<div class="body">
@@ -51,8 +51,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: {
@@ -94,8 +92,6 @@ export default Vue.extend({
 		(this as any).os.streams.messagingIndexStream.dispose(this.connectionId);
 	},
 	methods: {
-		getAcct,
-		getUserName,
 		isMe(message) {
 			return message.userId == (this as any).os.i.id;
 		},
diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue
index 61616da14..7571cfc5f 100644
--- a/src/client/app/common/views/components/welcome-timeline.vue
+++ b/src/client/app/common/views/components/welcome-timeline.vue
@@ -1,13 +1,13 @@
 <template>
 <div class="mk-welcome-timeline">
 	<div v-for="note in notes">
-		<router-link class="avatar-anchor" :to="`/@${getAcct(note.user)}`" v-user-preview="note.user.id">
+		<router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.user.id">
 			<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
 		</router-link>
 		<div class="body">
 			<header>
-				<router-link class="name" :to="`/@${getAcct(note.user)}`" v-user-preview="note.user.id">{{ getUserName(note.user) }}</router-link>
-				<span class="username">@{{ getAcct(note.user) }}</span>
+				<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">{{ note.user | userName }}</router-link>
+				<span class="username">@{{ note.user | acct }}</span>
 				<div class="info">
 					<router-link class="created-at" :to="`/@${getAcct(note.user)}/${note.id}`">
 						<mk-time :time="note.createdAt"/>
@@ -24,8 +24,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -38,8 +36,6 @@ export default Vue.extend({
 		this.fetch();
 	},
 	methods: {
-		getAcct,
-		getUserName,
 		fetch(cb?) {
 			this.fetching = true;
 			(this as any).api('notes', {
diff --git a/src/client/app/common/views/filters/index.ts b/src/client/app/common/views/filters/index.ts
index 3a1d1ac23..1759c19c2 100644
--- a/src/client/app/common/views/filters/index.ts
+++ b/src/client/app/common/views/filters/index.ts
@@ -1,2 +1,4 @@
 require('./bytes');
 require('./number');
+require('./user');
+require('./note');
diff --git a/src/client/app/common/views/filters/note.ts b/src/client/app/common/views/filters/note.ts
new file mode 100644
index 000000000..a611dc868
--- /dev/null
+++ b/src/client/app/common/views/filters/note.ts
@@ -0,0 +1,5 @@
+import Vue from 'vue';
+
+Vue.filter('notePage', note => {
+	return '/notes/' + note.id;
+});
diff --git a/src/client/app/common/views/filters/user.ts b/src/client/app/common/views/filters/user.ts
new file mode 100644
index 000000000..167bb7758
--- /dev/null
+++ b/src/client/app/common/views/filters/user.ts
@@ -0,0 +1,15 @@
+import Vue from 'vue';
+import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
+
+Vue.filter('acct', user => {
+	return getAcct(user);
+});
+
+Vue.filter('userName', user => {
+	return getUserName(user);
+});
+
+Vue.filter('userPage', user => {
+	return '/@' + Vue.filter('acct')(user);
+});
diff --git a/src/client/app/desktop/views/components/followers-window.vue b/src/client/app/desktop/views/components/followers-window.vue
index d37ca745a..16206299d 100644
--- a/src/client/app/desktop/views/components/followers-window.vue
+++ b/src/client/app/desktop/views/components/followers-window.vue
@@ -1,7 +1,7 @@
 <template>
 <mk-window width="400px" height="550px" @closed="$destroy">
 	<span slot="header" :class="$style.header">
-		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ name }}のフォロワー
+		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ user | userName }}のフォロワー
 	</span>
 	<mk-followers :user="user"/>
 </mk-window>
@@ -9,15 +9,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
-	props: ['user'],
-	computed {
-		name() {
-			return getUserName(this.user);
-		}
-	}
+	props: ['user']
 });
 </script>
 
diff --git a/src/client/app/desktop/views/components/following-window.vue b/src/client/app/desktop/views/components/following-window.vue
index cbd8ec5f9..cc3d77198 100644
--- a/src/client/app/desktop/views/components/following-window.vue
+++ b/src/client/app/desktop/views/components/following-window.vue
@@ -1,7 +1,7 @@
 <template>
 <mk-window width="400px" height="550px" @closed="$destroy">
 	<span slot="header" :class="$style.header">
-		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ name }}のフォロー
+		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ user | userName }}のフォロー
 	</span>
 	<mk-following :user="user"/>
 </mk-window>
@@ -9,15 +9,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
-	props: ['user'],
-	computed: {
-		name() {
-			return getUserName(this.user);
-		}
-	}
+	props: ['user']
 });
 </script>
 
diff --git a/src/client/app/desktop/views/components/friends-maker.vue b/src/client/app/desktop/views/components/friends-maker.vue
index acc4542d9..af5bde3ad 100644
--- a/src/client/app/desktop/views/components/friends-maker.vue
+++ b/src/client/app/desktop/views/components/friends-maker.vue
@@ -3,12 +3,12 @@
 	<p class="title">気になるユーザーをフォロー:</p>
 	<div class="users" v-if="!fetching && users.length > 0">
 		<div class="user" v-for="user in users" :key="user.id">
-			<router-link class="avatar-anchor" :to="`/@${getAcct(user)}`">
+			<router-link class="avatar-anchor" :to="user | userPage">
 				<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="user.id"/>
 			</router-link>
 			<div class="body">
-				<router-link class="name" :to="`/@${getAcct(user)}`" v-user-preview="user.id">{{ getUserName(user) }}</router-link>
-				<p class="username">@{{ getAcct(user) }}</p>
+				<router-link class="name" :to="user | userPage" v-user-preview="user.id">{{ user | userName }}</router-link>
+				<p class="username">@{{ user | acct }}</p>
 			</div>
 			<mk-follow-button :user="user"/>
 		</div>
@@ -22,8 +22,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -38,8 +36,6 @@ export default Vue.extend({
 		this.fetch();
 	},
 	methods: {
-		getAcct,
-		getUserName,
 		fetch() {
 			this.fetching = true;
 			this.users = [];
diff --git a/src/client/app/desktop/views/components/messaging-room-window.vue b/src/client/app/desktop/views/components/messaging-room-window.vue
index 7f8c35c2f..dbe326673 100644
--- a/src/client/app/desktop/views/components/messaging-room-window.vue
+++ b/src/client/app/desktop/views/components/messaging-room-window.vue
@@ -1,6 +1,6 @@
 <template>
 <mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="$destroy">
-	<span slot="header" :class="$style.header">%fa:comments%メッセージ: {{ name }}</span>
+	<span slot="header" :class="$style.header">%fa:comments%メッセージ: {{ user | userName }}</span>
 	<mk-messaging-room :user="user" :class="$style.content"/>
 </mk-window>
 </template>
@@ -9,14 +9,10 @@
 import Vue from 'vue';
 import { url } from '../../../config';
 import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['user'],
 	computed: {
-		name(): string {
-			return getUserName(this.user);
-		},
 		popout(): string {
 			return `${url}/i/messaging/${getAcct(this.user)}`;
 		}
diff --git a/src/client/app/desktop/views/components/note-detail.sub.vue b/src/client/app/desktop/views/components/note-detail.sub.vue
index 79f5de1f8..16bc2a1d9 100644
--- a/src/client/app/desktop/views/components/note-detail.sub.vue
+++ b/src/client/app/desktop/views/components/note-detail.sub.vue
@@ -1,16 +1,16 @@
 <template>
 <div class="sub" :title="title">
-	<router-link class="avatar-anchor" :to="`/@${acct}`">
+	<router-link class="avatar-anchor" :to="note.user | userPage">
 		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
 	</router-link>
 	<div class="main">
 		<header>
 			<div class="left">
-				<router-link class="name" :to="`/@${acct}`" v-user-preview="note.userId">{{ name }}</router-link>
-				<span class="username">@{{ acct }}</span>
+				<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
+				<span class="username">@{{ note.user | acct }}</span>
 			</div>
 			<div class="right">
-				<router-link class="time" :to="`/@${acct}/${note.id}`">
+				<router-link class="time" :to="note | notePage">
 					<mk-time :time="note.createdAt"/>
 				</router-link>
 			</div>
@@ -28,18 +28,10 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['note'],
 	computed: {
-		acct() {
-			return getAcct(this.note.user);
-		},
-		name() {
-			return getUserName(this.note.user);
-		},
 		title(): string {
 			return dateStringify(this.note.createdAt);
 		}
diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue
index eead82dd0..50bbb7698 100644
--- a/src/client/app/desktop/views/components/note-detail.vue
+++ b/src/client/app/desktop/views/components/note-detail.vue
@@ -18,22 +18,22 @@
 	</div>
 	<div class="renote" v-if="isRenote">
 		<p>
-			<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="note.userId">
+			<router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.userId">
 				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
 			</router-link>
 			%fa:retweet%
-			<router-link class="name" :href="`/@${acct}`">{{ name }}</router-link>
+			<router-link class="name" :href="note.user | userPage">{{ note.user | userName }}</router-link>
 			がRenote
 		</p>
 	</div>
 	<article>
-		<router-link class="avatar-anchor" :to="`/@${pAcct}`">
+		<router-link class="avatar-anchor" :to="p.user | userPage">
 			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
 		</router-link>
 		<header>
-			<router-link class="name" :to="`/@${pAcct}`" v-user-preview="p.user.id">{{ name }}</router-link>
-			<span class="username">@{{ pAcct }}</span>
-			<router-link class="time" :to="`/@${pAcct}/${p.id}`">
+			<router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link>
+			<span class="username">@{{ p.user | acct }}</span>
+			<router-link class="time" :to="p | notePage">
 				<mk-time :time="p.createdAt"/>
 			</router-link>
 		</header>
@@ -78,8 +78,6 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 import parse from '../../../../../text/parse';
 
 import MkPostFormWindow from './post-form-window.vue';
@@ -131,18 +129,6 @@ export default Vue.extend({
 		title(): string {
 			return dateStringify(this.p.createdAt);
 		},
-		acct(): string {
-			return getAcct(this.note.user);
-		},
-		name(): string {
-			return getUserName(this.note.user);
-		},
-		pAcct(): string {
-			return getAcct(this.p.user);
-		},
-		pName(): string {
-			return getUserName(this.p.user);
-		},
 		urls(): string[] {
 			if (this.p.text) {
 				const ast = parse(this.p.text);
diff --git a/src/client/app/desktop/views/components/note-preview.vue b/src/client/app/desktop/views/components/note-preview.vue
index bff199c09..ff3ecadc2 100644
--- a/src/client/app/desktop/views/components/note-preview.vue
+++ b/src/client/app/desktop/views/components/note-preview.vue
@@ -1,13 +1,13 @@
 <template>
 <div class="mk-note-preview" :title="title">
-	<router-link class="avatar-anchor" :to="`/@${acct}`">
+	<router-link class="avatar-anchor" :to="note.user | userPage">
 		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`" v-user-preview="note.userId">{{ name }}</router-link>
-			<span class="username">@{{ acct }}</span>
-			<router-link class="time" :to="`/@${acct}/${note.id}`">
+			<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
+			<span class="username">@{{ note.user | acct }}</span>
+			<router-link class="time" :to="note | notePage">
 				<mk-time :time="note.createdAt"/>
 			</router-link>
 		</header>
@@ -21,18 +21,10 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['note'],
 	computed: {
-		acct() {
-			return getAcct(this.note.user);
-		},
-		name() {
-			return getUserName(this.note.user);
-		},
 		title(): string {
 			return dateStringify(this.note.createdAt);
 		}
diff --git a/src/client/app/desktop/views/components/notes.note.sub.vue b/src/client/app/desktop/views/components/notes.note.sub.vue
index b49d12b92..e85478578 100644
--- a/src/client/app/desktop/views/components/notes.note.sub.vue
+++ b/src/client/app/desktop/views/components/notes.note.sub.vue
@@ -1,13 +1,13 @@
 <template>
 <div class="sub" :title="title">
-	<router-link class="avatar-anchor" :to="`/@${acct}`">
+	<router-link class="avatar-anchor" :to="note.user | userPage">
 		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`" v-user-preview="note.userId">{{ name }}</router-link>
-			<span class="username">@{{ acct }}</span>
-			<router-link class="created-at" :to="`/@${acct}/${note.id}`">
+			<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
+			<span class="username">@{{ note.user | acct }}</span>
+			<router-link class="created-at" :to="note | notePage">
 				<mk-time :time="note.createdAt"/>
 			</router-link>
 		</header>
@@ -21,18 +21,10 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['note'],
 	computed: {
-		acct() {
-			return getAcct(this.note.user);
-		},
-		name(): string {
-			return getUserName(this.note.user);
-		},
 		title(): string {
 			return dateStringify(this.note.createdAt);
 		}
diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue
index 0712069e5..8561643c9 100644
--- a/src/client/app/desktop/views/components/notes.note.vue
+++ b/src/client/app/desktop/views/components/notes.note.vue
@@ -5,29 +5,29 @@
 	</div>
 	<div class="renote" v-if="isRenote">
 		<p>
-			<router-link class="avatar-anchor" :to="`/@${getAcct(note.user)}`" v-user-preview="note.userId">
+			<router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.userId">
 				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
 			</router-link>
 			%fa:retweet%
 			<span>{{ '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.indexOf('{')) }}</span>
-			<a class="name" :href="`/@${getAcct(note.user)}`" v-user-preview="note.userId">{{ getUserName(note.user) }}</a>
+			<a class="name" :href="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</a>
 			<span>{{ '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-note.reposted-by%'.indexOf('}') + 1) }}</span>
 		</p>
 		<mk-time :time="note.createdAt"/>
 	</div>
 	<article>
-		<router-link class="avatar-anchor" :to="`/@${getAcct(p.user)}`">
+		<router-link class="avatar-anchor" :to="p.user | userPage">
 			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
 		</router-link>
 		<div class="main">
 			<header>
-				<router-link class="name" :to="`/@${getAcct(p.user)}`" v-user-preview="p.user.id">{{ getUserName(p.user) }}</router-link>
+				<router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link>
 				<span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span>
-				<span class="username">@{{ getAcct(p.user) }}</span>
+				<span class="username">@{{ p.user | acct }}</span>
 				<div class="info">
 					<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
 					<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
-					<router-link class="created-at" :to="url">
+					<router-link class="created-at" :to="p | notePage">
 						<mk-time :time="p.createdAt"/>
 					</router-link>
 				</div>
@@ -85,8 +85,6 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 import parse from '../../../../../text/parse';
 
 import MkPostFormWindow from './post-form-window.vue';
@@ -117,9 +115,7 @@ export default Vue.extend({
 		return {
 			isDetailOpened: false,
 			connection: null,
-			connectionId: null,
-			getAcct,
-			getUserName
+			connectionId: null
 		};
 	},
 
@@ -144,9 +140,6 @@ export default Vue.extend({
 		title(): string {
 			return dateStringify(this.p.createdAt);
 		},
-		url(): string {
-			return `/@${this.acct}/${this.p.id}`;
-		},
 		urls(): string[] {
 			if (this.p.text) {
 				const ast = parse(this.p.text);
diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
index 100a803cc..8b17c8c43 100644
--- a/src/client/app/desktop/views/components/notifications.vue
+++ b/src/client/app/desktop/views/components/notifications.vue
@@ -5,13 +5,13 @@
 			<div class="notification" :class="notification.type" :key="notification.id">
 				<mk-time :time="notification.createdAt"/>
 				<template v-if="notification.type == 'reaction'">
-					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">
+					<router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id">
 						<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>
 							<mk-reaction-icon :reaction="notification.reaction"/>
-							<router-link :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ getUserName(notification.user) }}</router-link>
+							<router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link>
 						</p>
 						<router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">
 							%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%
@@ -19,12 +19,12 @@
 					</div>
 				</template>
 				<template v-if="notification.type == 'renote'">
-					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">
+					<router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId">
 						<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:retweet%
-							<router-link :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">{{ getUserName(notification.note.user) }}</router-link>
+							<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
 						</p>
 						<router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">
 							%fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right%
@@ -32,54 +32,54 @@
 					</div>
 				</template>
 				<template v-if="notification.type == 'quote'">
-					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">
+					<router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId">
 						<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:quote-left%
-							<router-link :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">{{ getUserName(notification.note.user) }}</router-link>
+							<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
 						</p>
 						<router-link class="note-preview" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">{{ getNoteSummary(notification.note) }}</router-link>
 					</div>
 				</template>
 				<template v-if="notification.type == 'follow'">
-					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">
+					<router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id">
 						<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:user-plus%
-							<router-link :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ getUserName(notification.user) }}</router-link>
+							<router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link>
 						</p>
 					</div>
 				</template>
 				<template v-if="notification.type == 'reply'">
-					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">
+					<router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId">
 						<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:reply%
-							<router-link :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">{{ getUserName(notification.note.user) }}</router-link>
+							<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
 						</p>
 						<router-link class="note-preview" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">{{ getNoteSummary(notification.note) }}</router-link>
 					</div>
 				</template>
 				<template v-if="notification.type == 'mention'">
-					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">
+					<router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId">
 						<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:at%
-							<router-link :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">{{ getUserName(notification.note.user) }}</router-link>
+							<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
 						</p>
 						<a class="note-preview" :href="`/@${getAcct(notification.note.user)}/${notification.note.id}`">{{ getNoteSummary(notification.note) }}</a>
 					</div>
 				</template>
 				<template v-if="notification.type == 'poll_vote'">
-					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">
+					<router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id">
 						<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
-						<p>%fa:chart-pie%<a :href="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ getUserName(notification.user) }}</a></p>
+						<p>%fa:chart-pie%<a :href="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</a></p>
 						<router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">
 							%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%
 						</router-link>
@@ -102,9 +102,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
 import getNoteSummary from '../../../../../renderers/get-note-summary';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -154,8 +152,6 @@ export default Vue.extend({
 		(this as any).os.stream.dispose(this.connectionId);
 	},
 	methods: {
-		getAcct,
-		getUserName,
 		fetchMoreNotifications() {
 			this.fetchingMoreNotifications = true;
 
diff --git a/src/client/app/desktop/views/components/post-detail.sub.vue b/src/client/app/desktop/views/components/post-detail.sub.vue
new file mode 100644
index 000000000..16bc2a1d9
--- /dev/null
+++ b/src/client/app/desktop/views/components/post-detail.sub.vue
@@ -0,0 +1,122 @@
+<template>
+<div class="sub" :title="title">
+	<router-link class="avatar-anchor" :to="note.user | userPage">
+		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
+	</router-link>
+	<div class="main">
+		<header>
+			<div class="left">
+				<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
+				<span class="username">@{{ note.user | acct }}</span>
+			</div>
+			<div class="right">
+				<router-link class="time" :to="note | notePage">
+					<mk-time :time="note.createdAt"/>
+				</router-link>
+			</div>
+		</header>
+		<div class="body">
+			<mk-note-html v-if="note.text" :text="note.text" :i="os.i" :class="$style.text"/>
+			<div class="media" v-if="note.media > 0">
+				<mk-media-list :media-list="note.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: ['note'],
+	computed: {
+		title(): string {
+			return dateStringify(this.note.createdAt);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.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
+
+</style>
+
+<style lang="stylus" module>
+.text
+	cursor default
+	display block
+	margin 0
+	padding 0
+	overflow-wrap break-word
+	font-size 1em
+	color #717171
+</style>
diff --git a/src/client/app/desktop/views/components/post-detail.vue b/src/client/app/desktop/views/components/post-detail.vue
new file mode 100644
index 000000000..50bbb7698
--- /dev/null
+++ b/src/client/app/desktop/views/components/post-detail.vue
@@ -0,0 +1,434 @@
+<template>
+<div class="mk-note-detail" :title="title">
+	<button
+		class="read-more"
+		v-if="p.reply && p.reply.replyId && context == null"
+		title="会話をもっと読み込む"
+		@click="fetchContext"
+		:disabled="contextFetching"
+	>
+		<template v-if="!contextFetching">%fa:ellipsis-v%</template>
+		<template v-if="contextFetching">%fa:spinner .pulse%</template>
+	</button>
+	<div class="context">
+		<x-sub v-for="note in context" :key="note.id" :note="note"/>
+	</div>
+	<div class="reply-to" v-if="p.reply">
+		<x-sub :note="p.reply"/>
+	</div>
+	<div class="renote" v-if="isRenote">
+		<p>
+			<router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.userId">
+				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
+			</router-link>
+			%fa:retweet%
+			<router-link class="name" :href="note.user | userPage">{{ note.user | userName }}</router-link>
+			がRenote
+		</p>
+	</div>
+	<article>
+		<router-link class="avatar-anchor" :to="p.user | userPage">
+			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
+		</router-link>
+		<header>
+			<router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link>
+			<span class="username">@{{ p.user | acct }}</span>
+			<router-link class="time" :to="p | notePage">
+				<mk-time :time="p.createdAt"/>
+			</router-link>
+		</header>
+		<div class="body">
+			<mk-note-html :class="$style.text" v-if="p.text" :text="p.text" :i="os.i"/>
+			<div class="media" v-if="p.media.length > 0">
+				<mk-media-list :media-list="p.media"/>
+			</div>
+			<mk-poll v-if="p.poll" :note="p"/>
+			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
+			<div class="tags" v-if="p.tags && p.tags.length > 0">
+				<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
+			</div>
+			<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
+			<div class="map" v-if="p.geo" ref="map"></div>
+			<div class="renote" v-if="p.renote">
+				<mk-note-preview :note="p.renote"/>
+			</div>
+		</div>
+		<footer>
+			<mk-reactions-viewer :note="p"/>
+			<button @click="reply" title="返信">
+				%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
+			</button>
+			<button @click="renote" title="Renote">
+				%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
+			</button>
+			<button :class="{ reacted: p.myReaction != 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">
+		<x-sub v-for="note in replies" :key="note.id" :note="note"/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import dateStringify from '../../../common/scripts/date-stringify';
+import parse from '../../../../../text/parse';
+
+import MkPostFormWindow from './post-form-window.vue';
+import MkRenoteFormWindow from './renote-form-window.vue';
+import MkNoteMenu from '../../../common/views/components/note-menu.vue';
+import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
+import XSub from './note-detail.sub.vue';
+
+export default Vue.extend({
+	components: {
+		XSub
+	},
+
+	props: {
+		note: {
+			type: Object,
+			required: true
+		},
+		compact: {
+			default: false
+		}
+	},
+
+	data() {
+		return {
+			context: [],
+			contextFetching: false,
+			replies: []
+		};
+	},
+
+	computed: {
+		isRenote(): boolean {
+			return (this.note.renote &&
+				this.note.text == null &&
+				this.note.mediaIds.length == 0 &&
+				this.note.poll == null);
+		},
+		p(): any {
+			return this.isRenote ? this.note.renote : this.note;
+		},
+		reactionsCount(): number {
+			return this.p.reactionCounts
+				? Object.keys(this.p.reactionCounts)
+					.map(key => this.p.reactionCounts[key])
+					.reduce((a, b) => a + b)
+				: 0;
+		},
+		title(): string {
+			return dateStringify(this.p.createdAt);
+		},
+		urls(): string[] {
+			if (this.p.text) {
+				const ast = parse(this.p.text);
+				return 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 as any).api('notes/replies', {
+				noteId: this.p.id,
+				limit: 8
+			}).then(replies => {
+				this.replies = replies;
+			});
+		}
+
+		// Draw map
+		if (this.p.geo) {
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
+			if (shouldShowMap) {
+				(this as any).os.getGoogleMaps().then(maps => {
+					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
+					const map = new maps.Map(this.$refs.map, {
+						center: uluru,
+						zoom: 15
+					});
+					new maps.Marker({
+						position: uluru,
+						map: map
+					});
+				});
+			}
+		}
+	},
+
+	methods: {
+		fetchContext() {
+			this.contextFetching = true;
+
+			// Fetch context
+			(this as any).api('notes/context', {
+				noteId: this.p.replyId
+			}).then(context => {
+				this.contextFetching = false;
+				this.context = context.reverse();
+			});
+		},
+		reply() {
+			(this as any).os.new(MkPostFormWindow, {
+				reply: this.p
+			});
+		},
+		renote() {
+			(this as any).os.new(MkRenoteFormWindow, {
+				note: this.p
+			});
+		},
+		react() {
+			(this as any).os.new(MkReactionPicker, {
+				source: this.$refs.reactButton,
+				note: this.p
+			});
+		},
+		menu() {
+			(this as any).os.new(MkNoteMenu, {
+				source: this.$refs.menuButton,
+				note: this.p
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+.mk-note-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
+
+	> .renote
+		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
+
+			> .renote
+				margin 8px 0
+
+				> .mk-note-preview
+					padding 16px
+					border dashed 1px #c0dac6
+					border-radius 8px
+
+			> .location
+				margin 4px 0
+				font-size 12px
+				color #ccc
+
+			> .map
+				width 100%
+				height 300px
+
+				&:empty
+					display none
+
+			> .mk-url-preview
+				margin-top 8px
+
+			> .tags
+				margin 4px 0 0 0
+
+				> *
+					display inline-block
+					margin 0 8px 0 0
+					padding 2px 8px 2px 16px
+					font-size 90%
+					color #8d969e
+					background #edf0f3
+					border-radius 4px
+
+					&:before
+						content ""
+						display block
+						position absolute
+						top 0
+						bottom 0
+						left 4px
+						width 8px
+						height 8px
+						margin auto 0
+						background #fff
+						border-radius 100%
+
+					&:hover
+						text-decoration none
+						background #e2e7ec
+
+		> 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>
+
+<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/client/app/desktop/views/components/post-preview.vue b/src/client/app/desktop/views/components/post-preview.vue
new file mode 100644
index 000000000..ff3ecadc2
--- /dev/null
+++ b/src/client/app/desktop/views/components/post-preview.vue
@@ -0,0 +1,99 @@
+<template>
+<div class="mk-note-preview" :title="title">
+	<router-link class="avatar-anchor" :to="note.user | userPage">
+		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
+	</router-link>
+	<div class="main">
+		<header>
+			<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
+			<span class="username">@{{ note.user | acct }}</span>
+			<router-link class="time" :to="note | notePage">
+				<mk-time :time="note.createdAt"/>
+			</router-link>
+		</header>
+		<div class="body">
+			<mk-sub-note-content class="text" :note="note"/>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import dateStringify from '../../../common/scripts/date-stringify';
+
+export default Vue.extend({
+	props: ['note'],
+	computed: {
+		title(): string {
+			return dateStringify(this.note.createdAt);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-note-preview
+	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 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
+			white-space nowrap
+
+			> .name
+				margin 0 .5em 0 0
+				padding 0
+				color #607073
+				font-size 1em
+				font-weight bold
+				text-decoration none
+				white-space normal
+
+				&:hover
+					text-decoration underline
+
+			> .username
+				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/client/app/desktop/views/components/posts.post.sub.vue b/src/client/app/desktop/views/components/posts.post.sub.vue
new file mode 100644
index 000000000..e85478578
--- /dev/null
+++ b/src/client/app/desktop/views/components/posts.post.sub.vue
@@ -0,0 +1,108 @@
+<template>
+<div class="sub" :title="title">
+	<router-link class="avatar-anchor" :to="note.user | userPage">
+		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
+	</router-link>
+	<div class="main">
+		<header>
+			<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
+			<span class="username">@{{ note.user | acct }}</span>
+			<router-link class="created-at" :to="note | notePage">
+				<mk-time :time="note.createdAt"/>
+			</router-link>
+		</header>
+		<div class="body">
+			<mk-sub-note-content class="text" :note="note"/>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import dateStringify from '../../../common/scripts/date-stringify';
+
+export default Vue.extend({
+	props: ['note'],
+	computed: {
+		title(): string {
+			return dateStringify(this.note.createdAt);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.sub
+	margin 0
+	padding 16px
+	font-size 0.9em
+
+	&: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 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%
+
+</style>
diff --git a/src/client/app/desktop/views/components/posts.post.vue b/src/client/app/desktop/views/components/posts.post.vue
new file mode 100644
index 000000000..322bf2922
--- /dev/null
+++ b/src/client/app/desktop/views/components/posts.post.vue
@@ -0,0 +1,585 @@
+<template>
+<div class="note" tabindex="-1" :title="title" @keydown="onKeydown">
+	<div class="reply-to" v-if="p.reply">
+		<x-sub :note="p.reply"/>
+	</div>
+	<div class="renote" v-if="isRenote">
+		<p>
+			<router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.userId">
+				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
+			</router-link>
+			%fa:retweet%
+			<span>{{ '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.indexOf('{')) }}</span>
+			<a class="name" :href="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</a>
+			<span>{{ '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-note.reposted-by%'.indexOf('}') + 1) }}</span>
+		</p>
+		<mk-time :time="note.createdAt"/>
+	</div>
+	<article>
+		<router-link class="avatar-anchor" :to="p.user | userPage">
+			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
+		</router-link>
+		<div class="main">
+			<header>
+				<router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link>
+				<span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span>
+				<span class="username">@{{ p.user | acct }}</span>
+				<div class="info">
+					<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
+					<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
+					<router-link class="created-at" :to="url">
+						<mk-time :time="p.createdAt"/>
+					</router-link>
+				</div>
+			</header>
+			<div class="body">
+				<p class="channel" v-if="p.channel">
+					<a :href="`${_CH_URL_}/${p.channel.id}`" target="_blank">{{ p.channel.title }}</a>:
+				</p>
+				<div class="text">
+					<a class="reply" v-if="p.reply">%fa:reply%</a>
+					<mk-note-html v-if="p.textHtml" :text="p.text" :i="os.i" :class="$style.text"/>
+					<a class="rp" v-if="p.renote">RP:</a>
+				</div>
+				<div class="media" v-if="p.media.length > 0">
+					<mk-media-list :media-list="p.media"/>
+				</div>
+				<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
+				<div class="tags" v-if="p.tags && p.tags.length > 0">
+					<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
+				</div>
+				<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
+				<div class="map" v-if="p.geo" ref="map"></div>
+				<div class="renote" v-if="p.renote">
+					<mk-note-preview :note="p.renote"/>
+				</div>
+				<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
+			</div>
+			<footer>
+				<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
+				<button @click="reply" title="%i18n:desktop.tags.mk-timeline-note.reply%">
+					%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
+				</button>
+				<button @click="renote" title="%i18n:desktop.tags.mk-timeline-note.renote%">
+					%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
+				</button>
+				<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:desktop.tags.mk-timeline-note.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 title="%i18n:desktop.tags.mk-timeline-note.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-note-status-graph width="462" height="130" :note="p"/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import dateStringify from '../../../common/scripts/date-stringify';
+import parse from '../../../../../text/parse';
+
+import MkPostFormWindow from './post-form-window.vue';
+import MkRenoteFormWindow from './renote-form-window.vue';
+import MkNoteMenu from '../../../common/views/components/note-menu.vue';
+import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
+import XSub from './notes.note.sub.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({
+	components: {
+		XSub
+	},
+
+	props: ['note'],
+
+	data() {
+		return {
+			isDetailOpened: false,
+			connection: null,
+			connectionId: null
+		};
+	},
+
+	computed: {
+		isRenote(): boolean {
+			return (this.note.renote &&
+				this.note.text == null &&
+				this.note.mediaIds.length == 0 &&
+				this.note.poll == null);
+		},
+		p(): any {
+			return this.isRenote ? this.note.renote : this.note;
+		},
+		reactionsCount(): number {
+			return this.p.reactionCounts
+				? Object.keys(this.p.reactionCounts)
+					.map(key => this.p.reactionCounts[key])
+					.reduce((a, b) => a + b)
+				: 0;
+		},
+		title(): string {
+			return dateStringify(this.p.createdAt);
+		},
+		urls(): string[] {
+			if (this.p.text) {
+				const ast = parse(this.p.text);
+				return ast
+					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
+					.map(t => t.url);
+			} else {
+				return null;
+			}
+		}
+	},
+
+	created() {
+		if ((this as any).os.isSignedIn) {
+			this.connection = (this as any).os.stream.getConnection();
+			this.connectionId = (this as any).os.stream.use();
+		}
+	},
+
+	mounted() {
+		this.capture(true);
+
+		if ((this as any).os.isSignedIn) {
+			this.connection.on('_connected_', this.onStreamConnected);
+		}
+
+		// Draw map
+		if (this.p.geo) {
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
+			if (shouldShowMap) {
+				(this as any).os.getGoogleMaps().then(maps => {
+					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
+					const map = new maps.Map(this.$refs.map, {
+						center: uluru,
+						zoom: 15
+					});
+					new maps.Marker({
+						position: uluru,
+						map: map
+					});
+				});
+			}
+		}
+	},
+
+	beforeDestroy() {
+		this.decapture(true);
+
+		if ((this as any).os.isSignedIn) {
+			this.connection.off('_connected_', this.onStreamConnected);
+			(this as any).os.stream.dispose(this.connectionId);
+		}
+	},
+
+	methods: {
+		capture(withHandler = false) {
+			if ((this as any).os.isSignedIn) {
+				this.connection.send({
+					type: 'capture',
+					id: this.p.id
+				});
+				if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
+			}
+		},
+		decapture(withHandler = false) {
+			if ((this as any).os.isSignedIn) {
+				this.connection.send({
+					type: 'decapture',
+					id: this.p.id
+				});
+				if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated);
+			}
+		},
+		onStreamConnected() {
+			this.capture();
+		},
+		onStreamNoteUpdated(data) {
+			const note = data.note;
+			if (note.id == this.note.id) {
+				this.$emit('update:note', note);
+			} else if (note.id == this.note.renoteId) {
+				this.note.renote = note;
+			}
+		},
+		reply() {
+			(this as any).os.new(MkPostFormWindow, {
+				reply: this.p
+			});
+		},
+		renote() {
+			(this as any).os.new(MkRenoteFormWindow, {
+				note: this.p
+			});
+		},
+		react() {
+			(this as any).os.new(MkReactionPicker, {
+				source: this.$refs.reactButton,
+				note: this.p
+			});
+		},
+		menu() {
+			(this as any).os.new(MkNoteMenu, {
+				source: this.$refs.menuButton,
+				note: this.p
+			});
+		},
+		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.renote();
+					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>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+.note
+	margin 0
+	padding 0
+	background #fff
+	border-bottom solid 1px #eaeaea
+
+	&:first-child
+		border-top-left-radius 6px
+		border-top-right-radius 6px
+
+		> .renote
+			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
+
+	> .renote
+		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-note-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
+				align-items center
+				margin-bottom 4px
+				white-space nowrap
+
+				> .name
+					display block
+					margin 0 .5em 0 0
+					padding 0
+					overflow hidden
+					color #627079
+					font-size 1em
+					font-weight bold
+					text-decoration none
+					text-overflow ellipsis
+
+					&:hover
+						text-decoration underline
+
+				> .is-bot
+					margin 0 .5em 0 0
+					padding 1px 6px
+					font-size 12px
+					color #aaa
+					border solid 1px #ddd
+					border-radius 3px
+
+				> .username
+					margin 0 .5em 0 0
+					color #ccc
+
+				> .info
+					margin-left auto
+					font-size 0.9em
+
+					> .mobile
+						margin-right 8px
+						color #ccc
+
+					> .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
+
+					>>> .quote
+						margin 8px
+						padding 6px 12px
+						color #aaa
+						border-left solid 3px #eee
+
+					> .reply
+						margin-right 8px
+						color #717171
+
+					> .rp
+						margin-left 4px
+						font-style oblique
+						color #a0bf46
+
+				> .location
+					margin 4px 0
+					font-size 12px
+					color #ccc
+
+				> .map
+					width 100%
+					height 300px
+
+					&:empty
+						display none
+
+				> .tags
+					margin 4px 0 0 0
+
+					> *
+						display inline-block
+						margin 0 8px 0 0
+						padding 2px 8px 2px 16px
+						font-size 90%
+						color #8d969e
+						background #edf0f3
+						border-radius 4px
+
+						&:before
+							content ""
+							display block
+							position absolute
+							top 0
+							bottom 0
+							left 4px
+							width 8px
+							height 8px
+							margin auto 0
+							background #fff
+							border-radius 100%
+
+						&:hover
+							text-decoration none
+							background #e2e7ec
+
+				.mk-url-preview
+					margin-top 8px
+
+				> .channel
+					margin 0
+
+				> .mk-poll
+					font-size 80%
+
+				> .renote
+					margin 8px 0
+
+					> .mk-note-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>
+
+<style lang="stylus" module>
+.text
+
+	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
+</style>
diff --git a/src/client/app/desktop/views/components/settings.mute.vue b/src/client/app/desktop/views/components/settings.mute.vue
index 6bdc76653..94492ad26 100644
--- a/src/client/app/desktop/views/components/settings.mute.vue
+++ b/src/client/app/desktop/views/components/settings.mute.vue
@@ -5,7 +5,7 @@
 	</div>
 	<div class="users" v-if="users.length != 0">
 		<div v-for="user in users" :key="user.id">
-			<p><b>{{ getUserName(user) }}</b> @{{ getAcct(user) }}</p>
+			<p><b>{{ user | userName }}</b> @{{ user | acct }}</p>
 		</div>
 	</div>
 </div>
@@ -13,8 +13,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -23,10 +21,6 @@ export default Vue.extend({
 			users: []
 		};
 	},
-	methods: {
-		getAcct,
-		getUserName
-	},
 	mounted() {
 		(this as any).api('mute/list').then(x => {
 			this.users = x.users;
diff --git a/src/client/app/desktop/views/components/ui.header.vue b/src/client/app/desktop/views/components/ui.header.vue
index 527d10843..2b63030cd 100644
--- a/src/client/app/desktop/views/components/ui.header.vue
+++ b/src/client/app/desktop/views/components/ui.header.vue
@@ -4,7 +4,7 @@
 	<div class="main" ref="main">
 		<div class="backdrop"></div>
 		<div class="main">
-			<p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ name }}</b>さん</p>
+			<p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ os.i | userName }}</b>さん</p>
 			<div class="container" ref="mainContainer">
 				<div class="left">
 					<x-nav/>
@@ -33,14 +33,7 @@ import XNotifications from './ui.header.notifications.vue';
 import XPost from './ui.header.post.vue';
 import XClock from './ui.header.clock.vue';
 
-import getUserName from '../../../../../renderers/get-user-name';
-
 export default Vue.extend({
-	computed: {
-		name(): string {
-			return getUserName((this as any).os.i);
-		}
-	},
 	components: {
 		XNav,
 		XSearch,
diff --git a/src/client/app/desktop/views/components/user-preview.vue b/src/client/app/desktop/views/components/user-preview.vue
index 1cc53743a..24337eea2 100644
--- a/src/client/app/desktop/views/components/user-preview.vue
+++ b/src/client/app/desktop/views/components/user-preview.vue
@@ -2,12 +2,12 @@
 <div class="mk-user-preview">
 	<template v-if="u != null">
 		<div class="banner" :style="u.bannerUrl ? `background-image: url(${u.bannerUrl}?thumbnail&size=512)` : ''"></div>
-		<router-link class="avatar" :to="`/@${getAcct(u)}`">
+		<router-link class="avatar" :to="u | userPage">
 			<img :src="`${u.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="title">
-			<router-link class="name" :to="`/@${getAcct(u)}`">{{ u.name }}</router-link>
-			<p class="username">@{{ getAcct(u) }}</p>
+			<router-link class="name" :to="u | userPage">{{ u.name }}</router-link>
+			<p class="username">@{{ u | acct }}</p>
 		</div>
 		<div class="description">{{ u.description }}</div>
 		<div class="status">
diff --git a/src/client/app/desktop/views/components/users-list.item.vue b/src/client/app/desktop/views/components/users-list.item.vue
index c7a132ecf..005c9cd6d 100644
--- a/src/client/app/desktop/views/components/users-list.item.vue
+++ b/src/client/app/desktop/views/components/users-list.item.vue
@@ -1,12 +1,12 @@
 <template>
 <div class="root item">
-	<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="user.id">
+	<router-link class="avatar-anchor" :to="user | userPage" v-user-preview="user.id">
 		<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`" v-user-preview="user.id">{{ name }}</router-link>
-			<span class="username">@{{ acct }}</span>
+			<router-link class="name" :to="user | userPage" v-user-preview="user.id">{{ user | userName }}</router-link>
+			<span class="username">@{{ user | acct }}</span>
 		</header>
 		<div class="body">
 			<p class="followed" v-if="user.isFollowed">フォローされています</p>
@@ -19,19 +19,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
-	props: ['user'],
-	computed: {
-		acct() {
-			return getAcct(this.user);
-		},
-		name() {
-			return getUserName(this.user);
-		}
-	}
+	props: ['user']
 });
 </script>
 
diff --git a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
index 351b1264f..4113ef13a 100644
--- a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
+++ b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
@@ -3,8 +3,8 @@
 	<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="`/@${getAcct(user)}`" :key="user.id">
-		<img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="getUserName(user)" v-user-preview="user.id"/>
+	<router-link v-for="user in users" :to="user | userPage" :key="user.id">
+		<img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="user | userName" v-user-preview="user.id"/>
 	</router-link>
 	</div>
 	<p class="empty" v-if="!fetching && users.length == 0">%i18n:desktop.tags.mk-user.followers-you-know.no-users%</p>
@@ -13,8 +13,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../../acct/render';
-import getUserName from '../../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['user'],
@@ -24,10 +22,6 @@ export default Vue.extend({
 			fetching: true
 		};
 	},
-	methods: {
-		getAcct,
-		getUserName
-	},
 	mounted() {
 		(this as any).api('users/followers', {
 			userId: this.user.id,
diff --git a/src/client/app/desktop/views/pages/user/user.friends.vue b/src/client/app/desktop/views/pages/user/user.friends.vue
index c9213cb50..8512e8027 100644
--- a/src/client/app/desktop/views/pages/user/user.friends.vue
+++ b/src/client/app/desktop/views/pages/user/user.friends.vue
@@ -4,12 +4,12 @@
 	<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="`/@${getAcct(friend)}`">
+			<router-link class="avatar-anchor" :to="friend | userPage">
 				<img class="avatar" :src="`${friend.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="friend.id"/>
 			</router-link>
 			<div class="body">
-				<router-link class="name" :to="`/@${getAcct(friend)}`" v-user-preview="friend.id">{{ friend.name }}</router-link>
-				<p class="username">@{{ getAcct(friend) }}</p>
+				<router-link class="name" :to="friend | userPage" v-user-preview="friend.id">{{ friend.name }}</router-link>
+				<p class="username">@{{ friend | acct }}</p>
 			</div>
 			<mk-follow-button :user="friend"/>
 		</div>
diff --git a/src/client/app/desktop/views/pages/user/user.header.vue b/src/client/app/desktop/views/pages/user/user.header.vue
index f67bf5da2..67e52d173 100644
--- a/src/client/app/desktop/views/pages/user/user.header.vue
+++ b/src/client/app/desktop/views/pages/user/user.header.vue
@@ -7,8 +7,8 @@
 	<div class="container">
 		<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=150`" alt="avatar"/>
 		<div class="title">
-			<p class="name">{{ name }}</p>
-			<p class="username">@{{ acct }}</p>
+			<p class="name">{{ user | userName }}</p>
+			<p class="username">@{{ user | acct }}</p>
 			<p class="location" v-if="user.host === null && user.profile.location">%fa:map-marker%{{ user.profile.location }}</p>
 		</div>
 		<footer>
@@ -22,19 +22,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../../acct/render';
-import getUserName from '../../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['user'],
-	computed: {
-		acct() {
-			return getAcct(this.user);
-		},
-		name() {
-			return getUserName(this.user);
-		}
-	},
 	mounted() {
 		window.addEventListener('load', this.onScroll);
 		window.addEventListener('scroll', this.onScroll);
diff --git a/src/client/app/desktop/views/pages/user/user.vue b/src/client/app/desktop/views/pages/user/user.vue
index d07b462b5..3644286fb 100644
--- a/src/client/app/desktop/views/pages/user/user.vue
+++ b/src/client/app/desktop/views/pages/user/user.vue
@@ -45,7 +45,7 @@ export default Vue.extend({
 				this.user = user;
 				this.fetching = false;
 				Progress.done();
-				document.title = getUserName(user) + ' | Misskey';
+				document.title = getUserName(this.user) + ' | Misskey';
 			});
 		}
 	}
diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue
index 41b015b8a..bc6ebae77 100644
--- a/src/client/app/desktop/views/pages/welcome.vue
+++ b/src/client/app/desktop/views/pages/welcome.vue
@@ -8,7 +8,7 @@
 					<p>ようこそ! <b>Misskey</b>はTwitter風ミニブログSNSです。思ったことや皆と共有したいことを投稿しましょう。タイムラインを見れば、皆の関心事をすぐにチェックすることもできます。<a :href="aboutUrl">詳しく...</a></p>
 					<p><button class="signup" @click="signup">はじめる</button><button class="signin" @click="signin">ログイン</button></p>
 					<div class="users">
-						<router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="`/@${getAcct(user)}`" v-user-preview="user.id">
+						<router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="user | userPage" v-user-preview="user.id">
 							<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 						</router-link>
 					</div>
diff --git a/src/client/app/desktop/views/widgets/channel.channel.note.vue b/src/client/app/desktop/views/widgets/channel.channel.note.vue
index 313a2e3f4..776791906 100644
--- a/src/client/app/desktop/views/widgets/channel.channel.note.vue
+++ b/src/client/app/desktop/views/widgets/channel.channel.note.vue
@@ -2,8 +2,8 @@
 <div class="note">
 	<header>
 		<a class="index" @click="reply">{{ note.index }}:</a>
-		<router-link class="name" :to="`/@${acct}`" v-user-preview="note.user.id"><b>{{ name }}</b></router-link>
-		<span>ID:<i>{{ acct }}</i></span>
+		<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id"><b>{{ note.user | userName }}</b></router-link>
+		<span>ID:<i>{{ note.user | acct }}</i></span>
 	</header>
 	<div>
 		<a v-if="note.reply">&gt;&gt;{{ note.reply.index }}</a>
@@ -19,19 +19,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['note'],
-	computed: {
-		acct() {
-			return getAcct(this.note.user);
-		},
-		name() {
-			return getUserName(this.note.user);
-		}
-	},
 	methods: {
 		reply() {
 			this.$emit('reply', this.note);
diff --git a/src/client/app/desktop/views/widgets/channel.channel.post.vue b/src/client/app/desktop/views/widgets/channel.channel.post.vue
new file mode 100644
index 000000000..776791906
--- /dev/null
+++ b/src/client/app/desktop/views/widgets/channel.channel.post.vue
@@ -0,0 +1,65 @@
+<template>
+<div class="note">
+	<header>
+		<a class="index" @click="reply">{{ note.index }}:</a>
+		<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id"><b>{{ note.user | userName }}</b></router-link>
+		<span>ID:<i>{{ note.user | acct }}</i></span>
+	</header>
+	<div>
+		<a v-if="note.reply">&gt;&gt;{{ note.reply.index }}</a>
+		{{ note.text }}
+		<div class="media" v-if="note.media">
+			<a v-for="file in note.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: ['note'],
+	methods: {
+		reply() {
+			this.$emit('reply', this.note);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.note
+	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/client/app/desktop/views/widgets/profile.vue b/src/client/app/desktop/views/widgets/profile.vue
index 98e42222e..1b4b11de3 100644
--- a/src/client/app/desktop/views/widgets/profile.vue
+++ b/src/client/app/desktop/views/widgets/profile.vue
@@ -15,14 +15,13 @@
 		title="クリックでアバター編集"
 		v-user-preview="os.i.id"
 	/>
-	<router-link class="name" :to="`/@${os.i.username}`">{{ name }}</router-link>
-	<p class="username">@{{ os.i.username }}</p>
+	<router-link class="name" :to="os.i | userPage">{{ os.i | userName }}</router-link>
+	<p class="username">@{{ os.i | acct }}</p>
 </div>
 </template>
 
 <script lang="ts">
 import define from '../../../common/define-widget';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default define({
 	name: 'profile',
@@ -30,11 +29,6 @@ export default define({
 		design: 0
 	})
 }).extend({
-	computed: {
-		name() {
-			return getUserName(this.os.i);
-		}
-	},
 	methods: {
 		func() {
 			if (this.props.design == 2) {
diff --git a/src/client/app/desktop/views/widgets/users.vue b/src/client/app/desktop/views/widgets/users.vue
index a5dabb68f..c7075d9a5 100644
--- a/src/client/app/desktop/views/widgets/users.vue
+++ b/src/client/app/desktop/views/widgets/users.vue
@@ -7,12 +7,12 @@
 	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 	<template v-else-if="users.length != 0">
 		<div class="user" v-for="_user in users">
-			<router-link class="avatar-anchor" :to="`/@${getAcct(_user)}`">
+			<router-link class="avatar-anchor" :to="_user | userPage">
 				<img class="avatar" :src="`${_user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="_user.id"/>
 			</router-link>
 			<div class="body">
-				<router-link class="name" :to="`/@${getAcct(_user)}`" v-user-preview="_user.id">{{ getUserName(_user) }}</router-link>
-				<p class="username">@{{ getAcct(_user) }}</p>
+				<router-link class="name" :to="_user | userPage" v-user-preview="_user.id">{{ _user | userName }}</router-link>
+				<p class="username">@{{ _user | acct }}</p>
 			</div>
 			<mk-follow-button :user="_user"/>
 		</div>
@@ -23,8 +23,6 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 const limit = 3;
 
@@ -45,8 +43,6 @@ export default define({
 		this.fetch();
 	},
 	methods: {
-		getAcct,
-		getUserName,
 		func() {
 			this.props.compact = !this.props.compact;
 		},
diff --git a/src/client/app/mobile/views/components/note-card.vue b/src/client/app/mobile/views/components/note-card.vue
index 9ad0d3e29..393fa9b83 100644
--- a/src/client/app/mobile/views/components/note-card.vue
+++ b/src/client/app/mobile/views/components/note-card.vue
@@ -1,8 +1,8 @@
 <template>
 <div class="mk-note-card">
-	<a :href="`/@${acct}/${note.id}`">
+	<a :href="note | notePage">
 		<header>
-			<img :src="`${acct}?thumbnail&size=64`" alt="avatar"/><h3>{{ name }}</h3>
+			<img :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/><h3>{{ note.user | userName }}</h3>
 		</header>
 		<div>
 			{{ text }}
@@ -15,18 +15,10 @@
 <script lang="ts">
 import Vue from 'vue';
 import summary from '../../../../../renderers/get-note-summary';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['note'],
 	computed: {
-		acct() {
-			return getAcct(this.note.user);
-		},
-		name() {
-			return getUserName(this.note.user);
-		},
 		text(): string {
 			return summary(this.note);
 		}
diff --git a/src/client/app/mobile/views/components/note-detail.sub.vue b/src/client/app/mobile/views/components/note-detail.sub.vue
index 38aea4ba2..06f442d30 100644
--- a/src/client/app/mobile/views/components/note-detail.sub.vue
+++ b/src/client/app/mobile/views/components/note-detail.sub.vue
@@ -1,13 +1,13 @@
 <template>
 <div class="root sub">
-	<router-link class="avatar-anchor" :to="`/@${acct}`">
+	<router-link class="avatar-anchor" :to="note.user | userPage">
 		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`">{{ getUserName(note.user) }}</router-link>
-			<span class="username">@{{ acct }}</span>
-			<router-link class="time" :to="`/@${acct}/${note.id}`">
+			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
+			<span class="username">@{{ note.user | acct }}</span>
+			<router-link class="time" :to="note | notePage">
 				<mk-time :time="note.createdAt"/>
 			</router-link>
 		</header>
@@ -20,19 +20,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
-	props: ['note'],
-	computed: {
-		acct() {
-			return getAcct(this.note.user);
-		},
-		name() {
-			return getUserName(this.note.user);
-		}
-	}
+	props: ['note']
 });
 </script>
 
diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue
index 483f5aaf3..de32f0a74 100644
--- a/src/client/app/mobile/views/components/note-detail.vue
+++ b/src/client/app/mobile/views/components/note-detail.vue
@@ -17,24 +17,20 @@
 	</div>
 	<div class="renote" v-if="isRenote">
 		<p>
-			<router-link class="avatar-anchor" :to="`/@${acct}`">
+			<router-link class="avatar-anchor" :to="note.user | userPage">
 				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
 			</router-link>
-			%fa:retweet%
-			<router-link class="name" :to="`/@${acct}`">
-				{{ name }}
-			</router-link>
-			がRenote
+			%fa:retweet%<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>がRenote
 		</p>
 	</div>
 	<article>
 		<header>
-			<router-link class="avatar-anchor" :to="`/@${pAcct}`">
+			<router-link class="avatar-anchor" :to="p.user | userPage">
 				<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 			</router-link>
 			<div>
-				<router-link class="name" :to="`/@${pAcct}`">{{ pName }}</router-link>
-				<span class="username">@{{ pAcct }}</span>
+				<router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link>
+				<span class="username">@{{ p.user | acct }}</span>
 			</div>
 		</header>
 		<div class="body">
@@ -80,8 +76,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 import parse from '../../../../../text/parse';
 
 import MkNoteMenu from '../../../common/views/components/note-menu.vue';
@@ -112,18 +106,6 @@ export default Vue.extend({
 	},
 
 	computed: {
-		acct(): string {
-			return getAcct(this.note.user);
-		},
-		name(): string {
-			return getUserName(this.note.user);
-		},
-		pAcct(): string {
-			return getAcct(this.p.user);
-		},
-		pName(): string {
-			return getUserName(this.p.user);
-		},
 		isRenote(): boolean {
 			return (this.note.renote &&
 				this.note.text == null &&
diff --git a/src/client/app/mobile/views/components/note-preview.vue b/src/client/app/mobile/views/components/note-preview.vue
index 8c8d8645b..b9a6db315 100644
--- a/src/client/app/mobile/views/components/note-preview.vue
+++ b/src/client/app/mobile/views/components/note-preview.vue
@@ -1,13 +1,13 @@
 <template>
 <div class="mk-note-preview">
-	<router-link class="avatar-anchor" :to="`/@${acct}`">
+	<router-link class="avatar-anchor" :to="note.user | userPage">
 		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`">{{ name }}</router-link>
-			<span class="username">@{{ acct }}</span>
-			<router-link class="time" :to="`/@${acct}/${note.id}`">
+			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
+			<span class="username">@{{ note.user | acct }}</span>
+			<router-link class="time" :to="note | notePage">
 				<mk-time :time="note.createdAt"/>
 			</router-link>
 		</header>
@@ -20,19 +20,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
-	props: ['note'],
-	computed: {
-		acct() {
-			return getAcct(this.note.user);
-		},
-		name() {
-			return getUserName(this.note.user);
-		}
-	}
+	props: ['note']
 });
 </script>
 
diff --git a/src/client/app/mobile/views/components/note.sub.vue b/src/client/app/mobile/views/components/note.sub.vue
index 96f8265cc..d489f3a05 100644
--- a/src/client/app/mobile/views/components/note.sub.vue
+++ b/src/client/app/mobile/views/components/note.sub.vue
@@ -1,13 +1,13 @@
 <template>
 <div class="sub">
-	<router-link class="avatar-anchor" :to="`/@${getAcct(note.user)}`">
+	<router-link class="avatar-anchor" :to="note.user | userPage">
 		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${getAcct(note.user)}`">{{ getUserName(note.user) }}</router-link>
-			<span class="username">@{{ getAcct(note.user) }}</span>
-			<router-link class="created-at" :to="`/@${getAcct(note.user)}/${note.id}`">
+			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
+			<span class="username">@{{ note.user | acct }}</span>
+			<router-link class="created-at" :to="note | notePage">
 				<mk-time :time="note.createdAt"/>
 			</router-link>
 		</header>
@@ -20,17 +20,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
-	props: ['note'],
-	data() {
-		return {
-			getAcct,
-			getUserName
-		};
-	}
+	props: ['note']
 });
 </script>
 
diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue
index 295fe4d6a..033de4f42 100644
--- a/src/client/app/mobile/views/components/note.vue
+++ b/src/client/app/mobile/views/components/note.vue
@@ -5,28 +5,28 @@
 	</div>
 	<div class="renote" v-if="isRenote">
 		<p>
-			<router-link class="avatar-anchor" :to="`/@${getAcct(note.user)}`">
+			<router-link class="avatar-anchor" :to="note.user | userPage">
 				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 			</router-link>
 			%fa:retweet%
 			<span>{{ '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.substr(0, '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.indexOf('{')) }}</span>
-			<router-link class="name" :to="`/@${getAcct(note.user)}`">{{ getUserName(note.user) }}</router-link>
+			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
 			<span>{{ '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.substr('%i18n:mobile.tags.mk-timeline-note.reposted-by%'.indexOf('}') + 1) }}</span>
 		</p>
 		<mk-time :time="note.createdAt"/>
 	</div>
 	<article>
-		<router-link class="avatar-anchor" :to="`/@${getAcct(p.user)}`">
+		<router-link class="avatar-anchor" :to="p.user | userPage">
 			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
 		</router-link>
 		<div class="main">
 			<header>
-				<router-link class="name" :to="`/@${getAcct(p.user)}`">{{ getUserName(p.user) }}</router-link>
+				<router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link>
 				<span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span>
-				<span class="username">@{{ getAcct(p.user) }}</span>
+				<span class="username">@{{ p.user | acct }}</span>
 				<div class="info">
 					<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
-					<router-link class="created-at" :to="url">
+					<router-link class="created-at" :to="p | notePage">
 						<mk-time :time="p.createdAt"/>
 					</router-link>
 				</div>
@@ -77,8 +77,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 import parse from '../../../../../text/parse';
 
 import MkNoteMenu from '../../../common/views/components/note-menu.vue';
@@ -95,9 +93,7 @@ export default Vue.extend({
 	data() {
 		return {
 			connection: null,
-			connectionId: null,
-			getAcct,
-			getUserName
+			connectionId: null
 		};
 	},
 
@@ -118,9 +114,6 @@ export default Vue.extend({
 					.reduce((a, b) => a + b)
 				: 0;
 		},
-		url(): string {
-			return `/@${this.pAcct}/${this.p.id}`;
-		},
 		urls(): string[] {
 			if (this.p.text) {
 				const ast = parse(this.p.text);
diff --git a/src/client/app/mobile/views/components/notification-preview.vue b/src/client/app/mobile/views/components/notification-preview.vue
index f0921f91d..d39b2fbf9 100644
--- a/src/client/app/mobile/views/components/notification-preview.vue
+++ b/src/client/app/mobile/views/components/notification-preview.vue
@@ -3,7 +3,7 @@
 	<template v-if="notification.type == 'reaction'">
 		<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p><mk-reaction-icon :reaction="notification.reaction"/>{{ getUserName(notification.user) }}</p>
+			<p><mk-reaction-icon :reaction="notification.reaction"/>{{ notification.user | userName }}</p>
 			<p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%</p>
 		</div>
 	</template>
@@ -11,7 +11,7 @@
 	<template v-if="notification.type == 'renote'">
 		<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:retweet%{{ getUserName(notification.note.user) }}</p>
+			<p>%fa:retweet%{{ notification.note.user | userName }}</p>
 			<p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right%</p>
 		</div>
 	</template>
@@ -19,7 +19,7 @@
 	<template v-if="notification.type == 'quote'">
 		<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:quote-left%{{ getUserName(notification.note.user) }}</p>
+			<p>%fa:quote-left%{{ notification.note.user | userName }}</p>
 			<p class="note-preview">{{ getNoteSummary(notification.note) }}</p>
 		</div>
 	</template>
@@ -27,14 +27,14 @@
 	<template v-if="notification.type == 'follow'">
 		<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:user-plus%{{ getUserName(notification.user) }}</p>
+			<p>%fa:user-plus%{{ notification.user | userName }}</p>
 		</div>
 	</template>
 
 	<template v-if="notification.type == 'reply'">
 		<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:reply%{{ getUserName(notification.note.user) }}</p>
+			<p>%fa:reply%{{ notification.note.user | userName }}</p>
 			<p class="note-preview">{{ getNoteSummary(notification.note) }}</p>
 		</div>
 	</template>
@@ -42,7 +42,7 @@
 	<template v-if="notification.type == 'mention'">
 		<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:at%{{ getUserName(notification.note.user) }}</p>
+			<p>%fa:at%{{ notification.note.user | userName }}</p>
 			<p class="note-preview">{{ getNoteSummary(notification.note) }}</p>
 		</div>
 	</template>
@@ -50,7 +50,7 @@
 	<template v-if="notification.type == 'poll_vote'">
 		<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:chart-pie%{{ getUserName(notification.user) }}</p>
+			<p>%fa:chart-pie%{{ notification.user | userName }}</p>
 			<p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%</p>
 		</div>
 	</template>
@@ -60,14 +60,12 @@
 <script lang="ts">
 import Vue from 'vue';
 import getNoteSummary from '../../../../../renderers/get-note-summary';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['notification'],
 	data() {
 		return {
-			getNoteSummary,
-			getUserName
+			getNoteSummary
 		};
 	}
 });
diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue
index 4c98e1990..5456c2c17 100644
--- a/src/client/app/mobile/views/components/notification.vue
+++ b/src/client/app/mobile/views/components/notification.vue
@@ -2,13 +2,13 @@
 <div class="mk-notification">
 	<div class="notification reaction" v-if="notification.type == 'reaction'">
 		<mk-time :time="notification.createdAt"/>
-		<router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`">
+		<router-link class="avatar-anchor" :to="notification.user | userPage">
 			<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="text">
 			<p>
 				<mk-reaction-icon :reaction="notification.reaction"/>
-				<router-link :to="`/@${getAcct(notification.user)}`">{{ getUserName(notification.user) }}</router-link>
+				<router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link>
 			</p>
 			<router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">
 				%fa:quote-left%{{ getNoteSummary(notification.note) }}
@@ -19,13 +19,13 @@
 
 	<div class="notification renote" v-if="notification.type == 'renote'">
 		<mk-time :time="notification.createdAt"/>
-		<router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`">
+		<router-link class="avatar-anchor" :to="notification.user | userPage">
 			<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="text">
 			<p>
 				%fa:retweet%
-				<router-link :to="`/@${getAcct(notification.user)}`">{{ getUserName(notification.user) }}</router-link>
+				<router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link>
 			</p>
 			<router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">
 				%fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right%
@@ -39,13 +39,13 @@
 
 	<div class="notification follow" v-if="notification.type == 'follow'">
 		<mk-time :time="notification.createdAt"/>
-		<router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`">
+		<router-link class="avatar-anchor" :to="notification.user | userPage">
 			<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="text">
 			<p>
 				%fa:user-plus%
-				<router-link :to="`/@${getAcct(notification.user)}`">{{ getUserName(notification.user) }}</router-link>
+				<router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link>
 			</p>
 		</div>
 	</div>
@@ -60,13 +60,13 @@
 
 	<div class="notification poll_vote" v-if="notification.type == 'poll_vote'">
 		<mk-time :time="notification.createdAt"/>
-		<router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`">
+		<router-link class="avatar-anchor" :to="notification.user | userPage">
 			<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="text">
 			<p>
 				%fa:chart-pie%
-				<router-link :to="`/@${getAcct(notification.user)}`">{{ getUserName(notification.user) }}</router-link>
+				<router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link>
 			</p>
 			<router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">
 				%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%
@@ -79,16 +79,12 @@
 <script lang="ts">
 import Vue from 'vue';
 import getNoteSummary from '../../../../../renderers/get-note-summary';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['notification'],
 	data() {
 		return {
-			getNoteSummary,
-			getAcct,
-			getUserName
+			getNoteSummary
 		};
 	}
 });
diff --git a/src/client/app/mobile/views/components/post-card.vue b/src/client/app/mobile/views/components/post-card.vue
new file mode 100644
index 000000000..393fa9b83
--- /dev/null
+++ b/src/client/app/mobile/views/components/post-card.vue
@@ -0,0 +1,85 @@
+<template>
+<div class="mk-note-card">
+	<a :href="note | notePage">
+		<header>
+			<img :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/><h3>{{ note.user | userName }}</h3>
+		</header>
+		<div>
+			{{ text }}
+		</div>
+		<mk-time :time="note.createdAt"/>
+	</a>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import summary from '../../../../../renderers/get-note-summary';
+
+export default Vue.extend({
+	props: ['note'],
+	computed: {
+		text(): string {
+			return summary(this.note);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-note-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/client/app/mobile/views/components/post-detail.sub.vue b/src/client/app/mobile/views/components/post-detail.sub.vue
new file mode 100644
index 000000000..06f442d30
--- /dev/null
+++ b/src/client/app/mobile/views/components/post-detail.sub.vue
@@ -0,0 +1,103 @@
+<template>
+<div class="root sub">
+	<router-link class="avatar-anchor" :to="note.user | userPage">
+		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+	</router-link>
+	<div class="main">
+		<header>
+			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
+			<span class="username">@{{ note.user | acct }}</span>
+			<router-link class="time" :to="note | notePage">
+				<mk-time :time="note.createdAt"/>
+			</router-link>
+		</header>
+		<div class="body">
+			<mk-sub-note-content class="text" :note="note"/>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: ['note']
+});
+</script>
+
+<style lang="stylus" scoped>
+.root.sub
+	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/client/app/mobile/views/components/post-detail.vue b/src/client/app/mobile/views/components/post-detail.vue
new file mode 100644
index 000000000..de32f0a74
--- /dev/null
+++ b/src/client/app/mobile/views/components/post-detail.vue
@@ -0,0 +1,444 @@
+<template>
+<div class="mk-note-detail">
+	<button
+		class="more"
+		v-if="p.reply && p.reply.replyId && 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">
+		<x-sub v-for="note in context" :key="note.id" :note="note"/>
+	</div>
+	<div class="reply-to" v-if="p.reply">
+		<x-sub :note="p.reply"/>
+	</div>
+	<div class="renote" v-if="isRenote">
+		<p>
+			<router-link class="avatar-anchor" :to="note.user | userPage">
+				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
+			</router-link>
+			%fa:retweet%<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>がRenote
+		</p>
+	</div>
+	<article>
+		<header>
+			<router-link class="avatar-anchor" :to="p.user | userPage">
+				<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+			</router-link>
+			<div>
+				<router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link>
+				<span class="username">@{{ p.user | acct }}</span>
+			</div>
+		</header>
+		<div class="body">
+			<mk-note-html v-if="p.text" :ast="p.text" :i="os.i" :class="$style.text"/>
+			<div class="tags" v-if="p.tags && p.tags.length > 0">
+				<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
+			</div>
+			<div class="media" v-if="p.media.length > 0">
+				<mk-media-list :media-list="p.media"/>
+			</div>
+			<mk-poll v-if="p.poll" :note="p"/>
+			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
+			<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
+			<div class="map" v-if="p.geo" ref="map"></div>
+			<div class="renote" v-if="p.renote">
+				<mk-note-preview :note="p.renote"/>
+			</div>
+		</div>
+		<router-link class="time" :to="`/@${pAcct}/${p.id}`">
+			<mk-time :time="p.createdAt" mode="detail"/>
+		</router-link>
+		<footer>
+			<mk-reactions-viewer :note="p"/>
+			<button @click="reply" title="%i18n:mobile.tags.mk-note-detail.reply%">
+				%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
+			</button>
+			<button @click="renote" title="Renote">
+				%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
+			</button>
+			<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:mobile.tags.mk-note-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">
+		<x-sub v-for="note in replies" :key="note.id" :note="note"/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import parse from '../../../../../text/parse';
+
+import MkNoteMenu from '../../../common/views/components/note-menu.vue';
+import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
+import XSub from './note-detail.sub.vue';
+
+export default Vue.extend({
+	components: {
+		XSub
+	},
+
+	props: {
+		note: {
+			type: Object,
+			required: true
+		},
+		compact: {
+			default: false
+		}
+	},
+
+	data() {
+		return {
+			context: [],
+			contextFetching: false,
+			replies: []
+		};
+	},
+
+	computed: {
+		isRenote(): boolean {
+			return (this.note.renote &&
+				this.note.text == null &&
+				this.note.mediaIds.length == 0 &&
+				this.note.poll == null);
+		},
+		p(): any {
+			return this.isRenote ? this.note.renote : this.note;
+		},
+		reactionsCount(): number {
+			return this.p.reactionCounts
+				? Object.keys(this.p.reactionCounts)
+					.map(key => this.p.reactionCounts[key])
+					.reduce((a, b) => a + b)
+				: 0;
+		},
+		urls(): string[] {
+			if (this.p.text) {
+				const ast = parse(this.p.text);
+				return 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 as any).api('notes/replies', {
+				noteId: this.p.id,
+				limit: 8
+			}).then(replies => {
+				this.replies = replies;
+			});
+		}
+
+		// Draw map
+		if (this.p.geo) {
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
+			if (shouldShowMap) {
+				(this as any).os.getGoogleMaps().then(maps => {
+					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
+					const map = new maps.Map(this.$refs.map, {
+						center: uluru,
+						zoom: 15
+					});
+					new maps.Marker({
+						position: uluru,
+						map: map
+					});
+				});
+			}
+		}
+	},
+
+	methods: {
+		fetchContext() {
+			this.contextFetching = true;
+
+			// Fetch context
+			(this as any).api('notes/context', {
+				noteId: this.p.replyId
+			}).then(context => {
+				this.contextFetching = false;
+				this.context = context.reverse();
+			});
+		},
+		reply() {
+			(this as any).apis.post({
+				reply: this.p
+			});
+		},
+		renote() {
+			(this as any).apis.post({
+				renote: this.p
+			});
+		},
+		react() {
+			(this as any).os.new(MkReactionPicker, {
+				source: this.$refs.reactButton,
+				note: this.p,
+				compact: true
+			});
+		},
+		menu() {
+			(this as any).os.new(MkNoteMenu, {
+				source: this.$refs.menuButton,
+				note: this.p,
+				compact: true
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+.mk-note-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
+
+	> .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
+
+	> .renote
+		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
+
+			> .renote
+				margin 8px 0
+
+				> .mk-note-preview
+					padding 16px
+					border dashed 1px #c0dac6
+					border-radius 8px
+
+			> .location
+				margin 4px 0
+				font-size 12px
+				color #ccc
+
+			> .map
+				width 100%
+				height 200px
+
+				&:empty
+					display none
+
+			> .mk-url-preview
+				margin-top 8px
+
+			> .media
+				> img
+					display block
+					max-width 100%
+
+			> .tags
+				margin 4px 0 0 0
+
+				> *
+					display inline-block
+					margin 0 8px 0 0
+					padding 2px 8px 2px 16px
+					font-size 90%
+					color #8d969e
+					background #edf0f3
+					border-radius 4px
+
+					&:before
+						content ""
+						display block
+						position absolute
+						top 0
+						bottom 0
+						left 4px
+						width 8px
+						height 8px
+						margin auto 0
+						background #fff
+						border-radius 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>
+
+<style lang="stylus" module>
+.text
+	display block
+	margin 0
+	padding 0
+	overflow-wrap break-word
+	font-size 16px
+	color #717171
+
+	@media (min-width 500px)
+		font-size 24px
+
+</style>
diff --git a/src/client/app/mobile/views/components/post-preview.vue b/src/client/app/mobile/views/components/post-preview.vue
new file mode 100644
index 000000000..b9a6db315
--- /dev/null
+++ b/src/client/app/mobile/views/components/post-preview.vue
@@ -0,0 +1,100 @@
+<template>
+<div class="mk-note-preview">
+	<router-link class="avatar-anchor" :to="note.user | userPage">
+		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+	</router-link>
+	<div class="main">
+		<header>
+			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
+			<span class="username">@{{ note.user | acct }}</span>
+			<router-link class="time" :to="note | notePage">
+				<mk-time :time="note.createdAt"/>
+			</router-link>
+		</header>
+		<div class="body">
+			<mk-sub-note-content class="text" :note="note"/>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: ['note']
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-note-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>
diff --git a/src/client/app/mobile/views/components/post.vue b/src/client/app/mobile/views/components/post.vue
new file mode 100644
index 000000000..033de4f42
--- /dev/null
+++ b/src/client/app/mobile/views/components/post.vue
@@ -0,0 +1,523 @@
+<template>
+<div class="note" :class="{ renote: isRenote }">
+	<div class="reply-to" v-if="p.reply">
+		<x-sub :note="p.reply"/>
+	</div>
+	<div class="renote" v-if="isRenote">
+		<p>
+			<router-link class="avatar-anchor" :to="note.user | userPage">
+				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+			</router-link>
+			%fa:retweet%
+			<span>{{ '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.substr(0, '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.indexOf('{')) }}</span>
+			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
+			<span>{{ '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.substr('%i18n:mobile.tags.mk-timeline-note.reposted-by%'.indexOf('}') + 1) }}</span>
+		</p>
+		<mk-time :time="note.createdAt"/>
+	</div>
+	<article>
+		<router-link class="avatar-anchor" :to="p.user | userPage">
+			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
+		</router-link>
+		<div class="main">
+			<header>
+				<router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link>
+				<span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span>
+				<span class="username">@{{ p.user | acct }}</span>
+				<div class="info">
+					<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
+					<router-link class="created-at" :to="p | notePage">
+						<mk-time :time="p.createdAt"/>
+					</router-link>
+				</div>
+			</header>
+			<div class="body">
+				<p class="channel" v-if="p.channel != null"><a target="_blank">{{ p.channel.title }}</a>:</p>
+				<div class="text">
+					<a class="reply" v-if="p.reply">
+						%fa:reply%
+					</a>
+					<mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/>
+					<a class="rp" v-if="p.renote != null">RP:</a>
+				</div>
+				<div class="media" v-if="p.media.length > 0">
+					<mk-media-list :media-list="p.media"/>
+				</div>
+				<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
+				<div class="tags" v-if="p.tags && p.tags.length > 0">
+					<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
+				</div>
+				<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
+				<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
+				<div class="map" v-if="p.geo" ref="map"></div>
+				<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
+				<div class="renote" v-if="p.renote">
+					<mk-note-preview :note="p.renote"/>
+				</div>
+			</div>
+			<footer>
+				<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
+				<button @click="reply">
+					%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
+				</button>
+				<button @click="renote" title="Renote">
+					%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
+				</button>
+				<button :class="{ reacted: p.myReaction != 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 parse from '../../../../../text/parse';
+
+import MkNoteMenu from '../../../common/views/components/note-menu.vue';
+import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
+import XSub from './note.sub.vue';
+
+export default Vue.extend({
+	components: {
+		XSub
+	},
+
+	props: ['note'],
+
+	data() {
+		return {
+			connection: null,
+			connectionId: null
+		};
+	},
+
+	computed: {
+		isRenote(): boolean {
+			return (this.note.renote &&
+				this.note.text == null &&
+				this.note.mediaIds.length == 0 &&
+				this.note.poll == null);
+		},
+		p(): any {
+			return this.isRenote ? this.note.renote : this.note;
+		},
+		reactionsCount(): number {
+			return this.p.reactionCounts
+				? Object.keys(this.p.reactionCounts)
+					.map(key => this.p.reactionCounts[key])
+					.reduce((a, b) => a + b)
+				: 0;
+		},
+		urls(): string[] {
+			if (this.p.text) {
+				const ast = parse(this.p.text);
+				return ast
+					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
+					.map(t => t.url);
+			} else {
+				return null;
+			}
+		}
+	},
+
+	created() {
+		if ((this as any).os.isSignedIn) {
+			this.connection = (this as any).os.stream.getConnection();
+			this.connectionId = (this as any).os.stream.use();
+		}
+	},
+
+	mounted() {
+		this.capture(true);
+
+		if ((this as any).os.isSignedIn) {
+			this.connection.on('_connected_', this.onStreamConnected);
+		}
+
+		// Draw map
+		if (this.p.geo) {
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
+			if (shouldShowMap) {
+				(this as any).os.getGoogleMaps().then(maps => {
+					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
+					const map = new maps.Map(this.$refs.map, {
+						center: uluru,
+						zoom: 15
+					});
+					new maps.Marker({
+						position: uluru,
+						map: map
+					});
+				});
+			}
+		}
+	},
+
+	beforeDestroy() {
+		this.decapture(true);
+
+		if ((this as any).os.isSignedIn) {
+			this.connection.off('_connected_', this.onStreamConnected);
+			(this as any).os.stream.dispose(this.connectionId);
+		}
+	},
+
+	methods: {
+		capture(withHandler = false) {
+			if ((this as any).os.isSignedIn) {
+				this.connection.send({
+					type: 'capture',
+					id: this.p.id
+				});
+				if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
+			}
+		},
+		decapture(withHandler = false) {
+			if ((this as any).os.isSignedIn) {
+				this.connection.send({
+					type: 'decapture',
+					id: this.p.id
+				});
+				if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated);
+			}
+		},
+		onStreamConnected() {
+			this.capture();
+		},
+		onStreamNoteUpdated(data) {
+			const note = data.note;
+			if (note.id == this.note.id) {
+				this.$emit('update:note', note);
+			} else if (note.id == this.note.renoteId) {
+				this.note.renote = note;
+			}
+		},
+		reply() {
+			(this as any).apis.post({
+				reply: this.p
+			});
+		},
+		renote() {
+			(this as any).apis.post({
+				renote: this.p
+			});
+		},
+		react() {
+			(this as any).os.new(MkReactionPicker, {
+				source: this.$refs.reactButton,
+				note: this.p,
+				compact: true
+			});
+		},
+		menu() {
+			(this as any).os.new(MkNoteMenu, {
+				source: this.$refs.menuButton,
+				note: this.p,
+				compact: true
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+.note
+	font-size 12px
+	border-bottom solid 1px #eaeaea
+
+	&:first-child
+		border-radius 8px 8px 0 0
+
+		> .renote
+			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
+
+	> .renote
+		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-note-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
+				align-items center
+				white-space nowrap
+
+				@media (min-width 500px)
+					margin-bottom 2px
+
+				> .name
+					display block
+					margin 0 0.5em 0 0
+					padding 0
+					overflow hidden
+					color #627079
+					font-size 1em
+					font-weight bold
+					text-decoration none
+					text-overflow ellipsis
+
+					&:hover
+						text-decoration underline
+
+				> .is-bot
+					margin 0 0.5em 0 0
+					padding 1px 6px
+					font-size 12px
+					color #aaa
+					border solid 1px #ddd
+					border-radius 3px
+
+				> .username
+					margin 0 0.5em 0 0
+					color #ccc
+
+				> .info
+					margin-left auto
+					font-size 0.9em
+
+					> .mobile
+						margin-right 6px
+						color #c0c0c0
+
+					> .created-at
+						color #c0c0c0
+
+			> .body
+
+				> .text
+					display block
+					margin 0
+					padding 0
+					overflow-wrap break-word
+					font-size 1.1em
+					color #717171
+
+					>>> .quote
+						margin 8px
+						padding 6px 12px
+						color #aaa
+						border-left solid 3px #eee
+
+					> .reply
+						margin-right 8px
+						color #717171
+
+					> .rp
+						margin-left 4px
+						font-style oblique
+						color #a0bf46
+
+					[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-url-preview
+					margin-top 8px
+
+				> .channel
+					margin 0
+
+				> .tags
+					margin 4px 0 0 0
+
+					> *
+						display inline-block
+						margin 0 8px 0 0
+						padding 2px 8px 2px 16px
+						font-size 90%
+						color #8d969e
+						background #edf0f3
+						border-radius 4px
+
+						&:before
+							content ""
+							display block
+							position absolute
+							top 0
+							bottom 0
+							left 4px
+							width 8px
+							height 8px
+							margin auto 0
+							background #fff
+							border-radius 100%
+
+				> .media
+					> img
+						display block
+						max-width 100%
+
+				> .location
+					margin 4px 0
+					font-size 12px
+					color #ccc
+
+				> .map
+					width 100%
+					height 200px
+
+					&:empty
+						display none
+
+				> .app
+					font-size 12px
+					color #ccc
+
+				> .mk-poll
+					font-size 80%
+
+				> .renote
+					margin 8px 0
+
+					> .mk-note-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>
+
+<style lang="stylus" module>
+.text
+	code
+		padding 4px 8px
+		margin 0 0.5em
+		font-size 80%
+		color #525252
+		background #f8f8f8
+		border-radius 2px
+
+	pre > code
+		padding 16px
+		margin 0
+</style>
diff --git a/src/client/app/mobile/views/components/ui.header.vue b/src/client/app/mobile/views/components/ui.header.vue
index f664341cd..f1b24bf2d 100644
--- a/src/client/app/mobile/views/components/ui.header.vue
+++ b/src/client/app/mobile/views/components/ui.header.vue
@@ -3,7 +3,7 @@
 	<mk-special-message/>
 	<div class="main" ref="main">
 		<div class="backdrop"></div>
-		<p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ name }}</b>さん</p>
+		<p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ os.i | userName }}</b>さん</p>
 		<div class="content" ref="mainContainer">
 			<button class="nav" @click="$parent.isDrawerOpening = true">%fa:bars%</button>
 			<template v-if="hasUnreadNotifications || hasUnreadMessagingMessages || hasGameInvitations">%fa:circle%</template>
@@ -19,15 +19,9 @@
 <script lang="ts">
 import Vue from 'vue';
 import * as anime from 'animejs';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['func'],
-	computed: {
-		name() {
-			return getUserName(this.os.i);
-		}
-	},
 	data() {
 		return {
 			hasUnreadNotifications: false,
diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue
index 61dd8ca9d..764f9374e 100644
--- a/src/client/app/mobile/views/components/ui.nav.vue
+++ b/src/client/app/mobile/views/components/ui.nav.vue
@@ -11,7 +11,7 @@
 		<div class="body" v-if="isOpen">
 			<router-link class="me" v-if="os.isSignedIn" :to="`/@${os.i.username}`">
 				<img class="avatar" :src="`${os.i.avatarUrl}?thumbnail&size=128`" alt="avatar"/>
-				<p class="name">{{ name }}</p>
+				<p class="name">{{ os.i | userName }}</p>
 			</router-link>
 			<div class="links">
 				<ul>
@@ -39,16 +39,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import { docsUrl, chUrl, lang } from '../../../config';
-import getUserName from '../../../../../renderers/get-user-name';
+import { docsUrl, lang } from '../../../config';
 
 export default Vue.extend({
 	props: ['isOpen'],
-	computed: {
-		name() {
-			return getUserName(this.os.i);
-		}
-	},
 	data() {
 		return {
 			hasUnreadNotifications: false,
@@ -56,8 +50,7 @@ export default Vue.extend({
 			hasGameInvitations: false,
 			connection: null,
 			connectionId: null,
-			aboutUrl: `${docsUrl}/${lang}/about`,
-			chUrl
+			aboutUrl: `${docsUrl}/${lang}/about`
 		};
 	},
 	mounted() {
diff --git a/src/client/app/mobile/views/components/user-card.vue b/src/client/app/mobile/views/components/user-card.vue
index e8698a62f..432560a54 100644
--- a/src/client/app/mobile/views/components/user-card.vue
+++ b/src/client/app/mobile/views/components/user-card.vue
@@ -1,31 +1,21 @@
 <template>
 <div class="mk-user-card">
 	<header :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=1024)` : ''">
-		<a :href="`/@${acct}`">
+		<a :href="user | userPage">
 			<img :src="`${user.avatarUrl}?thumbnail&size=200`" alt="avatar"/>
 		</a>
 	</header>
-	<a class="name" :href="`/@${acct}`" target="_blank">{{ name }}</a>
-	<p class="username">@{{ acct }}</p>
+	<a class="name" :href="user | userPage" target="_blank">{{ user | userName }}</a>
+	<p class="username">@{{ user | acct }}</p>
 	<mk-follow-button :user="user"/>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
-	props: ['user'],
-	computed: {
-		acct() {
-			return getAcct(this.user);
-		},
-		name() {
-			return getUserName(this.user);
-		}
-	}
+	props: ['user']
 });
 </script>
 
diff --git a/src/client/app/mobile/views/components/user-preview.vue b/src/client/app/mobile/views/components/user-preview.vue
index 72a6bcf8a..23a83b5e3 100644
--- a/src/client/app/mobile/views/components/user-preview.vue
+++ b/src/client/app/mobile/views/components/user-preview.vue
@@ -1,12 +1,12 @@
 <template>
 <div class="mk-user-preview">
-	<router-link class="avatar-anchor" :to="`/@${acct}`">
+	<router-link class="avatar-anchor" :to="user | userPage">
 		<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`">{{ name }}</router-link>
-			<span class="username">@{{ acct }}</span>
+			<router-link class="name" :to="user | userPage">{{ user | userName }}</router-link>
+			<span class="username">@{{ user | acct }}</span>
 		</header>
 		<div class="body">
 			<div class="description">{{ user.description }}</div>
@@ -17,19 +17,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
-	props: ['user'],
-	computed: {
-		acct() {
-			return getAcct(this.user);
-		},
-		name() {
-			return getUserName(this.user);
-		}
-	}
+	props: ['user']
 });
 </script>
 
diff --git a/src/client/app/mobile/views/pages/following.vue b/src/client/app/mobile/views/pages/following.vue
index cc2442e47..d0dcc117c 100644
--- a/src/client/app/mobile/views/pages/following.vue
+++ b/src/client/app/mobile/views/pages/following.vue
@@ -20,7 +20,6 @@
 import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
 import parseAcct from '../../../../../acct/parse';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -30,8 +29,8 @@ export default Vue.extend({
 		};
 	},
 	computed: {
-		name() {
-			return getUserName(this.user);
+		name(): string {
+			return Vue.filter('userName')(this.user);
 		}
 	},
 	watch: {
diff --git a/src/client/app/mobile/views/pages/messaging-room.vue b/src/client/app/mobile/views/pages/messaging-room.vue
index ae6662dc0..3b6fb11db 100644
--- a/src/client/app/mobile/views/pages/messaging-room.vue
+++ b/src/client/app/mobile/views/pages/messaging-room.vue
@@ -1,7 +1,7 @@
 <template>
 <mk-ui>
 	<span slot="header">
-		<template v-if="user">%fa:R comments%{{ name }}</template>
+		<template v-if="user">%fa:R comments%{{ user | userName }}</template>
 		<template v-else><mk-ellipsis/></template>
 	</span>
 	<mk-messaging-room v-if="!fetching" :user="user" :is-naked="true"/>
@@ -11,7 +11,6 @@
 <script lang="ts">
 import Vue from 'vue';
 import parseAcct from '../../../../../acct/parse';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -20,11 +19,6 @@ export default Vue.extend({
 			user: null
 		};
 	},
-	computed: {
-		name() {
-			return getUserName(this.user);
-		}
-	},
 	watch: {
 		$route: 'fetch'
 	},
@@ -39,7 +33,7 @@ export default Vue.extend({
 				this.user = user;
 				this.fetching = false;
 
-				document.title = `%i18n:mobile.tags.mk-messaging-room-page.message%: ${this.name} | Misskey`;
+				document.title = `%i18n:mobile.tags.mk-messaging-room-page.message%: ${Vue.filter('userName')(this.user)} | Misskey`;
 			});
 		}
 	}
diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue
index 58a9d4e37..8d248f5cb 100644
--- a/src/client/app/mobile/views/pages/settings.vue
+++ b/src/client/app/mobile/views/pages/settings.vue
@@ -20,7 +20,6 @@
 <script lang="ts">
 import Vue from 'vue';
 import { version, codename } from '../../../config';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -30,8 +29,8 @@ export default Vue.extend({
 		};
 	},
 	computed: {
-		name() {
-			return getUserName(this.os.i);
+		name(): string {
+			return Vue.filter('userName')((this as any).os.i);
 		}
 	},
 	mounted() {
diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue
index aac8b628b..fb9220ee8 100644
--- a/src/client/app/mobile/views/pages/user.vue
+++ b/src/client/app/mobile/views/pages/user.vue
@@ -1,6 +1,6 @@
 <template>
 <mk-ui>
-	<span slot="header" v-if="!fetching">%fa:user% {{ user }}</span>
+	<span slot="header" v-if="!fetching">%fa:user% {{ user | userName }}</span>
 	<main v-if="!fetching">
 		<header>
 			<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=1024)` : ''"></div>
@@ -12,8 +12,8 @@
 					<mk-follow-button v-if="os.isSignedIn && os.i.id != user.id" :user="user"/>
 				</div>
 				<div class="title">
-					<h1>{{ getUserName(user) }}</h1>
-					<span class="username">@{{ getAcct(user) }}</span>
+					<h1>{{ user | userName }}</h1>
+					<span class="username">@{{ user | acct }}</span>
 					<span class="followed" v-if="user.isFollowed">%i18n:mobile.tags.mk-user.follows-you%</span>
 				</div>
 				<div class="description">{{ user.description }}</div>
@@ -61,8 +61,6 @@
 import Vue from 'vue';
 import * as age from 's-age';
 import parseAcct from '../../../../../acct/parse';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 import Progress from '../../../common/scripts/loading';
 import XHome from './user/home.vue';
 
@@ -74,9 +72,7 @@ export default Vue.extend({
 		return {
 			fetching: true,
 			user: null,
-			page: 'home',
-			getAcct,
-			getUserName
+			page: 'home'
 		};
 	},
 	computed: {
@@ -102,7 +98,7 @@ export default Vue.extend({
 				this.fetching = false;
 
 				Progress.done();
-				document.title = this.getUserName(this.user) + ' | Misskey';
+				document.title = Vue.filter('userName')(this.user) + ' | Misskey';
 			});
 		}
 	}
diff --git a/src/client/app/mobile/views/pages/user/home.followers-you-know.vue b/src/client/app/mobile/views/pages/user/home.followers-you-know.vue
index 1b128e2f2..2841c0d63 100644
--- a/src/client/app/mobile/views/pages/user/home.followers-you-know.vue
+++ b/src/client/app/mobile/views/pages/user/home.followers-you-know.vue
@@ -2,8 +2,8 @@
 <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="`/@${getAcct(user)}`">
-			<img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="getUserName(user)"/>
+		<a v-for="user in users" :key="user.id" :href="user | userPage">
+			<img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="user | userName"/>
 		</a>
 	</div>
 	<p class="empty" v-if="!fetching && users.length == 0">%i18n:mobile.tags.mk-user-overview-followers-you-know.no-users%</p>
@@ -12,8 +12,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../../acct/render';
-import getUserName from '../../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['user'],
@@ -23,14 +21,6 @@ export default Vue.extend({
 			users: []
 		};
 	},
-	computed: {
-		name() {
-			return getUserName(this.user);
-		}
-	},
-	methods: {
-		getAcct
-	},
 	mounted() {
 		(this as any).api('users/followers', {
 			userId: this.user.id,
diff --git a/src/client/app/mobile/views/widgets/profile.vue b/src/client/app/mobile/views/widgets/profile.vue
index bd257a3ff..502f886ce 100644
--- a/src/client/app/mobile/views/widgets/profile.vue
+++ b/src/client/app/mobile/views/widgets/profile.vue
@@ -8,23 +8,16 @@
 			:src="`${os.i.avatarUrl}?thumbnail&size=96`"
 			alt="avatar"
 		/>
-		<router-link :class="$style.name" :to="`/@${os.i.username}`">{{ name }}</router-link>
+		<router-link :class="$style.name" :to="os.i | userPage">{{ os.i | userName }}</router-link>
 	</mk-widget-container>
 </div>
 </template>
 
 <script lang="ts">
 import define from '../../../common/define-widget';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default define({
 	name: 'profile'
-}).extend({
-	computed: {
-		name() {
-			return getUserName(this.os.i);
-		}
-	}
 });
 </script>
 

From bd9dfdd0c1b5e1c4ceb3ac12b4f42e21bc44ce7b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Apr 2018 18:54:03 +0900
Subject: [PATCH 1210/1250] Fix bug

---
 src/server/web/docs.ts | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/server/web/docs.ts b/src/server/web/docs.ts
index e332d4fab..889532e17 100644
--- a/src/server/web/docs.ts
+++ b/src/server/web/docs.ts
@@ -2,9 +2,10 @@
  * Docs Server
  */
 
+import * as path from 'path';
 import * as express from 'express';
 
-const docs = `${__dirname}/../../client/docs/`;
+const docs = path.resolve(`${__dirname}/../../client/docs/`);
 
 /**
  * Init app

From 130dd2739df294c4972b12498516fc0f496f531e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Apr 2018 18:56:31 +0900
Subject: [PATCH 1211/1250] oops

---
 .../views/components/post-detail.sub.vue      | 122 ----
 .../desktop/views/components/post-detail.vue  | 434 -------------
 .../desktop/views/components/post-preview.vue |  99 ---
 .../views/components/posts.post.sub.vue       | 108 ----
 .../desktop/views/components/posts.post.vue   | 585 ------------------
 .../views/widgets/channel.channel.post.vue    |  65 --
 .../app/mobile/views/components/post-card.vue |  85 ---
 .../views/components/post-detail.sub.vue      | 103 ---
 .../mobile/views/components/post-detail.vue   | 444 -------------
 .../mobile/views/components/post-preview.vue  | 100 ---
 .../app/mobile/views/components/post.vue      | 523 ----------------
 11 files changed, 2668 deletions(-)
 delete mode 100644 src/client/app/desktop/views/components/post-detail.sub.vue
 delete mode 100644 src/client/app/desktop/views/components/post-detail.vue
 delete mode 100644 src/client/app/desktop/views/components/post-preview.vue
 delete mode 100644 src/client/app/desktop/views/components/posts.post.sub.vue
 delete mode 100644 src/client/app/desktop/views/components/posts.post.vue
 delete mode 100644 src/client/app/desktop/views/widgets/channel.channel.post.vue
 delete mode 100644 src/client/app/mobile/views/components/post-card.vue
 delete mode 100644 src/client/app/mobile/views/components/post-detail.sub.vue
 delete mode 100644 src/client/app/mobile/views/components/post-detail.vue
 delete mode 100644 src/client/app/mobile/views/components/post-preview.vue
 delete mode 100644 src/client/app/mobile/views/components/post.vue

diff --git a/src/client/app/desktop/views/components/post-detail.sub.vue b/src/client/app/desktop/views/components/post-detail.sub.vue
deleted file mode 100644
index 16bc2a1d9..000000000
--- a/src/client/app/desktop/views/components/post-detail.sub.vue
+++ /dev/null
@@ -1,122 +0,0 @@
-<template>
-<div class="sub" :title="title">
-	<router-link class="avatar-anchor" :to="note.user | userPage">
-		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
-	</router-link>
-	<div class="main">
-		<header>
-			<div class="left">
-				<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
-				<span class="username">@{{ note.user | acct }}</span>
-			</div>
-			<div class="right">
-				<router-link class="time" :to="note | notePage">
-					<mk-time :time="note.createdAt"/>
-				</router-link>
-			</div>
-		</header>
-		<div class="body">
-			<mk-note-html v-if="note.text" :text="note.text" :i="os.i" :class="$style.text"/>
-			<div class="media" v-if="note.media > 0">
-				<mk-media-list :media-list="note.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: ['note'],
-	computed: {
-		title(): string {
-			return dateStringify(this.note.createdAt);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.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
-
-</style>
-
-<style lang="stylus" module>
-.text
-	cursor default
-	display block
-	margin 0
-	padding 0
-	overflow-wrap break-word
-	font-size 1em
-	color #717171
-</style>
diff --git a/src/client/app/desktop/views/components/post-detail.vue b/src/client/app/desktop/views/components/post-detail.vue
deleted file mode 100644
index 50bbb7698..000000000
--- a/src/client/app/desktop/views/components/post-detail.vue
+++ /dev/null
@@ -1,434 +0,0 @@
-<template>
-<div class="mk-note-detail" :title="title">
-	<button
-		class="read-more"
-		v-if="p.reply && p.reply.replyId && context == null"
-		title="会話をもっと読み込む"
-		@click="fetchContext"
-		:disabled="contextFetching"
-	>
-		<template v-if="!contextFetching">%fa:ellipsis-v%</template>
-		<template v-if="contextFetching">%fa:spinner .pulse%</template>
-	</button>
-	<div class="context">
-		<x-sub v-for="note in context" :key="note.id" :note="note"/>
-	</div>
-	<div class="reply-to" v-if="p.reply">
-		<x-sub :note="p.reply"/>
-	</div>
-	<div class="renote" v-if="isRenote">
-		<p>
-			<router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.userId">
-				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
-			</router-link>
-			%fa:retweet%
-			<router-link class="name" :href="note.user | userPage">{{ note.user | userName }}</router-link>
-			がRenote
-		</p>
-	</div>
-	<article>
-		<router-link class="avatar-anchor" :to="p.user | userPage">
-			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
-		</router-link>
-		<header>
-			<router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link>
-			<span class="username">@{{ p.user | acct }}</span>
-			<router-link class="time" :to="p | notePage">
-				<mk-time :time="p.createdAt"/>
-			</router-link>
-		</header>
-		<div class="body">
-			<mk-note-html :class="$style.text" v-if="p.text" :text="p.text" :i="os.i"/>
-			<div class="media" v-if="p.media.length > 0">
-				<mk-media-list :media-list="p.media"/>
-			</div>
-			<mk-poll v-if="p.poll" :note="p"/>
-			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
-			<div class="tags" v-if="p.tags && p.tags.length > 0">
-				<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
-			</div>
-			<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
-			<div class="map" v-if="p.geo" ref="map"></div>
-			<div class="renote" v-if="p.renote">
-				<mk-note-preview :note="p.renote"/>
-			</div>
-		</div>
-		<footer>
-			<mk-reactions-viewer :note="p"/>
-			<button @click="reply" title="返信">
-				%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
-			</button>
-			<button @click="renote" title="Renote">
-				%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
-			</button>
-			<button :class="{ reacted: p.myReaction != 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">
-		<x-sub v-for="note in replies" :key="note.id" :note="note"/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import dateStringify from '../../../common/scripts/date-stringify';
-import parse from '../../../../../text/parse';
-
-import MkPostFormWindow from './post-form-window.vue';
-import MkRenoteFormWindow from './renote-form-window.vue';
-import MkNoteMenu from '../../../common/views/components/note-menu.vue';
-import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
-import XSub from './note-detail.sub.vue';
-
-export default Vue.extend({
-	components: {
-		XSub
-	},
-
-	props: {
-		note: {
-			type: Object,
-			required: true
-		},
-		compact: {
-			default: false
-		}
-	},
-
-	data() {
-		return {
-			context: [],
-			contextFetching: false,
-			replies: []
-		};
-	},
-
-	computed: {
-		isRenote(): boolean {
-			return (this.note.renote &&
-				this.note.text == null &&
-				this.note.mediaIds.length == 0 &&
-				this.note.poll == null);
-		},
-		p(): any {
-			return this.isRenote ? this.note.renote : this.note;
-		},
-		reactionsCount(): number {
-			return this.p.reactionCounts
-				? Object.keys(this.p.reactionCounts)
-					.map(key => this.p.reactionCounts[key])
-					.reduce((a, b) => a + b)
-				: 0;
-		},
-		title(): string {
-			return dateStringify(this.p.createdAt);
-		},
-		urls(): string[] {
-			if (this.p.text) {
-				const ast = parse(this.p.text);
-				return 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 as any).api('notes/replies', {
-				noteId: this.p.id,
-				limit: 8
-			}).then(replies => {
-				this.replies = replies;
-			});
-		}
-
-		// Draw map
-		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
-			if (shouldShowMap) {
-				(this as any).os.getGoogleMaps().then(maps => {
-					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
-					const map = new maps.Map(this.$refs.map, {
-						center: uluru,
-						zoom: 15
-					});
-					new maps.Marker({
-						position: uluru,
-						map: map
-					});
-				});
-			}
-		}
-	},
-
-	methods: {
-		fetchContext() {
-			this.contextFetching = true;
-
-			// Fetch context
-			(this as any).api('notes/context', {
-				noteId: this.p.replyId
-			}).then(context => {
-				this.contextFetching = false;
-				this.context = context.reverse();
-			});
-		},
-		reply() {
-			(this as any).os.new(MkPostFormWindow, {
-				reply: this.p
-			});
-		},
-		renote() {
-			(this as any).os.new(MkRenoteFormWindow, {
-				note: this.p
-			});
-		},
-		react() {
-			(this as any).os.new(MkReactionPicker, {
-				source: this.$refs.reactButton,
-				note: this.p
-			});
-		},
-		menu() {
-			(this as any).os.new(MkNoteMenu, {
-				source: this.$refs.menuButton,
-				note: this.p
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-@import '~const.styl'
-
-.mk-note-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
-
-	> .renote
-		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
-
-			> .renote
-				margin 8px 0
-
-				> .mk-note-preview
-					padding 16px
-					border dashed 1px #c0dac6
-					border-radius 8px
-
-			> .location
-				margin 4px 0
-				font-size 12px
-				color #ccc
-
-			> .map
-				width 100%
-				height 300px
-
-				&:empty
-					display none
-
-			> .mk-url-preview
-				margin-top 8px
-
-			> .tags
-				margin 4px 0 0 0
-
-				> *
-					display inline-block
-					margin 0 8px 0 0
-					padding 2px 8px 2px 16px
-					font-size 90%
-					color #8d969e
-					background #edf0f3
-					border-radius 4px
-
-					&:before
-						content ""
-						display block
-						position absolute
-						top 0
-						bottom 0
-						left 4px
-						width 8px
-						height 8px
-						margin auto 0
-						background #fff
-						border-radius 100%
-
-					&:hover
-						text-decoration none
-						background #e2e7ec
-
-		> 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>
-
-<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/client/app/desktop/views/components/post-preview.vue b/src/client/app/desktop/views/components/post-preview.vue
deleted file mode 100644
index ff3ecadc2..000000000
--- a/src/client/app/desktop/views/components/post-preview.vue
+++ /dev/null
@@ -1,99 +0,0 @@
-<template>
-<div class="mk-note-preview" :title="title">
-	<router-link class="avatar-anchor" :to="note.user | userPage">
-		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
-	</router-link>
-	<div class="main">
-		<header>
-			<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
-			<span class="username">@{{ note.user | acct }}</span>
-			<router-link class="time" :to="note | notePage">
-				<mk-time :time="note.createdAt"/>
-			</router-link>
-		</header>
-		<div class="body">
-			<mk-sub-note-content class="text" :note="note"/>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import dateStringify from '../../../common/scripts/date-stringify';
-
-export default Vue.extend({
-	props: ['note'],
-	computed: {
-		title(): string {
-			return dateStringify(this.note.createdAt);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-note-preview
-	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 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
-			white-space nowrap
-
-			> .name
-				margin 0 .5em 0 0
-				padding 0
-				color #607073
-				font-size 1em
-				font-weight bold
-				text-decoration none
-				white-space normal
-
-				&:hover
-					text-decoration underline
-
-			> .username
-				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/client/app/desktop/views/components/posts.post.sub.vue b/src/client/app/desktop/views/components/posts.post.sub.vue
deleted file mode 100644
index e85478578..000000000
--- a/src/client/app/desktop/views/components/posts.post.sub.vue
+++ /dev/null
@@ -1,108 +0,0 @@
-<template>
-<div class="sub" :title="title">
-	<router-link class="avatar-anchor" :to="note.user | userPage">
-		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
-	</router-link>
-	<div class="main">
-		<header>
-			<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
-			<span class="username">@{{ note.user | acct }}</span>
-			<router-link class="created-at" :to="note | notePage">
-				<mk-time :time="note.createdAt"/>
-			</router-link>
-		</header>
-		<div class="body">
-			<mk-sub-note-content class="text" :note="note"/>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import dateStringify from '../../../common/scripts/date-stringify';
-
-export default Vue.extend({
-	props: ['note'],
-	computed: {
-		title(): string {
-			return dateStringify(this.note.createdAt);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.sub
-	margin 0
-	padding 16px
-	font-size 0.9em
-
-	&: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 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%
-
-</style>
diff --git a/src/client/app/desktop/views/components/posts.post.vue b/src/client/app/desktop/views/components/posts.post.vue
deleted file mode 100644
index 322bf2922..000000000
--- a/src/client/app/desktop/views/components/posts.post.vue
+++ /dev/null
@@ -1,585 +0,0 @@
-<template>
-<div class="note" tabindex="-1" :title="title" @keydown="onKeydown">
-	<div class="reply-to" v-if="p.reply">
-		<x-sub :note="p.reply"/>
-	</div>
-	<div class="renote" v-if="isRenote">
-		<p>
-			<router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.userId">
-				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
-			</router-link>
-			%fa:retweet%
-			<span>{{ '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.indexOf('{')) }}</span>
-			<a class="name" :href="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</a>
-			<span>{{ '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-note.reposted-by%'.indexOf('}') + 1) }}</span>
-		</p>
-		<mk-time :time="note.createdAt"/>
-	</div>
-	<article>
-		<router-link class="avatar-anchor" :to="p.user | userPage">
-			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
-		</router-link>
-		<div class="main">
-			<header>
-				<router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link>
-				<span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span>
-				<span class="username">@{{ p.user | acct }}</span>
-				<div class="info">
-					<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
-					<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
-					<router-link class="created-at" :to="url">
-						<mk-time :time="p.createdAt"/>
-					</router-link>
-				</div>
-			</header>
-			<div class="body">
-				<p class="channel" v-if="p.channel">
-					<a :href="`${_CH_URL_}/${p.channel.id}`" target="_blank">{{ p.channel.title }}</a>:
-				</p>
-				<div class="text">
-					<a class="reply" v-if="p.reply">%fa:reply%</a>
-					<mk-note-html v-if="p.textHtml" :text="p.text" :i="os.i" :class="$style.text"/>
-					<a class="rp" v-if="p.renote">RP:</a>
-				</div>
-				<div class="media" v-if="p.media.length > 0">
-					<mk-media-list :media-list="p.media"/>
-				</div>
-				<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
-				<div class="tags" v-if="p.tags && p.tags.length > 0">
-					<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
-				</div>
-				<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
-				<div class="map" v-if="p.geo" ref="map"></div>
-				<div class="renote" v-if="p.renote">
-					<mk-note-preview :note="p.renote"/>
-				</div>
-				<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
-			</div>
-			<footer>
-				<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
-				<button @click="reply" title="%i18n:desktop.tags.mk-timeline-note.reply%">
-					%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
-				</button>
-				<button @click="renote" title="%i18n:desktop.tags.mk-timeline-note.renote%">
-					%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
-				</button>
-				<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:desktop.tags.mk-timeline-note.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 title="%i18n:desktop.tags.mk-timeline-note.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-note-status-graph width="462" height="130" :note="p"/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import dateStringify from '../../../common/scripts/date-stringify';
-import parse from '../../../../../text/parse';
-
-import MkPostFormWindow from './post-form-window.vue';
-import MkRenoteFormWindow from './renote-form-window.vue';
-import MkNoteMenu from '../../../common/views/components/note-menu.vue';
-import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
-import XSub from './notes.note.sub.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({
-	components: {
-		XSub
-	},
-
-	props: ['note'],
-
-	data() {
-		return {
-			isDetailOpened: false,
-			connection: null,
-			connectionId: null
-		};
-	},
-
-	computed: {
-		isRenote(): boolean {
-			return (this.note.renote &&
-				this.note.text == null &&
-				this.note.mediaIds.length == 0 &&
-				this.note.poll == null);
-		},
-		p(): any {
-			return this.isRenote ? this.note.renote : this.note;
-		},
-		reactionsCount(): number {
-			return this.p.reactionCounts
-				? Object.keys(this.p.reactionCounts)
-					.map(key => this.p.reactionCounts[key])
-					.reduce((a, b) => a + b)
-				: 0;
-		},
-		title(): string {
-			return dateStringify(this.p.createdAt);
-		},
-		urls(): string[] {
-			if (this.p.text) {
-				const ast = parse(this.p.text);
-				return ast
-					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
-					.map(t => t.url);
-			} else {
-				return null;
-			}
-		}
-	},
-
-	created() {
-		if ((this as any).os.isSignedIn) {
-			this.connection = (this as any).os.stream.getConnection();
-			this.connectionId = (this as any).os.stream.use();
-		}
-	},
-
-	mounted() {
-		this.capture(true);
-
-		if ((this as any).os.isSignedIn) {
-			this.connection.on('_connected_', this.onStreamConnected);
-		}
-
-		// Draw map
-		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
-			if (shouldShowMap) {
-				(this as any).os.getGoogleMaps().then(maps => {
-					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
-					const map = new maps.Map(this.$refs.map, {
-						center: uluru,
-						zoom: 15
-					});
-					new maps.Marker({
-						position: uluru,
-						map: map
-					});
-				});
-			}
-		}
-	},
-
-	beforeDestroy() {
-		this.decapture(true);
-
-		if ((this as any).os.isSignedIn) {
-			this.connection.off('_connected_', this.onStreamConnected);
-			(this as any).os.stream.dispose(this.connectionId);
-		}
-	},
-
-	methods: {
-		capture(withHandler = false) {
-			if ((this as any).os.isSignedIn) {
-				this.connection.send({
-					type: 'capture',
-					id: this.p.id
-				});
-				if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
-			}
-		},
-		decapture(withHandler = false) {
-			if ((this as any).os.isSignedIn) {
-				this.connection.send({
-					type: 'decapture',
-					id: this.p.id
-				});
-				if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated);
-			}
-		},
-		onStreamConnected() {
-			this.capture();
-		},
-		onStreamNoteUpdated(data) {
-			const note = data.note;
-			if (note.id == this.note.id) {
-				this.$emit('update:note', note);
-			} else if (note.id == this.note.renoteId) {
-				this.note.renote = note;
-			}
-		},
-		reply() {
-			(this as any).os.new(MkPostFormWindow, {
-				reply: this.p
-			});
-		},
-		renote() {
-			(this as any).os.new(MkRenoteFormWindow, {
-				note: this.p
-			});
-		},
-		react() {
-			(this as any).os.new(MkReactionPicker, {
-				source: this.$refs.reactButton,
-				note: this.p
-			});
-		},
-		menu() {
-			(this as any).os.new(MkNoteMenu, {
-				source: this.$refs.menuButton,
-				note: this.p
-			});
-		},
-		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.renote();
-					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>
-
-<style lang="stylus" scoped>
-@import '~const.styl'
-
-.note
-	margin 0
-	padding 0
-	background #fff
-	border-bottom solid 1px #eaeaea
-
-	&:first-child
-		border-top-left-radius 6px
-		border-top-right-radius 6px
-
-		> .renote
-			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
-
-	> .renote
-		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-note-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
-				align-items center
-				margin-bottom 4px
-				white-space nowrap
-
-				> .name
-					display block
-					margin 0 .5em 0 0
-					padding 0
-					overflow hidden
-					color #627079
-					font-size 1em
-					font-weight bold
-					text-decoration none
-					text-overflow ellipsis
-
-					&:hover
-						text-decoration underline
-
-				> .is-bot
-					margin 0 .5em 0 0
-					padding 1px 6px
-					font-size 12px
-					color #aaa
-					border solid 1px #ddd
-					border-radius 3px
-
-				> .username
-					margin 0 .5em 0 0
-					color #ccc
-
-				> .info
-					margin-left auto
-					font-size 0.9em
-
-					> .mobile
-						margin-right 8px
-						color #ccc
-
-					> .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
-
-					>>> .quote
-						margin 8px
-						padding 6px 12px
-						color #aaa
-						border-left solid 3px #eee
-
-					> .reply
-						margin-right 8px
-						color #717171
-
-					> .rp
-						margin-left 4px
-						font-style oblique
-						color #a0bf46
-
-				> .location
-					margin 4px 0
-					font-size 12px
-					color #ccc
-
-				> .map
-					width 100%
-					height 300px
-
-					&:empty
-						display none
-
-				> .tags
-					margin 4px 0 0 0
-
-					> *
-						display inline-block
-						margin 0 8px 0 0
-						padding 2px 8px 2px 16px
-						font-size 90%
-						color #8d969e
-						background #edf0f3
-						border-radius 4px
-
-						&:before
-							content ""
-							display block
-							position absolute
-							top 0
-							bottom 0
-							left 4px
-							width 8px
-							height 8px
-							margin auto 0
-							background #fff
-							border-radius 100%
-
-						&:hover
-							text-decoration none
-							background #e2e7ec
-
-				.mk-url-preview
-					margin-top 8px
-
-				> .channel
-					margin 0
-
-				> .mk-poll
-					font-size 80%
-
-				> .renote
-					margin 8px 0
-
-					> .mk-note-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>
-
-<style lang="stylus" module>
-.text
-
-	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
-</style>
diff --git a/src/client/app/desktop/views/widgets/channel.channel.post.vue b/src/client/app/desktop/views/widgets/channel.channel.post.vue
deleted file mode 100644
index 776791906..000000000
--- a/src/client/app/desktop/views/widgets/channel.channel.post.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<template>
-<div class="note">
-	<header>
-		<a class="index" @click="reply">{{ note.index }}:</a>
-		<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id"><b>{{ note.user | userName }}</b></router-link>
-		<span>ID:<i>{{ note.user | acct }}</i></span>
-	</header>
-	<div>
-		<a v-if="note.reply">&gt;&gt;{{ note.reply.index }}</a>
-		{{ note.text }}
-		<div class="media" v-if="note.media">
-			<a v-for="file in note.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: ['note'],
-	methods: {
-		reply() {
-			this.$emit('reply', this.note);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.note
-	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/client/app/mobile/views/components/post-card.vue b/src/client/app/mobile/views/components/post-card.vue
deleted file mode 100644
index 393fa9b83..000000000
--- a/src/client/app/mobile/views/components/post-card.vue
+++ /dev/null
@@ -1,85 +0,0 @@
-<template>
-<div class="mk-note-card">
-	<a :href="note | notePage">
-		<header>
-			<img :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/><h3>{{ note.user | userName }}</h3>
-		</header>
-		<div>
-			{{ text }}
-		</div>
-		<mk-time :time="note.createdAt"/>
-	</a>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import summary from '../../../../../renderers/get-note-summary';
-
-export default Vue.extend({
-	props: ['note'],
-	computed: {
-		text(): string {
-			return summary(this.note);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-note-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/client/app/mobile/views/components/post-detail.sub.vue b/src/client/app/mobile/views/components/post-detail.sub.vue
deleted file mode 100644
index 06f442d30..000000000
--- a/src/client/app/mobile/views/components/post-detail.sub.vue
+++ /dev/null
@@ -1,103 +0,0 @@
-<template>
-<div class="root sub">
-	<router-link class="avatar-anchor" :to="note.user | userPage">
-		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-	</router-link>
-	<div class="main">
-		<header>
-			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
-			<span class="username">@{{ note.user | acct }}</span>
-			<router-link class="time" :to="note | notePage">
-				<mk-time :time="note.createdAt"/>
-			</router-link>
-		</header>
-		<div class="body">
-			<mk-sub-note-content class="text" :note="note"/>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
-	props: ['note']
-});
-</script>
-
-<style lang="stylus" scoped>
-.root.sub
-	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/client/app/mobile/views/components/post-detail.vue b/src/client/app/mobile/views/components/post-detail.vue
deleted file mode 100644
index de32f0a74..000000000
--- a/src/client/app/mobile/views/components/post-detail.vue
+++ /dev/null
@@ -1,444 +0,0 @@
-<template>
-<div class="mk-note-detail">
-	<button
-		class="more"
-		v-if="p.reply && p.reply.replyId && 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">
-		<x-sub v-for="note in context" :key="note.id" :note="note"/>
-	</div>
-	<div class="reply-to" v-if="p.reply">
-		<x-sub :note="p.reply"/>
-	</div>
-	<div class="renote" v-if="isRenote">
-		<p>
-			<router-link class="avatar-anchor" :to="note.user | userPage">
-				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
-			</router-link>
-			%fa:retweet%<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>がRenote
-		</p>
-	</div>
-	<article>
-		<header>
-			<router-link class="avatar-anchor" :to="p.user | userPage">
-				<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-			</router-link>
-			<div>
-				<router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link>
-				<span class="username">@{{ p.user | acct }}</span>
-			</div>
-		</header>
-		<div class="body">
-			<mk-note-html v-if="p.text" :ast="p.text" :i="os.i" :class="$style.text"/>
-			<div class="tags" v-if="p.tags && p.tags.length > 0">
-				<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
-			</div>
-			<div class="media" v-if="p.media.length > 0">
-				<mk-media-list :media-list="p.media"/>
-			</div>
-			<mk-poll v-if="p.poll" :note="p"/>
-			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
-			<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
-			<div class="map" v-if="p.geo" ref="map"></div>
-			<div class="renote" v-if="p.renote">
-				<mk-note-preview :note="p.renote"/>
-			</div>
-		</div>
-		<router-link class="time" :to="`/@${pAcct}/${p.id}`">
-			<mk-time :time="p.createdAt" mode="detail"/>
-		</router-link>
-		<footer>
-			<mk-reactions-viewer :note="p"/>
-			<button @click="reply" title="%i18n:mobile.tags.mk-note-detail.reply%">
-				%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
-			</button>
-			<button @click="renote" title="Renote">
-				%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
-			</button>
-			<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:mobile.tags.mk-note-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">
-		<x-sub v-for="note in replies" :key="note.id" :note="note"/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import parse from '../../../../../text/parse';
-
-import MkNoteMenu from '../../../common/views/components/note-menu.vue';
-import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
-import XSub from './note-detail.sub.vue';
-
-export default Vue.extend({
-	components: {
-		XSub
-	},
-
-	props: {
-		note: {
-			type: Object,
-			required: true
-		},
-		compact: {
-			default: false
-		}
-	},
-
-	data() {
-		return {
-			context: [],
-			contextFetching: false,
-			replies: []
-		};
-	},
-
-	computed: {
-		isRenote(): boolean {
-			return (this.note.renote &&
-				this.note.text == null &&
-				this.note.mediaIds.length == 0 &&
-				this.note.poll == null);
-		},
-		p(): any {
-			return this.isRenote ? this.note.renote : this.note;
-		},
-		reactionsCount(): number {
-			return this.p.reactionCounts
-				? Object.keys(this.p.reactionCounts)
-					.map(key => this.p.reactionCounts[key])
-					.reduce((a, b) => a + b)
-				: 0;
-		},
-		urls(): string[] {
-			if (this.p.text) {
-				const ast = parse(this.p.text);
-				return 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 as any).api('notes/replies', {
-				noteId: this.p.id,
-				limit: 8
-			}).then(replies => {
-				this.replies = replies;
-			});
-		}
-
-		// Draw map
-		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
-			if (shouldShowMap) {
-				(this as any).os.getGoogleMaps().then(maps => {
-					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
-					const map = new maps.Map(this.$refs.map, {
-						center: uluru,
-						zoom: 15
-					});
-					new maps.Marker({
-						position: uluru,
-						map: map
-					});
-				});
-			}
-		}
-	},
-
-	methods: {
-		fetchContext() {
-			this.contextFetching = true;
-
-			// Fetch context
-			(this as any).api('notes/context', {
-				noteId: this.p.replyId
-			}).then(context => {
-				this.contextFetching = false;
-				this.context = context.reverse();
-			});
-		},
-		reply() {
-			(this as any).apis.post({
-				reply: this.p
-			});
-		},
-		renote() {
-			(this as any).apis.post({
-				renote: this.p
-			});
-		},
-		react() {
-			(this as any).os.new(MkReactionPicker, {
-				source: this.$refs.reactButton,
-				note: this.p,
-				compact: true
-			});
-		},
-		menu() {
-			(this as any).os.new(MkNoteMenu, {
-				source: this.$refs.menuButton,
-				note: this.p,
-				compact: true
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-@import '~const.styl'
-
-.mk-note-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
-
-	> .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
-
-	> .renote
-		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
-
-			> .renote
-				margin 8px 0
-
-				> .mk-note-preview
-					padding 16px
-					border dashed 1px #c0dac6
-					border-radius 8px
-
-			> .location
-				margin 4px 0
-				font-size 12px
-				color #ccc
-
-			> .map
-				width 100%
-				height 200px
-
-				&:empty
-					display none
-
-			> .mk-url-preview
-				margin-top 8px
-
-			> .media
-				> img
-					display block
-					max-width 100%
-
-			> .tags
-				margin 4px 0 0 0
-
-				> *
-					display inline-block
-					margin 0 8px 0 0
-					padding 2px 8px 2px 16px
-					font-size 90%
-					color #8d969e
-					background #edf0f3
-					border-radius 4px
-
-					&:before
-						content ""
-						display block
-						position absolute
-						top 0
-						bottom 0
-						left 4px
-						width 8px
-						height 8px
-						margin auto 0
-						background #fff
-						border-radius 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>
-
-<style lang="stylus" module>
-.text
-	display block
-	margin 0
-	padding 0
-	overflow-wrap break-word
-	font-size 16px
-	color #717171
-
-	@media (min-width 500px)
-		font-size 24px
-
-</style>
diff --git a/src/client/app/mobile/views/components/post-preview.vue b/src/client/app/mobile/views/components/post-preview.vue
deleted file mode 100644
index b9a6db315..000000000
--- a/src/client/app/mobile/views/components/post-preview.vue
+++ /dev/null
@@ -1,100 +0,0 @@
-<template>
-<div class="mk-note-preview">
-	<router-link class="avatar-anchor" :to="note.user | userPage">
-		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-	</router-link>
-	<div class="main">
-		<header>
-			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
-			<span class="username">@{{ note.user | acct }}</span>
-			<router-link class="time" :to="note | notePage">
-				<mk-time :time="note.createdAt"/>
-			</router-link>
-		</header>
-		<div class="body">
-			<mk-sub-note-content class="text" :note="note"/>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
-	props: ['note']
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-note-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>
diff --git a/src/client/app/mobile/views/components/post.vue b/src/client/app/mobile/views/components/post.vue
deleted file mode 100644
index 033de4f42..000000000
--- a/src/client/app/mobile/views/components/post.vue
+++ /dev/null
@@ -1,523 +0,0 @@
-<template>
-<div class="note" :class="{ renote: isRenote }">
-	<div class="reply-to" v-if="p.reply">
-		<x-sub :note="p.reply"/>
-	</div>
-	<div class="renote" v-if="isRenote">
-		<p>
-			<router-link class="avatar-anchor" :to="note.user | userPage">
-				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-			</router-link>
-			%fa:retweet%
-			<span>{{ '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.substr(0, '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.indexOf('{')) }}</span>
-			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
-			<span>{{ '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.substr('%i18n:mobile.tags.mk-timeline-note.reposted-by%'.indexOf('}') + 1) }}</span>
-		</p>
-		<mk-time :time="note.createdAt"/>
-	</div>
-	<article>
-		<router-link class="avatar-anchor" :to="p.user | userPage">
-			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
-		</router-link>
-		<div class="main">
-			<header>
-				<router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link>
-				<span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span>
-				<span class="username">@{{ p.user | acct }}</span>
-				<div class="info">
-					<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
-					<router-link class="created-at" :to="p | notePage">
-						<mk-time :time="p.createdAt"/>
-					</router-link>
-				</div>
-			</header>
-			<div class="body">
-				<p class="channel" v-if="p.channel != null"><a target="_blank">{{ p.channel.title }}</a>:</p>
-				<div class="text">
-					<a class="reply" v-if="p.reply">
-						%fa:reply%
-					</a>
-					<mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/>
-					<a class="rp" v-if="p.renote != null">RP:</a>
-				</div>
-				<div class="media" v-if="p.media.length > 0">
-					<mk-media-list :media-list="p.media"/>
-				</div>
-				<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
-				<div class="tags" v-if="p.tags && p.tags.length > 0">
-					<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
-				</div>
-				<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
-				<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
-				<div class="map" v-if="p.geo" ref="map"></div>
-				<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
-				<div class="renote" v-if="p.renote">
-					<mk-note-preview :note="p.renote"/>
-				</div>
-			</div>
-			<footer>
-				<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
-				<button @click="reply">
-					%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
-				</button>
-				<button @click="renote" title="Renote">
-					%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
-				</button>
-				<button :class="{ reacted: p.myReaction != 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 parse from '../../../../../text/parse';
-
-import MkNoteMenu from '../../../common/views/components/note-menu.vue';
-import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
-import XSub from './note.sub.vue';
-
-export default Vue.extend({
-	components: {
-		XSub
-	},
-
-	props: ['note'],
-
-	data() {
-		return {
-			connection: null,
-			connectionId: null
-		};
-	},
-
-	computed: {
-		isRenote(): boolean {
-			return (this.note.renote &&
-				this.note.text == null &&
-				this.note.mediaIds.length == 0 &&
-				this.note.poll == null);
-		},
-		p(): any {
-			return this.isRenote ? this.note.renote : this.note;
-		},
-		reactionsCount(): number {
-			return this.p.reactionCounts
-				? Object.keys(this.p.reactionCounts)
-					.map(key => this.p.reactionCounts[key])
-					.reduce((a, b) => a + b)
-				: 0;
-		},
-		urls(): string[] {
-			if (this.p.text) {
-				const ast = parse(this.p.text);
-				return ast
-					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
-					.map(t => t.url);
-			} else {
-				return null;
-			}
-		}
-	},
-
-	created() {
-		if ((this as any).os.isSignedIn) {
-			this.connection = (this as any).os.stream.getConnection();
-			this.connectionId = (this as any).os.stream.use();
-		}
-	},
-
-	mounted() {
-		this.capture(true);
-
-		if ((this as any).os.isSignedIn) {
-			this.connection.on('_connected_', this.onStreamConnected);
-		}
-
-		// Draw map
-		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
-			if (shouldShowMap) {
-				(this as any).os.getGoogleMaps().then(maps => {
-					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
-					const map = new maps.Map(this.$refs.map, {
-						center: uluru,
-						zoom: 15
-					});
-					new maps.Marker({
-						position: uluru,
-						map: map
-					});
-				});
-			}
-		}
-	},
-
-	beforeDestroy() {
-		this.decapture(true);
-
-		if ((this as any).os.isSignedIn) {
-			this.connection.off('_connected_', this.onStreamConnected);
-			(this as any).os.stream.dispose(this.connectionId);
-		}
-	},
-
-	methods: {
-		capture(withHandler = false) {
-			if ((this as any).os.isSignedIn) {
-				this.connection.send({
-					type: 'capture',
-					id: this.p.id
-				});
-				if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
-			}
-		},
-		decapture(withHandler = false) {
-			if ((this as any).os.isSignedIn) {
-				this.connection.send({
-					type: 'decapture',
-					id: this.p.id
-				});
-				if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated);
-			}
-		},
-		onStreamConnected() {
-			this.capture();
-		},
-		onStreamNoteUpdated(data) {
-			const note = data.note;
-			if (note.id == this.note.id) {
-				this.$emit('update:note', note);
-			} else if (note.id == this.note.renoteId) {
-				this.note.renote = note;
-			}
-		},
-		reply() {
-			(this as any).apis.post({
-				reply: this.p
-			});
-		},
-		renote() {
-			(this as any).apis.post({
-				renote: this.p
-			});
-		},
-		react() {
-			(this as any).os.new(MkReactionPicker, {
-				source: this.$refs.reactButton,
-				note: this.p,
-				compact: true
-			});
-		},
-		menu() {
-			(this as any).os.new(MkNoteMenu, {
-				source: this.$refs.menuButton,
-				note: this.p,
-				compact: true
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-@import '~const.styl'
-
-.note
-	font-size 12px
-	border-bottom solid 1px #eaeaea
-
-	&:first-child
-		border-radius 8px 8px 0 0
-
-		> .renote
-			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
-
-	> .renote
-		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-note-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
-				align-items center
-				white-space nowrap
-
-				@media (min-width 500px)
-					margin-bottom 2px
-
-				> .name
-					display block
-					margin 0 0.5em 0 0
-					padding 0
-					overflow hidden
-					color #627079
-					font-size 1em
-					font-weight bold
-					text-decoration none
-					text-overflow ellipsis
-
-					&:hover
-						text-decoration underline
-
-				> .is-bot
-					margin 0 0.5em 0 0
-					padding 1px 6px
-					font-size 12px
-					color #aaa
-					border solid 1px #ddd
-					border-radius 3px
-
-				> .username
-					margin 0 0.5em 0 0
-					color #ccc
-
-				> .info
-					margin-left auto
-					font-size 0.9em
-
-					> .mobile
-						margin-right 6px
-						color #c0c0c0
-
-					> .created-at
-						color #c0c0c0
-
-			> .body
-
-				> .text
-					display block
-					margin 0
-					padding 0
-					overflow-wrap break-word
-					font-size 1.1em
-					color #717171
-
-					>>> .quote
-						margin 8px
-						padding 6px 12px
-						color #aaa
-						border-left solid 3px #eee
-
-					> .reply
-						margin-right 8px
-						color #717171
-
-					> .rp
-						margin-left 4px
-						font-style oblique
-						color #a0bf46
-
-					[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-url-preview
-					margin-top 8px
-
-				> .channel
-					margin 0
-
-				> .tags
-					margin 4px 0 0 0
-
-					> *
-						display inline-block
-						margin 0 8px 0 0
-						padding 2px 8px 2px 16px
-						font-size 90%
-						color #8d969e
-						background #edf0f3
-						border-radius 4px
-
-						&:before
-							content ""
-							display block
-							position absolute
-							top 0
-							bottom 0
-							left 4px
-							width 8px
-							height 8px
-							margin auto 0
-							background #fff
-							border-radius 100%
-
-				> .media
-					> img
-						display block
-						max-width 100%
-
-				> .location
-					margin 4px 0
-					font-size 12px
-					color #ccc
-
-				> .map
-					width 100%
-					height 200px
-
-					&:empty
-						display none
-
-				> .app
-					font-size 12px
-					color #ccc
-
-				> .mk-poll
-					font-size 80%
-
-				> .renote
-					margin 8px 0
-
-					> .mk-note-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>
-
-<style lang="stylus" module>
-.text
-	code
-		padding 4px 8px
-		margin 0 0.5em
-		font-size 80%
-		color #525252
-		background #f8f8f8
-		border-radius 2px
-
-	pre > code
-		padding 16px
-		margin 0
-</style>

From 1fc56be0d2354c21a0086951e571c17475038c0c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Apr 2018 19:18:15 +0900
Subject: [PATCH 1212/1250] Fix bug

---
 .../views/components/messaging-room.message.vue      |  6 +-----
 src/client/app/common/views/components/messaging.vue |  2 +-
 .../app/common/views/components/welcome-timeline.vue |  2 +-
 .../app/desktop/views/components/notifications.vue   | 12 ++++++------
 .../app/desktop/views/components/user-preview.vue    |  4 +---
 .../app/desktop/views/pages/user/user.friends.vue    |  4 ----
 src/client/app/desktop/views/pages/welcome.vue       |  2 --
 src/client/app/desktop/views/widgets/polls.vue       | 10 ++--------
 src/client/app/desktop/views/widgets/trends.vue      | 10 ++--------
 .../app/mobile/views/components/notification.vue     |  6 +++---
 src/client/app/mobile/views/pages/user.vue           |  4 ++--
 .../app/mobile/views/pages/user/home.photos.vue      |  6 +-----
 12 files changed, 20 insertions(+), 48 deletions(-)

diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue
index 7200b59bb..fdb820a4a 100644
--- a/src/client/app/common/views/components/messaging-room.message.vue
+++ b/src/client/app/common/views/components/messaging-room.message.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="message" :data-is-me="isMe">
-	<router-link class="avatar-anchor" :to="`/@${acct}`" :title="acct" target="_blank">
+	<router-link class="avatar-anchor" :to="message.user | userPage" :title="acct" target="_blank">
 		<img class="avatar" :src="`${message.user.avatarUrl}?thumbnail&size=80`" alt=""/>
 	</router-link>
 	<div class="content">
@@ -34,7 +34,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
 import parse from '../../../../../text/parse';
 
 export default Vue.extend({
@@ -44,9 +43,6 @@ export default Vue.extend({
 		}
 	},
 	computed: {
-		acct(): string {
-			return getAcct(this.message.user);
-		},
 		isMe(): boolean {
 			return this.message.userId == (this as any).os.i.id;
 		},
diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue
index 751e4de50..05bf88974 100644
--- a/src/client/app/common/views/components/messaging.vue
+++ b/src/client/app/common/views/components/messaging.vue
@@ -24,7 +24,7 @@
 		<template>
 			<a v-for="message in messages"
 				class="user"
-				:href="`/i/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
+				:href="`/i/messaging/${isMe(message) ? message.recipient : message.user | acct}`"
 				:data-is-me="isMe(message)"
 				:data-is-read="message.isRead"
 				@click.prevent="navigate(isMe(message) ? message.recipient : message.user)"
diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue
index 7571cfc5f..a80bc04f7 100644
--- a/src/client/app/common/views/components/welcome-timeline.vue
+++ b/src/client/app/common/views/components/welcome-timeline.vue
@@ -9,7 +9,7 @@
 				<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">{{ note.user | userName }}</router-link>
 				<span class="username">@{{ note.user | acct }}</span>
 				<div class="info">
-					<router-link class="created-at" :to="`/@${getAcct(note.user)}/${note.id}`">
+					<router-link class="created-at" :to="note | notePage">
 						<mk-time :time="note.createdAt"/>
 					</router-link>
 				</div>
diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
index 8b17c8c43..598c2ad2f 100644
--- a/src/client/app/desktop/views/components/notifications.vue
+++ b/src/client/app/desktop/views/components/notifications.vue
@@ -13,7 +13,7 @@
 							<mk-reaction-icon :reaction="notification.reaction"/>
 							<router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link>
 						</p>
-						<router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">
+						<router-link class="note-ref" :to="notification.note | notePage">
 							%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%
 						</router-link>
 					</div>
@@ -26,7 +26,7 @@
 						<p>%fa:retweet%
 							<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
 						</p>
-						<router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">
+						<router-link class="note-ref" :to="notification.note | notePage">
 							%fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right%
 						</router-link>
 					</div>
@@ -39,7 +39,7 @@
 						<p>%fa:quote-left%
 							<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
 						</p>
-						<router-link class="note-preview" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">{{ getNoteSummary(notification.note) }}</router-link>
+						<router-link class="note-preview" :to="notification.note | notePage">{{ getNoteSummary(notification.note) }}</router-link>
 					</div>
 				</template>
 				<template v-if="notification.type == 'follow'">
@@ -60,7 +60,7 @@
 						<p>%fa:reply%
 							<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
 						</p>
-						<router-link class="note-preview" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">{{ getNoteSummary(notification.note) }}</router-link>
+						<router-link class="note-preview" :to="notification.note | notePage">{{ getNoteSummary(notification.note) }}</router-link>
 					</div>
 				</template>
 				<template v-if="notification.type == 'mention'">
@@ -71,7 +71,7 @@
 						<p>%fa:at%
 							<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
 						</p>
-						<a class="note-preview" :href="`/@${getAcct(notification.note.user)}/${notification.note.id}`">{{ getNoteSummary(notification.note) }}</a>
+						<a class="note-preview" :href="notification.note | notePage">{{ getNoteSummary(notification.note) }}</a>
 					</div>
 				</template>
 				<template v-if="notification.type == 'poll_vote'">
@@ -80,7 +80,7 @@
 					</router-link>
 					<div class="text">
 						<p>%fa:chart-pie%<a :href="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</a></p>
-						<router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">
+						<router-link class="note-ref" :to="notification.note | notePage">
 							%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%
 						</router-link>
 					</div>
diff --git a/src/client/app/desktop/views/components/user-preview.vue b/src/client/app/desktop/views/components/user-preview.vue
index 24337eea2..bcd79dc2a 100644
--- a/src/client/app/desktop/views/components/user-preview.vue
+++ b/src/client/app/desktop/views/components/user-preview.vue
@@ -29,7 +29,6 @@
 <script lang="ts">
 import Vue from 'vue';
 import * as anime from 'animejs';
-import getAcct from '../../../../../acct/render';
 import parseAcct from '../../../../../acct/parse';
 
 export default Vue.extend({
@@ -41,8 +40,7 @@ export default Vue.extend({
 	},
 	data() {
 		return {
-			u: null,
-			getAcct
+			u: null
 		};
 	},
 	mounted() {
diff --git a/src/client/app/desktop/views/pages/user/user.friends.vue b/src/client/app/desktop/views/pages/user/user.friends.vue
index 8512e8027..4b5ec88d5 100644
--- a/src/client/app/desktop/views/pages/user/user.friends.vue
+++ b/src/client/app/desktop/views/pages/user/user.friends.vue
@@ -20,7 +20,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../../acct/render';
 
 export default Vue.extend({
 	props: ['user'],
@@ -30,9 +29,6 @@ export default Vue.extend({
 			fetching: true
 		};
 	},
-	methods: {
-		getAcct
-	},
 	mounted() {
 		(this as any).api('users/get_frequently_replied_users', {
 			userId: this.user.id,
diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue
index bc6ebae77..93d17b58f 100644
--- a/src/client/app/desktop/views/pages/welcome.vue
+++ b/src/client/app/desktop/views/pages/welcome.vue
@@ -43,7 +43,6 @@
 <script lang="ts">
 import Vue from 'vue';
 import { docsUrl, copyright, lang } from '../../../config';
-import getAcct from '../../../../../acct/render';
 
 const shares = [
 	'Everything!',
@@ -98,7 +97,6 @@ export default Vue.extend({
 		clearInterval(this.clock);
 	},
 	methods: {
-		getAcct,
 		signup() {
 			this.$modal.show('signup');
 		},
diff --git a/src/client/app/desktop/views/widgets/polls.vue b/src/client/app/desktop/views/widgets/polls.vue
index eb49a4cd5..6ce980821 100644
--- a/src/client/app/desktop/views/widgets/polls.vue
+++ b/src/client/app/desktop/views/widgets/polls.vue
@@ -5,8 +5,8 @@
 		<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="`/@${ acct }/${ poll.id }`">{{ poll.text }}</router-link></p>
-		<p v-if="!poll.text"><router-link to="`/@${ acct }/${ poll.id }`">%fa:link%</router-link></p>
+		<p v-if="poll.text"><router-link to="poll | notePage">{{ poll.text }}</router-link></p>
+		<p v-if="!poll.text"><router-link to="poll | notePage">%fa:link%</router-link></p>
 		<mk-poll :note="poll"/>
 	</div>
 	<p class="empty" v-if="!fetching && poll == null">%i18n:desktop.tags.mk-recommended-polls-home-widget.nothing%</p>
@@ -16,7 +16,6 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
-import getAcct from '../../../../../acct/render';
 
 export default define({
 	name: 'polls',
@@ -24,11 +23,6 @@ export default define({
 		compact: false
 	})
 }).extend({
-	computed: {
-		acct() {
-			return getAcct(this.poll.user);
-		},
-	},
 	data() {
 		return {
 			poll: null,
diff --git a/src/client/app/desktop/views/widgets/trends.vue b/src/client/app/desktop/views/widgets/trends.vue
index c2c7636bb..20e298730 100644
--- a/src/client/app/desktop/views/widgets/trends.vue
+++ b/src/client/app/desktop/views/widgets/trends.vue
@@ -6,8 +6,8 @@
 	</template>
 	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 	<div class="note" v-else-if="note != null">
-		<p class="text"><router-link :to="`/@${ acct }/${ note.id }`">{{ note.text }}</router-link></p>
-		<p class="author">―<router-link :to="`/@${ acct }`">@{{ acct }}</router-link></p>
+		<p class="text"><router-link :to="note | notePage">{{ note.text }}</router-link></p>
+		<p class="author">―<router-link :to="note.user | userPage">@{{ note.user | acct }}</router-link></p>
 	</div>
 	<p class="empty" v-else>%i18n:desktop.tags.mk-trends-home-widget.nothing%</p>
 </div>
@@ -15,7 +15,6 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
-import getAcct from '../../../../../acct/render';
 
 export default define({
 	name: 'trends',
@@ -23,11 +22,6 @@ export default define({
 		compact: false
 	})
 }).extend({
-	computed: {
-		acct() {
-			return getAcct(this.note.user);
-		},
-	},
 	data() {
 		return {
 			note: null,
diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue
index 5456c2c17..4f7c8968b 100644
--- a/src/client/app/mobile/views/components/notification.vue
+++ b/src/client/app/mobile/views/components/notification.vue
@@ -10,7 +10,7 @@
 				<mk-reaction-icon :reaction="notification.reaction"/>
 				<router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link>
 			</p>
-			<router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">
+			<router-link class="note-ref" :to="notification.note | notePage">
 				%fa:quote-left%{{ getNoteSummary(notification.note) }}
 				%fa:quote-right%
 			</router-link>
@@ -27,7 +27,7 @@
 				%fa:retweet%
 				<router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link>
 			</p>
-			<router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">
+			<router-link class="note-ref" :to="notification.note | notePage">
 				%fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right%
 			</router-link>
 		</div>
@@ -68,7 +68,7 @@
 				%fa:chart-pie%
 				<router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link>
 			</p>
-			<router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">
+			<router-link class="note-ref" :to="notification.note | notePage">
 				%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%
 			</router-link>
 		</div>
diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue
index fb9220ee8..f650f8aa8 100644
--- a/src/client/app/mobile/views/pages/user.vue
+++ b/src/client/app/mobile/views/pages/user.vue
@@ -30,11 +30,11 @@
 						<b>{{ user.notesCount | number }}</b>
 						<i>%i18n:mobile.tags.mk-user.notes%</i>
 					</a>
-					<a :href="`@${getAcct(user)}/following`">
+					<a :href="`${user | userPage}/following`">
 						<b>{{ user.followingCount | number }}</b>
 						<i>%i18n:mobile.tags.mk-user.following%</i>
 					</a>
-					<a :href="`@${getAcct(user)}/followers`">
+					<a :href="`${user | userPage}/followers`">
 						<b>{{ user.followersCount | number }}</b>
 						<i>%i18n:mobile.tags.mk-user.followers%</i>
 					</a>
diff --git a/src/client/app/mobile/views/pages/user/home.photos.vue b/src/client/app/mobile/views/pages/user/home.photos.vue
index 1c5926081..0e0d6926a 100644
--- a/src/client/app/mobile/views/pages/user/home.photos.vue
+++ b/src/client/app/mobile/views/pages/user/home.photos.vue
@@ -5,7 +5,7 @@
 		<a v-for="image in images"
 			class="img"
 			:style="`background-image: url(${image.media.url}?thumbnail&size=256)`"
-			:href="`/@${getAcct(image.note.user)}/${image.note.id}`"
+			:href="image.note | notePage"
 		></a>
 	</div>
 	<p class="empty" v-if="!fetching && images.length == 0">%i18n:mobile.tags.mk-user-overview-photos.no-photos%</p>
@@ -14,7 +14,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../../acct/render';
 
 export default Vue.extend({
 	props: ['user'],
@@ -24,9 +23,6 @@ export default Vue.extend({
 			images: []
 		};
 	},
-	methods: {
-		getAcct
-	},
 	mounted() {
 		(this as any).api('users/notes', {
 			userId: this.user.id,

From e90158283803b2ca93a266ac690211c98f000d7d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Apr 2018 19:23:52 +0900
Subject: [PATCH 1213/1250] Fix bug

---
 src/client/app/common/views/filters/user.ts | 4 ++--
 src/client/app/desktop/script.ts            | 2 +-
 src/client/app/mobile/script.ts             | 2 +-
 src/client/app/mobile/views/pages/user.vue  | 4 ++--
 4 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/client/app/common/views/filters/user.ts b/src/client/app/common/views/filters/user.ts
index 167bb7758..c5bb39f67 100644
--- a/src/client/app/common/views/filters/user.ts
+++ b/src/client/app/common/views/filters/user.ts
@@ -10,6 +10,6 @@ Vue.filter('userName', user => {
 	return getUserName(user);
 });
 
-Vue.filter('userPage', user => {
-	return '/@' + Vue.filter('acct')(user);
+Vue.filter('userPage', (user, path?) => {
+	return '/@' + Vue.filter('acct')(user) + (path ? '/' + path : '');
 });
diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts
index f57d42aa6..b3152e708 100644
--- a/src/client/app/desktop/script.ts
+++ b/src/client/app/desktop/script.ts
@@ -57,7 +57,7 @@ init(async (launch) => {
 			{ path: '/othello', component: MkOthello },
 			{ path: '/othello/:game', component: MkOthello },
 			{ path: '/@:user', component: MkUser },
-			{ path: '/@:user/:note', component: MkNote }
+			{ path: '/notes/:note', component: MkNote }
 		]
 	});
 
diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts
index 6265d0d45..1de489197 100644
--- a/src/client/app/mobile/script.ts
+++ b/src/client/app/mobile/script.ts
@@ -68,7 +68,7 @@ init((launch) => {
 			{ path: '/@:user', component: MkUser },
 			{ path: '/@:user/followers', component: MkFollowers },
 			{ path: '/@:user/following', component: MkFollowing },
-			{ path: '/@:user/:note', component: MkNote }
+			{ path: '/notes/:note', component: MkNote }
 		]
 	});
 
diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue
index f650f8aa8..d30fae7ba 100644
--- a/src/client/app/mobile/views/pages/user.vue
+++ b/src/client/app/mobile/views/pages/user.vue
@@ -30,11 +30,11 @@
 						<b>{{ user.notesCount | number }}</b>
 						<i>%i18n:mobile.tags.mk-user.notes%</i>
 					</a>
-					<a :href="`${user | userPage}/following`">
+					<a :href="user | userPage('following')">
 						<b>{{ user.followingCount | number }}</b>
 						<i>%i18n:mobile.tags.mk-user.following%</i>
 					</a>
-					<a :href="`${user | userPage}/followers`">
+					<a :href="user | userPage('followers')">
 						<b>{{ user.followersCount | number }}</b>
 						<i>%i18n:mobile.tags.mk-user.followers%</i>
 					</a>

From f44fad551f79aa8be548f72f7817a870ce832365 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Apr 2018 19:26:23 +0900
Subject: [PATCH 1214/1250] Fox bug

---
 src/client/app/mobile/views/components/note-detail.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue
index de32f0a74..63e28b7f5 100644
--- a/src/client/app/mobile/views/components/note-detail.vue
+++ b/src/client/app/mobile/views/components/note-detail.vue
@@ -34,7 +34,7 @@
 			</div>
 		</header>
 		<div class="body">
-			<mk-note-html v-if="p.text" :ast="p.text" :i="os.i" :class="$style.text"/>
+			<mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/>
 			<div class="tags" v-if="p.tags && p.tags.length > 0">
 				<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
 			</div>
@@ -49,7 +49,7 @@
 				<mk-note-preview :note="p.renote"/>
 			</div>
 		</div>
-		<router-link class="time" :to="`/@${pAcct}/${p.id}`">
+		<router-link class="time" :to="p | notePage">
 			<mk-time :time="p.createdAt" mode="detail"/>
 		</router-link>
 		<footer>

From f9c5cb2639e918eb5bd8df1408d544d1d9070ee1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Apr 2018 19:28:16 +0900
Subject: [PATCH 1215/1250] Fix bug

---
 .../app/common/views/components/messaging-room.message.vue      | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue
index fdb820a4a..60e5258b6 100644
--- a/src/client/app/common/views/components/messaging-room.message.vue
+++ b/src/client/app/common/views/components/messaging-room.message.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="message" :data-is-me="isMe">
-	<router-link class="avatar-anchor" :to="message.user | userPage" :title="acct" target="_blank">
+	<router-link class="avatar-anchor" :to="message.user | userPage" :title="message.user | acct" target="_blank">
 		<img class="avatar" :src="`${message.user.avatarUrl}?thumbnail&size=80`" alt=""/>
 	</router-link>
 	<div class="content">

From d39a8be887d8d93263a5d145a326fe3b7f3229de Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Apr 2018 19:29:00 +0900
Subject: [PATCH 1216/1250] v4737

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index eb237a3d2..9f10f1cdf 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4703",
+	"version": "0.0.4737",
 	"codename": "nighthike",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",

From bb7cbfc08183479c0fd47f0621c7205b02207a17 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Apr 2018 19:40:11 +0900
Subject: [PATCH 1217/1250] Disable en

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

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
 };
 

From a2014d802153574c17264fcb567da7644f23a22a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Apr 2018 19:52:17 +0900
Subject: [PATCH 1218/1250] Fix bug

---
 src/client/app/common/views/components/messaging.vue    | 4 +++-
 src/client/app/desktop/views/pages/user/user.header.vue | 6 +++---
 2 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue
index 05bf88974..4da842f5d 100644
--- a/src/client/app/common/views/components/messaging.vue
+++ b/src/client/app/common/views/components/messaging.vue
@@ -24,7 +24,7 @@
 		<template>
 			<a v-for="message in messages"
 				class="user"
-				:href="`/i/messaging/${isMe(message) ? message.recipient : message.user | acct}`"
+				:href="`/i/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
 				:data-is-me="isMe(message)"
 				:data-is-read="message.isRead"
 				@click.prevent="navigate(isMe(message) ? message.recipient : message.user)"
@@ -51,6 +51,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import getAcct from '../../../../../acct/render';
 
 export default Vue.extend({
 	props: {
@@ -92,6 +93,7 @@ export default Vue.extend({
 		(this as any).os.streams.messagingIndexStream.dispose(this.connectionId);
 	},
 	methods: {
+		getAcct,
 		isMe(message) {
 			return message.userId == (this as any).os.i.id;
 		},
diff --git a/src/client/app/desktop/views/pages/user/user.header.vue b/src/client/app/desktop/views/pages/user/user.header.vue
index 67e52d173..e026820b3 100644
--- a/src/client/app/desktop/views/pages/user/user.header.vue
+++ b/src/client/app/desktop/views/pages/user/user.header.vue
@@ -12,9 +12,9 @@
 			<p class="location" v-if="user.host === null && user.profile.location">%fa:map-marker%{{ user.profile.location }}</p>
 		</div>
 		<footer>
-			<router-link :to="`/@${acct}`" :data-active="$parent.page == 'home'">%fa:home%概要</router-link>
-			<router-link :to="`/@${acct}/media`" :data-active="$parent.page == 'media'">%fa:image%メディア</router-link>
-			<router-link :to="`/@${acct}/graphs`" :data-active="$parent.page == 'graphs'">%fa:chart-bar%グラフ</router-link>
+			<router-link :to="user | userPage" :data-active="$parent.page == 'home'">%fa:home%概要</router-link>
+			<router-link :to="user | userPage('media')" :data-active="$parent.page == 'media'">%fa:image%メディア</router-link>
+			<router-link :to="user | userPage('graphs')" :data-active="$parent.page == 'graphs'">%fa:chart-bar%グラフ</router-link>
 		</footer>
 	</div>
 </div>

From 87740e89864ad391e7fe5c622d4e950680d9aca8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Apr 2018 19:53:15 +0900
Subject: [PATCH 1219/1250] Clean up

---
 .../app/desktop/views/components/ui.header.nav.vue     | 10 +---------
 src/client/app/mobile/views/components/ui.nav.vue      |  1 -
 2 files changed, 1 insertion(+), 10 deletions(-)

diff --git a/src/client/app/desktop/views/components/ui.header.nav.vue b/src/client/app/desktop/views/components/ui.header.nav.vue
index 7582e8afc..5d7ae0a4e 100644
--- a/src/client/app/desktop/views/components/ui.header.nav.vue
+++ b/src/client/app/desktop/views/components/ui.header.nav.vue
@@ -23,19 +23,12 @@
 				</a>
 			</li>
 		</template>
-		<li class="ch">
-			<a :href="chUrl" target="_blank">
-				%fa:tv%
-				<p>%i18n:desktop.tags.mk-ui-header-nav.ch%</p>
-			</a>
-		</li>
 	</ul>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-import { chUrl } from '../../../config';
 import MkMessagingWindow from './messaging-window.vue';
 import MkGameWindow from './game-window.vue';
 
@@ -45,8 +38,7 @@ export default Vue.extend({
 			hasUnreadMessagingMessages: false,
 			hasGameInvitations: false,
 			connection: null,
-			connectionId: null,
-			chUrl
+			connectionId: null
 		};
 	},
 	mounted() {
diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue
index 764f9374e..f96e28540 100644
--- a/src/client/app/mobile/views/components/ui.nav.vue
+++ b/src/client/app/mobile/views/components/ui.nav.vue
@@ -21,7 +21,6 @@
 					<li><router-link to="/othello">%fa:gamepad%ゲーム<template v-if="hasGameInvitations">%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 to="/i/drive">%fa:cloud%%i18n:mobile.tags.mk-ui-nav.drive%%fa:angle-right%</router-link></li>
 				</ul>
 				<ul>

From e9114bf50c2edd530e027e9a12b99ce5a75ace62 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Apr 2018 19:53:39 +0900
Subject: [PATCH 1220/1250] v4741

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 9f10f1cdf..4dd1da072 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4737",
+	"version": "0.0.4741",
 	"codename": "nighthike",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",

From fe96f2f761d17d93b009d2ceec5fc4356f6defe0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Apr 2018 20:00:30 +0900
Subject: [PATCH 1221/1250] oops

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

diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue
index 4da842f5d..e6c32f80d 100644
--- a/src/client/app/common/views/components/messaging.vue
+++ b/src/client/app/common/views/components/messaging.vue
@@ -33,7 +33,7 @@
 				<div>
 					<img class="avatar" :src="`${isMe(message) ? message.recipient.avatarUrl : message.user.avatarUrl}?thumbnail&size=64`" alt=""/>
 					<header>
-						<span class="name">{{ isMe(message) ? message.recipient : message.use | userName }}</span>
+						<span class="name">{{ isMe(message) ? message.recipient : message.user | userName }}</span>
 						<span class="username">@{{ isMe(message) ? message.recipient : message.user | acct }}</span>
 						<mk-time :time="message.createdAt"/>
 					</header>

From 0c0c37108b3eadc07afec5c7c60edec46c67c58d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Apr 2018 20:01:30 +0900
Subject: [PATCH 1222/1250] v4743

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 4dd1da072..4b3d79944 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4741",
+	"version": "0.0.4743",
 	"codename": "nighthike",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",

From 5ea78e344027110ccd90beaa60d92a3fb5264b52 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Apr 2018 20:03:38 +0900
Subject: [PATCH 1223/1250] Fix bug

---
 src/server/api/service/twitter.ts | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/server/api/service/twitter.ts b/src/server/api/service/twitter.ts
index e51ce92ba..e5239fa17 100644
--- a/src/server/api/service/twitter.ts
+++ b/src/server/api/service/twitter.ts
@@ -70,7 +70,8 @@ module.exports = (app: express.Application) => {
 
 	const twAuth = autwh({
 		consumerKey: config.twitter.consumer_key,
-		consumerSecret: config.twitter.consumer_secret
+		consumerSecret: config.twitter.consumer_secret,
+		callbackUrl: `${config.url}/api/tw/cb`
 	});
 
 	app.get('/connect/twitter', async (req, res): Promise<any> => {

From e27cb22ad0c413d4eae625e1a9abca025c43931b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 10 Apr 2018 02:04:30 +0900
Subject: [PATCH 1224/1250] Fix bug

---
 src/client/app/mobile/views/pages/user.vue | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue
index d30fae7ba..3d9fbda94 100644
--- a/src/client/app/mobile/views/pages/user.vue
+++ b/src/client/app/mobile/views/pages/user.vue
@@ -208,11 +208,13 @@ main
 						font-size 14px
 
 	> nav
+		position -webkit-sticky
 		position sticky
 		top 48px
 		box-shadow 0 4px 4px rgba(0, 0, 0, 0.3)
 		background-color #313a42
 		z-index 1
+
 		> .nav-container
 			display flex
 			justify-content center

From f33b5894012bc51c59270ce583b58664873a6960 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 10 Apr 2018 02:12:17 +0900
Subject: [PATCH 1225/1250] Refactor

---
 src/queue/processors/http/deliver.ts    | 2 +-
 src/remote/{ => activitypub}/request.ts | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)
 rename src/remote/{ => activitypub}/request.ts (91%)

diff --git a/src/queue/processors/http/deliver.ts b/src/queue/processors/http/deliver.ts
index 422e355b5..cf843fad0 100644
--- a/src/queue/processors/http/deliver.ts
+++ b/src/queue/processors/http/deliver.ts
@@ -1,6 +1,6 @@
 import * as kue from 'kue';
 
-import request from '../../../remote/request';
+import request from '../../../remote/activitypub/request';
 
 export default async (job: kue.Job, done): Promise<void> => {
 	try {
diff --git a/src/remote/request.ts b/src/remote/activitypub/request.ts
similarity index 91%
rename from src/remote/request.ts
rename to src/remote/activitypub/request.ts
index 81e7c05c7..85f43eb91 100644
--- a/src/remote/request.ts
+++ b/src/remote/activitypub/request.ts
@@ -3,8 +3,8 @@ import { sign } from 'http-signature';
 import { URL } from 'url';
 import * as debug from 'debug';
 
-import config from '../config';
-import { ILocalUser } from '../models/user';
+import config from '../../config';
+import { ILocalUser } from '../../models/user';
 
 const log = debug('misskey:activitypub:deliver');
 

From 19edff6054053ec141b2a9886743ac680c49dbda Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 10 Apr 2018 02:20:06 +0900
Subject: [PATCH 1226/1250] Refactor

---
 src/services/drive/upload-from-url.ts | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/services/drive/upload-from-url.ts b/src/services/drive/upload-from-url.ts
index 676586cd1..b3bb47033 100644
--- a/src/services/drive/upload-from-url.ts
+++ b/src/services/drive/upload-from-url.ts
@@ -19,7 +19,7 @@ export default async (url, user, folderId = null, uri = null): Promise<IDriveFil
 	log(`name: ${name}`);
 
 	// Create temp file
-	const path = await new Promise((res: (string) => void, rej) => {
+	const path = await new Promise<string>((res, rej) => {
 		tmp.file((e, path) => {
 			if (e) return rej(e);
 			res(path);
@@ -44,8 +44,8 @@ export default async (url, user, folderId = null, uri = null): Promise<IDriveFil
 	log(`created: ${driveFile._id}`);
 
 	// clean-up
-	fs.unlink(path, (e) => {
-		if (e) log(e.stack);
+	fs.unlink(path, e => {
+		if (e) console.error(e);
 	});
 
 	return driveFile;

From b7359694abc14fcca663dc771db0b46d7b8bf143 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 10 Apr 2018 02:34:47 +0900
Subject: [PATCH 1227/1250] Refactor

---
 src/services/drive/add-file.ts | 54 +++++++++++++---------------------
 1 file changed, 21 insertions(+), 33 deletions(-)

diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts
index 64a2f1834..4889b357a 100644
--- a/src/services/drive/add-file.ts
+++ b/src/services/drive/add-file.ts
@@ -10,12 +10,12 @@ import * as debug from 'debug';
 import fileType = require('file-type');
 import prominence = require('prominence');
 
-import DriveFile, { IMetadata, getGridFSBucket } from '../../models/drive-file';
+import DriveFile, { IMetadata, getGridFSBucket, IDriveFile } from '../../models/drive-file';
 import DriveFolder from '../../models/drive-folder';
 import { pack } from '../../models/drive-file';
 import event, { publishDriveStream } from '../../publishers/stream';
 import getAcct from '../../acct/render';
-import config from '../../config';
+import { IUser } from '../../models/user';
 
 const gm = _gm.subClass({
 	imageMagick: true
@@ -34,20 +34,20 @@ const addToGridFS = (name: string, readable: stream.Readable, type: string, meta
 	getGridFSBucket()
 		.then(bucket => new Promise((resolve, reject) => {
 			const writeStream = bucket.openUploadStream(name, { contentType: type, metadata });
-			writeStream.once('finish', (doc) => { resolve(doc); });
+			writeStream.once('finish', resolve);
 			writeStream.on('error', reject);
 			readable.pipe(writeStream);
 		}));
 
 const addFile = async (
-	user: any,
+	user: IUser,
 	path: string,
 	name: string = null,
 	comment: string = null,
 	folderId: mongodb.ObjectID = null,
 	force: boolean = false,
 	uri: string = null
-) => {
+): Promise<IDriveFile> => {
 	log(`registering ${name} (user: ${getAcct(user)}, path: ${path})`);
 
 	// Calculate hash, get content type and get file size
@@ -251,13 +251,13 @@ const addFile = async (
  * @return Object that represents added file
  */
 export default (user: any, file: string | stream.Readable, ...args) => new Promise<any>((resolve, reject) => {
+	const isStream = typeof file === 'object' && typeof file.read === 'function';
+
 	// Get file path
-	new Promise((res: (v: [string, boolean]) => void, rej) => {
+	new Promise<string>((res, rej) => {
 		if (typeof file === 'string') {
-			res([file, false]);
-			return;
-		}
-		if (typeof file === 'object' && typeof file.read === 'function') {
+			res(file);
+		} else if (isStream) {
 			tmpFile()
 				.then(path => {
 					const readable: stream.Readable = file;
@@ -265,22 +265,23 @@ export default (user: any, file: string | stream.Readable, ...args) => new Promi
 					readable
 						.on('error', rej)
 						.on('end', () => {
-							res([path, true]);
+							res(path);
 						})
 						.pipe(writable)
 						.on('error', rej);
 				})
 				.catch(rej);
+		} else {
+			rej(new Error('un-compatible file.'));
 		}
-		rej(new Error('un-compatible file.'));
 	})
-	.then(([path, shouldCleanup]): Promise<any> => new Promise((res, rej) => {
+	.then(path => new Promise<IDriveFile>((res, rej) => {
 		addFile(user, path, ...args)
 			.then(file => {
 				res(file);
-				if (shouldCleanup) {
-					fs.unlink(path, (e) => {
-						if (e) log(e.stack);
+				if (isStream) {
+					fs.unlink(path, e => {
+						if (e) console.error(e.stack);
 					});
 				}
 			})
@@ -288,26 +289,13 @@ export default (user: any, file: string | stream.Readable, ...args) => new Promi
 	}))
 	.then(file => {
 		log(`drive file has been created ${file._id}`);
+
 		resolve(file);
 
-		pack(file).then(serializedFile => {
+		pack(file).then(packedFile => {
 			// Publish drive_file_created event
-			event(user._id, 'drive_file_created', serializedFile);
-			publishDriveStream(user._id, 'file_created', serializedFile);
-
-			// Register to search database
-			if (config.elasticsearch.enable) {
-				const es = require('../db/elasticsearch');
-				es.index({
-					index: 'misskey',
-					type: 'drive_file',
-					id: file._id.toString(),
-					body: {
-						name: file.name,
-						userId: user._id.toString()
-					}
-				});
-			}
+			event(user._id, 'drive_file_created', packedFile);
+			publishDriveStream(user._id, 'file_created', packedFile);
 		});
 	})
 	.catch(reject);

From c258fd35e34a01456fc850819e160416f5b53844 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 10 Apr 2018 02:36:28 +0900
Subject: [PATCH 1228/1250] :v:

---
 src/server/file/index.ts | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/server/file/index.ts b/src/server/file/index.ts
index 062d260cb..8d21b0ba4 100644
--- a/src/server/file/index.ts
+++ b/src/server/file/index.ts
@@ -83,7 +83,6 @@ function thumbnail(data: stream.Readable, type: string, resize: number): ISend {
 		.compress('jpeg')
 		.quality(80)
 		.interlace('line')
-		.noProfile() // Remove EXIF
 		.stream();
 
 	return {

From 8f982e942de935b2153dd60600b8c1ffa064fadf Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 10 Apr 2018 02:38:48 +0900
Subject: [PATCH 1229/1250] Fix bug

---
 src/server/api/service/github.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts
index 6b1d5d25b..bc8d3c6a7 100644
--- a/src/server/api/service/github.ts
+++ b/src/server/api/service/github.ts
@@ -11,7 +11,7 @@ module.exports = async (app: express.Application) => {
 	if (config.github_bot == null) return;
 
 	const bot = await User.findOne({
-		username_lower: config.github_bot.username.toLowerCase()
+		usernameLower: config.github_bot.username.toLowerCase()
 	});
 
 	if (bot == null) {

From 3d0bfde659719d924cbcd2c6d489692b9e3856a9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 10 Apr 2018 02:39:40 +0900
Subject: [PATCH 1230/1250] v4751

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 4b3d79944..3a1c5d3c4 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4743",
+	"version": "0.0.4751",
 	"codename": "nighthike",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",

From 9e1cb772d44fb372711fa1e9ed2e4432c963d6f9 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 9 Apr 2018 17:59:18 +0000
Subject: [PATCH 1231/1250] fix(package): update @types/mongodb to version
 3.0.11

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 3a1c5d3c4..ef3432a2e 100644
--- a/package.json
+++ b/package.json
@@ -60,7 +60,7 @@
 		"@types/license-checker": "15.0.0",
 		"@types/mkdirp": "0.5.2",
 		"@types/mocha": "5.0.0",
-		"@types/mongodb": "3.0.10",
+		"@types/mongodb": "3.0.11",
 		"@types/monk": "6.0.0",
 		"@types/morgan": "1.7.35",
 		"@types/ms": "0.7.30",

From e2ce2e1ed24051ca4d0fcd4d5e9c99be4f99391d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 10 Apr 2018 04:02:25 +0900
Subject: [PATCH 1232/1250] Refactor

---
 src/services/drive/add-file.ts        | 22 +++++++++-------------
 src/services/drive/upload-from-url.ts | 10 ++++------
 2 files changed, 13 insertions(+), 19 deletions(-)

diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts
index 4889b357a..30aae24ba 100644
--- a/src/services/drive/add-file.ts
+++ b/src/services/drive/add-file.ts
@@ -23,10 +23,10 @@ const gm = _gm.subClass({
 
 const log = debug('misskey:drive:add-file');
 
-const tmpFile = (): Promise<string> => new Promise((resolve, reject) => {
-	tmp.file((e, path) => {
+const tmpFile = (): Promise<[string, any]> => new Promise((resolve, reject) => {
+	tmp.file((e, path, fd, cleanup) => {
 		if (e) return reject(e);
-		resolve(path);
+		resolve([path, cleanup]);
 	});
 });
 
@@ -254,18 +254,18 @@ export default (user: any, file: string | stream.Readable, ...args) => new Promi
 	const isStream = typeof file === 'object' && typeof file.read === 'function';
 
 	// Get file path
-	new Promise<string>((res, rej) => {
+	new Promise<[string, any]>((res, rej) => {
 		if (typeof file === 'string') {
-			res(file);
+			res([file, null]);
 		} else if (isStream) {
 			tmpFile()
-				.then(path => {
+				.then(([path, cleanup]) => {
 					const readable: stream.Readable = file;
 					const writable = fs.createWriteStream(path);
 					readable
 						.on('error', rej)
 						.on('end', () => {
-							res(path);
+							res([path, cleanup]);
 						})
 						.pipe(writable)
 						.on('error', rej);
@@ -275,15 +275,11 @@ export default (user: any, file: string | stream.Readable, ...args) => new Promi
 			rej(new Error('un-compatible file.'));
 		}
 	})
-	.then(path => new Promise<IDriveFile>((res, rej) => {
+	.then(([path, cleanup]) => new Promise<IDriveFile>((res, rej) => {
 		addFile(user, path, ...args)
 			.then(file => {
 				res(file);
-				if (isStream) {
-					fs.unlink(path, e => {
-						if (e) console.error(e.stack);
-					});
-				}
+				if (cleanup) cleanup();
 			})
 			.catch(rej);
 	}))
diff --git a/src/services/drive/upload-from-url.ts b/src/services/drive/upload-from-url.ts
index b3bb47033..fc8805260 100644
--- a/src/services/drive/upload-from-url.ts
+++ b/src/services/drive/upload-from-url.ts
@@ -19,10 +19,10 @@ export default async (url, user, folderId = null, uri = null): Promise<IDriveFil
 	log(`name: ${name}`);
 
 	// Create temp file
-	const path = await new Promise<string>((res, rej) => {
-		tmp.file((e, path) => {
+	const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
+		tmp.file((e, path, fd, cleanup) => {
 			if (e) return rej(e);
-			res(path);
+			res([path, cleanup]);
 		});
 	});
 
@@ -44,9 +44,7 @@ export default async (url, user, folderId = null, uri = null): Promise<IDriveFil
 	log(`created: ${driveFile._id}`);
 
 	// clean-up
-	fs.unlink(path, e => {
-		if (e) console.error(e);
-	});
+	cleanup();
 
 	return driveFile;
 };

From 3eee92aae4762a27cf9cc7e10f6c8f2625bb96d5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 10 Apr 2018 04:03:25 +0900
Subject: [PATCH 1233/1250] Refactor

---
 src/services/drive/upload-from-url.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/services/drive/upload-from-url.ts b/src/services/drive/upload-from-url.ts
index fc8805260..a741cbda4 100644
--- a/src/services/drive/upload-from-url.ts
+++ b/src/services/drive/upload-from-url.ts
@@ -33,7 +33,7 @@ export default async (url, user, folderId = null, uri = null): Promise<IDriveFil
 			.on('error', rej)
 			.on('end', () => {
 				writable.close();
-				res(path);
+				res();
 			})
 			.pipe(writable)
 			.on('error', rej);

From b72d7e9fe5cb9a12e098d0609142304d437bb032 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 10 Apr 2018 04:11:52 +0900
Subject: [PATCH 1234/1250] Fix bug

---
 src/services/drive/upload-from-url.ts | 17 ++++++++++++++---
 1 file changed, 14 insertions(+), 3 deletions(-)

diff --git a/src/services/drive/upload-from-url.ts b/src/services/drive/upload-from-url.ts
index a741cbda4..08e039770 100644
--- a/src/services/drive/upload-from-url.ts
+++ b/src/services/drive/upload-from-url.ts
@@ -39,12 +39,23 @@ export default async (url, user, folderId = null, uri = null): Promise<IDriveFil
 			.on('error', rej);
 	});
 
-	const driveFile = await create(user, path, name, null, folderId, false, uri);
+	let driveFile: IDriveFile;
+	let error;
 
-	log(`created: ${driveFile._id}`);
+	try {
+		driveFile = await create(user, path, name, null, folderId, false, uri);
+		log(`created: ${driveFile._id}`);
+	} catch (e) {
+		error = e;
+		log(`failed: ${e}`);
+	}
 
 	// clean-up
 	cleanup();
 
-	return driveFile;
+	if (error) {
+		throw error;
+	} else {
+		return driveFile;
+	}
 };

From acaec82841474df419fcb3b3dfdfed06b9b547db Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 10 Apr 2018 04:15:20 +0900
Subject: [PATCH 1235/1250] Fix bug

---
 src/server/api/endpoints/notes.ts | 9 +++------
 1 file changed, 3 insertions(+), 6 deletions(-)

diff --git a/src/server/api/endpoints/notes.ts b/src/server/api/endpoints/notes.ts
index 3e3b67a66..a70ac0588 100644
--- a/src/server/api/endpoints/notes.ts
+++ b/src/server/api/endpoints/notes.ts
@@ -5,10 +5,7 @@ import $ from 'cafy';
 import Note, { pack } from '../../../models/note';
 
 /**
- * Lists all notes
- *
- * @param {any} params
- * @return {Promise<any>}
+ * Get all notes
  */
 module.exports = (params) => new Promise(async (res, rej) => {
 	// Get 'reply' parameter
@@ -73,7 +70,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
 	}
 
 	if (media != undefined) {
-		query.mediaIds = media ? { $exists: true, $ne: null } : null;
+		query.mediaIds = media ? { $exists: true, $ne: null } : [];
 	}
 
 	if (poll != undefined) {
@@ -93,5 +90,5 @@ module.exports = (params) => new Promise(async (res, rej) => {
 		});
 
 	// Serialize
-	res(await Promise.all(notes.map(async note => await pack(note))));
+	res(await Promise.all(notes.map(note => pack(note))));
 });

From 0ad79194dab76e0c48c84e48b4d83a8bdaec73c2 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 9 Apr 2018 19:23:46 +0000
Subject: [PATCH 1236/1250] fix(package): update mongodb to version 3.0.6

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index ef3432a2e..a7bfc3218 100644
--- a/package.json
+++ b/package.json
@@ -148,7 +148,7 @@
 		"mkdirp": "0.5.1",
 		"mocha": "5.0.5",
 		"moji": "0.5.1",
-		"mongodb": "3.0.5",
+		"mongodb": "3.0.6",
 		"monk": "6.0.5",
 		"morgan": "1.9.0",
 		"ms": "2.1.1",

From 75508f5e6f9adcde966ef82a94fb3b6c73ee5d1d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 10 Apr 2018 04:38:02 +0900
Subject: [PATCH 1237/1250] =?UTF-8?q?=E5=86=8D=E5=B8=B0=E7=9A=84=E3=81=ABN?=
 =?UTF-8?q?ote=E3=82=92=E3=83=AC=E3=83=B3=E3=83=80=E3=83=AA=E3=83=B3?=
 =?UTF-8?q?=E3=82=B0=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/remote/activitypub/renderer/note.ts | 14 +++++++++++---
 1 file changed, 11 insertions(+), 3 deletions(-)

diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts
index 7cc388dc3..c364b1324 100644
--- a/src/remote/activitypub/renderer/note.ts
+++ b/src/remote/activitypub/renderer/note.ts
@@ -5,7 +5,7 @@ import DriveFile from '../../../models/drive-file';
 import Note, { INote } from '../../../models/note';
 import User from '../../../models/user';
 
-export default async (note: INote) => {
+export default async function renderNote(note: INote, dive = true) {
 	const promisedFiles = note.mediaIds
 		? DriveFile.find({ _id: { $in: note.mediaIds } })
 		: Promise.resolve([]);
@@ -23,7 +23,15 @@ export default async (note: INote) => {
 			});
 
 			if (inReplyToUser !== null) {
-				inReplyTo = inReplyToNote.uri || `${config.url}/notes/${inReplyToNote._id}`;
+				if (inReplyToNote.uri) {
+					inReplyTo = inReplyToNote.uri;
+				} else {
+					if (dive) {
+						inReplyTo = await renderNote(inReplyToNote, false);
+					} else {
+						inReplyTo = `${config.url}/notes/${inReplyToNote._id}`;
+					}
+				}
 			}
 		}
 	} else {
@@ -48,4 +56,4 @@ export default async (note: INote) => {
 		attachment: (await promisedFiles).map(renderDocument),
 		tag: (note.tags || []).map(renderHashtag)
 	};
-};
+}

From 7452819f004016c4c56d2e30edac42b99bb3ca25 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 10 Apr 2018 05:02:44 +0900
Subject: [PATCH 1238/1250] Add todo

---
 src/models/user.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/models/user.ts b/src/models/user.ts
index b05bb8812..adc9e6da9 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -167,6 +167,8 @@ export const pack = (
 		_user = deepcopy(user);
 	}
 
+	// TODO: ここでエラーにするのではなくダミーのユーザーデータを返す
+	// SEE: https://github.com/syuilo/misskey/issues/1432
 	if (!_user) return reject('invalid user arg.');
 
 	// Me

From 0e2ce826d8bc60e679b09d145f4eed4c4fcb1175 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 9 Apr 2018 23:40:00 +0000
Subject: [PATCH 1239/1250] fix(package): update @types/mongodb to version
 3.0.12

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index a7bfc3218..2a60a794b 100644
--- a/package.json
+++ b/package.json
@@ -60,7 +60,7 @@
 		"@types/license-checker": "15.0.0",
 		"@types/mkdirp": "0.5.2",
 		"@types/mocha": "5.0.0",
-		"@types/mongodb": "3.0.11",
+		"@types/mongodb": "3.0.12",
 		"@types/monk": "6.0.0",
 		"@types/morgan": "1.7.35",
 		"@types/ms": "0.7.30",

From 8970554a411ca5171d682c0d31b17aa30a5d085c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 10 Apr 2018 18:45:44 +0900
Subject: [PATCH 1240/1250] oops

---
 src/client/docs/layout.pug | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/client/docs/layout.pug b/src/client/docs/layout.pug
index 9dfd0ab7a..29d2a3ff6 100644
--- a/src/client/docs/layout.pug
+++ b/src/client/docs/layout.pug
@@ -16,18 +16,18 @@ html(lang= lang)
 		nav
 			ul
 				each doc in common.docs
-					li: a(href=`/${lang}/${doc.name}`)= doc.title[lang] || doc.title['ja']
+					li: a(href=`/docs/${lang}/${doc.name}`)= doc.title[lang] || doc.title['ja']
 			section
 				h2 API
 				ul
 					li Entities
 						ul
 							each entity in common.entities
-								li: a(href=`/${lang}/api/entities/${common.kebab(entity)}`)= entity
+								li: a(href=`/docs/${lang}/api/entities/${common.kebab(entity)}`)= entity
 					li Endpoints
 						ul
 							each endpoint in common.endpoints
-								li: a(href=`/${lang}/api/endpoints/${common.kebab(endpoint)}`)= endpoint
+								li: a(href=`/docs/${lang}/api/endpoints/${common.kebab(endpoint)}`)= endpoint
 		main
 			article
 				block main

From c3f3bd9d8056ef0c19931acdc62e373cf00b9117 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 10 Apr 2018 21:47:56 +0900
Subject: [PATCH 1241/1250] Fix #1437 and some clean ups

---
 src/server/api/endpoints/following/create.ts |  4 ----
 src/server/api/endpoints/following/delete.ts | 20 +++++---------------
 2 files changed, 5 insertions(+), 19 deletions(-)

diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts
index 0a642f50b..27e5eb31d 100644
--- a/src/server/api/endpoints/following/create.ts
+++ b/src/server/api/endpoints/following/create.ts
@@ -8,10 +8,6 @@ import create from '../../../../services/following/create';
 
 /**
  * Follow a user
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	const follower = user;
diff --git a/src/server/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts
index 0d0a6c713..ca0703ca2 100644
--- a/src/server/api/endpoints/following/delete.ts
+++ b/src/server/api/endpoints/following/delete.ts
@@ -4,14 +4,10 @@
 import $ from 'cafy';
 import User from '../../../../models/user';
 import Following from '../../../../models/following';
-import { createHttp } from '../../../../queue';
+import deleteFollowing from '../../../../services/following/delete';
 
 /**
  * Unfollow a user
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	const follower = user;
@@ -49,15 +45,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		return rej('already not following');
 	}
 
-	createHttp({
-		type: 'unfollow',
-		id: exist._id
-	}).save(error => {
-		if (error) {
-			return rej('unfollow failed');
-		}
+	// Delete following
+	deleteFollowing(follower, followee);
 
-		// Send response
-		res();
-	});
+	// Send response
+	res();
 });

From 99b4602460be170ce5a4184a810422d99cfb5399 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 11 Apr 2018 01:26:05 +0900
Subject: [PATCH 1242/1250] Add recover.html again

---
 src/client/assets/recover.html | 28 ++++++++++++++++++++++++++++
 1 file changed, 28 insertions(+)
 create mode 100644 src/client/assets/recover.html

diff --git a/src/client/assets/recover.html b/src/client/assets/recover.html
new file mode 100644
index 000000000..b1889c72e
--- /dev/null
+++ b/src/client/assets/recover.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+
+<html>
+	<head>
+		<meta charset="utf-8">
+		<title>Misskeyのリカバリ</title>
+		<script>
+			const yn = window.confirm('キャッシュをクリアしますか?(他のタブでMisskeyを開いている状態だと正常にクリアできないので、他のMisskeyのタブをすべて閉じてから行ってください)\n\nDo you want to clear caches? (Please close all other Misskey tabs before clear cache)');
+			if (yn) {
+				try {
+					navigator.serviceWorker.controller.postMessage('clear');
+					navigator.serviceWorker.getRegistrations().then(registrations => {
+						registrations.forEach(registration => registration.unregister());
+					});
+				} catch (e) {
+					console.error(e);
+				}
+				alert('キャッシュをクリアしました。\n\ncache cleared.');
+				alert('まもなくページを再度読み込みします。再度読み込みが終わると、再度キャッシュをクリアするか尋ねられるので、「キャンセル」を選択して抜けてください。\n\nWe will reload the page shortly. After that, you are asked whether you want to clear the cache again, so please select "Cancel" and exit.');
+				setTimeout(() => {
+					location.reload(true);
+				}, 100);
+			} else {
+				location.href = '/';
+			}
+		</script>
+	</head>
+</html>

From a35a6398e3971ba7c53ad339a7e4baabc100f6be Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 11 Apr 2018 01:41:35 +0900
Subject: [PATCH 1243/1250] Add new recover script

---
 src/client/assets/version.html | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)
 create mode 100644 src/client/assets/version.html

diff --git a/src/client/assets/version.html b/src/client/assets/version.html
new file mode 100644
index 000000000..f80cbcbd1
--- /dev/null
+++ b/src/client/assets/version.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+
+<html>
+	<head>
+		<meta charset="utf-8">
+		<title>Misskeyのリカバリ</title>
+		<script>
+			const v = window.prompt('Enter version');
+			if (v) {
+				localStorage.setItem('v', v);
+				setTimeout(() => {
+					location.reload(true);
+				}, 500);
+			} else {
+				location.href = '/';
+			}
+		</script>
+	</head>
+</html>

From 75c6694604340abc86c7af6432528e286aa645c4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 11 Apr 2018 11:48:04 +0900
Subject: [PATCH 1244/1250] Update recovery scripts

---
 src/client/assets/404.js       | 10 +++++++---
 src/client/assets/version.html | 16 ++++++++++------
 2 files changed, 17 insertions(+), 9 deletions(-)

diff --git a/src/client/assets/404.js b/src/client/assets/404.js
index f897f0db6..9e498fe7c 100644
--- a/src/client/assets/404.js
+++ b/src/client/assets/404.js
@@ -1,5 +1,11 @@
 const yn = window.confirm(
-	'サーバー上に存在しないスクリプトがリクエストされました。お使いのMisskeyのバージョンが古いことが原因の可能性があります。Misskeyを更新しますか?');
+	'サーバー上に存在しないスクリプトがリクエストされました。お使いのMisskeyのバージョンが古いことが原因の可能性があります。Misskeyを更新しますか?\n\nA script that does not exist on the server was requested. It may be caused by an old version of Misskey you’re using. Do you want to delete the cache?');
+
+const langYn = window.confirm('また、言語を日本語に設定すると解決する場合があります。日本語に設定しますか?\n\nAlso, setting the language to Japanese may solve the problem. Would you like to set it to Japanese?');
+
+if (langYn) {
+	localStorage.setItem('lang', 'ja');
+}
 
 if (yn) {
 	// Clear cache (serive worker)
@@ -16,6 +22,4 @@ if (yn) {
 	localStorage.removeItem('v');
 
 	location.reload(true);
-} else {
-	alert('問題が解決しない場合はサーバー管理者までお問い合せください。');
 }
diff --git a/src/client/assets/version.html b/src/client/assets/version.html
index f80cbcbd1..d8a98279a 100644
--- a/src/client/assets/version.html
+++ b/src/client/assets/version.html
@@ -5,15 +5,19 @@
 		<meta charset="utf-8">
 		<title>Misskeyのリカバリ</title>
 		<script>
-			const v = window.prompt('Enter version');
+			const v = window.prompt('Enter version:');
 			if (v) {
 				localStorage.setItem('v', v);
-				setTimeout(() => {
-					location.reload(true);
-				}, 500);
-			} else {
-				location.href = '/';
 			}
+
+			const lang = window.prompt('Enter language (optional):');
+			if (lang && lang.length > 0) {
+				localStorage.setItem('lang', lang);
+			}
+
+			setTimeout(() => {
+				location.href = '/';
+			}, 500);
 		</script>
 	</head>
 </html>

From 57edfa980fdf0248a10c40d76b2bc5bf969da821 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 11 Apr 2018 11:48:46 +0900
Subject: [PATCH 1245/1250] v4771

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 2a60a794b..1e0e50551 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "0.0.4751",
+	"version": "0.0.4771",
 	"codename": "nighthike",
 	"license": "MIT",
 	"description": "A miniblog-based SNS",

From 5ba55d6d9becdbba3e0615ca60d04f1e24d7e6a1 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 11 Apr 2018 15:54:41 +0900
Subject: [PATCH 1246/1250] Update sw.js

---
 src/client/app/sw.js | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/client/app/sw.js b/src/client/app/sw.js
index 669703b16..19d2ed06e 100644
--- a/src/client/app/sw.js
+++ b/src/client/app/sw.js
@@ -43,8 +43,6 @@ self.addEventListener('fetch', ev => {
 
 // プッシュ通知を受け取ったとき
 self.addEventListener('push', ev => {
-	console.log('pushed');
-
 	// クライアント取得
 	ev.waitUntil(self.clients.matchAll({
 		includeUncontrolled: true

From 16870b86b0c17e30d97146eb1c63e8dd2a873519 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 11 Apr 2018 15:55:04 +0900
Subject: [PATCH 1247/1250] Update sw.js

---
 src/client/app/sw.js | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/client/app/sw.js b/src/client/app/sw.js
index 19d2ed06e..ac7ea20ac 100644
--- a/src/client/app/sw.js
+++ b/src/client/app/sw.js
@@ -52,8 +52,6 @@ self.addEventListener('push', ev => {
 
 		const { type, body } = ev.data.json();
 
-		console.log(type, body);
-
 		const n = composeNotification(type, body);
 		return self.registration.showNotification(n.title, {
 			body: n.body,

From 99774413f4a734a55936c7d6540fece1bb35a6f2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 11 Apr 2018 17:40:01 +0900
Subject: [PATCH 1248/1250] =?UTF-8?q?=E3=82=B9=E3=83=88=E3=83=AA=E3=83=BC?=
 =?UTF-8?q?=E3=83=A0=E7=B5=8C=E7=94=B1=E3=81=A7API=E3=81=AB=E3=83=AA?=
 =?UTF-8?q?=E3=82=AF=E3=82=A8=E3=82=B9=E3=83=88=E3=81=A7=E3=81=8D=E3=82=8B?=
 =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/client/app/common/mios.ts           | 15 ++++---
 src/models/app.ts                       |  2 +-
 src/server/api/api-handler.ts           | 60 ++++++++-----------------
 src/server/api/authenticate.ts          | 46 ++++---------------
 src/server/api/call.ts                  | 55 +++++++++++++++++++++++
 src/server/api/endpoints/app/show.ts    | 18 +++-----
 src/server/api/endpoints/i.ts           |  4 +-
 src/server/api/endpoints/i/update.ts    | 10 ++---
 src/server/api/endpoints/meta.ts        |  3 --
 src/server/api/endpoints/sw/register.ts |  8 +---
 src/server/api/index.ts                 |  1 -
 src/server/api/limitter.ts              | 12 ++---
 src/server/api/reply.ts                 | 13 ------
 src/server/api/stream/home.ts           | 24 ++++++++--
 src/server/api/streaming.ts             | 45 ++-----------------
 15 files changed, 137 insertions(+), 179 deletions(-)
 create mode 100644 src/server/api/call.ts
 delete mode 100644 src/server/api/reply.ts

diff --git a/src/client/app/common/mios.ts b/src/client/app/common/mios.ts
index 7baf974ad..5e0c7d2f3 100644
--- a/src/client/app/common/mios.ts
+++ b/src/client/app/common/mios.ts
@@ -444,23 +444,28 @@ export default class MiOS extends EventEmitter {
 		// Append a credential
 		if (this.isSignedIn) (data as any).i = this.i.token;
 
-		// TODO
-		//const viaStream = localStorage.getItem('enableExperimental') == 'true';
+		const viaStream = localStorage.getItem('enableExperimental') == 'true';
 
 		return new Promise((resolve, reject) => {
-			/*if (viaStream) {
+			if (viaStream) {
 				const stream = this.stream.borrow();
 				const id = Math.random().toString();
+
 				stream.once(`api-res:${id}`, res => {
-					resolve(res);
+					if (res.res) {
+						resolve(res.res);
+					} else {
+						reject(res.e);
+					}
 				});
+
 				stream.send({
 					type: 'api',
 					id,
 					endpoint,
 					data
 				});
-			} else {*/
+			} else {
 				const req = {
 					id: uuid(),
 					date: new Date(),
diff --git a/src/models/app.ts b/src/models/app.ts
index 446f0c62f..45c95d92d 100644
--- a/src/models/app.ts
+++ b/src/models/app.ts
@@ -19,7 +19,7 @@ export type IApp = {
 	nameId: string;
 	nameIdLower: string;
 	description: string;
-	permission: string;
+	permission: string[];
 	callbackUrl: string;
 };
 
diff --git a/src/server/api/api-handler.ts b/src/server/api/api-handler.ts
index fb603a0e2..409069b6a 100644
--- a/src/server/api/api-handler.ts
+++ b/src/server/api/api-handler.ts
@@ -2,55 +2,33 @@ import * as express from 'express';
 
 import { Endpoint } from './endpoints';
 import authenticate from './authenticate';
-import { IAuthContext } from './authenticate';
-import _reply from './reply';
-import limitter from './limitter';
+import call from './call';
+import { IUser } from '../../models/user';
+import { IApp } from '../../models/app';
 
 export default async (endpoint: Endpoint, req: express.Request, res: express.Response) => {
-	const reply = _reply.bind(null, res);
-	let ctx: IAuthContext;
+	const reply = (x?: any, y?: any) => {
+		if (x === undefined) {
+			res.sendStatus(204);
+		} else if (typeof x === 'number') {
+			res.status(x).send({
+				error: x === 500 ? 'INTERNAL_ERROR' : y
+			});
+		} else {
+			res.send(x);
+		}
+	};
+
+	let user: IUser;
+	let app: IApp;
 
 	// Authentication
 	try {
-		ctx = await authenticate(req);
+		[user, app] = await authenticate(req.body['i']);
 	} catch (e) {
 		return reply(403, 'AUTHENTICATION_FAILED');
 	}
 
-	if (endpoint.secure && !ctx.isSecure) {
-		return reply(403, 'ACCESS_DENIED');
-	}
-
-	if (endpoint.withCredential && ctx.user == null) {
-		return reply(401, 'PLZ_SIGNIN');
-	}
-
-	if (ctx.app && endpoint.kind) {
-		if (!ctx.app.permission.some(p => p === endpoint.kind)) {
-			return reply(403, 'ACCESS_DENIED');
-		}
-	}
-
-	if (endpoint.withCredential && endpoint.limit) {
-		try {
-			await limitter(endpoint, ctx); // Rate limit
-		} catch (e) {
-			// drop request if limit exceeded
-			return reply(429);
-		}
-	}
-
-	let exec = require(`${__dirname}/endpoints/${endpoint.name}`);
-
-	if (endpoint.withFile) {
-		exec = exec.bind(null, req.file);
-	}
-
 	// API invoking
-	try {
-		const res = await exec(req.body, ctx.user, ctx.app, ctx.isSecure);
-		reply(res);
-	} catch (e) {
-		reply(400, e);
-	}
+	call(endpoint, user, app, req.body, req).then(reply).catch(e => reply(400, e));
 };
diff --git a/src/server/api/authenticate.ts b/src/server/api/authenticate.ts
index adbeeb3b3..836fb7cfe 100644
--- a/src/server/api/authenticate.ts
+++ b/src/server/api/authenticate.ts
@@ -1,50 +1,24 @@
-import * as express from 'express';
-import App from '../../models/app';
+import App, { IApp } from '../../models/app';
 import { default as User, IUser } from '../../models/user';
 import AccessToken from '../../models/access-token';
 import isNativeToken from './common/is-native-token';
 
-export interface IAuthContext {
-	/**
-	 * App which requested
-	 */
-	app: any;
-
-	/**
-	 * Authenticated user
-	 */
-	user: IUser;
-
-	/**
-	 * Whether requested with a User-Native Token
-	 */
-	isSecure: boolean;
-}
-
-export default (req: express.Request) => new Promise<IAuthContext>(async (resolve, reject) => {
-	const token = req.body['i'] as string;
-
+export default (token: string) => new Promise<[IUser, IApp]>(async (resolve, reject) => {
 	if (token == null) {
-		return resolve({
-			app: null,
-			user: null,
-			isSecure: false
-		});
+		resolve([null, null]);
+		return;
 	}
 
 	if (isNativeToken(token)) {
+		// Fetch user
 		const user: IUser = await User
-			.findOne({ 'token': token });
+			.findOne({ token });
 
 		if (user === null) {
 			return reject('user not found');
 		}
 
-		return resolve({
-			app: null,
-			user: user,
-			isSecure: true
-		});
+		resolve([user, null]);
 	} else {
 		const accessToken = await AccessToken.findOne({
 			hash: token.toLowerCase()
@@ -60,10 +34,6 @@ export default (req: express.Request) => new Promise<IAuthContext>(async (resolv
 		const user = await User
 			.findOne({ _id: accessToken.userId });
 
-		return resolve({
-			app: app,
-			user: user,
-			isSecure: false
-		});
+		resolve([user, app]);
 	}
 });
diff --git a/src/server/api/call.ts b/src/server/api/call.ts
new file mode 100644
index 000000000..1bfe94bb7
--- /dev/null
+++ b/src/server/api/call.ts
@@ -0,0 +1,55 @@
+import * as express from 'express';
+
+import endpoints, { Endpoint } from './endpoints';
+import limitter from './limitter';
+import { IUser } from '../../models/user';
+import { IApp } from '../../models/app';
+
+export default (endpoint: string | Endpoint, user: IUser, app: IApp, data: any, req?: express.Request) => new Promise(async (ok, rej) => {
+	const isSecure = user != null && app == null;
+
+	//console.log(endpoint, user, app, data);
+
+	const ep = typeof endpoint == 'string' ? endpoints.find(e => e.name == endpoint) : endpoint;
+
+	if (ep.secure && !isSecure) {
+		return rej('ACCESS_DENIED');
+	}
+
+	if (ep.withCredential && user == null) {
+		return rej('SIGNIN_REQUIRED');
+	}
+
+	if (app && ep.kind) {
+		if (!app.permission.some(p => p === ep.kind)) {
+			return rej('PERMISSION_DENIED');
+		}
+	}
+
+	if (ep.withCredential && ep.limit) {
+		try {
+			await limitter(ep, user); // Rate limit
+		} catch (e) {
+			// drop request if limit exceeded
+			return rej('RATE_LIMIT_EXCEEDED');
+		}
+	}
+
+	let exec = require(`${__dirname}/endpoints/${ep.name}`);
+
+	if (ep.withFile && req) {
+		exec = exec.bind(null, req.file);
+	}
+
+	let res;
+
+	// API invoking
+	try {
+		res = await exec(data, user, app);
+	} catch (e) {
+		rej(e);
+		return;
+	}
+
+	ok(res);
+});
diff --git a/src/server/api/endpoints/app/show.ts b/src/server/api/endpoints/app/show.ts
index 3a3c25f47..99a2093b6 100644
--- a/src/server/api/endpoints/app/show.ts
+++ b/src/server/api/endpoints/app/show.ts
@@ -36,14 +36,10 @@ import App, { pack } from '../../../../models/app';
 
 /**
  * Show an app
- *
- * @param {any} params
- * @param {any} user
- * @param {any} _
- * @param {any} isSecure
- * @return {Promise<any>}
  */
-module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) => {
+module.exports = (params, user, app) => new Promise(async (res, rej) => {
+	const isSecure = user != null && app == null;
+
 	// Get 'appId' parameter
 	const [appId, appIdErr] = $(params.appId).optional.id().$;
 	if (appIdErr) return rej('invalid appId param');
@@ -57,16 +53,16 @@ module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) =>
 	}
 
 	// Lookup app
-	const app = appId !== undefined
+	const ap = appId !== undefined
 		? await App.findOne({ _id: appId })
 		: await App.findOne({ nameIdLower: nameId.toLowerCase() });
 
-	if (app === null) {
+	if (ap === null) {
 		return rej('app not found');
 	}
 
 	// Send response
-	res(await pack(app, user, {
-		includeSecret: isSecure && app.userId.equals(user._id)
+	res(await pack(ap, user, {
+		includeSecret: isSecure && ap.userId.equals(user._id)
 	}));
 });
diff --git a/src/server/api/endpoints/i.ts b/src/server/api/endpoints/i.ts
index 0be30500c..379c3c4d8 100644
--- a/src/server/api/endpoints/i.ts
+++ b/src/server/api/endpoints/i.ts
@@ -6,7 +6,9 @@ import User, { pack } from '../../../models/user';
 /**
  * Show myself
  */
-module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) => {
+module.exports = (params, user, app) => new Promise(async (res, rej) => {
+	const isSecure = user != null && app == null;
+
 	// Serialize
 	res(await pack(user, user, {
 		detail: true,
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index 36be2774f..f3c9d777b 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -7,14 +7,10 @@ import event from '../../../../publishers/stream';
 
 /**
  * Update myself
- *
- * @param {any} params
- * @param {any} user
- * @param {any} _
- * @param {boolean} isSecure
- * @return {Promise<any>}
  */
-module.exports = async (params, user, _, isSecure) => new Promise(async (res, rej) => {
+module.exports = async (params, user, app) => new Promise(async (res, rej) => {
+	const isSecure = user != null && app == null;
+
 	// Get 'name' parameter
 	const [name, nameErr] = $(params.name).optional.nullable.string().pipe(isValidName).$;
 	if (nameErr) return rej('invalid name param');
diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts
index 8574362fc..f6a276a2b 100644
--- a/src/server/api/endpoints/meta.ts
+++ b/src/server/api/endpoints/meta.ts
@@ -35,9 +35,6 @@ import Meta from '../../../models/meta';
 
 /**
  * Show core info
- *
- * @param {any} params
- * @return {Promise<any>}
  */
 module.exports = (params) => new Promise(async (res, rej) => {
 	const meta: any = (await Meta.findOne()) || {};
diff --git a/src/server/api/endpoints/sw/register.ts b/src/server/api/endpoints/sw/register.ts
index ef3428057..3fe0bda4e 100644
--- a/src/server/api/endpoints/sw/register.ts
+++ b/src/server/api/endpoints/sw/register.ts
@@ -6,14 +6,8 @@ import Subscription from '../../../../models/sw-subscription';
 
 /**
  * subscribe service worker
- *
- * @param {any} params
- * @param {any} user
- * @param {any} _
- * @param {boolean} isSecure
- * @return {Promise<any>}
  */
-module.exports = async (params, user, _, isSecure) => new Promise(async (res, rej) => {
+module.exports = async (params, user, app) => new Promise(async (res, rej) => {
 	// Get 'endpoint' parameter
 	const [endpoint, endpointErr] = $(params.endpoint).string().$;
 	if (endpointErr) return rej('invalid endpoint param');
diff --git a/src/server/api/index.ts b/src/server/api/index.ts
index e89d19609..5fbacd8a0 100644
--- a/src/server/api/index.ts
+++ b/src/server/api/index.ts
@@ -7,7 +7,6 @@ import * as bodyParser from 'body-parser';
 import * as cors from 'cors';
 import * as multer from 'multer';
 
-// import authenticate from './authenticate';
 import endpoints from './endpoints';
 
 /**
diff --git a/src/server/api/limitter.ts b/src/server/api/limitter.ts
index 638fac78b..b84e16ecd 100644
--- a/src/server/api/limitter.ts
+++ b/src/server/api/limitter.ts
@@ -2,12 +2,12 @@ import * as Limiter from 'ratelimiter';
 import * as debug from 'debug';
 import limiterDB from '../../db/redis';
 import { Endpoint } from './endpoints';
-import { IAuthContext } from './authenticate';
 import getAcct from '../../acct/render';
+import { IUser } from '../../models/user';
 
 const log = debug('misskey:limitter');
 
-export default (endpoint: Endpoint, ctx: IAuthContext) => new Promise((ok, reject) => {
+export default (endpoint: Endpoint, user: IUser) => new Promise((ok, reject) => {
 	const limitation = endpoint.limit;
 
 	const key = limitation.hasOwnProperty('key')
@@ -32,7 +32,7 @@ export default (endpoint: Endpoint, ctx: IAuthContext) => new Promise((ok, rejec
 	// Short-term limit
 	function min() {
 		const minIntervalLimiter = new Limiter({
-			id: `${ctx.user._id}:${key}:min`,
+			id: `${user._id}:${key}:min`,
 			duration: limitation.minInterval,
 			max: 1,
 			db: limiterDB
@@ -43,7 +43,7 @@ export default (endpoint: Endpoint, ctx: IAuthContext) => new Promise((ok, rejec
 				return reject('ERR');
 			}
 
-			log(`@${getAcct(ctx.user)} ${endpoint.name} min remaining: ${info.remaining}`);
+			log(`@${getAcct(user)} ${endpoint.name} min remaining: ${info.remaining}`);
 
 			if (info.remaining === 0) {
 				reject('BRIEF_REQUEST_INTERVAL');
@@ -60,7 +60,7 @@ export default (endpoint: Endpoint, ctx: IAuthContext) => new Promise((ok, rejec
 	// Long term limit
 	function max() {
 		const limiter = new Limiter({
-			id: `${ctx.user._id}:${key}`,
+			id: `${user._id}:${key}`,
 			duration: limitation.duration,
 			max: limitation.max,
 			db: limiterDB
@@ -71,7 +71,7 @@ export default (endpoint: Endpoint, ctx: IAuthContext) => new Promise((ok, rejec
 				return reject('ERR');
 			}
 
-			log(`@${getAcct(ctx.user)} ${endpoint.name} max remaining: ${info.remaining}`);
+			log(`@${getAcct(user)} ${endpoint.name} max remaining: ${info.remaining}`);
 
 			if (info.remaining === 0) {
 				reject('RATE_LIMIT_EXCEEDED');
diff --git a/src/server/api/reply.ts b/src/server/api/reply.ts
deleted file mode 100644
index e47fc85b9..000000000
--- a/src/server/api/reply.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import * as express from 'express';
-
-export default (res: express.Response, x?: any, y?: any) => {
-	if (x === undefined) {
-		res.sendStatus(204);
-	} else if (typeof x === 'number') {
-		res.status(x).send({
-			error: x === 500 ? 'INTERNAL_ERROR' : y
-		});
-	} else {
-		res.send(x);
-	}
-};
diff --git a/src/server/api/stream/home.ts b/src/server/api/stream/home.ts
index 359ef74af..e9c0924f3 100644
--- a/src/server/api/stream/home.ts
+++ b/src/server/api/stream/home.ts
@@ -2,14 +2,22 @@ import * as websocket from 'websocket';
 import * as redis from 'redis';
 import * as debug from 'debug';
 
-import User from '../../../models/user';
+import User, { IUser } from '../../../models/user';
 import Mute from '../../../models/mute';
 import { pack as packNote } from '../../../models/note';
 import readNotification from '../common/read-notification';
+import call from '../call';
+import { IApp } from '../../../models/app';
 
 const log = debug('misskey');
 
-export default async function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any) {
+export default async function(
+	request: websocket.request,
+	connection: websocket.connection,
+	subscriber: redis.RedisClient,
+	user: IUser,
+	app: IApp
+) {
 	// Subscribe Home stream channel
 	subscriber.subscribe(`misskey:user-stream:${user._id}`);
 
@@ -67,7 +75,17 @@ export default async function(request: websocket.request, connection: websocket.
 
 		switch (msg.type) {
 			case 'api':
-				// TODO
+				call(msg.endpoint, user, app, msg.data).then(res => {
+					connection.send(JSON.stringify({
+						type: `api-res:${msg.id}`,
+						body: { res }
+					}));
+				}).catch(e => {
+					connection.send(JSON.stringify({
+						type: `api-res:${msg.id}`,
+						body: { e }
+					}));
+				});
 				break;
 
 			case 'alive':
diff --git a/src/server/api/streaming.ts b/src/server/api/streaming.ts
index 26946b524..d586d7c08 100644
--- a/src/server/api/streaming.ts
+++ b/src/server/api/streaming.ts
@@ -2,9 +2,6 @@ import * as http from 'http';
 import * as websocket from 'websocket';
 import * as redis from 'redis';
 import config from '../../config';
-import { default as User, IUser } from '../../models/user';
-import AccessToken from '../../models/access-token';
-import isNativeToken from './common/is-native-token';
 
 import homeStream from './stream/home';
 import driveStream from './stream/drive';
@@ -16,6 +13,7 @@ import serverStream from './stream/server';
 import requestsStream from './stream/requests';
 import channelStream from './stream/channel';
 import { ParsedUrlQuery } from 'querystring';
+import authenticate from './authenticate';
 
 module.exports = (server: http.Server) => {
 	/**
@@ -53,7 +51,7 @@ module.exports = (server: http.Server) => {
 		}
 
 		const q = request.resourceURL.query as ParsedUrlQuery;
-		const user = await authenticate(q.i as string);
+		const [user, app] = await authenticate(q.i as string);
 
 		if (request.resourceURL.pathname === '/othello-game') {
 			othelloGameStream(request, connection, subscriber, user);
@@ -75,46 +73,9 @@ module.exports = (server: http.Server) => {
 			null;
 
 		if (channel !== null) {
-			channel(request, connection, subscriber, user);
+			channel(request, connection, subscriber, user, app);
 		} else {
 			connection.close();
 		}
 	});
 };
-
-/**
- * 接続してきたユーザーを取得します
- * @param token 送信されてきたトークン
- */
-function authenticate(token: string): Promise<IUser> {
-	if (token == null) {
-		return Promise.resolve(null);
-	}
-
-	return new Promise(async (resolve, reject) => {
-		if (isNativeToken(token)) {
-			// Fetch user
-			const user: IUser = await User
-				.findOne({
-					host: null,
-					'token': token
-				});
-
-			resolve(user);
-		} else {
-			const accessToken = await AccessToken.findOne({
-				hash: token
-			});
-
-			if (accessToken == null) {
-				return reject('invalid signature');
-			}
-
-			// Fetch user
-			const user: IUser = await User
-				.findOne({ _id: accessToken.userId });
-
-			resolve(user);
-		}
-	});
-}

From 58f6e6e57ae79781f6933867070e7f097888fe69 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 11 Apr 2018 17:42:56 +0900
Subject: [PATCH 1249/1250] Fix bug

---
 src/server/api/endpoints/users/notes.ts | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/src/server/api/endpoints/users/notes.ts b/src/server/api/endpoints/users/notes.ts
index f9f6345e3..e91b75e1d 100644
--- a/src/server/api/endpoints/users/notes.ts
+++ b/src/server/api/endpoints/users/notes.ts
@@ -8,10 +8,6 @@ import User from '../../../../models/user';
 
 /**
  * Get notes of a user
- *
- * @param {any} params
- * @param {any} me
- * @return {Promise<any>}
  */
 module.exports = (params, me) => new Promise(async (res, rej) => {
 	// Get 'userId' parameter
@@ -118,7 +114,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	if (withMedia) {
 		query.mediaIds = {
 			$exists: true,
-			$ne: null
+			$ne: []
 		};
 	}
 	//#endregion

From c3c28a9d8f5d380cd07fd55f2ea484181e460bff Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 11 Apr 2018 18:24:42 +0900
Subject: [PATCH 1250/1250] wip #1443

---
 src/models/note.ts | 32 ++++++++++++++++++++++++++++++++
 src/models/user.ts | 36 +++++++++++++++++++++++++++++++++++-
 2 files changed, 67 insertions(+), 1 deletion(-)

diff --git a/src/models/note.ts b/src/models/note.ts
index f509fa66c..a11da196c 100644
--- a/src/models/note.ts
+++ b/src/models/note.ts
@@ -69,6 +69,38 @@ export type INote = {
 	};
 };
 
+// TODO
+export async function physicalDelete(note: string | mongo.ObjectID | INote) {
+	let n: INote;
+
+	// Populate
+	if (mongo.ObjectID.prototype.isPrototypeOf(note)) {
+		n = await Note.findOne({
+			_id: note
+		});
+	} else if (typeof note === 'string') {
+		n = await Note.findOne({
+			_id: new mongo.ObjectID(note)
+		});
+	} else {
+		n = note as INote;
+	}
+
+	if (n == null) return;
+
+	// この投稿の返信をすべて削除
+	const replies = await Note.find({
+		replyId: n._id
+	});
+	await Promise.all(replies.map(r => physicalDelete(r)));
+
+	// この投稿のWatchをすべて削除
+
+	// この投稿のReactionをすべて削除
+
+	// この投稿に対するFavoriteをすべて削除
+}
+
 /**
  * Pack a note for API response
  *
diff --git a/src/models/user.ts b/src/models/user.ts
index adc9e6da9..a2800a380 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -2,7 +2,7 @@ import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
 import rap from '@prezzemolo/rap';
 import db from '../db/mongodb';
-import { INote, pack as packNote } from './note';
+import Note, { INote, pack as packNote, physicalDelete as physicalDeleteNote } from './note';
 import Following from './following';
 import Mute from './mute';
 import getFriends from '../server/api/common/get-friends';
@@ -121,6 +121,40 @@ export function init(user): IUser {
 	return user;
 }
 
+// TODO
+export async function physicalDelete(user: string | mongo.ObjectID | IUser) {
+	let u: IUser;
+
+	// Populate
+	if (mongo.ObjectID.prototype.isPrototypeOf(user)) {
+		u = await User.findOne({
+			_id: user
+		});
+	} else if (typeof user === 'string') {
+		u = await User.findOne({
+			_id: new mongo.ObjectID(user)
+		});
+	} else {
+		u = user as IUser;
+	}
+
+	if (u == null) return;
+
+	// このユーザーが行った投稿をすべて削除
+	const notes = await Note.find({ userId: u._id });
+	await Promise.all(notes.map(n => physicalDeleteNote(n)));
+
+	// このユーザーのお気に入りをすべて削除
+
+	// このユーザーが行ったメッセージをすべて削除
+
+	// このユーザーのドライブのファイルをすべて削除
+
+	// このユーザーに関するfollowingをすべて削除
+
+	// このユーザーを削除
+}
+
 /**
  * Pack a user for API response
  *