From cdee14faa262b284c6d10cb53d6bf4ccaf4da5db Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 21 Aug 2017 21:49:00 +0000
Subject: [PATCH 001/364] chore(package): update @types/bcryptjs to version
 2.4.1

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

diff --git a/package.json b/package.json
index 141e95c88b..ff3f6c79fe 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,7 @@
     "test": "gulp test"
   },
   "devDependencies": {
-    "@types/bcryptjs": "2.4.0",
+    "@types/bcryptjs": "2.4.1",
     "@types/body-parser": "1.16.4",
     "@types/chai": "4.0.3",
     "@types/chai-http": "3.0.2",

From a5cdce9e2969174a6dc7b7a7e7a5fb422ad84a39 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 21 Aug 2017 23:07:14 +0000
Subject: [PATCH 002/364] chore(package): update @types/event-stream to version
 3.3.32

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

diff --git a/package.json b/package.json
index 47006ae5dd..2aba95b111 100644
--- a/package.json
+++ b/package.json
@@ -31,7 +31,7 @@
     "@types/debug": "0.0.30",
     "@types/deep-equal": "1.0.1",
     "@types/elasticsearch": "5.0.14",
-    "@types/event-stream": "3.3.31",
+    "@types/event-stream": "3.3.32",
     "@types/express": "4.0.36",
     "@types/gm": "1.17.32",
     "@types/gulp": "4.0.3",

From 1842a4ce1103415aa026b80d173cc054a0002ccb Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 21 Aug 2017 23:15:05 +0000
Subject: [PATCH 003/364] chore(package): update @types/express to version
 4.0.37

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

diff --git a/package.json b/package.json
index 47006ae5dd..5798983c36 100644
--- a/package.json
+++ b/package.json
@@ -32,7 +32,7 @@
     "@types/deep-equal": "1.0.1",
     "@types/elasticsearch": "5.0.14",
     "@types/event-stream": "3.3.31",
-    "@types/express": "4.0.36",
+    "@types/express": "4.0.37",
     "@types/gm": "1.17.32",
     "@types/gulp": "4.0.3",
     "@types/gulp-htmlmin": "1.3.30",

From ee06ab75ba50d0e9f61d0ba28d877d543e6ad635 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 21 Aug 2017 23:22:31 +0000
Subject: [PATCH 004/364] chore(package): update @types/mocha to version 2.2.42

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

diff --git a/package.json b/package.json
index 47006ae5dd..537f6d3c0e 100644
--- a/package.json
+++ b/package.json
@@ -47,7 +47,7 @@
     "@types/is-root": "1.0.0",
     "@types/is-url": "1.2.28",
     "@types/js-yaml": "3.9.0",
-    "@types/mocha": "2.2.41",
+    "@types/mocha": "2.2.42",
     "@types/mongodb": "2.2.10",
     "@types/monk": "1.0.5",
     "@types/morgan": "1.7.32",

From d0c58df5f484142ec4343c2939cd542281a64d05 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 21 Aug 2017 23:24:16 +0000
Subject: [PATCH 005/364] chore(package): update @types/monk to version 1.0.6

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

diff --git a/package.json b/package.json
index 47006ae5dd..a49300834c 100644
--- a/package.json
+++ b/package.json
@@ -49,7 +49,7 @@
     "@types/js-yaml": "3.9.0",
     "@types/mocha": "2.2.41",
     "@types/mongodb": "2.2.10",
-    "@types/monk": "1.0.5",
+    "@types/monk": "1.0.6",
     "@types/morgan": "1.7.32",
     "@types/ms": "0.7.29",
     "@types/multer": "1.3.2",

From 147367b55b6774a4dda1fecee20fa93ffd06c0fb Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 21 Aug 2017 23:25:29 +0000
Subject: [PATCH 006/364] chore(package): update @types/ms to version 0.7.30

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

diff --git a/package.json b/package.json
index 47006ae5dd..bdcbe8743f 100644
--- a/package.json
+++ b/package.json
@@ -51,7 +51,7 @@
     "@types/mongodb": "2.2.10",
     "@types/monk": "1.0.5",
     "@types/morgan": "1.7.32",
-    "@types/ms": "0.7.29",
+    "@types/ms": "0.7.30",
     "@types/multer": "1.3.2",
     "@types/node": "8.0.24",
     "@types/ratelimiter": "2.1.28",

From fe3d634d7fe2445b0cf57b2ed0696eaa1615f035 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 21 Aug 2017 23:27:10 +0000
Subject: [PATCH 007/364] chore(package): update @types/request to version
 2.0.2

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

diff --git a/package.json b/package.json
index 47006ae5dd..e9c0700ed0 100644
--- a/package.json
+++ b/package.json
@@ -56,7 +56,7 @@
     "@types/node": "8.0.24",
     "@types/ratelimiter": "2.1.28",
     "@types/redis": "2.6.0",
-    "@types/request": "2.0.1",
+    "@types/request": "2.0.2",
     "@types/rimraf": "2.0.0",
     "@types/riot": "3.6.0",
     "@types/serve-favicon": "2.2.28",

From 73c85a52be7e06c087237761e887620e5f2f9221 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 21 Aug 2017 23:28:23 +0000
Subject: [PATCH 008/364] chore(package): update @types/serve-favicon to
 version 2.2.29

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

diff --git a/package.json b/package.json
index 47006ae5dd..639261cb99 100644
--- a/package.json
+++ b/package.json
@@ -59,7 +59,7 @@
     "@types/request": "2.0.1",
     "@types/rimraf": "2.0.0",
     "@types/riot": "3.6.0",
-    "@types/serve-favicon": "2.2.28",
+    "@types/serve-favicon": "2.2.29",
     "@types/uuid": "3.4.0",
     "@types/webpack": "3.0.9",
     "@types/webpack-stream": "3.2.7",

From 26e7dbcbed2c3c8b3b8c5f4f74d8b190aa1119a5 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 21 Aug 2017 23:28:43 +0000
Subject: [PATCH 009/364] chore(package): update @types/uuid to version 3.4.1

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

diff --git a/package.json b/package.json
index 47006ae5dd..4ce09491d4 100644
--- a/package.json
+++ b/package.json
@@ -60,7 +60,7 @@
     "@types/rimraf": "2.0.0",
     "@types/riot": "3.6.0",
     "@types/serve-favicon": "2.2.28",
-    "@types/uuid": "3.4.0",
+    "@types/uuid": "3.4.1",
     "@types/webpack": "3.0.9",
     "@types/webpack-stream": "3.2.7",
     "@types/websocket": "0.0.34",

From e3427d93011af09d38ac78a76be118d5babe72f4 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 22 Aug 2017 08:48:33 +0000
Subject: [PATCH 010/364] chore(package): update gulp-typescript to version
 3.2.2

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

diff --git a/package.json b/package.json
index 915a969f46..1df56fb853 100644
--- a/package.json
+++ b/package.json
@@ -77,7 +77,7 @@
     "gulp-rename": "1.2.2",
     "gulp-replace": "0.6.1",
     "gulp-tslint": "8.1.2",
-    "gulp-typescript": "3.2.1",
+    "gulp-typescript": "3.2.2",
     "gulp-uglify": "3.0.0",
     "gulp-util": "3.0.8",
     "mocha": "3.5.0",

From a5496e6458343d644329c7f9047b987ec085ce7f Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 22 Aug 2017 15:58:23 +0000
Subject: [PATCH 011/364] chore(package): update @types/webpack to version
 3.0.10

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

diff --git a/package.json b/package.json
index 915a969f46..13a46cb3a0 100644
--- a/package.json
+++ b/package.json
@@ -61,7 +61,7 @@
     "@types/riot": "3.6.0",
     "@types/serve-favicon": "2.2.28",
     "@types/uuid": "3.4.1",
-    "@types/webpack": "3.0.9",
+    "@types/webpack": "3.0.10",
     "@types/webpack-stream": "3.2.7",
     "@types/websocket": "0.0.34",
     "chai": "4.1.1",

From 0fe36f8e462b1ea07617b53996b32e356c808867 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 23 Aug 2017 20:04:36 +0000
Subject: [PATCH 012/364] fix(package): update riot to version 3.6.3

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

diff --git a/package.json b/package.json
index 915a969f46..4df4f05726 100644
--- a/package.json
+++ b/package.json
@@ -137,7 +137,7 @@
     "redis": "2.8.0",
     "request": "2.81.0",
     "rimraf": "2.6.1",
-    "riot": "3.6.2",
+    "riot": "3.6.3",
     "rndstr": "1.0.0",
     "s-age": "1.1.0",
     "serve-favicon": "2.4.3",

From b83ec0eb7ac1953fa519997665edf88262995e2d Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 24 Aug 2017 17:42:01 +0000
Subject: [PATCH 013/364] chore(package): update @types/node to version 8.0.25

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

diff --git a/package.json b/package.json
index 915a969f46..6f8be109fc 100644
--- a/package.json
+++ b/package.json
@@ -53,7 +53,7 @@
     "@types/morgan": "1.7.32",
     "@types/ms": "0.7.30",
     "@types/multer": "1.3.2",
-    "@types/node": "8.0.24",
+    "@types/node": "8.0.25",
     "@types/ratelimiter": "2.1.28",
     "@types/redis": "2.6.0",
     "@types/request": "2.0.2",

From 099f356f17bfda11f1220b2a084b22e4bc41c802 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 24 Aug 2017 17:51:42 +0000
Subject: [PATCH 014/364] chore(package): update @types/request to version
 2.0.3

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

diff --git a/package.json b/package.json
index 915a969f46..0e8fd18d0e 100644
--- a/package.json
+++ b/package.json
@@ -56,7 +56,7 @@
     "@types/node": "8.0.24",
     "@types/ratelimiter": "2.1.28",
     "@types/redis": "2.6.0",
-    "@types/request": "2.0.2",
+    "@types/request": "2.0.3",
     "@types/rimraf": "2.0.0",
     "@types/riot": "3.6.0",
     "@types/serve-favicon": "2.2.28",

From cbac13e99e71ba1a2159e638d2e4d0e4781a5f78 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 24 Aug 2017 20:26:19 +0000
Subject: [PATCH 015/364] fix(package): update debug to version 3.0.1

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

diff --git a/package.json b/package.json
index 915a969f46..103b1708bf 100644
--- a/package.json
+++ b/package.json
@@ -105,7 +105,7 @@
     "cors": "2.8.4",
     "cropperjs": "1.0.0-rc.3",
     "crypto": "1.0.1",
-    "debug": "3.0.0",
+    "debug": "3.0.1",
     "deep-equal": "1.0.1",
     "deepcopy": "0.6.3",
     "diskusage": "^0.2.2",

From 0f33e257e5fa9270470174aa674a68623f6a2dca Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Fri, 25 Aug 2017 07:11:44 +0000
Subject: [PATCH 016/364] fix(package): update reconnecting-websocket to
 version 3.2.1

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

diff --git a/package.json b/package.json
index 915a969f46..00149d17fc 100644
--- a/package.json
+++ b/package.json
@@ -133,7 +133,7 @@
     "pug": "2.0.0-rc.3",
     "ratelimiter": "3.0.3",
     "recaptcha-promise": "0.1.3",
-    "reconnecting-websocket": "3.2.0",
+    "reconnecting-websocket": "3.2.1",
     "redis": "2.8.0",
     "request": "2.81.0",
     "rimraf": "2.6.1",

From 6f294c91e8d1873d71e2a70391a06d122fc2aff9 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Fri, 25 Aug 2017 23:38:20 +0000
Subject: [PATCH 017/364] chore(package): update tslint to version 5.7.0

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

diff --git a/package.json b/package.json
index 915a969f46..564b65615c 100644
--- a/package.json
+++ b/package.json
@@ -87,7 +87,7 @@
     "stylus": "0.54.5",
     "stylus-loader": "3.0.1",
     "swagger-jsdoc": "1.9.7",
-    "tslint": "5.6.0",
+    "tslint": "5.7.0",
     "uglify-es": "3.0.27",
     "uglify-es-webpack-plugin": "0.10.0",
     "uglify-js": "git+https://github.com/mishoo/UglifyJS2.git#harmony",

From 7172ccd72c2ef40d68a252d7ae5cf45ddc575116 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 27 Aug 2017 16:53:15 +0900
Subject: [PATCH 018/364] Use createIndex() instead of index() to avoid
 deprecation warnings

---
 src/api/models/access-token.ts | 4 ++--
 src/api/models/app.ts          | 6 +++---
 src/api/models/drive-file.ts   | 2 +-
 src/api/models/user.ts         | 4 ++--
 4 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/src/api/models/access-token.ts b/src/api/models/access-token.ts
index 2a8a512ddc..9985be5013 100644
--- a/src/api/models/access-token.ts
+++ b/src/api/models/access-token.ts
@@ -2,7 +2,7 @@ import db from '../../db/mongodb';
 
 const collection = db.get('access_tokens');
 
-(collection as any).index('token'); // fuck type definition
-(collection as any).index('hash'); // fuck type definition
+(collection as any).createIndex('token'); // fuck type definition
+(collection as any).createIndex('hash'); // fuck type definition
 
 export default collection as any; // fuck type definition
diff --git a/src/api/models/app.ts b/src/api/models/app.ts
index bf5dc80c2c..68f2f448b0 100644
--- a/src/api/models/app.ts
+++ b/src/api/models/app.ts
@@ -2,9 +2,9 @@ import db from '../../db/mongodb';
 
 const collection = db.get('apps');
 
-(collection as any).index('name_id'); // fuck type definition
-(collection as any).index('name_id_lower'); // fuck type definition
-(collection as any).index('secret'); // fuck type definition
+(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
 
diff --git a/src/api/models/drive-file.ts b/src/api/models/drive-file.ts
index 4c7204b1f4..8d158cf563 100644
--- a/src/api/models/drive-file.ts
+++ b/src/api/models/drive-file.ts
@@ -2,7 +2,7 @@ import db from '../../db/mongodb';
 
 const collection = db.get('drive_files');
 
-(collection as any).index('hash'); // fuck type definition
+(collection as any).createIndex('hash'); // fuck type definition
 
 export default collection as any; // fuck type definition
 
diff --git a/src/api/models/user.ts b/src/api/models/user.ts
index cd16459891..9f8cf0161d 100644
--- a/src/api/models/user.ts
+++ b/src/api/models/user.ts
@@ -2,8 +2,8 @@ import db from '../../db/mongodb';
 
 const collection = db.get('users');
 
-(collection as any).index('username'); // fuck type definition
-(collection as any).index('token'); // fuck type definition
+(collection as any).createIndex('username'); // fuck type definition
+(collection as any).createIndex('token'); // fuck type definition
 
 export default collection as any; // fuck type definition
 

From 1b9ed129a91056ae36b31fcacde39c2fb2c039d8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 27 Aug 2017 16:53:52 +0900
Subject: [PATCH 019/364] [Test] Clean up

---
 test/api.js | 2 --
 1 file changed, 2 deletions(-)

diff --git a/test/api.js b/test/api.js
index 9e1d4ff61b..1e731b5549 100644
--- a/test/api.js
+++ b/test/api.js
@@ -53,8 +53,6 @@ describe('API', () => {
 		db.get('auth_sessions').drop()
 	]));
 
-	afterEach(cb => setTimeout(cb, 100));
-
 	it('greet server', done => {
 		_chai.request(server)
 			.get('/')

From 49e1cea200d5cde1b7ad4c6f21017c4d80ec80ff Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 27 Aug 2017 16:59:11 +0900
Subject: [PATCH 020/364] [CI] Update the node.js version to 8.4.0

---
 .travis.yml  | 2 +-
 appveyor.yml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 91e1244432..76de4930d0 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,7 +4,7 @@
 language: node_js
 
 node_js:
-  - 7.10.0
+  - 8.4.0
 
 env:
   - CXX=g++-4.8 NODE_ENV=production
diff --git a/appveyor.yml b/appveyor.yml
index d26cbc27e8..a4aa652cec 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -3,7 +3,7 @@
 
 environment:
   matrix:
-    - nodejs_version: 7.10.0
+    - nodejs_version: 8.4.0
 
 build: off
 

From 3b77bc8299eee89a7945438863953a5cdd08e934 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 28 Aug 2017 00:03:57 +0900
Subject: [PATCH 021/364] Implement #734

---
 CHANGELOG.md                             |  4 +
 locales/en.yml                           |  4 +
 locales/ja.yml                           |  4 +
 src/api/serializers/post.ts              | 93 ++++++++++++++++--------
 src/web/app/desktop/tags/pages/post.tag  | 34 +++++++--
 src/web/app/desktop/tags/post-detail.tag | 76 +++++++------------
 6 files changed, 132 insertions(+), 83 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 72a584ddb0..95d21ac05f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog
 =========
 主に notable な changes を書いていきます
 
+unreleased
+----------
+* 投稿ページに次の投稿/前の投稿リンクを作成 (#734)
+
 2380
 ----
 アプリケーションが作れない問題を修正
diff --git a/locales/en.yml b/locales/en.yml
index 55a588f99f..9bf6446641 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -231,6 +231,10 @@ desktop:
       attaches: "{} media attached"
       uploading-media: "Uploading {} media"
 
+    mk-post-page:
+      prev: "Previous post"
+      next: "Next post"
+
     mk-timeline-post:
       reposted-by: "Reposted by {}"
       reply: "Reply"
diff --git a/locales/ja.yml b/locales/ja.yml
index e5b2beaed1..d2b282bff6 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -231,6 +231,10 @@ desktop:
       attaches: "添付: {}メディア"
       uploading-media: "{}個のメディアをアップロード中"
 
+    mk-post-page:
+      prev: "前の投稿"
+      next: "次の投稿"
+
     mk-timeline-post:
       reposted-by: "{}がRepost"
       reply: "返信"
diff --git a/src/api/serializers/post.ts b/src/api/serializers/post.ts
index 3c96884dd1..13773bda9e 100644
--- a/src/api/serializers/post.ts
+++ b/src/api/serializers/post.ts
@@ -73,44 +73,79 @@ const self = (
 		));
 	}
 
-	if (_post.reply_to_id && opts.detail) {
-		// Populate reply to post
-		_post.reply_to = await self(_post.reply_to_id, me, {
-			detail: false
+	// When requested a detailed post data
+	if (opts.detail) {
+		// Get previous post info
+		const prev = await Post.findOne({
+			user_id: _post.user_id,
+			_id: {
+				$lt: id
+			}
+		}, {
+			fields: {
+				_id: true
+			},
+			sort: {
+				_id: -1
+			}
 		});
-	}
+		_post.prev = prev ? prev._id : null;
 
-	if (_post.repost_id && opts.detail) {
-		// Populate repost
-		_post.repost = await self(_post.repost_id, me, {
-			detail: _post.text == null
+		// Get next post info
+		const next = await Post.findOne({
+			user_id: _post.user_id,
+			_id: {
+				$gt: id
+			}
+		}, {
+			fields: {
+				_id: true
+			},
+			sort: {
+				_id: 1
+			}
 		});
-	}
+		_post.next = next ? next._id : null;
 
-	// Poll
-	if (me && _post.poll && opts.detail) {
-		const vote = await Vote
-			.findOne({
-				user_id: me._id,
-				post_id: id
+		if (_post.reply_to_id) {
+			// Populate reply to post
+			_post.reply_to = await self(_post.reply_to_id, me, {
+				detail: false
 			});
-
-		if (vote != null) {
-			_post.poll.choices.filter(c => c.id == vote.choice)[0].is_voted = true;
 		}
-	}
 
-	// Fetch my reaction
-	if (me && opts.detail) {
-		const reaction = await Reaction
-			.findOne({
-				user_id: me._id,
-				post_id: id,
-				deleted_at: { $exists: false }
+		if (_post.repost_id) {
+			// Populate repost
+			_post.repost = await self(_post.repost_id, me, {
+				detail: _post.text == null
 			});
+		}
 
-		if (reaction) {
-			_post.my_reaction = reaction.reaction;
+		// Poll
+		if (me && _post.poll) {
+			const vote = await Vote
+				.findOne({
+					user_id: me._id,
+					post_id: id
+				});
+
+			if (vote != null) {
+				_post.poll.choices.filter(c => c.id == vote.choice)[0].is_voted = true;
+			}
+		}
+
+		// Fetch my reaction
+		if (me) {
+			const reaction = await Reaction
+				.findOne({
+					user_id: me._id,
+					post_id: id,
+					deleted_at: { $exists: false }
+				});
+
+			if (reaction) {
+				_post.my_reaction = reaction.reaction;
+			}
 		}
 	}
 
diff --git a/src/web/app/desktop/tags/pages/post.tag b/src/web/app/desktop/tags/pages/post.tag
index c91e98bbd4..f270b43ac2 100644
--- a/src/web/app/desktop/tags/pages/post.tag
+++ b/src/web/app/desktop/tags/pages/post.tag
@@ -1,7 +1,9 @@
 <mk-post-page>
 	<mk-ui ref="ui">
-		<main>
+		<main if={ !parent.fetching }>
+			<a if={ parent.post.next } href={ parent.post.next }><i class="fa fa-angle-up"></i>%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 }><i class="fa fa-angle-down"></i>%i18n:desktop.tags.mk-post-page.prev%</a>
 		</main>
 	</mk-ui>
 	<style>
@@ -10,6 +12,19 @@
 
 			main
 				padding 16px
+				text-align center
+
+				> a
+					display inline-block
+
+					&:first-child
+						margin-bottom 4px
+
+					&:last-child
+						margin-top 4px
+
+					> i
+						margin-right 4px
 
 				> mk-post-detail
 					margin 0 auto
@@ -18,16 +33,23 @@
 	<script>
 		import Progress from '../../../common/scripts/loading';
 
-		this.post = this.opts.post;
+		this.mixin('api');
+
+		this.fetching = true;
+		this.post = null;
 
 		this.on('mount', () => {
 			Progress.start();
 
-			this.refs.ui.refs.detail.on('post-fetched', () => {
-				Progress.set(0.5);
-			});
+			this.api('posts/show', {
+				post_id: this.opts.post
+			}).then(post => {
+
+				this.update({
+					fetching: false,
+					post: post
+				});
 
-			this.refs.ui.refs.detail.on('loaded', () => {
 				Progress.done();
 			});
 		});
diff --git a/src/web/app/desktop/tags/post-detail.tag b/src/web/app/desktop/tags/post-detail.tag
index b162a4084a..2a962816e1 100644
--- a/src/web/app/desktop/tags/post-detail.tag
+++ b/src/web/app/desktop/tags/post-detail.tag
@@ -1,8 +1,5 @@
 <mk-post-detail title={ title }>
-	<div class="fetching" if={ fetching }>
-		<mk-ellipsis-icon/>
-	</div>
-	<div class="main" if={ !fetching }>
+	<div class="main">
 		<button class="read-more" if={ p.reply_to && p.reply_to.reply_to_id && context == null } title="会話をもっと読み込む" onclick={ loadContext } disabled={ contextFetching }>
 			<i class="fa fa-ellipsis-v" if={ !contextFetching }></i>
 			<i class="fa fa-spinner fa-pulse" if={ contextFetching }></i>
@@ -71,13 +68,11 @@
 			padding 0
 			width 640px
 			overflow hidden
+			text-align left
 			background #fff
 			border solid 1px rgba(0, 0, 0, 0.1)
 			border-radius 8px
 
-			> .fetching
-				padding 64px 0
-
 			> .main
 
 				> .read-more
@@ -262,56 +257,41 @@
 		this.mixin('api');
 		this.mixin('user-preview');
 
-		this.fetching = true;
 		this.contextFetching = false;
 		this.context = null;
-		this.post = 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', () => {
-			this.api('posts/show', {
-				post_id: this.opts.post
-			}).then(post => {
-				const isRepost = post.repost != null;
-				const p = isRepost ? post.repost : post;
-				p.reactions_count = p.reaction_counts ? Object.keys(p.reaction_counts).map(key => p.reaction_counts[key]).reduce((a, b) => a + b) : 0;
+			if (this.p.text) {
+				const tokens = this.p.ast;
 
-				this.update({
-					fetching: false,
-					post: post,
-					isRepost: isRepost,
-					p: p,
-					title: dateStringify(p.created_at)
+				this.refs.text.innerHTML = compile(tokens);
+
+				this.refs.text.children.forEach(e => {
+					if (e.tagName == 'MK-URL') riot.mount(e);
 				});
 
-				this.trigger('loaded');
-
-				if (this.p.text) {
-					const tokens = this.p.ast;
-
-					this.refs.text.innerHTML = compile(tokens);
-
-					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
 					});
+				});
+			}
 
-					// 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
-				this.api('posts/replies', {
-					post_id: this.p.id,
-					limit: 8
-				}).then(replies => {
-					this.update({
-						replies: replies
-					});
+			// Get replies
+			this.api('posts/replies', {
+				post_id: this.p.id,
+				limit: 8
+			}).then(replies => {
+				this.update({
+					replies: replies
 				});
 			});
 		});

From e2be59f56c99386d9c2a123cd031b2d94b588948 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 28 Aug 2017 09:45:26 +0900
Subject: [PATCH 022/364] [Client] Fix #736

---
 CHANGELOG.md                              | 3 ++-
 src/web/app/mobile/tags/home-timeline.tag | 6 ++++++
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 95d21ac05f..b819cd27f0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,8 @@ ChangeLog
 
 unreleased
 ----------
-* 投稿ページに次の投稿/前の投稿リンクを作成 (#734)
+* Improvement: 投稿ページに次の投稿/前の投稿リンクを作成 (#734)
+* Fix: モバイル版でおすすめユーザーをフォローしてもタイムラインが更新されない (#736)
 
 2380
 ----
diff --git a/src/web/app/mobile/tags/home-timeline.tag b/src/web/app/mobile/tags/home-timeline.tag
index 5d5399f322..7357d13916 100644
--- a/src/web/app/mobile/tags/home-timeline.tag
+++ b/src/web/app/mobile/tags/home-timeline.tag
@@ -23,6 +23,12 @@
 			});
 		});
 
+		this.fetch = () => {
+			this.api('posts/timeline').then(posts => {
+				this.refs.timeline.setPosts(posts);
+			});
+		};
+
 		this.on('mount', () => {
 			this.stream.on('post', this.onStreamPost);
 			this.stream.on('follow', this.onStreamFollow);

From 3075467e2fc5781373594c360c83954218dfccc2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 28 Aug 2017 10:01:31 +0900
Subject: [PATCH 023/364] [Server] Remove needless log

---
 src/api/stream/server.ts | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/api/stream/server.ts b/src/api/stream/server.ts
index 6de5337499..0db6643d40 100644
--- a/src/api/stream/server.ts
+++ b/src/api/stream/server.ts
@@ -14,7 +14,6 @@ export default function homeStream(request: websocket.request, connection: webso
 	ev.addListener('stats', onStats);
 
 	connection.on('close', () => {
-		console.log('yooo');
 		ev.removeListener('stats', onStats);
 	});
 }

From baab3e7ad833a4e8d631c6fea09023b6012e5c6f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 28 Aug 2017 10:11:23 +0900
Subject: [PATCH 024/364] =?UTF-8?q?=E3=82=BF=E3=82=A4=E3=83=A0=E3=83=A9?=
 =?UTF-8?q?=E3=82=A4=E3=83=B3=E3=81=AE=E6=8A=95=E7=A8=BF=E3=82=92=E3=83=80?=
 =?UTF-8?q?=E3=83=96=E3=83=AB=E3=82=AF=E3=83=AA=E3=83=83=E3=82=AF=E3=81=99?=
 =?UTF-8?q?=E3=82=8B=E3=81=93=E3=81=A8=E3=81=A7=E8=A9=B3=E7=B4=B0=E3=81=AA?=
 =?UTF-8?q?=E6=83=85=E5=A0=B1=E3=81=8C=E8=A6=8B=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

---
 CHANGELOG.md                                  |  3 +-
 .../app/desktop/tags/detailed-post-window.tag | 80 +++++++++++++++++++
 src/web/app/desktop/tags/index.js             |  1 +
 src/web/app/desktop/tags/timeline-post.tag    |  8 +-
 4 files changed, 90 insertions(+), 2 deletions(-)
 create mode 100644 src/web/app/desktop/tags/detailed-post-window.tag

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b819cd27f0..d850457df3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,8 @@ ChangeLog
 
 unreleased
 ----------
-* Improvement: 投稿ページに次の投稿/前の投稿リンクを作成 (#734)
+* Improve: 投稿ページに次の投稿/前の投稿リンクを作成 (#734)
+* Improve: タイムラインの投稿をダブルクリックすることで詳細な情報が見れるように
 * Fix: モバイル版でおすすめユーザーをフォローしてもタイムラインが更新されない (#736)
 
 2380
diff --git a/src/web/app/desktop/tags/detailed-post-window.tag b/src/web/app/desktop/tags/detailed-post-window.tag
new file mode 100644
index 0000000000..04f9acf974
--- /dev/null
+++ b/src/web/app/desktop/tags/detailed-post-window.tag
@@ -0,0 +1,80 @@
+<mk-detailed-post-window>
+	<div class="bg" ref="bg" onclick={ bgClick }></div>
+	<div class="main" ref="main" if={ !fetching }>
+		<mk-post-detail ref="detail" post={ post }/>
+	</div>
+	<style>
+		: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>
+		import 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.unmount()
+			});
+		};
+
+		this.bgClick = () => {
+			this.close();
+		};
+	</script>
+</mk-detailed-post-window>
diff --git a/src/web/app/desktop/tags/index.js b/src/web/app/desktop/tags/index.js
index 177ba41293..11243c00a0 100644
--- a/src/web/app/desktop/tags/index.js
+++ b/src/web/app/desktop/tags/index.js
@@ -91,3 +91,4 @@ require('./user-following-window.tag');
 require('./user-followers-window.tag');
 require('./list-user.tag');
 require('./ui-notification.tag');
+require('./detailed-post-window.tag');
diff --git a/src/web/app/desktop/tags/timeline-post.tag b/src/web/app/desktop/tags/timeline-post.tag
index 150b928dfd..0438b146ca 100644
--- a/src/web/app/desktop/tags/timeline-post.tag
+++ b/src/web/app/desktop/tags/timeline-post.tag
@@ -1,4 +1,4 @@
-<mk-timeline-post tabindex="-1" title={ title } onkeydown={ onKeyDown }>
+<mk-timeline-post tabindex="-1" title={ title } onkeydown={ onKeyDown } dblclick={ onDblClick }>
 	<div class="reply-to" if={ p.reply_to }>
 		<mk-timeline-post-sub post={ p.reply_to }/>
 	</div>
@@ -473,6 +473,12 @@
 			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) {

From 9f1e63a1908a603b75a31fe7b6a4a83c82e8e2a7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 28 Aug 2017 10:19:27 +0900
Subject: [PATCH 025/364] [CI] Except the release branch

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

diff --git a/.travis.yml b/.travis.yml
index 76de4930d0..ed53af9e20 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,10 @@
 # travis file
 # https://docs.travis-ci.com/user/customizing-the-build
 
+branches:
+  except:
+    - release
+
 language: node_js
 
 node_js:

From ac26397777bd318045069ee8880210a2727e9c28 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, 28 Aug 2017 11:14:25 +0900
Subject: [PATCH 026/364] Re: [CI] Except the release branch

---
 appveyor.yml | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/appveyor.yml b/appveyor.yml
index a4aa652cec..03a42b9b44 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,6 +1,10 @@
 # appveyor file
 # http://www.appveyor.com/docs/appveyor-yml
 
+branches:
+  except:
+    - release
+
 environment:
   matrix:
     - nodejs_version: 8.4.0

From 09e443aeb308912b26fb3c228948a3d7a91827d0 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, 28 Aug 2017 11:41:51 +0900
Subject: [PATCH 027/364] Re: Re: [CI] Except the release branch

---
 .travis/.gitignore-release | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/.travis/.gitignore-release b/.travis/.gitignore-release
index ad1d3724fc..ae1157b33e 100644
--- a/.travis/.gitignore-release
+++ b/.travis/.gitignore-release
@@ -6,3 +6,5 @@
 !/tools
 !/elasticsearch
 !/package.json
+!/.travis.yml
+!/appveyor.yml

From 29ed58e4b349e30e4c389cae1050e1a689a7d466 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 28 Aug 2017 19:43:24 +0900
Subject: [PATCH 028/364] #163

---
 CHANGELOG.md                                  |   1 +
 locales/en.yml                                |  16 +++
 locales/ja.yml                                |  16 +++
 src/web/app/mobile/router.js                  |   6 +
 src/web/app/mobile/tags/index.js              |   1 +
 src/web/app/mobile/tags/page/settings.tag     |  74 ++++++++++--
 .../app/mobile/tags/page/settings/profile.tag | 106 ++++++++++++++++++
 7 files changed, 213 insertions(+), 7 deletions(-)
 create mode 100644 src/web/app/mobile/tags/page/settings/profile.tag

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d850457df3..fb41a5ab7e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@ ChangeLog
 
 unreleased
 ----------
+* New: モバイル版からプロフィールを設定できるように
 * Improve: 投稿ページに次の投稿/前の投稿リンクを作成 (#734)
 * Improve: タイムラインの投稿をダブルクリックすることで詳細な情報が見れるように
 * Fix: モバイル版でおすすめユーザーをフォローしてもタイムラインが更新されない (#736)
diff --git a/locales/en.yml b/locales/en.yml
index 9bf6446641..6d636a1108 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -336,12 +336,28 @@ mobile:
     mk-search-page:
       search: "Search"
 
+    mk-settings:
+      signed-in-as: "Signed in as {}"
+
     mk-settings-page:
       profile: "Profile"
       applications: "Applications"
       twitter-integration: "Twitter integration"
       signin-history: "Sign in history"
+      api: "API"
       settings: "Settings"
+      signout: "Sign out"
+
+    mk-profile-setting-page:
+      title: "Profile Settings"
+
+    mk-profile-setting:
+      name: "Name"
+      location: "Location"
+      description: "Description"
+      birthday: "Birthday"
+      save: "Save"
+      saved: "Profile updated successfully"
 
     mk-user-followers-page:
       followers-of: "Followers of {}"
diff --git a/locales/ja.yml b/locales/ja.yml
index d2b282bff6..75b1fd6275 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -336,12 +336,28 @@ mobile:
     mk-search-page:
       search: "検索"
 
+    mk-settings:
+      signed-in-as: "{}としてサインイン中"
+
     mk-settings-page:
       profile: "プロフィール"
       applications: "アプリケーション"
       twitter-integration: "Twitter連携"
       signin-history: "ログイン履歴"
+      api: "API"
       settings: "設定"
+      signout: "サインアウト"
+
+    mk-profile-setting-page:
+      title: "プロフィール設定"
+
+    mk-profile-setting:
+      name: "名前"
+      location: "場所"
+      description: "自己紹介"
+      birthday: "誕生日"
+      save: "保存"
+      saved: "プロフィールを保存しました"
 
     mk-user-followers-page:
       followers-of: "{}のフォロワー"
diff --git a/src/web/app/mobile/router.js b/src/web/app/mobile/router.js
index d0b45d9614..de4108a593 100644
--- a/src/web/app/mobile/router.js
+++ b/src/web/app/mobile/router.js
@@ -15,6 +15,7 @@ export default me => {
 	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/api',             settingsApi);
 	route('/i/settings/twitter',         settingsTwitter);
@@ -63,6 +64,10 @@ export default me => {
 		mount(document.createElement('mk-settings-page'));
 	}
 
+	function settingsProfile() {
+		mount(document.createElement('mk-profile-setting-page'));
+	}
+
 	function settingsSignin() {
 		mount(document.createElement('mk-signin-history-page'));
 	}
@@ -130,6 +135,7 @@ export default me => {
 };
 
 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/tags/index.js b/src/web/app/mobile/tags/index.js
index 02d1541fcd..2e6b478079 100644
--- a/src/web/app/mobile/tags/index.js
+++ b/src/web/app/mobile/tags/index.js
@@ -14,6 +14,7 @@ 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/api.tag');
 require('./page/settings/authorized-apps.tag');
diff --git a/src/web/app/mobile/tags/page/settings.tag b/src/web/app/mobile/tags/page/settings.tag
index 58094a876a..710591071d 100644
--- a/src/web/app/mobile/tags/page/settings.tag
+++ b/src/web/app/mobile/tags/page/settings.tag
@@ -1,12 +1,6 @@
 <mk-settings-page>
 	<mk-ui ref="ui">
-		<ul>
-			<li><a><i class="fa fa-user"></i>%i18n:mobile.tags.mk-settings-page.profile%</a></li>
-			<li><a href="./settings/authorized-apps"><i class="fa fa-puzzle-piece"></i>%i18n:mobile.tags.mk-settings-page.applications%</a></li>
-			<li><a href="./settings/twitter"><i class="fa fa-twitter"></i>%i18n:mobile.tags.mk-settings-page.twitter-integration%</a></li>
-			<li><a href="./settings/signin-history"><i class="fa fa-sign-in"></i>%i18n:mobile.tags.mk-settings-page.signin-history%</a></li>
-			<li><a href="./settings/api"><i class="fa fa-key"></i>API</a></li>
-		</ul>
+		<mk-settings />
 	</mk-ui>
 	<style>
 		:scope
@@ -18,6 +12,72 @@
 		this.on('mount', () => {
 			document.title = 'Misskey | %i18n:mobile.tags.mk-settings-page.settings%';
 			ui.trigger('title', '<i class="fa fa-cog"></i>%i18n:mobile.tags.mk-settings-page.settings%');
+			document.documentElement.style.background = '#eee';
 		});
 	</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"><i class="fa fa-user"></i>%i18n:mobile.tags.mk-settings-page.profile%<i class="fa fa-angle-right"></i></a></li>
+		<li><a href="./settings/authorized-apps"><i class="fa fa-puzzle-piece"></i>%i18n:mobile.tags.mk-settings-page.applications%<i class="fa fa-angle-right"></i></a></li>
+		<li><a href="./settings/twitter"><i class="fa fa-twitter"></i>%i18n:mobile.tags.mk-settings-page.twitter-integration%<i class="fa fa-angle-right"></i></a></li>
+		<li><a href="./settings/signin-history"><i class="fa fa-sign-in"></i>%i18n:mobile.tags.mk-settings-page.signin-history%<i class="fa fa-angle-right"></i></a></li>
+		<li><a href="./settings/api"><i class="fa fa-key"></i>%i18n:mobile.tags.mk-settings-page.api%<i class="fa fa-angle-right"></i></a></li>
+	</ul>
+	<ul>
+		<li><a onclick={ signout }><i class="fa fa-power-off"></i>%i18n:mobile.tags.mk-settings-page.signout%</a></li>
+	</ul>
+	<style>
+		:scope
+			display block
+
+			> p
+				display block
+				margin 24px
+				text-align center
+				color #555
+
+			> ul
+				display block
+				margin 16px 0
+				padding 0
+				list-style none
+				border-top solid 1px #aaa
+
+				> li
+					display block
+					background #fff
+					border-bottom solid 1px #aaa
+
+					> a
+						$height = 48px
+
+						display block
+						position relative
+						padding 0 16px
+						line-height $height
+						color #4d635e
+
+						> i:nth-of-type(1)
+							margin-right 4px
+
+						> i: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>
+		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
new file mode 100644
index 0000000000..dfe0586c1c
--- /dev/null
+++ b/src/web/app/mobile/tags/page/settings/profile.tag
@@ -0,0 +1,106 @@
+<mk-profile-setting-page>
+	<mk-ui ref="ui">
+		<mk-profile-setting/>
+	</mk-ui>
+	<style>
+		:scope
+			display block
+	</style>
+	<script>
+		import ui from '../../../scripts/ui-event';
+
+		this.on('mount', () => {
+			document.title = 'Misskey | %i18n:mobile.tags.mk-profile-setting-page.title%';
+			ui.trigger('title', '<i class="fa fa-user"></i>%i18n:mobile.tags.mk-profile-setting-page.title%');
+			document.documentElement.style.background = '#eee';
+		});
+	</script>
+</mk-profile-setting-page>
+
+<mk-profile-setting>
+	<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>
+	<button class="save" onclick={ save } disabled={ saving }><i class="fa fa-check"></i>%i18n:mobile.tags.mk-profile-setting.save%</button>
+	<style>
+		:scope
+			display block
+
+			> label
+				display block
+				margin 0
+				padding 16px 0
+
+				> p:first-child
+					display block
+					margin 0
+					padding 0 0 4px 8px
+					font-weight bold
+					color #333
+
+				> input[type="text"]
+				> textarea
+					display block
+					width 100%
+					padding 12px
+					font-size 16px
+					border none
+					border-radius none
+
+				> textarea
+					min-height 80px
+
+			> .save
+				display block
+				margin 8px
+				padding 16px
+				width calc(100% - 16px)
+				font-size 16px
+				color $theme-color-foreground
+				background $theme-color
+				border-radius 4px
+
+				&:disabled
+					opacity 0.7
+
+				> i
+					margin-right 4px
+
+	</style>
+	<script>
+		this.mixin('i');
+		this.mixin('api');
+
+		this.save = () => {
+			this.update({
+				saving: true
+			});
+
+			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
+			}).then(() => {
+				this.update({
+					saving: false
+				});
+
+				alert('%i18n:mobile.tags.mk-profile-setting.saved%');
+			});
+		};
+	</script>
+</mk-profile-setting>

From 93c8374021dc40806a300820563b8d920252a2fa Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 28 Aug 2017 19:43:34 +0900
Subject: [PATCH 029/364] :art:

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

diff --git a/src/web/app/reset.styl b/src/web/app/reset.styl
index 940a9ed18e..85bbd11473 100644
--- a/src/web/app/reset.styl
+++ b/src/web/app/reset.styl
@@ -14,6 +14,7 @@ body
 input:not([type])
 input[type='text']
 input[type='password']
+input[type='search']
 input[type='email']
 textarea
 button

From fda0c9c00113eb1c8a1591e8d1be7f4dc2edaf4a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 28 Aug 2017 19:43:47 +0900
Subject: [PATCH 030/364] Fix bug

---
 src/web/app/mobile/tags/page/settings/api.tag             | 2 +-
 src/web/app/mobile/tags/page/settings/authorized-apps.tag | 2 +-
 src/web/app/mobile/tags/page/settings/signin.tag          | 2 +-
 src/web/app/mobile/tags/page/settings/twitter.tag         | 2 +-
 4 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/web/app/mobile/tags/page/settings/api.tag b/src/web/app/mobile/tags/page/settings/api.tag
index cfffeacb5a..46419eb3db 100644
--- a/src/web/app/mobile/tags/page/settings/api.tag
+++ b/src/web/app/mobile/tags/page/settings/api.tag
@@ -7,7 +7,7 @@
 			display block
 	</style>
 	<script>
-		const ui = require('../../../scripts/ui-event');
+		import ui from '../../../scripts/ui-event';
 
 		this.on('mount', () => {
 			document.title = 'Misskey | API';
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 e962871ec7..78efd13e47 100644
--- a/src/web/app/mobile/tags/page/settings/authorized-apps.tag
+++ b/src/web/app/mobile/tags/page/settings/authorized-apps.tag
@@ -7,7 +7,7 @@
 			display block
 	</style>
 	<script>
-		const ui = require('../../../scripts/ui-event');
+		import ui from '../../../scripts/ui-event';
 
 		this.on('mount', () => {
 			document.title = 'Misskey | %i18n:mobile.tags.mk-authorized-apps-page.application%';
diff --git a/src/web/app/mobile/tags/page/settings/signin.tag b/src/web/app/mobile/tags/page/settings/signin.tag
index 2305ea9fb4..a91ebfb140 100644
--- a/src/web/app/mobile/tags/page/settings/signin.tag
+++ b/src/web/app/mobile/tags/page/settings/signin.tag
@@ -7,7 +7,7 @@
 			display block
 	</style>
 	<script>
-		const ui = require('../../../scripts/ui-event');
+		import ui from '../../../scripts/ui-event';
 
 		this.on('mount', () => {
 			document.title = 'Misskey | %i18n:mobile.tags.mk-signin-history-page.signin-history%';
diff --git a/src/web/app/mobile/tags/page/settings/twitter.tag b/src/web/app/mobile/tags/page/settings/twitter.tag
index f4e9f7628b..870eeeb5bc 100644
--- a/src/web/app/mobile/tags/page/settings/twitter.tag
+++ b/src/web/app/mobile/tags/page/settings/twitter.tag
@@ -7,7 +7,7 @@
 			display block
 	</style>
 	<script>
-		const ui = require('../../../scripts/ui-event');
+		import ui from '../../../scripts/ui-event';
 
 		this.on('mount', () => {
 			document.title = 'Misskey | %i18n:mobile.tags.mk-twitter-setting-page.twitter-integration%';

From b7ed52df9a541415ae370c8e9a499130db053f44 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 28 Aug 2017 19:44:32 +0900
Subject: [PATCH 031/364] :v:

---
 CHANGELOG.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index fb41a5ab7e..d13750f0e5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,8 @@ unreleased
 * Improve: 投稿ページに次の投稿/前の投稿リンクを作成 (#734)
 * Improve: タイムラインの投稿をダブルクリックすることで詳細な情報が見れるように
 * Fix: モバイル版でおすすめユーザーをフォローしてもタイムラインが更新されない (#736)
+* Fix: モバイル版で設定にアクセスできない
+* デザインの調整
 
 2380
 ----

From db9636e5ba5dcb68c7bbb1cb99f6d0650a9e6c4e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 28 Aug 2017 19:46:02 +0900
Subject: [PATCH 032/364] :v:

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d13750f0e5..45c9bccc43 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ unreleased
 * Fix: モバイル版でおすすめユーザーをフォローしてもタイムラインが更新されない (#736)
 * Fix: モバイル版で設定にアクセスできない
 * デザインの調整
+* 依存関係の更新
 
 2380
 ----

From e616c5d15e0c71dcaaa7b385fe41f9a8c17be4f8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 28 Aug 2017 19:46:59 +0900
Subject: [PATCH 033/364] :v:

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 45c9bccc43..c0c7d7e7ad 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ ChangeLog
 unreleased
 ----------
 * New: モバイル版からプロフィールを設定できるように
+* New: モバイル版からサインアウトを行えるように
 * Improve: 投稿ページに次の投稿/前の投稿リンクを作成 (#734)
 * Improve: タイムラインの投稿をダブルクリックすることで詳細な情報が見れるように
 * Fix: モバイル版でおすすめユーザーをフォローしてもタイムラインが更新されない (#736)

From a5f80a81b431c483d55536106ba6b5bd3f116ec1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 28 Aug 2017 19:59:30 +0900
Subject: [PATCH 034/364] #163

---
 locales/en.yml                                |  6 +++
 locales/ja.yml                                |  6 +++
 .../app/mobile/tags/page/settings/profile.tag | 50 +++++++++++++++++++
 3 files changed, 62 insertions(+)

diff --git a/locales/en.yml b/locales/en.yml
index 6d636a1108..bfec7ebb54 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -356,6 +356,12 @@ mobile:
       location: "Location"
       description: "Description"
       birthday: "Birthday"
+      avatar: "Avatar"
+      banner: "Banner"
+      avatar-saved: "Avatar updated successfully"
+      banner-saved: "Banner updated successfully"
+      set-avatar: "Choose an avatar"
+      set-banner: "Choose a banner"
       save: "Save"
       saved: "Profile updated successfully"
 
diff --git a/locales/ja.yml b/locales/ja.yml
index 75b1fd6275..e2c537fc41 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -356,6 +356,12 @@ mobile:
       location: "場所"
       description: "自己紹介"
       birthday: "誕生日"
+      avatar: "アバター"
+      banner: "バナー"
+      avatar-saved: "アバターを保存しました"
+      banner-saved: "バナーを保存しました"
+      set-avatar: "アバターを選択する"
+      set-banner: "バナーを選択する"
       save: "保存"
       saved: "プロフィールを保存しました"
 
diff --git a/src/web/app/mobile/tags/page/settings/profile.tag b/src/web/app/mobile/tags/page/settings/profile.tag
index dfe0586c1c..1f9455cd2c 100644
--- a/src/web/app/mobile/tags/page/settings/profile.tag
+++ b/src/web/app/mobile/tags/page/settings/profile.tag
@@ -34,6 +34,14 @@
 		<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 onclick={ 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>
+	</label>
 	<button class="save" onclick={ save } disabled={ saving }><i class="fa fa-check"></i>%i18n:mobile.tags.mk-profile-setting.save%</button>
 	<style>
 		:scope
@@ -84,6 +92,48 @@
 		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.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.api('i/update', {
+					banner_id: file.id
+				}).then(() => {
+					this.update({
+						bannerSaving: false
+					});
+
+					alert('%i18n:mobile.tags.mk-profile-setting.banner-saved%');
+				});
+			});
+		};
+
 		this.save = () => {
 			this.update({
 				saving: true

From 7245a2fff2cee3bdaa6639011fa28c4a59289515 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 28 Aug 2017 20:03:16 +0900
Subject: [PATCH 035/364] v2458

---
 CHANGELOG.md | 4 ++--
 package.json | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c0c7d7e7ad..b0f6fff438 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,8 @@ ChangeLog
 =========
 主に notable な changes を書いていきます
 
-unreleased
-----------
+2458 (2017/08/28)
+-----------------
 * New: モバイル版からプロフィールを設定できるように
 * New: モバイル版からサインアウトを行えるように
 * Improve: 投稿ページに次の投稿/前の投稿リンクを作成 (#734)
diff --git a/package.json b/package.json
index 6f794e693f..d5412b6363 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2380",
+  "version": "0.0.2458",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",

From bce6c006b2a7d0add0ce5675d3098c57d32c1207 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 28 Aug 2017 20:23:47 +0900
Subject: [PATCH 036/364] Fix bug

---
 src/web/app/mobile/tags/drive-selector.tag |  7 ++++++-
 src/web/app/mobile/tags/drive.tag          | 16 ++++++++++------
 2 files changed, 16 insertions(+), 7 deletions(-)

diff --git a/src/web/app/mobile/tags/drive-selector.tag b/src/web/app/mobile/tags/drive-selector.tag
index 32845432f2..2edae67c1b 100644
--- a/src/web/app/mobile/tags/drive-selector.tag
+++ b/src/web/app/mobile/tags/drive-selector.tag
@@ -3,7 +3,7 @@
 		<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 }><i class="fa fa-times"></i></button>
-			<button class="ok" onclick={ ok }><i class="fa fa-check"></i></button>
+			<button if={ opts.multiple } class="ok" onclick={ ok }><i class="fa fa-check"></i></button>
 		</header>
 		<mk-drive ref="browser" select-file={ true } multiple={ opts.multiple }/>
 	</div>
@@ -68,6 +68,11 @@
 					files: files
 				});
 			});
+
+			this.refs.browser.on('selected', file => {
+				this.trigger('selected', file);
+				this.unmount();
+			});
 		});
 
 		this.cancel = () => {
diff --git a/src/web/app/mobile/tags/drive.tag b/src/web/app/mobile/tags/drive.tag
index e19325091d..9f3e647735 100644
--- a/src/web/app/mobile/tags/drive.tag
+++ b/src/web/app/mobile/tags/drive.tag
@@ -190,7 +190,7 @@
 		this.file = null;
 
 		this.isFileSelectMode = this.opts.selectFile;
-		this.multiple =this.opts.multiple;
+		this.multiple = this.opts.multiple;
 
 		this.on('mount', () => {
 			this.stream.on('drive_file_created', this.onStreamDriveFileCreated);
@@ -435,13 +435,17 @@
 
 		this.chooseFile = file => {
 			if (this.isFileSelectMode) {
-				if (this.selectedFiles.some(f => f.id == file.id)) {
-					this.selectedFiles = this.selectedFiles.filter(f => f.id != file.id);
+				if (this.multiple) {
+					if (this.selectedFiles.some(f => f.id == file.id)) {
+						this.selectedFiles = this.selectedFiles.filter(f => f.id != file.id);
+					} else {
+						this.selectedFiles.push(file);
+					}
+					this.update();
+					this.trigger('change-selection', this.selectedFiles);
 				} else {
-					this.selectedFiles.push(file);
+					this.trigger('selected', file);
 				}
-				this.update();
-				this.trigger('change-selection', this.selectedFiles);
 			} else {
 				this.cf(file);
 			}

From 488dce2cfbe43cec591b0c7ca1f9a20a6cd276a3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 28 Aug 2017 20:23:55 +0900
Subject: [PATCH 037/364] Fix design

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

diff --git a/src/web/app/mobile/tags/page/settings/profile.tag b/src/web/app/mobile/tags/page/settings/profile.tag
index 1f9455cd2c..de365c235e 100644
--- a/src/web/app/mobile/tags/page/settings/profile.tag
+++ b/src/web/app/mobile/tags/page/settings/profile.tag
@@ -66,7 +66,7 @@
 					padding 12px
 					font-size 16px
 					border none
-					border-radius none
+					border-radius 0
 
 				> textarea
 					min-height 80px

From 00deb459858ebde2e44148eb50fec6c63345ea84 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 28 Aug 2017 20:24:52 +0900
Subject: [PATCH 038/364] v2461

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b0f6fff438..436264d228 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog
 =========
 主に notable な changes を書いていきます
 
+-----------------
+* Fix: モバイル版からアバターとバナーの設定を行えなかった問題を修正
+* デザインの修正
+
 2458 (2017/08/28)
 -----------------
 * New: モバイル版からプロフィールを設定できるように
diff --git a/package.json b/package.json
index d5412b6363..beea91ecc5 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2458",
+  "version": "0.0.2461",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",

From e733b62dfae0df7398704beca703fa92ca52cca5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 28 Aug 2017 20:25:55 +0900
Subject: [PATCH 039/364] Fix Changelog

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 436264d228..bdbfdbe733 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@ ChangeLog
 =========
 主に notable な changes を書いていきます
 
+2461 (2017/08/28)
 -----------------
 * Fix: モバイル版からアバターとバナーの設定を行えなかった問題を修正
 * デザインの修正

From f546edb810bdc1db56a32ec0cf156a7d27efe38e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 28 Aug 2017 21:25:47 +0900
Subject: [PATCH 040/364] WIP #738

---
 DONORS.md | 4 ++++
 README.md | 2 ++
 2 files changed, 6 insertions(+)
 create mode 100644 DONORS.md

diff --git a/DONORS.md b/DONORS.md
new file mode 100644
index 0000000000..dec2a57060
--- /dev/null
+++ b/DONORS.md
@@ -0,0 +1,4 @@
+DONORS
+======
+
+(no particular order)
diff --git a/README.md b/README.md
index 9d2d38149c..22451385ce 100644
--- a/README.md
+++ b/README.md
@@ -36,6 +36,8 @@ Sponsors & Backers
 Misskey have no 100+ GitHub stars currently. However, donation are 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 displayed in [donors](./DONORS.md).
+
 Collaborators
 ----------------------------------------------------------------
 | ![syuilo][syuilo-icon] | ![Morisawa Aya][ayamorisawa-icon] | ![otofune][otofune-icon]        |

From 9c83d098566c1991a3fe6dbabcf6194f8080b4db 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, 28 Aug 2017 22:35:22 +0900
Subject: [PATCH 041/364] Update DONORS.md

---
 DONORS.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/DONORS.md b/DONORS.md
index dec2a57060..9bb85938d5 100644
--- a/DONORS.md
+++ b/DONORS.md
@@ -2,3 +2,5 @@ DONORS
 ======
 
 (no particular order)
+
+* スルメ https://surume.tk/

From ffaec0b9712df9a5024c0883a154442f02b72a03 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 28 Aug 2017 23:47:43 +0900
Subject: [PATCH 042/364] #497

---
 CHANGELOG.md                                  |  8 +++-
 locales/en.yml                                |  4 ++
 locales/ja.yml                                |  4 ++
 src/api/common/generate-native-user-token.ts  |  3 ++
 src/api/endpoints.ts                          |  4 ++
 src/api/endpoints/i/regenerate_token.ts       | 42 +++++++++++++++++++
 src/api/private/signup.ts                     |  4 +-
 src/web/app/common/scripts/home-stream.js     |  6 +++
 src/web/app/common/tags/api-info.tag          | 27 ------------
 src/web/app/common/tags/index.js              |  1 -
 .../app/desktop/scripts/password-dialog.js    | 11 +++++
 src/web/app/desktop/tags/input-dialog.tag     |  7 +++-
 src/web/app/desktop/tags/settings.tag         | 30 +++++++++++++
 src/web/app/mobile/tags/page/settings/api.tag | 19 +++++++++
 14 files changed, 137 insertions(+), 33 deletions(-)
 create mode 100644 src/api/common/generate-native-user-token.ts
 create mode 100644 src/api/endpoints/i/regenerate_token.ts
 delete mode 100644 src/web/app/common/tags/api-info.tag
 create mode 100644 src/web/app/desktop/scripts/password-dialog.js

diff --git a/CHANGELOG.md b/CHANGELOG.md
index bdbfdbe733..2d18b1b7f6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog
 =========
 主に notable な changes を書いていきます
 
+unlereased
+----------
+* New: トークンを再生成できるように (#497)
+
 2461 (2017/08/28)
 -----------------
 * Fix: モバイル版からアバターとバナーの設定を行えなかった問題を修正
@@ -11,8 +15,8 @@ ChangeLog
 -----------------
 * New: モバイル版からプロフィールを設定できるように
 * New: モバイル版からサインアウトを行えるように
-* Improve: 投稿ページに次の投稿/前の投稿リンクを作成 (#734)
-* Improve: タイムラインの投稿をダブルクリックすることで詳細な情報が見れるように
+* New: 投稿ページに次の投稿/前の投稿リンクを作成 (#734)
+* New: タイムラインの投稿をダブルクリックすることで詳細な情報が見れるように
 * Fix: モバイル版でおすすめユーザーをフォローしてもタイムラインが更新されない (#736)
 * Fix: モバイル版で設定にアクセスできない
 * デザインの調整
diff --git a/locales/en.yml b/locales/en.yml
index bfec7ebb54..950180278d 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -28,6 +28,7 @@ common:
   loading: "Loading"
   ok: "OK"
   update-available: "New version of Misskey is now available({newer}, current is {current}). Reload page to apply update."
+  my-token-regenerated: "Your token is just regenerated, so you will signout."
 
   tags:
     mk-messaging-form:
@@ -129,6 +130,9 @@ common:
 
 desktop:
   tags:
+    mk-api-info:
+      regenerate-token: "Please enter the password"
+
     mk-drive-browser-base-contextmenu:
       create-folder: "Create a folder"
       upload: "Upload a file"
diff --git a/locales/ja.yml b/locales/ja.yml
index e2c537fc41..2655eb4846 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -28,6 +28,7 @@ common:
   loading: "読み込み中"
   ok: "わかった"
   update-available: "Misskeyの新しいバージョンがあります({newer}。現在{current}を利用中)。ページを再度読み込みすると更新が適用されます。"
+  my-token-regenerated: "あなたのトークンが更新されたのでサインアウトします。"
 
   tags:
     mk-messaging-form:
@@ -129,6 +130,9 @@ common:
 
 desktop:
   tags:
+    mk-api-info:
+      regenerate-token: "パスワードを入力してください"
+
     mk-drive-browser-base-contextmenu:
       create-folder: "フォルダーを作成"
       upload: "ファイルをアップロード"
diff --git a/src/api/common/generate-native-user-token.ts b/src/api/common/generate-native-user-token.ts
new file mode 100644
index 0000000000..2082b89a5a
--- /dev/null
+++ b/src/api/common/generate-native-user-token.ts
@@ -0,0 +1,3 @@
+import rndstr from 'rndstr';
+
+export default () => `!${rndstr('a-zA-Z0-9', 32)}`;
diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts
index 5bbc480a8e..a658c9a42e 100644
--- a/src/api/endpoints.ts
+++ b/src/api/endpoints.ts
@@ -159,6 +159,10 @@ const endpoints: Endpoint[] = [
 		},
 		kind: 'account-write'
 	},
+	{
+		name: 'i/regenerate_token',
+		withCredential: true
+	},
 	{
 		name: 'i/appdata/get',
 		withCredential: true
diff --git a/src/api/endpoints/i/regenerate_token.ts b/src/api/endpoints/i/regenerate_token.ts
new file mode 100644
index 0000000000..ccebbc8101
--- /dev/null
+++ b/src/api/endpoints/i/regenerate_token.ts
@@ -0,0 +1,42 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import * as bcrypt from 'bcryptjs';
+import User from '../../models/user';
+import event from '../../event';
+import generateUserToken from '../../common/generate-native-user-token';
+
+/**
+ * Regenerate native token
+ *
+ * @param {any} params
+ * @param {any} user
+ * @return {Promise<any>}
+ */
+module.exports = async (params, user) => new Promise(async (res, rej) => {
+	// Get 'password' parameter
+	const [password, passwordErr] = $(params.password).string().$;
+	if (passwordErr) return rej('invalid password param');
+
+	// Compare password
+	const same = bcrypt.compareSync(password, user.password);
+
+	if (!same) {
+		return rej('incorrect password');
+	}
+
+	// Generate secret
+	const secret = generateUserToken();
+
+	await User.update(user._id, {
+		$set: {
+			token: secret
+		}
+	});
+
+	res();
+
+	// Publish i updated event
+	event(user._id, 'my_token_regenerated');
+});
diff --git a/src/api/private/signup.ts b/src/api/private/signup.ts
index 2375c22845..899fa88472 100644
--- a/src/api/private/signup.ts
+++ b/src/api/private/signup.ts
@@ -1,10 +1,10 @@
 import * as express from 'express';
 import * as bcrypt from 'bcryptjs';
-import rndstr from 'rndstr';
 import recaptcha = require('recaptcha-promise');
 import User from '../models/user';
 import { validateUsername, validatePassword } from '../models/user';
 import serialize from '../serializers/user';
+import generateUserToken from '../common/generate-native-user-token';
 import config from '../../conf';
 
 recaptcha.init({
@@ -58,7 +58,7 @@ export default async (req: express.Request, res: express.Response) => {
 	const hash = bcrypt.hashSync(password, salt);
 
 	// Generate secret
-	const secret = `!${rndstr('a-zA-Z0-9', 32)}`;
+	const secret = generateUserToken();
 
 	// Create account
 	const account = await User.insert({
diff --git a/src/web/app/common/scripts/home-stream.js b/src/web/app/common/scripts/home-stream.js
index 24f13cd291..c54cbd7f19 100644
--- a/src/web/app/common/scripts/home-stream.js
+++ b/src/web/app/common/scripts/home-stream.js
@@ -1,6 +1,7 @@
 'use strict';
 
 import Stream from './stream';
+import signout from './signout';
 
 /**
  * Home stream connection
@@ -12,6 +13,11 @@ class Connection extends Stream {
 		});
 
 		this.on('i_updated', me.update);
+
+		this.on('my_token_regenerated', () => {
+			alert('%i18n:common.my-token-regenerated%');
+			signout();
+		});
 	}
 }
 
diff --git a/src/web/app/common/tags/api-info.tag b/src/web/app/common/tags/api-info.tag
deleted file mode 100644
index 612f20a7a8..0000000000
--- a/src/web/app/common/tags/api-info.tag
+++ /dev/null
@@ -1,27 +0,0 @@
-<mk-api-info>
-	<p>Token:<code>{ I.token }</code></p>
-	<p>APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。</p>
-	<p>アカウントを乗っ取られてしまう可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。</p>
-	<p>万が一このトークンが漏れたりその可能性がある場合は
-		<button class="regenerate" onclick={ regenerateToken }>トークンを再生成</button>できます。(副作用として、ログインしているすべてのデバイスでログアウトが発生します)
-	</p>
-	<style>
-		:scope
-			display block
-			color #4a535a
-
-			code
-				padding 4px
-				background #eee
-
-			.regenerate
-				display inline
-				color $theme-color
-
-				&:hover
-					text-decoration underline
-	</style>
-	<script>
-		this.mixin('i');
-	</script>
-</mk-api-info>
diff --git a/src/web/app/common/tags/index.js b/src/web/app/common/tags/index.js
index 5dc4ef4546..1ee8dab42d 100644
--- a/src/web/app/common/tags/index.js
+++ b/src/web/app/common/tags/index.js
@@ -14,7 +14,6 @@ require('./forkit.tag');
 require('./introduction.tag');
 require('./copyright.tag');
 require('./signin-history.tag');
-require('./api-info.tag');
 require('./twitter-setting.tag');
 require('./authorized-apps.tag');
 require('./poll.tag');
diff --git a/src/web/app/desktop/scripts/password-dialog.js b/src/web/app/desktop/scripts/password-dialog.js
new file mode 100644
index 0000000000..2bdc93e421
--- /dev/null
+++ b/src/web/app/desktop/scripts/password-dialog.js
@@ -0,0 +1,11 @@
+import * as riot from 'riot';
+
+export default (title, onOk, onCancel) => {
+	const dialog = document.body.appendChild(document.createElement('mk-input-dialog'));
+	return riot.mount(dialog, {
+		title: title,
+		type: 'password',
+		onOk: onOk,
+		onCancel: onCancel
+	});
+};
diff --git a/src/web/app/desktop/tags/input-dialog.tag b/src/web/app/desktop/tags/input-dialog.tag
index f343c4625a..78fd62ee8b 100644
--- a/src/web/app/desktop/tags/input-dialog.tag
+++ b/src/web/app/desktop/tags/input-dialog.tag
@@ -5,7 +5,7 @@
 		</yield>
 		<yield to="content">
 			<div class="body">
-				<input ref="text" oninput={ parent.update } onkeydown={ parent.onKeydown } placeholder={ parent.placeholder }/>
+				<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>
@@ -126,6 +126,7 @@
 		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;
@@ -156,6 +157,10 @@
 			this.refs.window.close();
 		};
 
+		this.onInput = () => {
+			this.update();
+		};
+
 		this.onKeydown = e => {
 			if (e.which == 13) { // Enter
 				e.preventDefault();
diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag
index a89cfda0e4..7fc6acb4a8 100644
--- a/src/web/app/desktop/tags/settings.tag
+++ b/src/web/app/desktop/tags/settings.tag
@@ -211,3 +211,33 @@
 		};
 	</script>
 </mk-settings>
+
+<mk-api-info>
+	<p>Token:<code>{ I.token }</code></p>
+	<p>APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。</p>
+	<p>アカウントを乗っ取られてしまう可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。</p>
+	<p>万が一このトークンが漏れたりその可能性がある場合は<a class="regenerate" onclick={ regenerateToken }>トークンを再生成</a>できます。(副作用として、ログインしているすべてのデバイスでログアウトが発生します)</p>
+	<style>
+		:scope
+			display block
+			color #4a535a
+
+			code
+				padding 4px
+				background #eee
+	</style>
+	<script>
+		import passwordDialog from '../scripts/password-dialog';
+
+		this.mixin('i');
+		this.mixin('api');
+
+		this.regenerateToken = () => {
+			passwordDialog('%i18n:desktop.tags.mk-api-info.regenerate-token%', password => {
+				this.api('i/regenerate_token', {
+					password: password
+				})
+			});
+		};
+	</script>
+</mk-api-info>
diff --git a/src/web/app/mobile/tags/page/settings/api.tag b/src/web/app/mobile/tags/page/settings/api.tag
index 46419eb3db..25413e2d80 100644
--- a/src/web/app/mobile/tags/page/settings/api.tag
+++ b/src/web/app/mobile/tags/page/settings/api.tag
@@ -15,3 +15,22 @@
 		});
 	</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 cdcd8e5c40197c807c69aea19899bc556c8b783b 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, 28 Aug 2017 23:49:14 +0900
Subject: [PATCH 043/364] Update regenerate_token.ts

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

diff --git a/src/api/endpoints/i/regenerate_token.ts b/src/api/endpoints/i/regenerate_token.ts
index ccebbc8101..f96d10ebfc 100644
--- a/src/api/endpoints/i/regenerate_token.ts
+++ b/src/api/endpoints/i/regenerate_token.ts
@@ -37,6 +37,6 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 
 	res();
 
-	// Publish i updated event
+	// Publish event
 	event(user._id, 'my_token_regenerated');
 });

From 8f0e6a70cf22f1248b99bd3b300feed8b1b1efc8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 29 Aug 2017 00:20:47 +0900
Subject: [PATCH 044/364] #364

---
 CHANGELOG.md                           |  1 +
 locales/en.yml                         |  9 ++++++
 locales/ja.yml                         |  9 ++++++
 src/api/endpoints.ts                   |  4 +++
 src/api/endpoints/i/change_password.ts | 42 ++++++++++++++++++++++++++
 src/web/app/desktop/tags/settings.tag  | 38 +++++++++++++++++++++--
 6 files changed, 101 insertions(+), 2 deletions(-)
 create mode 100644 src/api/endpoints/i/change_password.ts

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2d18b1b7f6..4e49f9ca49 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ ChangeLog
 unlereased
 ----------
 * New: トークンを再生成できるように (#497)
+* New: パスワードを変更する機能 (#364)
 
 2461 (2017/08/28)
 -----------------
diff --git a/locales/en.yml b/locales/en.yml
index 950180278d..a24b8725ae 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -208,6 +208,12 @@ desktop:
       settings: "Settings"
       signout: "Sign out"
 
+    mk-password-setting:
+      reset: "Change your password"
+      enter-current-password: "Enter the current password"
+      enter-new-password: "Enter the new password"
+      changed: "Password updated successfully"
+
     mk-post-form:
       post-placeholder: "What's happening?"
       reply-placeholder: "Reply to this post..."
@@ -239,6 +245,9 @@ desktop:
       prev: "Previous post"
       next: "Next post"
 
+    mk-settings:
+      password: "Password"
+
     mk-timeline-post:
       reposted-by: "Reposted by {}"
       reply: "Reply"
diff --git a/locales/ja.yml b/locales/ja.yml
index 2655eb4846..88e0b76d82 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -208,6 +208,12 @@ desktop:
       settings: "設定"
       signout: "サインアウト"
 
+    mk-password-setting:
+      reset: "パスワードを変更する"
+      enter-current-password: "現在のパスワードを入力してください"
+      enter-new-password: "新しいパスワードを入力してください"
+      changed: "パスワードを変更しました"
+
     mk-post-form:
       post-placeholder: "いまどうしてる?"
       reply-placeholder: "この投稿への返信..."
@@ -239,6 +245,9 @@ desktop:
       prev: "前の投稿"
       next: "次の投稿"
 
+    mk-settings:
+      password: "パスワード"
+
     mk-timeline-post:
       reposted-by: "{}がRepost"
       reply: "返信"
diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts
index a658c9a42e..c6661533e8 100644
--- a/src/api/endpoints.ts
+++ b/src/api/endpoints.ts
@@ -159,6 +159,10 @@ const endpoints: Endpoint[] = [
 		},
 		kind: 'account-write'
 	},
+	{
+		name: 'i/change_password',
+		withCredential: true
+	},
 	{
 		name: 'i/regenerate_token',
 		withCredential: true
diff --git a/src/api/endpoints/i/change_password.ts b/src/api/endpoints/i/change_password.ts
new file mode 100644
index 0000000000..faceded29d
--- /dev/null
+++ b/src/api/endpoints/i/change_password.ts
@@ -0,0 +1,42 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import * as bcrypt from 'bcryptjs';
+import User from '../../models/user';
+
+/**
+ * Change password
+ *
+ * @param {any} params
+ * @param {any} 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 'new_password' parameter
+	const [newPassword, newPasswordErr] = $(params.new_password).string().$;
+	if (newPasswordErr) return rej('invalid new_password param');
+
+	// Compare password
+	const same = bcrypt.compareSync(currentPassword, user.password);
+
+	if (!same) {
+		return rej('incorrect password');
+	}
+
+	// Generate hash of password
+	const salt = bcrypt.genSaltSync(8);
+	const hash = bcrypt.hashSync(newPassword, salt);
+
+	await User.update(user._id, {
+		$set: {
+			password: hash
+		}
+	});
+
+	res();
+});
diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag
index 7fc6acb4a8..80a42d6652 100644
--- a/src/web/app/desktop/tags/settings.tag
+++ b/src/web/app/desktop/tags/settings.tag
@@ -7,7 +7,7 @@
 		<p class={ active: page == 'apps' } onmousedown={ setPage.bind(null, 'apps') }><i class="fa fa-fw fa-puzzle-piece"></i>アプリ</p>
 		<p class={ active: page == 'twitter' } onmousedown={ setPage.bind(null, 'twitter') }><i class="fa fa-fw fa-twitter"></i>Twitter</p>
 		<p class={ active: page == 'signin' } onmousedown={ setPage.bind(null, 'signin') }><i class="fa fa-fw fa-sign-in"></i>ログイン履歴</p>
-		<p class={ active: page == 'password' } onmousedown={ setPage.bind(null, 'password') }><i class="fa fa-fw fa-unlock-alt"></i>パスワード</p>
+		<p class={ active: page == 'password' } onmousedown={ setPage.bind(null, 'password') }><i class="fa fa-fw fa-unlock-alt"></i>%i18n:desktop.tags.mk-settings.password%</p>
 		<p class={ active: page == 'api' } onmousedown={ setPage.bind(null, 'api') }><i class="fa fa-fw fa-key"></i>API</p>
 	</div>
 	<div class="pages">
@@ -58,6 +58,11 @@
 			<mk-signin-history/>
 		</section>
 
+		<section class="password" show={ page == 'password' }>
+			<h1>%i18n:desktop.tags.mk-settings.password%</h1>
+			<mk-password-setting/>
+		</section>
+
 		<section class="api" show={ page == 'api' }>
 			<h1>API</h1>
 			<mk-api-info/>
@@ -236,8 +241,37 @@
 			passwordDialog('%i18n:desktop.tags.mk-api-info.regenerate-token%', password => {
 				this.api('i/regenerate_token', {
 					password: password
-				})
+				});
 			});
 		};
 	</script>
 </mk-api-info>
+
+<mk-password-setting>
+	<button onclick={ reset }>%i18n:desktop.tags.mk-password-setting.reset%</button>
+	<style>
+		:scope
+			display block
+			color #4a535a
+	</style>
+	<script>
+		import passwordDialog from '../scripts/password-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 => {
+					this.api('i/change_password', {
+						current_password: currentPassword,
+						new_password: newPassword
+					}).then(() => {
+						notify('%i18n:desktop.tags.mk-password-setting.changed%');
+					});
+				});
+			});
+		};
+	</script>
+</mk-password-setting>

From 79ab7560400085fcf428d1f8010772e46e03d6e5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 29 Aug 2017 00:23:12 +0900
Subject: [PATCH 045/364] v2470

---
 CHANGELOG.md | 4 ++--
 package.json | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4e49f9ca49..dc0442ba01 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,8 @@ ChangeLog
 =========
 主に notable な changes を書いていきます
 
-unlereased
-----------
+2470 (2017/08/29)
+-----------------
 * New: トークンを再生成できるように (#497)
 * New: パスワードを変更する機能 (#364)
 
diff --git a/package.json b/package.json
index beea91ecc5..fab513e253 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2461",
+  "version": "0.0.2470",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",

From b577f076be3e0eebac398c5f82d41b05f256768c 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, 29 Aug 2017 00:28:59 +0900
Subject: [PATCH 046/364] Update README.md

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 22451385ce..3020977bfc 100644
--- a/README.md
+++ b/README.md
@@ -36,7 +36,7 @@ Sponsors & Backers
 Misskey have no 100+ GitHub stars currently. However, donation are 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 displayed in [donors](./DONORS.md).
+**Note:** When you donate to Misskey, your name will be displayed in [donors](./DONORS.md).
 
 Collaborators
 ----------------------------------------------------------------

From afc6e07c3f925081d1e0d459f6b4cee210fa77ec 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, 29 Aug 2017 00:41:30 +0900
Subject: [PATCH 047/364] Update DONORS.md

---
 DONORS.md | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/DONORS.md b/DONORS.md
index 9bb85938d5..dc000de26d 100644
--- a/DONORS.md
+++ b/DONORS.md
@@ -4,3 +4,7 @@ DONORS
 (no particular order)
 
 * スルメ https://surume.tk/
+
+---
+
+:heart: Thanks for donating, guys!

From b5436d780cbb20a5035668e24e87a2d6f36ff1bd 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, 29 Aug 2017 00:47:20 +0900
Subject: [PATCH 048/364] Update DONORS.md

---
 DONORS.md | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/DONORS.md b/DONORS.md
index dc000de26d..d022c4ef64 100644
--- a/DONORS.md
+++ b/DONORS.md
@@ -5,6 +5,12 @@ DONORS
 
 * スルメ https://surume.tk/
 
+:heart: Thanks for donating, guys!
+
 ---
 
-:heart: Thanks for donating, guys!
+You donated, but you are not listed here? please contact to us!
+
+If you want to donate to Misskey, please get in touch with [@syuilo][syuilo-link].
+
+[syuilo-link]: https://syuilo.com

From 01693c8c1607c45ae6367ede10f73e33ac64ef92 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 28 Aug 2017 15:53:51 +0000
Subject: [PATCH 049/364] chore(package): update @types/mongodb to version
 2.2.11

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

diff --git a/package.json b/package.json
index fab513e253..3fade49978 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,7 @@
     "@types/is-url": "1.2.28",
     "@types/js-yaml": "3.9.0",
     "@types/mocha": "2.2.42",
-    "@types/mongodb": "2.2.10",
+    "@types/mongodb": "2.2.11",
     "@types/monk": "1.0.6",
     "@types/morgan": "1.7.32",
     "@types/ms": "0.7.30",

From 15d08564c8cb553e48677daee20648d076323424 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 29 Aug 2017 02:46:28 +0900
Subject: [PATCH 050/364] #739

---
 CHANGELOG.md                          |  4 ++++
 locales/en.yml                        |  2 ++
 locales/ja.yml                        |  2 ++
 src/web/app/desktop/tags/dialog.tag   |  3 +++
 src/web/app/desktop/tags/settings.tag | 19 ++++++++++++++-----
 5 files changed, 25 insertions(+), 5 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index dc0442ba01..8a310513bc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog
 =========
 主に notable な changes を書いていきます
 
+unreleased
+----------
+* New: パスワードを変更する際に新しいパスワードを二度入力させる (#739)
+
 2470 (2017/08/29)
 -----------------
 * New: トークンを再生成できるように (#497)
diff --git a/locales/en.yml b/locales/en.yml
index a24b8725ae..439b7ef069 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -212,6 +212,8 @@ desktop:
       reset: "Change your password"
       enter-current-password: "Enter the current password"
       enter-new-password: "Enter the new password"
+      enter-new-password-again: "Enter the new password again"
+      not-match: "New password not matched"
       changed: "Password updated successfully"
 
     mk-post-form:
diff --git a/locales/ja.yml b/locales/ja.yml
index 88e0b76d82..258e192394 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -212,6 +212,8 @@ desktop:
       reset: "パスワードを変更する"
       enter-current-password: "現在のパスワードを入力してください"
       enter-new-password: "新しいパスワードを入力してください"
+      enter-new-password-again: "もう一度新しいパスワードを入力してください"
+      not-match: "新しいパスワードが一致しません"
       changed: "パスワードを変更しました"
 
     mk-post-form:
diff --git a/src/web/app/desktop/tags/dialog.tag b/src/web/app/desktop/tags/dialog.tag
index 9905123eeb..743fd63942 100644
--- a/src/web/app/desktop/tags/dialog.tag
+++ b/src/web/app/desktop/tags/dialog.tag
@@ -44,6 +44,9 @@
 					// color #43A4EC
 					font-weight bold
 
+					&:empty
+						display none
+
 					> i
 						margin-right 0.5em
 
diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag
index 80a42d6652..eabddfb432 100644
--- a/src/web/app/desktop/tags/settings.tag
+++ b/src/web/app/desktop/tags/settings.tag
@@ -256,6 +256,7 @@
 	</style>
 	<script>
 		import passwordDialog from '../scripts/password-dialog';
+		import dialog from '../scripts/dialog';
 		import notify from '../scripts/notify';
 
 		this.mixin('i');
@@ -264,11 +265,19 @@
 		this.reset = () => {
 			passwordDialog('%i18n:desktop.tags.mk-password-setting.enter-current-password%', currentPassword => {
 				passwordDialog('%i18n:desktop.tags.mk-password-setting.enter-new-password%', newPassword => {
-					this.api('i/change_password', {
-						current_password: currentPassword,
-						new_password: newPassword
-					}).then(() => {
-						notify('%i18n:desktop.tags.mk-password-setting.changed%');
+					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%');
+						});
 					});
 				});
 			});

From 57e6571afdd00f9ce489dd66e06455b9b753ff0b 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, 29 Aug 2017 02:56:07 +0900
Subject: [PATCH 051/364] MAKE EMOJIS GREAT AGAIN

---
 src/api/service/github.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/api/service/github.ts b/src/api/service/github.ts
index a631808ba5..1c78267c0f 100644
--- a/src/api/service/github.ts
+++ b/src/api/service/github.ts
@@ -111,12 +111,12 @@ module.exports = async (app: express.Application) => {
 
 	handler.on('watch', event => {
 		const sender = event.sender;
-		post(`Starred by **${sender.login}**`);
+		post(`⭐️ Starred by **${sender.login}** ⭐️`);
 	});
 
 	handler.on('fork', event => {
 		const repo = event.forkee;
-		post(`Forked:\n${repo.html_url}`);
+		post(`🍴 Forked:\n${repo.html_url} 🍴`);
 	});
 
 	handler.on('pull_request', event => {

From 4540b75e58f3a0ddbd8a581888c368425f4b291a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 29 Aug 2017 09:58:05 +0900
Subject: [PATCH 052/364] =?UTF-8?q?=E6=8A=95=E7=A8=BF=E3=81=AE=E3=83=AA?=
 =?UTF-8?q?=E3=83=B3=E3=82=AF=E3=81=8C=E6=A9=9F=E8=83=BD=E3=81=97=E3=81=A6?=
 =?UTF-8?q?=E3=81=84=E3=81=AA=E3=81=84=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE?=
 =?UTF-8?q?=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 CHANGELOG.md                             | 1 +
 src/web/app/desktop/tags/post-detail.tag | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8a310513bc..f7e5544192 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ ChangeLog
 unreleased
 ----------
 * New: パスワードを変更する際に新しいパスワードを二度入力させる (#739)
+* Fix: 投稿のリンクが機能していない問題を修正
 
 2470 (2017/08/29)
 -----------------
diff --git a/src/web/app/desktop/tags/post-detail.tag b/src/web/app/desktop/tags/post-detail.tag
index 2a962816e1..7a90dccf39 100644
--- a/src/web/app/desktop/tags/post-detail.tag
+++ b/src/web/app/desktop/tags/post-detail.tag
@@ -30,7 +30,7 @@
 			<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={ url }>
+				<a class="time" href={ '/' + p.user.username + '/' + p.id }>
 					<mk-time time={ p.created_at }/>
 				</a>
 			</header>

From 2d34939ec3cb7ade469276495d70ffec629ce3b5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 29 Aug 2017 10:11:04 +0900
Subject: [PATCH 053/364] #738

---
 CHANGELOG.md                                  | 2 ++
 locales/en.yml                                | 9 +++++++++
 locales/ja.yml                                | 9 +++++++++
 src/web/app/desktop/tags/home-widgets/nav.tag | 2 +-
 4 files changed, 21 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f7e5544192..871652f37f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,9 @@ ChangeLog
 unreleased
 ----------
 * New: パスワードを変更する際に新しいパスワードを二度入力させる (#739)
+* New: ドナーを表示する (#738)
 * Fix: 投稿のリンクが機能していない問題を修正
+* l10n
 
 2470 (2017/08/29)
 -----------------
diff --git a/locales/en.yml b/locales/en.yml
index 439b7ef069..782630ef42 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -193,6 +193,15 @@ desktop:
     mk-drive-browser-nav-folder:
       drive: "Drive"
 
+    mk-nav-home-widget:
+      about: "About"
+      stats: "Stats"
+      status: "Status"
+      wiki: "Wiki"
+      donors: "Donors"
+      repository: "Repository"
+      develop: "Developers"
+
     mk-ui-header-nav:
       home: "Home"
       messaging: "Messages"
diff --git a/locales/ja.yml b/locales/ja.yml
index 258e192394..7b1f052f91 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -193,6 +193,15 @@ desktop:
     mk-drive-browser-nav-folder:
       drive: "ドライブ"
 
+    mk-nav-home-widget:
+      about: "Misskeyについて"
+      stats: "統計"
+      status: "ステータス"
+      wiki: "Wiki"
+      donors: "ドナー"
+      repository: "リポジトリ"
+      develop: "開発者"
+
     mk-ui-header-nav:
       home: "ホーム"
       messaging: "メッセージ"
diff --git a/src/web/app/desktop/tags/home-widgets/nav.tag b/src/web/app/desktop/tags/home-widgets/nav.tag
index 499d66014b..54bfb87a11 100644
--- a/src/web/app/desktop/tags/home-widgets/nav.tag
+++ b/src/web/app/desktop/tags/home-widgets/nav.tag
@@ -1,4 +1,4 @@
-<mk-nav-home-widget><a href={ CONFIG.aboutUrl }>Misskeyについて</a><i>・</i><a href={ CONFIG.statsUrl }>統計</a><i>・</i><a href={ CONFIG.statusUrl }>ステータス</a><i>・</i><a href="http://zawazawa.jp/misskey/">Wiki</a><i>・</i><a href="https://github.com/syuilo/misskey">リポジトリ</a><i>・</i><a href={ CONFIG.devUrl }>開発者</a><i>・</i><a href="https://twitter.com/misskey_xyz" target="_blank">Follow us on <i class="fa fa-twitter"></i></a>
+<mk-nav-home-widget><a href={ CONFIG.aboutUrl }>%i18n:desktop.tags.mk-nav-home-widget.about%</a><i>・</i><a href={ CONFIG.statsUrl }>%i18n:desktop.tags.mk-nav-home-widget.stats%</a><i>・</i><a href={ CONFIG.statusUrl }>%i18n:desktop.tags.mk-nav-home-widget.status%</a><i>・</i><a href="http://zawazawa.jp/misskey/">%i18n:desktop.tags.mk-nav-home-widget.wiki%</a><i>・</i><a href="https://github.com/syuilo/misskey/blob/master/DONORS.md">%i18n:desktop.tags.mk-nav-home-widget.donors%</a><i>・</i><a href="https://github.com/syuilo/misskey">%i18n:desktop.tags.mk-nav-home-widget.repository%</a><i>・</i><a href={ CONFIG.devUrl }>%i18n:desktop.tags.mk-nav-home-widget.develop%</a><i>・</i><a href="https://twitter.com/misskey_xyz" target="_blank">Follow us on <i class="fa fa-twitter"></i></a>
 	<style>
 		:scope
 			display block

From a0528dba996ceecb6f42c738b0ce8afcec5ec37c 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, 29 Aug 2017 11:37:08 +0900
Subject: [PATCH 054/364] Update CHANGELOG.md

---
 CHANGELOG.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 871652f37f..66039a227a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,5 @@
-ChangeLog
-=========
+ChangeLog (Release Notes)
+=========================
 主に notable な changes を書いていきます
 
 unreleased

From 76ebad11cd49b4838bfd7cfee37692b3b95c755f 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, 29 Aug 2017 11:42:12 +0900
Subject: [PATCH 055/364] Update README.md

---
 README.md | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/README.md b/README.md
index 3020977bfc..7d256993ee 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,10 @@ Contribution
 ----------------------------------------------------------------
 Please see [Contribution guide](./CONTRIBUTING.md).
 
+Release Notes
+----------------------------------------------------------------
+Please see [ChangeLog](./CHANGELOG.md).
+
 Sponsors & Backers
 ----------------------------------------------------------------
 Misskey have no 100+ GitHub stars currently. However, donation are always welcome!

From baa8492b2b1dc9c368036c62acfb48d94e937e84 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 29 Aug 2017 13:26:14 +0900
Subject: [PATCH 056/364] :art:

---
 CHANGELOG.md                                  |   1 +
 locales/en.yml                                |   1 +
 locales/ja.yml                                |   1 +
 src/web/app/mobile/tags/page/settings.tag     |  29 ++-
 .../app/mobile/tags/page/settings/profile.tag | 169 ++++++++++++------
 5 files changed, 138 insertions(+), 63 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 66039a227a..faefb4d72f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@ unreleased
 * New: ドナーを表示する (#738)
 * Fix: 投稿のリンクが機能していない問題を修正
 * l10n
+* デザインの調整
 
 2470 (2017/08/29)
 -----------------
diff --git a/locales/en.yml b/locales/en.yml
index 782630ef42..b0488a775c 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -376,6 +376,7 @@ mobile:
       title: "Profile Settings"
 
     mk-profile-setting:
+      will-be-published: "These profiles will be published."
       name: "Name"
       location: "Location"
       description: "Description"
diff --git a/locales/ja.yml b/locales/ja.yml
index 7b1f052f91..9a040f798d 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -376,6 +376,7 @@ mobile:
       title: "プロフィール設定"
 
     mk-profile-setting:
+      will-be-published: "これらのプロフィールは公開されます。"
       name: "名前"
       location: "場所"
       description: "自己紹介"
diff --git a/src/web/app/mobile/tags/page/settings.tag b/src/web/app/mobile/tags/page/settings.tag
index 710591071d..b129b97bd1 100644
--- a/src/web/app/mobile/tags/page/settings.tag
+++ b/src/web/app/mobile/tags/page/settings.tag
@@ -12,7 +12,7 @@
 		this.on('mount', () => {
 			document.title = 'Misskey | %i18n:mobile.tags.mk-settings-page.settings%';
 			ui.trigger('title', '<i class="fa fa-cog"></i>%i18n:mobile.tags.mk-settings-page.settings%');
-			document.documentElement.style.background = '#eee';
+			document.documentElement.style.background = '#313a42';
 		});
 	</script>
 </mk-settings-page>
@@ -37,19 +37,36 @@
 				display block
 				margin 24px
 				text-align center
-				color #555
+				color #cad2da
 
 			> ul
+				$radius = 8px
+
 				display block
-				margin 16px 0
+				margin 16px auto
 				padding 0
+				max-width 500px
+				width calc(100% - 32px)
 				list-style none
-				border-top solid 1px #aaa
+				background #fff
+				border solid 1px rgba(0, 0, 0, 0.2)
+				border-radius $radius
 
 				> li
 					display block
-					background #fff
-					border-bottom solid 1px #aaa
+					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
diff --git a/src/web/app/mobile/tags/page/settings/profile.tag b/src/web/app/mobile/tags/page/settings/profile.tag
index de365c235e..fb78d2f719 100644
--- a/src/web/app/mobile/tags/page/settings/profile.tag
+++ b/src/web/app/mobile/tags/page/settings/profile.tag
@@ -12,80 +12,135 @@
 		this.on('mount', () => {
 			document.title = 'Misskey | %i18n:mobile.tags.mk-profile-setting-page.title%';
 			ui.trigger('title', '<i class="fa fa-user"></i>%i18n:mobile.tags.mk-profile-setting-page.title%');
-			document.documentElement.style.background = '#eee';
+			document.documentElement.style.background = '#313a42';
 		});
 	</script>
 </mk-profile-setting-page>
 
 <mk-profile-setting>
-	<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 onclick={ 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>
-	</label>
-	<button class="save" onclick={ save } disabled={ saving }><i class="fa fa-check"></i>%i18n:mobile.tags.mk-profile-setting.save%</button>
+	<div>
+		<p><i class="fa fa-info-circle"></i>%i18n:mobile.tags.mk-profile-setting.will-be-published%</p>
+		<div class="form">
+			<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 onclick={ 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>
+			</label>
+		</div>
+		<button class="save" onclick={ save } disabled={ saving }><i class="fa fa-check"></i>%i18n:mobile.tags.mk-profile-setting.save%</button>
+	</div>
 	<style>
 		:scope
 			display block
 
-			> label
-				display block
-				margin 0
-				padding 16px 0
+			> div
+				margin 16px auto
+				max-width 500px
+				width calc(100% - 32px)
 
-				> p:first-child
+				> p
 					display block
-					margin 0
-					padding 0 0 4px 8px
-					font-weight bold
-					color #333
+					margin 0 0 8px 0
+					padding 16px
+					color #276f86
+					border solid 1px #a9d5de
+					border-radius 8px
+					background-color #f8ffff
 
-				> input[type="text"]
-				> textarea
+					> i
+						margin-right 6px
+
+				> .form
+					position relative
+					background #fff
+					border solid 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
+
+					> 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 1px #ddd
+							border-radius 4px
+
+						> textarea
+							min-height 80px
+
+				> .save
 					display block
+					margin 8px 0 0 0
+					padding 16px
 					width 100%
-					padding 12px
 					font-size 16px
-					border none
-					border-radius 0
+					color $theme-color-foreground
+					background $theme-color
+					border-radius 8px
 
-				> textarea
-					min-height 80px
+					&:disabled
+						opacity 0.7
 
-			> .save
-				display block
-				margin 8px
-				padding 16px
-				width calc(100% - 16px)
-				font-size 16px
-				color $theme-color-foreground
-				background $theme-color
-				border-radius 4px
-
-				&:disabled
-					opacity 0.7
-
-				> i
-					margin-right 4px
+					> i
+						margin-right 4px
 
 	</style>
 	<script>

From be75ab35ff1ac52a4cc0538450ac6ac7bd82161e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 29 Aug 2017 14:12:11 +0900
Subject: [PATCH 057/364] =?UTF-8?q?=E3=82=A2=E3=82=AB=E3=82=A6=E3=83=B3?=
 =?UTF-8?q?=E3=83=88=E4=BD=9C=E6=88=90=E3=83=95=E3=82=A9=E3=83=BC=E3=83=A0?=
 =?UTF-8?q?=E3=81=AE=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E3=83=9A=E3=83=BC?=
 =?UTF-8?q?=E3=82=B8URL=E3=83=97=E3=83=AC=E3=83=93=E3=83=A5=E3=83=BC?=
 =?UTF-8?q?=E3=81=8C=E6=AD=A3=E3=81=97=E3=81=8F=E6=A9=9F=E8=83=BD=E3=81=97?=
 =?UTF-8?q?=E3=81=A6=E3=81=84=E3=81=AA=E3=81=8B=E3=81=A3=E3=81=9F=E5=95=8F?=
 =?UTF-8?q?=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 CHANGELOG.md                       | 1 +
 src/web/app/common/tags/signup.tag | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index faefb4d72f..8023cc8676 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ unreleased
 * New: パスワードを変更する際に新しいパスワードを二度入力させる (#739)
 * New: ドナーを表示する (#738)
 * Fix: 投稿のリンクが機能していない問題を修正
+* Fix: アカウント作成フォームのユーザーページURLプレビューが正しく機能していなかった問題を修正
 * l10n
 * デザインの調整
 
diff --git a/src/web/app/common/tags/signup.tag b/src/web/app/common/tags/signup.tag
index 0359f4fab9..17de0347f5 100644
--- a/src/web/app/common/tags/signup.tag
+++ b/src/web/app/common/tags/signup.tag
@@ -3,7 +3,7 @@
 		<label class="username">
 			<p class="caption"><i class="fa fa-at"></i>%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' }>{ '/' + refs.username.value }</p>
+			<p class="profile-page-url-preview" if={ refs.username.value != '' && username-state != 'invalidFormat' && username-state != 'minRange' && username-state != 'maxRange' }>{ CONFIG.url + '/' + refs.username.value }</p>
 			<p class="info" if={ usernameState == 'wait' } style="color:#999"><i class="fa fa-fw fa-spinner fa-pulse"></i>%i18n:common.tags.mk-signup.checking%</p>
 			<p class="info" if={ usernameState == 'ok' } style="color:#3CB7B5"><i class="fa fa-fw fa-check"></i>%i18n:common.tags.mk-signup.available%</p>
 			<p class="info" if={ usernameState == 'unavailable' } style="color:#FF1161"><i class="fa fa-fw fa-exclamation-triangle"></i>%i18n:common.tags.mk-signup.unavailable%</p>

From e96fe5789dfa06160b8e5176aad2813eca537f56 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 29 Aug 2017 14:12:18 +0900
Subject: [PATCH 058/364] :art:

---
 src/web/app/mobile/tags/page/settings/profile.tag | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/web/app/mobile/tags/page/settings/profile.tag b/src/web/app/mobile/tags/page/settings/profile.tag
index fb78d2f719..4d00f19d5d 100644
--- a/src/web/app/mobile/tags/page/settings/profile.tag
+++ b/src/web/app/mobile/tags/page/settings/profile.tag
@@ -61,10 +61,11 @@
 					display block
 					margin 0 0 8px 0
 					padding 16px
-					color #276f86
+					color #79d4e6
+					//color #276f86
+					//background #f8ffff
 					border solid 1px #a9d5de
 					border-radius 8px
-					background-color #f8ffff
 
 					> i
 						margin-right 6px
@@ -72,7 +73,7 @@
 				> .form
 					position relative
 					background #fff
-					border solid 1px rgba(0, 0, 0, 0.2)
+					box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
 					border-radius 8px
 
 					&:before

From 776a4fe01eb94712c52791cd2348ee8aa41299a9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 29 Aug 2017 14:17:11 +0900
Subject: [PATCH 059/364] v2487

---
 CHANGELOG.md | 4 ++--
 package.json | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8023cc8676..b5f4b1e42c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,8 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
-unreleased
-----------
+2487 (2017/08/29)
+-----------------
 * New: パスワードを変更する際に新しいパスワードを二度入力させる (#739)
 * New: ドナーを表示する (#738)
 * Fix: 投稿のリンクが機能していない問題を修正
diff --git a/package.json b/package.json
index 3fade49978..b476cd723d 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2470",
+  "version": "0.0.2487",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",

From e2b497de2a07ce2b301bfe201c42494f4d4feb9b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 29 Aug 2017 15:32:52 +0900
Subject: [PATCH 060/364] :v:

---
 CHANGELOG.md                                  |  5 ++++
 .../app/mobile/tags/page/settings/profile.tag | 29 ++++++++++++++++++-
 2 files changed, 33 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b5f4b1e42c..3195a42d9f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,11 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+unlereased
+----------
+* ユーザビリティの向上
+* デザインの調整
+
 2487 (2017/08/29)
 -----------------
 * New: パスワードを変更する際に新しいパスワードを二度入力させる (#739)
diff --git a/src/web/app/mobile/tags/page/settings/profile.tag b/src/web/app/mobile/tags/page/settings/profile.tag
index 4d00f19d5d..f427db3272 100644
--- a/src/web/app/mobile/tags/page/settings/profile.tag
+++ b/src/web/app/mobile/tags/page/settings/profile.tag
@@ -21,6 +21,9 @@
 	<div>
 		<p><i class="fa fa-info-circle"></i>%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>
 			<label>
 				<p>%i18n:mobile.tags.mk-profile-setting.name%</p>
 				<input ref="name" type="text" value={ I.name }/>
@@ -60,7 +63,7 @@
 				> p
 					display block
 					margin 0 0 8px 0
-					padding 16px
+					padding 12px 16px
 					color #79d4e6
 					//color #276f86
 					//background #f8ffff
@@ -98,6 +101,20 @@
 						border-bottom solid 8px transparent
 						border-left solid 8px transparent
 
+					> div
+						height 128px
+						background-color #e4e4e4
+						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
@@ -190,6 +207,16 @@
 			});
 		};
 
+		this.clickAvatar = e => {
+			this.setAvatar();
+			return false;
+		};
+
+		this.clickBanner = e => {
+			this.setBanner();
+			return false;
+		};
+
 		this.save = () => {
 			this.update({
 				saving: true

From 12c4b177645d84133abe5a0e36a31c369bcdf868 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 29 Aug 2017 15:37:16 +0900
Subject: [PATCH 061/364] v2489

---
 CHANGELOG.md | 4 ++--
 package.json | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3195a42d9f..7f6496468a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,8 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
-unlereased
-----------
+2489 (2017/08/29)
+-----------------
 * ユーザビリティの向上
 * デザインの調整
 
diff --git a/package.json b/package.json
index b476cd723d..b578aa926b 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2487",
+  "version": "0.0.2489",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",

From 6d14c3ed1314c8e28225766452e014244111c8ec Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 29 Aug 2017 15:46:58 +0900
Subject: [PATCH 062/364] :art:

---
 CHANGELOG.md                                      | 4 ++++
 src/web/app/mobile/tags/page/settings/profile.tag | 2 ++
 2 files changed, 6 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7f6496468a..3441bc7ce8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+unlereased
+----------
+* デザインの修正
+
 2489 (2017/08/29)
 -----------------
 * ユーザビリティの向上
diff --git a/src/web/app/mobile/tags/page/settings/profile.tag b/src/web/app/mobile/tags/page/settings/profile.tag
index f427db3272..0507a7529e 100644
--- a/src/web/app/mobile/tags/page/settings/profile.tag
+++ b/src/web/app/mobile/tags/page/settings/profile.tag
@@ -104,6 +104,8 @@
 					> div
 						height 128px
 						background-color #e4e4e4
+						background-size cover
+						background-position center
 						border-radius 8px 8px 0 0
 
 						> img

From 22c5dc36a546ae6743aa236b69363f1685de214a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 29 Aug 2017 16:05:38 +0900
Subject: [PATCH 063/364] v2491

---
 CHANGELOG.md                                      | 6 +++---
 package.json                                      | 2 +-
 src/web/app/mobile/tags/page/settings/profile.tag | 1 +
 3 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3441bc7ce8..6980e4a481 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,9 +2,9 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
-unlereased
-----------
-* デザインの修正
+2491 (2017/08/29)
+-----------------
+* デザインの修正と調整
 
 2489 (2017/08/29)
 -----------------
diff --git a/package.json b/package.json
index b578aa926b..a6aca1a20f 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2489",
+  "version": "0.0.2491",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",
diff --git a/src/web/app/mobile/tags/page/settings/profile.tag b/src/web/app/mobile/tags/page/settings/profile.tag
index 0507a7529e..66b3fbde2a 100644
--- a/src/web/app/mobile/tags/page/settings/profile.tag
+++ b/src/web/app/mobile/tags/page/settings/profile.tag
@@ -64,6 +64,7 @@
 					display block
 					margin 0 0 8px 0
 					padding 12px 16px
+					font-size 14px
 					color #79d4e6
 					//color #276f86
 					//background #f8ffff

From d41f5dbe775f81cc3a99b83232313c1b21d11622 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 29 Aug 2017 19:27:06 +0900
Subject: [PATCH 064/364] v2493

---
 CHANGELOG.md                                  |   4 +
 locales/en.yml                                |   4 +-
 locales/ja.yml                                |   4 +-
 package.json                                  |   2 +-
 src/web/app/common/tags/activity-table.tag    |   1 -
 src/web/app/mobile/tags/home-timeline.tag     |   2 +-
 src/web/app/mobile/tags/home.tag              |   1 +
 src/web/app/mobile/tags/init-following.tag    | 101 +--
 src/web/app/mobile/tags/notifications.tag     |  22 +-
 src/web/app/mobile/tags/page/home.tag         |   1 +
 .../app/mobile/tags/page/notifications.tag    |   1 +
 src/web/app/mobile/tags/page/post.tag         |  46 +-
 src/web/app/mobile/tags/page/search.tag       |   1 +
 .../app/mobile/tags/page/settings/profile.tag |  11 +-
 .../app/mobile/tags/page/user-followers.tag   |   1 +
 .../app/mobile/tags/page/user-following.tag   |   1 +
 src/web/app/mobile/tags/page/user.tag         |   1 +
 src/web/app/mobile/tags/post-detail.tag       | 618 ++++++++++--------
 src/web/app/mobile/tags/post-form.tag         |  71 +-
 src/web/app/mobile/tags/search-posts.tag      |   8 +
 src/web/app/mobile/tags/timeline.tag          |   6 +
 src/web/app/mobile/tags/user-timeline.tag     |   2 -
 src/web/app/mobile/tags/user.tag              |  18 +-
 src/web/app/mobile/tags/users-list.tag        |  20 +-
 24 files changed, 555 insertions(+), 392 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6980e4a481..dc50bb7b0a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+2493 (2017/08/29)
+-----------------
+* デザインの変更など
+
 2491 (2017/08/29)
 -----------------
 * デザインの修正と調整
diff --git a/locales/en.yml b/locales/en.yml
index b0488a775c..5e11339db5 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -355,7 +355,9 @@ mobile:
       notifications: "Notifications"
 
     mk-post-page:
-      submit: "Post"
+      title: "Post"
+      prev: "Previous post"
+      next: "Next post"
 
     mk-search-page:
       search: "Search"
diff --git a/locales/ja.yml b/locales/ja.yml
index 9a040f798d..62ac4cb81f 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -355,7 +355,9 @@ mobile:
       notifications: "通知"
 
     mk-post-page:
-      submit: "投稿"
+      title: "投稿"
+      prev: "前の投稿"
+      next: "次の投稿"
 
     mk-search-page:
       search: "検索"
diff --git a/package.json b/package.json
index a6aca1a20f..5e33be56a2 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2491",
+  "version": "0.0.2493",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",
diff --git a/src/web/app/common/tags/activity-table.tag b/src/web/app/common/tags/activity-table.tag
index 6331e7c9c3..1d26d1788a 100644
--- a/src/web/app/common/tags/activity-table.tag
+++ b/src/web/app/common/tags/activity-table.tag
@@ -17,7 +17,6 @@
 			display block
 			max-width 600px
 			margin 0 auto
-			background #fff
 
 			> svg
 				display block
diff --git a/src/web/app/mobile/tags/home-timeline.tag b/src/web/app/mobile/tags/home-timeline.tag
index 7357d13916..051158597d 100644
--- a/src/web/app/mobile/tags/home-timeline.tag
+++ b/src/web/app/mobile/tags/home-timeline.tag
@@ -6,7 +6,7 @@
 			display block
 
 			> mk-init-following
-				border-bottom solid 1px #eee
+				margin-bottom 8px
 
 	</style>
 	<script>
diff --git a/src/web/app/mobile/tags/home.tag b/src/web/app/mobile/tags/home.tag
index 48b5a67c38..d92e3ae4e5 100644
--- a/src/web/app/mobile/tags/home.tag
+++ b/src/web/app/mobile/tags/home.tag
@@ -7,6 +7,7 @@
 			> mk-home-timeline
 				max-width 600px
 				margin 0 auto
+				padding 8px
 
 			@media (min-width 500px)
 				padding 16px
diff --git a/src/web/app/mobile/tags/init-following.tag b/src/web/app/mobile/tags/init-following.tag
index 0c54d3a6a1..2fb7499d26 100644
--- a/src/web/app/mobile/tags/init-following.tag
+++ b/src/web/app/mobile/tags/init-following.tag
@@ -1,10 +1,14 @@
 <mk-init-following>
 	<p class="title">気になるユーザーをフォロー:</p>
 	<div class="users" 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=""/></a>
-			<div class="body"><a class="name" href={ '/' + username } target="_blank">{ name }</a>
-				<p class="username">@{ username }</p>
-			</div>
+		<div class="user" each={ users }>
+			<header style={ banner_url ? 'background-image: url(' + banner_url + '?thumbnail&size=1024)' : '' }>
+				<a href={ '/' + username }>
+					<img src={ avatar_url + '?thumbnail&size=200' } alt="avatar"/>
+				</a>
+			</header>
+			<a class="name" href={ '/' + username } target="_blank">{ name }</a>
+			<p class="username">@{ username }</p>
 			<mk-follow-button user={ this }/>
 		</div>
 	</div>
@@ -15,63 +19,65 @@
 	<style>
 		:scope
 			display block
-			padding 16px
+			background #fff
+			border-radius 8px
+			box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
 
 			> .title
-				margin 0 0 12px 0
+				margin 0
+				padding 8px 16px
 				font-size 1em
 				font-weight bold
 				color #888
 
 			> .users
-				&:after
-					content ""
-					display block
-					clear both
+				overflow-x scroll
+				white-space nowrap
+				padding 16px
+				background #eee
 
 				> .user
-					padding 16px
-					width 238px
-					float left
+					display inline-block
+					width 200px
+					text-align center
+					border-radius 8px
+					background #fff
 
-					&:after
-						content ""
+					&:not(:last-child)
+						margin-right 16px
+
+					> header
 						display block
-						clear both
+						height 80px
+						background-color #ddd
+						background-size cover
+						background-position center
+						border-radius 8px 8px 0 0
 
-					> .avatar-anchor
+						> a
+							> img
+								position absolute
+								top 20px
+								left calc(50% - 40px)
+								width 80px
+								height 80px
+								border solid 2px #fff
+								border-radius 8px
+
+					> .name
 						display block
-						float left
-						margin 0 12px 0 0
+						margin 24px 0 2px 0
+						font-size 16px
+						color #555
 
-						> .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
+					> .username
+						margin 0
+						font-size 15px
+						color #ccc
 
 					> mk-follow-button
-						position absolute
-						top 16px
-						right 16px
+						display inline-block
+						margin 8px 0 16px 0
 
 			> .empty
 				margin 0
@@ -90,7 +96,8 @@
 
 			> .refresh
 				display block
-				margin 0 8px 0 0
+				margin 0
+				padding 8px 16px
 				text-align right
 				font-size 0.9em
 				color #999
@@ -117,7 +124,7 @@
 					color #222
 
 				> i
-					padding 14px
+					padding 10px
 
 	</style>
 	<script>
diff --git a/src/web/app/mobile/tags/notifications.tag b/src/web/app/mobile/tags/notifications.tag
index 21a941e630..2f314769db 100644
--- a/src/web/app/mobile/tags/notifications.tag
+++ b/src/web/app/mobile/tags/notifications.tag
@@ -1,9 +1,7 @@
 <mk-notifications>
 	<div class="notifications" if={ notifications.length != 0 }>
 		<virtual each={ notification, i in notifications }>
-			<div>
-				<mk-notification notification={ notification }/>
-			</div>
+			<mk-notification notification={ notification }/>
 			<p class="date" if={ i != notifications.length - 1 && notification._date != notifications[i + 1]._date }><span><i class="fa fa-angle-up"></i>{ notification._datetext }</span><span><i class="fa fa-angle-down"></i>{ notifications[i + 1]._datetext }</span></p>
 		</virtual>
 	</div>
@@ -15,20 +13,28 @@
 	<style>
 		: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
 
-				> div
+				> mk-notification
+					margin 0 auto
+					max-width 500px
 					border-bottom solid 1px rgba(0, 0, 0, 0.05)
 
 					&:last-child
 						border-bottom none
 
-					> mk-notification
-						margin 0 auto
-						max-width 500px
-
 				> .date
 					display block
 					margin 0
diff --git a/src/web/app/mobile/tags/page/home.tag b/src/web/app/mobile/tags/page/home.tag
index 32c80fd20e..efb5068a57 100644
--- a/src/web/app/mobile/tags/page/home.tag
+++ b/src/web/app/mobile/tags/page/home.tag
@@ -20,6 +20,7 @@
 		this.on('mount', () => {
 			document.title = 'Misskey'
 			ui.trigger('title', '<i class="fa fa-home"></i>%i18n:mobile.tags.mk-home.home%');
+			document.documentElement.style.background = '#313a42';
 
 			ui.trigger('func', () => {
 				openPostForm();
diff --git a/src/web/app/mobile/tags/page/notifications.tag b/src/web/app/mobile/tags/page/notifications.tag
index f90cd1628d..06a5be039f 100644
--- a/src/web/app/mobile/tags/page/notifications.tag
+++ b/src/web/app/mobile/tags/page/notifications.tag
@@ -13,6 +13,7 @@
 		this.on('mount', () => {
 			document.title = 'Misskey | %i18n:mobile.tags.mk-notifications-page.notifications%';
 			ui.trigger('title', '<i class="fa fa-bell-o"></i>%i18n:mobile.tags.mk-notifications-page.notifications%');
+			document.documentElement.style.background = '#313a42';
 
 			Progress.start();
 
diff --git a/src/web/app/mobile/tags/page/post.tag b/src/web/app/mobile/tags/page/post.tag
index 7ab4ea2714..198acf1798 100644
--- a/src/web/app/mobile/tags/page/post.tag
+++ b/src/web/app/mobile/tags/page/post.tag
@@ -1,7 +1,9 @@
 <mk-post-page>
 	<mk-ui ref="ui">
-		<main>
+		<main if={ !parent.fetching }>
+			<a if={ parent.post.next } href={ parent.post.next }><i class="fa fa-angle-up"></i>%i18n:mobile.tags.mk-post-page.next%</a>
 			<mk-post-detail ref="post" post={ parent.post }/>
+			<a if={ parent.post.prev } href={ parent.post.prev }><i class="fa fa-angle-down"></i>%i18n:mobile.tags.mk-post-page.prev%</a>
 		</main>
 	</mk-ui>
 	<style>
@@ -9,31 +11,51 @@
 			display block
 
 			main
-				background #fff
+				text-align center
 
-				> mk-post-detail
-					width 100%
-					max-width 500px
-					margin 0 auto
+				> 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
+
+					> i
+						margin-right 4px
 	</style>
 	<script>
 		import ui from '../../scripts/ui-event';
 		import Progress from '../../../common/scripts/loading';
 
-		this.post = this.opts.post;
+		this.mixin('api');
+
+		this.fetching = true;
+		this.post = null;
 
 		this.on('mount', () => {
 			document.title = 'Misskey';
-			ui.trigger('title', '<i class="fa fa-sticky-note-o"></i>%i18n:mobile.tags.mk-post-page.submit%');
+			ui.trigger('title', '<i class="fa fa-sticky-note-o"></i>%i18n:mobile.tags.mk-post-page.title%');
+			document.documentElement.style.background = '#313a42';
 
 			Progress.start();
 
-			this.refs.ui.refs.post.on('post-fetched', () => {
-				Progress.set(0.5);
-			});
+			this.api('posts/show', {
+				post_id: this.opts.post
+			}).then(post => {
+
+				this.update({
+					fetching: false,
+					post: post
+				});
 
-			this.refs.ui.refs.post.on('loaded', () => {
 				Progress.done();
 			});
 		});
diff --git a/src/web/app/mobile/tags/page/search.tag b/src/web/app/mobile/tags/page/search.tag
index 869d5c8533..a66f07971a 100644
--- a/src/web/app/mobile/tags/page/search.tag
+++ b/src/web/app/mobile/tags/page/search.tag
@@ -14,6 +14,7 @@
 			document.title = `%i18n:mobile.tags.mk-search-page.search%: ${this.opts.query} | Misskey`
 			// TODO: クエリをHTMLエスケープ
 			ui.trigger('title', '<i class="fa fa-search"></i>' + this.opts.query);
+			document.documentElement.style.background = '#313a42';
 
 			Progress.start();
 
diff --git a/src/web/app/mobile/tags/page/settings/profile.tag b/src/web/app/mobile/tags/page/settings/profile.tag
index 66b3fbde2a..7e1bedbf47 100644
--- a/src/web/app/mobile/tags/page/settings/profile.tag
+++ b/src/web/app/mobile/tags/page/settings/profile.tag
@@ -56,9 +56,13 @@
 			display block
 
 			> div
-				margin 16px auto
+				margin 8px auto
 				max-width 500px
-				width calc(100% - 32px)
+				width calc(100% - 16px)
+
+				@media (min-width 500px)
+					margin 16px auto
+					width calc(100% - 32px)
 
 				> p
 					display block
@@ -66,9 +70,10 @@
 					padding 12px 16px
 					font-size 14px
 					color #79d4e6
+					border solid 1px #71afbb
 					//color #276f86
 					//background #f8ffff
-					border solid 1px #a9d5de
+					//border solid 1px #a9d5de
 					border-radius 8px
 
 					> i
diff --git a/src/web/app/mobile/tags/page/user-followers.tag b/src/web/app/mobile/tags/page/user-followers.tag
index f6fcffebe2..cffb2b58c4 100644
--- a/src/web/app/mobile/tags/page/user-followers.tag
+++ b/src/web/app/mobile/tags/page/user-followers.tag
@@ -29,6 +29,7 @@
 				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();
diff --git a/src/web/app/mobile/tags/page/user-following.tag b/src/web/app/mobile/tags/page/user-following.tag
index 4b289b6aa3..369cb46422 100644
--- a/src/web/app/mobile/tags/page/user-following.tag
+++ b/src/web/app/mobile/tags/page/user-following.tag
@@ -29,6 +29,7 @@
 				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();
diff --git a/src/web/app/mobile/tags/page/user.tag b/src/web/app/mobile/tags/page/user.tag
index 05ccef3113..1abeab492a 100644
--- a/src/web/app/mobile/tags/page/user.tag
+++ b/src/web/app/mobile/tags/page/user.tag
@@ -13,6 +13,7 @@
 		this.user = this.opts.user;
 
 		this.on('mount', () => {
+			document.documentElement.style.background = '#313a42';
 			Progress.start();
 
 			this.refs.ui.refs.user.on('loaded', user => {
diff --git a/src/web/app/mobile/tags/post-detail.tag b/src/web/app/mobile/tags/post-detail.tag
index 9d62a2b591..9215bafdbc 100644
--- a/src/web/app/mobile/tags/post-detail.tag
+++ b/src/web/app/mobile/tags/post-detail.tag
@@ -1,261 +1,265 @@
 <mk-post-detail>
-	<div class="fetching" if={ fetching }>
-		<mk-ellipsis-icon/>
+	<button class="read-more" if={ p.reply_to && p.reply_to.reply_to_id && context == null } onclick={ loadContext } disabled={ loadingContext }>
+		<i class="fa fa-ellipsis-v" if={ !contextFetching }></i>
+		<i class="fa fa-spinner fa-pulse" if={ contextFetching }></i>
+	</button>
+	<div class="context">
+		<virtual each={ post in context }>
+			<mk-post-detail-sub post={ post }/>
+		</virtual>
 	</div>
-	<div class="main" if={ !fetching }>
-		<button class="read-more" if={ p.reply_to && p.reply_to.reply_to_id && context == null } onclick={ loadContext } disabled={ loadingContext }>
-			<i class="fa fa-ellipsis-v" if={ !contextFetching }></i>
-			<i class="fa fa-spinner fa-pulse" if={ contextFetching }></i>
-		</button>
-		<div class="context">
-			<virtual each={ post in context }>
-				<mk-post-preview post={ post }/>
-			</virtual>
-		</div>
-		<div class="reply-to" if={ p.reply_to }>
-			<mk-post-preview post={ p.reply_to }/>
-		</div>
-		<div class="repost" 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>
-					<i class="fa fa-retweet"></i><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" if={ p.media }>
-					<virtual each={ file in p.media }><img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/></virtual>
-				</div>
-				<mk-poll if={ p.poll } post={ p }/>
-			</div>
-			<a class="time" href={ url }>
-				<mk-time time={ p.created_at } mode="detail"/>
+	<div class="reply-to" if={ p.reply_to }>
+		<mk-post-detail-sub post={ p.reply_to }/>
+	</div>
+	<div class="repost" 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>
+				<i class="fa fa-retweet"></i><a class="name" href={ '/' + post.user.username }>
+				{ post.user.name }
 			</a>
-			<footer>
-				<mk-reactions-viewer post={ p }/>
-				<button onclick={ reply } title="%i18n:mobile.tags.mk-post-detail.reply%"><i class="fa fa-reply"></i>
-					<p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
-				</button>
-				<button onclick={ repost } title="Repost"><i class="fa fa-retweet"></i>
-					<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%"><i class="fa fa-plus"></i>
-					<p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
-				</button>
-				<button><i class="fa fa-ellipsis-h"></i></button>
-			</footer>
-		</article>
-		<div class="replies">
-			<virtual each={ post in replies }>
-				<mk-post-preview post={ post }/>
-			</virtual>
+			が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" if={ p.media }>
+				<virtual each={ file in p.media }><img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/></virtual>
+			</div>
+			<mk-poll if={ p.poll } post={ p }/>
 		</div>
+		<a class="time" href={ url }>
+			<mk-time time={ p.created_at } mode="detail"/>
+		</a>
+		<footer>
+			<mk-reactions-viewer post={ p }/>
+			<button onclick={ reply } title="%i18n:mobile.tags.mk-post-detail.reply%"><i class="fa fa-reply"></i>
+				<p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
+			</button>
+			<button onclick={ repost } title="Repost"><i class="fa fa-retweet"></i>
+				<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%"><i class="fa fa-plus"></i>
+				<p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
+			</button>
+			<button><i class="fa fa-ellipsis-h"></i></button>
+		</footer>
+	</article>
+	<div class="replies">
+		<virtual each={ post in replies }>
+			<mk-post-detail-sub post={ post }/>
+		</virtual>
 	</div>
 	<style>
 		:scope
 			display block
-			margin 0
+			overflow hidden
+			margin 8px auto
 			padding 0
+			max-width 500px
+			width calc(100% - 16px)
+			text-align left
+			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)
 
 			> .fetching
 				padding 64px 0
 
-			> .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
+				box-shadow none
 
-				> .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
 
-					&:hover
-						background #f6f6f6
+				&:active
+					background #f0f0f0
 
-					&:active
-						background #f0f0f0
+				&:disabled
+					color #ccc
 
-					&: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
-
-						i
-							margin-right 4px
-
-						.name
-							font-weight bold
-
-					& + article
-						padding-top 8px
-
-				> .reply-to
+			> .context
+				> *
 					border-bottom 1px solid #eef0f2
 
-				> article
-					padding 14px 16px 9px 16px
+			> .repost
+				color #9dbb00
+				background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
 
-					@media (min-width 500px)
-						padding 28px 32px 18px 32px
+				> p
+					margin 0
+					padding 16px 32px
 
-					&:after
-						content ""
+					.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
+
+					i
+						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
-						clear both
+						padding 0 .5em 0 0
 
-					&: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
+						> .avatar
 							display block
+							width 54px
+							height 54px
 							margin 0
-							padding 0
-							overflow-wrap break-word
-							font-size 16px
-							color #717171
+							border-radius 8px
+							vertical-align bottom
 
 							@media (min-width 500px)
-								font-size 24px
+								width 60px
+								height 60px
 
-							.link
-								&:after
-									content "\f14c"
-									display inline-block
-									padding-left 2px
-									font-family FontAwesome
-									font-size .9em
-									font-weight 400
-									font-style normal
+					> div
 
-							> 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 28px 0 0
-							padding 8px
-							background transparent
-							border none
-							box-shadow none
-							font-size 1em
-							color #ddd
-							cursor pointer
+						> .name
+							display inline-block
+							margin .4em 0
+							color #777
+							font-size 16px
+							font-weight bold
+							text-align left
+							text-decoration none
 
 							&:hover
-								color #666
+								text-decoration underline
 
-							> .count
-								display inline
-								margin 0 0 0 8px
-								color #999
+						> .username
+							display block
+							text-align left
+							margin 0
+							color #ccc
 
-							&.reacted
-								color $theme-color
+				> .body
+					padding 8px 0
 
-				> .replies
-					> *
-						border-top 1px solid #eef0f2
+					> .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
+
+						.link
+							&:after
+								content "\f14c"
+								display inline-block
+								padding-left 2px
+								font-family FontAwesome
+								font-size .9em
+								font-weight 400
+								font-style normal
+
+						> 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 28px 0 0
+						padding 8px
+						background transparent
+						border none
+						box-shadow 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>
@@ -265,56 +269,42 @@
 
 		this.mixin('api');
 
-		this.fetching = true;
+		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.post = null;
 
 		this.on('mount', () => {
-			this.api('posts/show', {
-				post_id: this.opts.post
-			}).then(post => {
-				const isRepost = post.repost != null;
-				const p = isRepost ? post.repost : post;
-				p.reactions_count = p.reaction_counts ? Object.keys(p.reaction_counts).map(key => p.reaction_counts[key]).reduce((a, b) => a + b) : 0;
+			if (this.p.text) {
+				const tokens = this.p.ast;
 
-				this.update({
-					fetching: false,
-					post: post,
-					isRepost: isRepost,
-					p: p,
-					summary: getPostSummary(p)
+				this.refs.text.innerHTML = compile(tokens);
+
+				this.refs.text.children.forEach(e => {
+					if (e.tagName == 'MK-URL') riot.mount(e);
 				});
 
-				this.trigger('loaded');
-
-				if (this.p.text) {
-					const tokens = this.p.ast;
-
-					this.refs.text.innerHTML = compile(tokens);
-
-					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
 					});
+				});
+			}
 
-					// 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
-				this.api('posts/replies', {
-					post_id: this.p.id,
-					limit: 8
-				}).then(replies => {
-					this.update({
-						replies: replies
-					});
+			// Get replies
+			this.api('posts/replies', {
+				post_id: this.p.id,
+				limit: 8
+			}).then(replies => {
+				this.update({
+					replies: replies
 				});
 			});
 		});
@@ -357,3 +347,101 @@
 		};
 	</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>
+		: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>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 28c7796840..cf267de94a 100644
--- a/src/web/app/mobile/tags/post-form.tag
+++ b/src/web/app/mobile/tags/post-form.tag
@@ -1,11 +1,9 @@
 <mk-post-form>
 	<header>
+		<button class="cancel" onclick={ cancel }><i class="fa fa-times"></i></button>
 		<div>
-			<button class="cancel" onclick={ cancel }><i class="fa fa-times"></i></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>
-			</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>
 		</div>
 	</header>
 	<div class="form">
@@ -30,46 +28,47 @@
 	<style>
 		:scope
 			display block
-			padding-top 50px
+			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
-				position fixed
-				z-index 1000
-				top 0
-				left 0
-				width 100%
+				z-index 1
 				height 50px
-				background #fff
+				box-shadow 0 1px 0 0 rgba(0, 0, 0, 0.1)
+
+				> .cancel
+					width 50px
+					line-height 50px
+					font-size 24px
+					color #555
 
 				> div
-					max-width 500px
-					margin 0 auto
+					position absolute
+					top 0
+					right 0
 
-					> .cancel
-						width 50px
+					> .text-count
 						line-height 50px
-						font-size 24px
-						color #555
+						color #657786
 
-					> div
-						position absolute
-						top 0
-						right 0
+					> .submit
+						margin 8px
+						padding 0 16px
+						line-height 34px
+						color $theme-color-foreground
+						background $theme-color
+						border-radius 4px
 
-						> .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
+						&:disabled
+							opacity 0.7
 
 			> .form
 				max-width 500px
diff --git a/src/web/app/mobile/tags/search-posts.tag b/src/web/app/mobile/tags/search-posts.tag
index 3e6caa1df2..967764bc2c 100644
--- a/src/web/app/mobile/tags/search-posts.tag
+++ b/src/web/app/mobile/tags/search-posts.tag
@@ -3,8 +3,16 @@
 	<style>
 		: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>
 		this.mixin('api');
diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag
index 11f4e0740b..9e39cf80d7 100644
--- a/src/web/app/mobile/tags/timeline.tag
+++ b/src/web/app/mobile/tags/timeline.tag
@@ -22,6 +22,8 @@
 		:scope
 			display block
 			background #fff
+			border-radius 8px
+			box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
 
 			> .init
 				padding 64px 0
@@ -47,6 +49,9 @@
 			> mk-timeline-post
 				border-bottom solid 1px #eaeaea
 
+				&:first-child
+					border-radius 8px 8px 0 0
+
 				&:last-of-type
 					border-bottom none
 
@@ -77,6 +82,7 @@
 					padding 16px
 					width 100%
 					color $theme-color
+					border-radius 0 0 8px 8px
 
 					&:disabled
 						opacity 0.7
diff --git a/src/web/app/mobile/tags/user-timeline.tag b/src/web/app/mobile/tags/user-timeline.tag
index f7b2b36da0..4dbe719f5a 100644
--- a/src/web/app/mobile/tags/user-timeline.tag
+++ b/src/web/app/mobile/tags/user-timeline.tag
@@ -5,8 +5,6 @@
 			display block
 			max-width 600px
 			margin 0 auto
-			background #fff
-
 	</style>
 	<script>
 		this.mixin('api');
diff --git a/src/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag
index 81eb6ba2e4..59c89ad1c8 100644
--- a/src/web/app/mobile/tags/user.tag
+++ b/src/web/app/mobile/tags/user.tag
@@ -57,7 +57,7 @@
 				> header
 					> .banner
 						padding-bottom 33.3%
-						background-color #f5f5f5
+						background-color #1b1b1b
 						background-size cover
 						background-position center
 
@@ -84,13 +84,13 @@
 									left -2px
 									bottom -2px
 									width 100%
-									border 2px solid #fff
+									border 2px solid #313a42
 									border-radius 6px
 
 									@media (min-width 500px)
 										left -4px
 										bottom -4px
-										border 4px solid #fff
+										border 4px solid #313a42
 										border-radius 12px
 
 							> mk-follow-button
@@ -104,7 +104,7 @@
 								margin 0
 								line-height 22px
 								font-size 20px
-								color #222
+								color #fff
 
 							> .username
 								display inline-block
@@ -131,7 +131,7 @@
 							> p
 								display inline
 								margin 0 16px 0 0
-								color #555
+								color #a9b9c1
 
 								> i
 									margin-right 4px
@@ -146,7 +146,7 @@
 								> b
 									margin-right 4px
 									font-size 16px
-									color #14171a
+									color #fff
 
 								> i
 									font-size 14px
@@ -159,7 +159,7 @@
 						justify-content center
 						margin 0 auto
 						max-width 600px
-						border-bottom solid 1px #ddd
+						border-bottom solid 1px rgba(0, 0, 0, 0.2)
 
 						> a
 							display block
@@ -177,8 +177,10 @@
 								border-color $theme-color
 
 				> .body
+					padding 8px
+
 					@media (min-width 500px)
-						padding 16px 0 0 0
+						padding 16px
 
 	</style>
 	<script>
diff --git a/src/web/app/mobile/tags/users-list.tag b/src/web/app/mobile/tags/users-list.tag
index fb70f184d5..295ae06694 100644
--- a/src/web/app/mobile/tags/users-list.tag
+++ b/src/web/app/mobile/tags/users-list.tag
@@ -14,14 +14,13 @@
 	<style>
 		:scope
 			display block
-			background #fff
 
 			> nav
 				display flex
 				justify-content center
 				margin 0 auto
 				max-width 600px
-				border-bottom solid 1px #ddd
+				border-bottom solid 1px rgba(0, 0, 0, 0.2)
 
 				> span
 					display block
@@ -43,14 +42,23 @@
 						padding 2px 5px
 						font-size 12px
 						line-height 1
-						color #888
-						background #eee
+						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)
+
 				> *
-					max-width 600px
-					margin 0 auto
 					border-bottom solid 1px rgba(0, 0, 0, 0.05)
 
 			> .no

From 27689a655c2aa5955e70b8243c22ad72ee463d82 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 29 Aug 2017 19:42:55 +0900
Subject: [PATCH 065/364] v2493-2

---
 CHANGELOG.md                     | 4 ++++
 package.json                     | 2 +-
 src/web/app/mobile/tags/user.tag | 2 +-
 3 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index dc50bb7b0a..4d282aae9b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+2493-2 (2017/08/29)
+-------------------
+* デザインの修正
+
 2493 (2017/08/29)
 -----------------
 * デザインの変更など
diff --git a/package.json b/package.json
index 5e33be56a2..dca1bff8ce 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2493",
+  "version": "0.0.2493-2",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",
diff --git a/src/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag
index 59c89ad1c8..fbf2f690a3 100644
--- a/src/web/app/mobile/tags/user.tag
+++ b/src/web/app/mobile/tags/user.tag
@@ -123,7 +123,7 @@
 
 						> .description
 							margin 8px 0
-							color #333
+							color #fff
 
 						> .info
 							margin 8px 0

From 9bfa0ee5e14745f2ba962baaa19ada7a9e30435e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 29 Aug 2017 21:49:09 +0900
Subject: [PATCH 066/364] [Client] Improve usability

---
 CHANGELOG.md                               | 4 ++++
 src/web/app/mobile/tags/init-following.tag | 1 +
 2 files changed, 5 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4d282aae9b..30ff40a624 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+unreleased
+----------
+* ユーザビリティの向上
+
 2493-2 (2017/08/29)
 -------------------
 * デザインの修正
diff --git a/src/web/app/mobile/tags/init-following.tag b/src/web/app/mobile/tags/init-following.tag
index 2fb7499d26..6db9d89b61 100644
--- a/src/web/app/mobile/tags/init-following.tag
+++ b/src/web/app/mobile/tags/init-following.tag
@@ -32,6 +32,7 @@
 
 			> .users
 				overflow-x scroll
+				-webkit-overflow-scrolling touch
 				white-space nowrap
 				padding 16px
 				background #eee

From e73abea25e644de43953a1b7e436e00a861417ee Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 29 Aug 2017 21:59:57 +0900
Subject: [PATCH 067/364] Refactor

---
 src/web/app/mobile/tags/index.js              |   2 -
 src/web/app/mobile/tags/timeline-post-sub.tag | 101 ----
 src/web/app/mobile/tags/timeline-post.tag     | 414 --------------
 src/web/app/mobile/tags/timeline.tag          | 517 ++++++++++++++++++
 4 files changed, 517 insertions(+), 517 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/timeline-post-sub.tag
 delete mode 100644 src/web/app/mobile/tags/timeline-post.tag

diff --git a/src/web/app/mobile/tags/index.js b/src/web/app/mobile/tags/index.js
index 2e6b478079..6f985a91fd 100644
--- a/src/web/app/mobile/tags/index.js
+++ b/src/web/app/mobile/tags/index.js
@@ -24,8 +24,6 @@ require('./page/messaging-room.tag');
 require('./home.tag');
 require('./home-timeline.tag');
 require('./timeline.tag');
-require('./timeline-post.tag');
-require('./timeline-post-sub.tag');
 require('./post-preview.tag');
 require('./sub-post-content.tag');
 require('./images-viewer.tag');
diff --git a/src/web/app/mobile/tags/timeline-post-sub.tag b/src/web/app/mobile/tags/timeline-post-sub.tag
deleted file mode 100644
index 3fff552e8f..0000000000
--- a/src/web/app/mobile/tags/timeline-post-sub.tag
+++ /dev/null
@@ -1,101 +0,0 @@
-<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>
-		: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>this.post = this.opts.post</script>
-</mk-timeline-post-sub>
diff --git a/src/web/app/mobile/tags/timeline-post.tag b/src/web/app/mobile/tags/timeline-post.tag
deleted file mode 100644
index 2395e9fb79..0000000000
--- a/src/web/app/mobile/tags/timeline-post.tag
+++ /dev/null
@@ -1,414 +0,0 @@
-<mk-timeline-post class={ repost: isRepost }>
-	<div class="reply-to" if={ p.reply_to }>
-		<mk-timeline-post-sub post={ p.reply_to }/>
-	</div>
-	<div class="repost" 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>
-			<i class="fa fa-retweet"></i>{'%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" 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">
-					<a class="reply" if={ p.reply_to }>
-						<i class="fa fa-reply"></i>
-					</a>
-					<p class="dummy"></p>
-					<a class="quote" if={ p.repost != null }>RP:</a>
-				</div>
-				<div class="media" if={ p.media }>
-					<mk-images-viewer 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 }><i class="fa fa-quote-right fa-flip-horizontal"></i>
-					<mk-post-preview class="repost" post={ p.repost }/>
-				</div>
-			</div>
-			<footer>
-				<mk-reactions-viewer post={ p } ref="reactionsViewer"/>
-				<button onclick={ reply }><i class="fa fa-reply"></i>
-					<p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
-				</button>
-				<button onclick={ repost } title="Repost"><i class="fa fa-retweet"></i>
-					<p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p>
-				</button>
-				<button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton"><i class="fa fa-plus"></i>
-					<p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
-				</button>
-			</footer>
-		</div>
-	</article>
-	<style>
-		:scope
-			display block
-			margin 0
-			padding 0
-			font-size 12px
-
-			@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
-
-					i
-						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
-
-							.link
-								&:after
-									content "\f14c"
-									display inline-block
-									padding-left 2px
-									font-family FontAwesome
-									font-size .9em
-									font-weight 400
-									font-style normal
-
-							mk-url-preview
-								margin-top 8px
-
-							> .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
-
-							> i: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 8px
-							background transparent
-							border none
-							box-shadow 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
-
-	</style>
-	<script>
-		import compile from '../../common/scripts/text-compiler';
-		import getPostSummary from '../../common/scripts/get-post-summary';
-		import openPostForm from '../scripts/open-post-form';
-
-		this.mixin('api');
-		this.mixin('stream');
-
-		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 => {
-			this.stream.send({
-				type: 'capture',
-				id: this.post.id
-			});
-			if (withHandler) this.stream.on('post-updated', this.onStreamPostUpdated);
-		};
-
-		this.decapture = withHandler => {
-			this.stream.send({
-				type: 'decapture',
-				id: this.post.id
-			});
-			if (withHandler) this.stream.off('post-updated', this.onStreamPostUpdated);
-		};
-
-		this.on('mount', () => {
-			this.capture(true);
-			this.stream.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));
-
-				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.stream.off('_connected_', this.onStreamConnected);
-		});
-
-		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
-			});
-		};
-	</script>
-</mk-timeline-post>
diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag
index 9e39cf80d7..6895384a56 100644
--- a/src/web/app/mobile/tags/timeline.tag
+++ b/src/web/app/mobile/tags/timeline.tag
@@ -144,3 +144,520 @@
 		};
 	</script>
 </mk-timeline>
+
+<mk-timeline-post class={ repost: isRepost }>
+	<div class="reply-to" if={ p.reply_to }>
+		<mk-timeline-post-sub post={ p.reply_to }/>
+	</div>
+	<div class="repost" 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>
+			<i class="fa fa-retweet"></i>{'%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" 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">
+					<a class="reply" if={ p.reply_to }>
+						<i class="fa fa-reply"></i>
+					</a>
+					<p class="dummy"></p>
+					<a class="quote" if={ p.repost != null }>RP:</a>
+				</div>
+				<div class="media" if={ p.media }>
+					<mk-images-viewer 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 }><i class="fa fa-quote-right fa-flip-horizontal"></i>
+					<mk-post-preview class="repost" post={ p.repost }/>
+				</div>
+			</div>
+			<footer>
+				<mk-reactions-viewer post={ p } ref="reactionsViewer"/>
+				<button onclick={ reply }><i class="fa fa-reply"></i>
+					<p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
+				</button>
+				<button onclick={ repost } title="Repost"><i class="fa fa-retweet"></i>
+					<p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p>
+				</button>
+				<button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton"><i class="fa fa-plus"></i>
+					<p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
+				</button>
+			</footer>
+		</div>
+	</article>
+	<style>
+		:scope
+			display block
+			margin 0
+			padding 0
+			font-size 12px
+
+			@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
+
+					i
+						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
+
+							.link
+								&:after
+									content "\f14c"
+									display inline-block
+									padding-left 2px
+									font-family FontAwesome
+									font-size .9em
+									font-weight 400
+									font-style normal
+
+							mk-url-preview
+								margin-top 8px
+
+							> .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
+
+							> i: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 8px
+							background transparent
+							border none
+							box-shadow 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
+
+	</style>
+	<script>
+		import compile from '../../common/scripts/text-compiler';
+		import getPostSummary from '../../common/scripts/get-post-summary';
+		import openPostForm from '../scripts/open-post-form';
+
+		this.mixin('api');
+		this.mixin('stream');
+
+		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 => {
+			this.stream.send({
+				type: 'capture',
+				id: this.post.id
+			});
+			if (withHandler) this.stream.on('post-updated', this.onStreamPostUpdated);
+		};
+
+		this.decapture = withHandler => {
+			this.stream.send({
+				type: 'decapture',
+				id: this.post.id
+			});
+			if (withHandler) this.stream.off('post-updated', this.onStreamPostUpdated);
+		};
+
+		this.on('mount', () => {
+			this.capture(true);
+			this.stream.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));
+
+				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.stream.off('_connected_', this.onStreamConnected);
+		});
+
+		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
+			});
+		};
+	</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>
+		: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>this.post = this.opts.post</script>
+</mk-timeline-post-sub>

From 0da4de955a90735089d44b04b4aaf47f3e45f522 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 29 Aug 2017 22:07:51 +0900
Subject: [PATCH 068/364] Refactor

---
 src/web/app/desktop/tags/index.js             |   2 -
 .../app/desktop/tags/timeline-post-sub.tag    | 107 ----
 src/web/app/desktop/tags/timeline-post.tag    | 493 --------------
 src/web/app/desktop/tags/timeline.tag         | 602 ++++++++++++++++++
 4 files changed, 602 insertions(+), 602 deletions(-)
 delete mode 100644 src/web/app/desktop/tags/timeline-post-sub.tag
 delete mode 100644 src/web/app/desktop/tags/timeline-post.tag

diff --git a/src/web/app/desktop/tags/index.js b/src/web/app/desktop/tags/index.js
index 11243c00a0..98bfc68804 100644
--- a/src/web/app/desktop/tags/index.js
+++ b/src/web/app/desktop/tags/index.js
@@ -26,7 +26,6 @@ require('./ui-header-search.tag');
 require('./notifications.tag');
 require('./post-form-window.tag');
 require('./post-form.tag');
-require('./timeline-post.tag');
 require('./post-preview.tag');
 require('./repost-form-window.tag');
 require('./home-widgets/user-recommendation.tag');
@@ -79,7 +78,6 @@ require('./search-posts.tag');
 require('./set-avatar-suggestion.tag');
 require('./set-banner-suggestion.tag');
 require('./repost-form.tag');
-require('./timeline-post-sub.tag');
 require('./sub-post-content.tag');
 require('./images-viewer.tag');
 require('./image-dialog.tag');
diff --git a/src/web/app/desktop/tags/timeline-post-sub.tag b/src/web/app/desktop/tags/timeline-post-sub.tag
deleted file mode 100644
index ab1e26721b..0000000000
--- a/src/web/app/desktop/tags/timeline-post-sub.tag
+++ /dev/null
@@ -1,107 +0,0 @@
-<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>
-		: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>
-		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/tags/timeline-post.tag b/src/web/app/desktop/tags/timeline-post.tag
deleted file mode 100644
index 0438b146ca..0000000000
--- a/src/web/app/desktop/tags/timeline-post.tag
+++ /dev/null
@@ -1,493 +0,0 @@
-<mk-timeline-post tabindex="-1" title={ title } onkeydown={ onKeyDown } dblclick={ onDblClick }>
-	<div class="reply-to" if={ p.reply_to }>
-		<mk-timeline-post-sub post={ p.reply_to }/>
-	</div>
-	<div class="repost" 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>
-			<i class="fa fa-retweet"></i>{'%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" 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>
-					<a class="created-at" href={ url }>
-						<mk-time time={ p.created_at }/>
-					</a>
-				</div>
-			</header>
-			<div class="body">
-				<div class="text" ref="text">
-					<a class="reply" if={ p.reply_to }>
-						<i class="fa fa-reply"></i>
-					</a>
-					<p class="dummy"></p>
-					<a class="quote" if={ p.repost != null }>RP:</a>
-				</div>
-				<div class="media" if={ p.media }>
-					<mk-images-viewer images={ p.media }/>
-				</div>
-				<mk-poll if={ p.poll } post={ p } ref="pollViewer"/>
-				<div class="repost" if={ p.repost }><i class="fa fa-quote-right fa-flip-horizontal"></i>
-					<mk-post-preview class="repost" post={ p.repost }/>
-				</div>
-			</div>
-			<footer>
-				<mk-reactions-viewer post={ p } ref="reactionsViewer"/>
-				<button onclick={ reply } title="%i18n:desktop.tags.mk-timeline-post.reply%"><i class="fa fa-reply"></i>
-					<p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
-				</button>
-				<button onclick={ repost } title="%i18n:desktop.tags.mk-timeline-post.repost%"><i class="fa fa-retweet"></i>
-					<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%"><i class="fa fa-plus"></i>
-					<p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
-				</button>
-				<button>
-					<i class="fa fa-ellipsis-h"></i>
-				</button>
-				<button onclick={ toggleDetail } title="%i18n:desktop.tags.mk-timeline-post.detail">
-					<i class="fa fa-caret-down" if={ !isDetailOpened }></i>
-					<i class="fa fa-caret-up" if={ isDetailOpened }></i>
-				</button>
-			</footer>
-		</div>
-	</article>
-	<div class="detail" if={ isDetailOpened }>
-		<mk-post-status-graph width="462" height="130" post={ p }/>
-	</div>
-	<style>
-		:scope
-			display block
-			margin 0
-			padding 0
-			background #fff
-
-			&: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
-
-					i
-						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
-
-							.link
-								&:after
-									content "\f14c"
-									display inline-block
-									padding-left 2px
-									font-family FontAwesome
-									font-size .9em
-									font-weight 400
-									font-style normal
-
-							> .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%
-
-						> mk-poll
-							font-size 80%
-
-						> .repost
-							margin 8px 0
-
-							> i: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>
-		import compile from '../../common/scripts/text-compiler';
-		import dateStringify from '../../common/scripts/date-stringify';
-
-		this.mixin('api');
-		this.mixin('stream');
-		this.mixin('user-preview');
-
-		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 => {
-			this.stream.send({
-				type: 'capture',
-				id: this.post.id
-			});
-			if (withHandler) this.stream.on('post-updated', this.onStreamPostUpdated);
-		};
-
-		this.decapture = withHandler => {
-			this.stream.send({
-				type: 'decapture',
-				id: this.post.id
-			});
-			if (withHandler) this.stream.off('post-updated', this.onStreamPostUpdated);
-		};
-
-		this.on('mount', () => {
-			this.capture(true);
-			this.stream.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));
-
-				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.stream.off('_connected_', this.onStreamConnected);
-		});
-
-		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.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>
diff --git a/src/web/app/desktop/tags/timeline.tag b/src/web/app/desktop/tags/timeline.tag
index d4cd50455c..91bf7a637a 100644
--- a/src/web/app/desktop/tags/timeline.tag
+++ b/src/web/app/desktop/tags/timeline.tag
@@ -90,3 +90,605 @@
 
 	</script>
 </mk-timeline>
+
+<mk-timeline-post tabindex="-1" title={ title } onkeydown={ onKeyDown } dblclick={ onDblClick }>
+	<div class="reply-to" if={ p.reply_to }>
+		<mk-timeline-post-sub post={ p.reply_to }/>
+	</div>
+	<div class="repost" 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>
+			<i class="fa fa-retweet"></i>{'%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" 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>
+					<a class="created-at" href={ url }>
+						<mk-time time={ p.created_at }/>
+					</a>
+				</div>
+			</header>
+			<div class="body">
+				<div class="text" ref="text">
+					<a class="reply" if={ p.reply_to }>
+						<i class="fa fa-reply"></i>
+					</a>
+					<p class="dummy"></p>
+					<a class="quote" if={ p.repost != null }>RP:</a>
+				</div>
+				<div class="media" if={ p.media }>
+					<mk-images-viewer images={ p.media }/>
+				</div>
+				<mk-poll if={ p.poll } post={ p } ref="pollViewer"/>
+				<div class="repost" if={ p.repost }><i class="fa fa-quote-right fa-flip-horizontal"></i>
+					<mk-post-preview class="repost" post={ p.repost }/>
+				</div>
+			</div>
+			<footer>
+				<mk-reactions-viewer post={ p } ref="reactionsViewer"/>
+				<button onclick={ reply } title="%i18n:desktop.tags.mk-timeline-post.reply%"><i class="fa fa-reply"></i>
+					<p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
+				</button>
+				<button onclick={ repost } title="%i18n:desktop.tags.mk-timeline-post.repost%"><i class="fa fa-retweet"></i>
+					<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%"><i class="fa fa-plus"></i>
+					<p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
+				</button>
+				<button>
+					<i class="fa fa-ellipsis-h"></i>
+				</button>
+				<button onclick={ toggleDetail } title="%i18n:desktop.tags.mk-timeline-post.detail">
+					<i class="fa fa-caret-down" if={ !isDetailOpened }></i>
+					<i class="fa fa-caret-up" if={ isDetailOpened }></i>
+				</button>
+			</footer>
+		</div>
+	</article>
+	<div class="detail" if={ isDetailOpened }>
+		<mk-post-status-graph width="462" height="130" post={ p }/>
+	</div>
+	<style>
+		:scope
+			display block
+			margin 0
+			padding 0
+			background #fff
+
+			&: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
+
+					i
+						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
+
+							.link
+								&:after
+									content "\f14c"
+									display inline-block
+									padding-left 2px
+									font-family FontAwesome
+									font-size .9em
+									font-weight 400
+									font-style normal
+
+							> .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%
+
+						> mk-poll
+							font-size 80%
+
+						> .repost
+							margin 8px 0
+
+							> i: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>
+		import compile from '../../common/scripts/text-compiler';
+		import dateStringify from '../../common/scripts/date-stringify';
+
+		this.mixin('api');
+		this.mixin('stream');
+		this.mixin('user-preview');
+
+		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 => {
+			this.stream.send({
+				type: 'capture',
+				id: this.post.id
+			});
+			if (withHandler) this.stream.on('post-updated', this.onStreamPostUpdated);
+		};
+
+		this.decapture = withHandler => {
+			this.stream.send({
+				type: 'decapture',
+				id: this.post.id
+			});
+			if (withHandler) this.stream.off('post-updated', this.onStreamPostUpdated);
+		};
+
+		this.on('mount', () => {
+			this.capture(true);
+			this.stream.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));
+
+				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.stream.off('_connected_', this.onStreamConnected);
+		});
+
+		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.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>
+		: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>
+		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>

From f6b132a1f50e44df9c66cfce1a7ac6b2a1193118 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 29 Aug 2017 22:13:32 +0900
Subject: [PATCH 069/364] Fix #743

---
 CHANGELOG.md                          |  1 +
 src/web/app/desktop/tags/timeline.tag | 22 ++++++++++++----------
 src/web/app/mobile/tags/timeline.tag  | 19 ++++++++++---------
 3 files changed, 23 insertions(+), 19 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 30ff40a624..06d88d08c4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@ ChangeLog (Release Notes)
 
 unreleased
 ----------
+* Fix: repostのborder-radiusが効いていない (#743)
 * ユーザビリティの向上
 
 2493-2 (2017/08/29)
diff --git a/src/web/app/desktop/tags/timeline.tag b/src/web/app/desktop/tags/timeline.tag
index 91bf7a637a..bce27cd7f3 100644
--- a/src/web/app/desktop/tags/timeline.tag
+++ b/src/web/app/desktop/tags/timeline.tag
@@ -10,16 +10,6 @@
 		:scope
 			display block
 
-			> mk-timeline-post
-				border-bottom solid 1px #eaeaea
-
-				&:first-child
-					border-top-left-radius 6px
-					border-top-right-radius 6px
-
-				&:last-of-type
-					border-bottom none
-
 			> .date
 				display block
 				margin 0
@@ -166,6 +156,18 @@
 			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
diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag
index 6895384a56..43470d197e 100644
--- a/src/web/app/mobile/tags/timeline.tag
+++ b/src/web/app/mobile/tags/timeline.tag
@@ -46,15 +46,6 @@
 					font-size 3em
 					color #ccc
 
-			> mk-timeline-post
-				border-bottom solid 1px #eaeaea
-
-				&:first-child
-					border-radius 8px 8px 0 0
-
-				&:last-of-type
-					border-bottom none
-
 			> .date
 				display block
 				margin 0
@@ -208,6 +199,16 @@
 			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

From 36a3bc1fdd47685bd16235cfc11b679e82602f02 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 29 Aug 2017 22:24:28 +0900
Subject: [PATCH 070/364] :art:

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

diff --git a/src/web/app/mobile/tags/page/settings/profile.tag b/src/web/app/mobile/tags/page/settings/profile.tag
index 7e1bedbf47..305f16fec5 100644
--- a/src/web/app/mobile/tags/page/settings/profile.tag
+++ b/src/web/app/mobile/tags/page/settings/profile.tag
@@ -146,7 +146,7 @@
 							padding 12px
 							font-size 16px
 							color #192427
-							border solid 1px #ddd
+							border solid 2px #ddd
 							border-radius 4px
 
 						> textarea

From 113489800330ec3b765c1733fff1bc2621118068 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 29 Aug 2017 22:27:42 +0900
Subject: [PATCH 071/364] v2498

---
 CHANGELOG.md   | 4 +++-
 package.json   | 2 +-
 src/const.json | 5 ++---
 3 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 06d88d08c4..c42008de51 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,10 +2,12 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
-unreleased
+2498 (2017/08/29)
 ----------
 * Fix: repostのborder-radiusが効いていない (#743)
+* テーマカラーを赤に戻してみた
 * ユーザビリティの向上
+* デザインの調整
 
 2493-2 (2017/08/29)
 -------------------
diff --git a/package.json b/package.json
index dca1bff8ce..fff34b2d4b 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2493-2",
+  "version": "0.0.2498",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",
diff --git a/src/const.json b/src/const.json
index 1032ed538f..eeb304c9f3 100644
--- a/src/const.json
+++ b/src/const.json
@@ -1,5 +1,4 @@
 {
-	"themeColor": "#87bb35",
-	"themeColorForeground": "#fff",
-	"idea": ["#f13049", "#f43636"]
+	"themeColor": "#f43636",
+	"themeColorForeground": "#fff"
 }

From 9f477549ddbf6bf5b892df993fd44f8e1cf0f2bc Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 30 Aug 2017 01:37:33 +0900
Subject: [PATCH 072/364] :v:

---
 CHANGELOG.md                     |   6 +-
 locales/en.yml                   |  14 ++
 locales/ja.yml                   |  14 ++
 src/web/app/mobile/router.js     |   2 +-
 src/web/app/mobile/tags/user.tag | 260 ++++++++++++++++++++++++++++++-
 5 files changed, 291 insertions(+), 5 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c42008de51..6b6c80bf7d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,12 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
-2498 (2017/08/29)
+unreleased
 ----------
+* New: モバイルのユーザーページを刷新
+
+2498 (2017/08/29)
+-----------------
 * Fix: repostのborder-radiusが効いていない (#743)
 * テーマカラーを赤に戻してみた
 * ユーザビリティの向上
diff --git a/locales/en.yml b/locales/en.yml
index 5e11339db5..231fc640ec 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -472,9 +472,23 @@ mobile:
       is-followed: "Followed you"
       following: "Following"
       followers: "Followers"
+      overview: "Overview"
       posts: "Timeline"
       media: "Media"
 
+    mk-user-overview:
+      recent-posts: "Recent posts"
+      images: "Images"
+      activity: "Activity"
+
+    mk-user-overview-posts:
+      loading: "Loading"
+      no-posts: "No posts"
+
+    mk-user-overview-photos:
+      loading: "Loading"
+      no-photos: "No photos"
+
     mk-users-list:
       all: "All"
       known: "You know"
diff --git a/locales/ja.yml b/locales/ja.yml
index 62ac4cb81f..651f529fb8 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -472,10 +472,24 @@ mobile:
       is-followed: "フォローされています"
       following: "フォロー"
       followers: "フォロワー"
+      overview: "概要"
       posts: "タイムライン"
       posts-count: "ポスト"
       media: "メディア"
 
+    mk-user-overview:
+      recent-posts: "最近の投稿"
+      images: "画像"
+      activity: "アクティビティ"
+
+    mk-user-overview-posts:
+      loading: "読み込み中"
+      no-posts: "投稿はありません"
+
+    mk-user-overview-photos:
+      loading: "読み込み中"
+      no-photos: "写真はありません"
+
     mk-users-list:
       all: "すべて"
       known: "知り合い"
diff --git a/src/web/app/mobile/router.js b/src/web/app/mobile/router.js
index de4108a593..d59b2ec3a1 100644
--- a/src/web/app/mobile/router.js
+++ b/src/web/app/mobile/router.js
@@ -23,7 +23,7 @@ export default me => {
 	route('/post/new',                   newPost);
 	route('/post::post',                 post);
 	route('/search::query',              search);
-	route('/:user',                      user.bind(null, 'posts'));
+	route('/:user',                      user.bind(null, 'overview'));
 	route('/:user/graphs',               user.bind(null, 'graphs'));
 	route('/:user/followers',            userFollowers);
 	route('/:user/following',            userFollowing);
diff --git a/src/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag
index fbf2f690a3..7d88957849 100644
--- a/src/web/app/mobile/tags/user.tag
+++ b/src/web/app/mobile/tags/user.tag
@@ -37,14 +37,15 @@
 						<i>%i18n:mobile.tags.mk-user.followers%</i>
 					</a>
 				</div>
-				<mk-activity-table user={ user }/>
 			</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.posts%</a>
 				<a data-is-active={ page == 'media' } onclick={ go.bind(null, 'media') }>%i18n:mobile.tags.mk-user.media%</a>
 			</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 }/>
 		</div>
@@ -55,6 +56,8 @@
 
 			> .user
 				> header
+					box-shadow 0 4px 4px rgba(0, 0, 0, 0.3)
+
 					> .banner
 						padding-bottom 33.3%
 						background-color #1b1b1b
@@ -159,7 +162,6 @@
 						justify-content center
 						margin 0 auto
 						max-width 600px
-						border-bottom solid 1px rgba(0, 0, 0, 0.2)
 
 						> a
 							display block
@@ -190,7 +192,7 @@
 		this.mixin('api');
 
 		this.username = this.opts.user;
-		this.page = this.opts.page ? this.opts.page : 'posts';
+		this.page = this.opts.page ? this.opts.page : 'overview';
 		this.fetching = true;
 
 		this.on('mount', () => {
@@ -211,3 +213,255 @@
 		};
 	</script>
 </mk-user>
+
+<mk-user-overview>
+	<section class="recent-posts">
+		<h2><i class="fa fa-comments-o"></i>%i18n:mobile.tags.mk-user-overview.recent-posts%</h2>
+		<div>
+			<mk-user-overview-posts user={ user }/>
+		</div>
+	</section>
+	<section class="images">
+		<h2><i class="fa fa-picture-o"></i>%i18n:mobile.tags.mk-user-overview.images%</h2>
+		<div>
+			<mk-user-overview-photos user={ user }/>
+		</div>
+	</section>
+	<section class="activity">
+		<h2><i class="fa fa-bar-chart"></i>%i18n:mobile.tags.mk-user-overview.activity%</h2>
+		<div>
+			<mk-activity-table user={ user }/>
+		</div>
+	</section>
+	<style>
+		:scope
+			display block
+			max-width 600px
+			margin 0 auto
+
+			> 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 16px
+					font-weight normal
+					color #465258
+					background #fff
+					border-radius 8px 8px 0 0
+
+					> i
+						margin-right 6px
+
+			> .activity
+				> div
+					padding 8px
+
+	</style>
+	<script>
+		this.user = this.opts.user;
+	</script>
+</mk-user-overview>
+
+<mk-user-overview-posts>
+	<p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:mobile.tags.mk-user-overview-posts.loading%<mk-ellipsis/></p>
+	<div 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>
+	<style>
+		: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>
+		this.mixin('api');
+
+		this.user = this.opts.user;
+		this.initializing = true;
+
+		this.on('mount', () => {
+			this.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>
+		: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-left 44px
+						white-space nowrap
+						text-overflow ellipsis
+
+				> div
+					padding 0 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, transparent 0%, #fff 100%)
+
+				> mk-time
+					display inline-block
+					padding 8px
+					color #aaa
+
+	</style>
+	<script>
+		import summary from '../../common/scripts/get-post-summary';
+
+		this.post = this.opts.post;
+		this.text = summary(this.post);
+	</script>
+</mk-user-overview-posts-post-card>
+
+<mk-user-overview-photos>
+	<p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:mobile.tags.mk-user-overview-photos.loading%<mk-ellipsis/></p>
+	<div class="stream" 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>
+	<style>
+		: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>
+		this.mixin('api');
+
+		this.images = [];
+		this.initializing = true;
+		this.user = this.opts.user;
+
+		this.on('mount', () => {
+			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({
+							post,
+							media
+						});
+					});
+				});
+				this.update();
+			});
+		});
+	</script>
+</mk-user-overview-photos>

From 76f7734b65208082f0ddf62d7d355628b7300de3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 30 Aug 2017 01:41:21 +0900
Subject: [PATCH 073/364] v2501

---
 CHANGELOG.md | 4 ++--
 package.json | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6b6c80bf7d..ead734d686 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,8 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
-unreleased
-----------
+2501 (2017/08/30)
+-----------------
 * New: モバイルのユーザーページを刷新
 
 2498 (2017/08/29)
diff --git a/package.json b/package.json
index fff34b2d4b..8afa4f5819 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2498",
+  "version": "0.0.2501",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",

From c15886e0e58af70bb87b357a4d6db24bf5966953 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 30 Aug 2017 01:54:09 +0900
Subject: [PATCH 074/364] v2502

---
 CHANGELOG.md                     | 4 ++++
 package.json                     | 2 +-
 src/web/app/mobile/tags/user.tag | 7 ++++---
 3 files changed, 9 insertions(+), 4 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ead734d686..09732dd66b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+2502 (2017/08/30)
+-----------------
+* デザインの修正・調整
+
 2501 (2017/08/30)
 -----------------
 * New: モバイルのユーザーページを刷新
diff --git a/package.json b/package.json
index 8afa4f5819..4139c94c60 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2501",
+  "version": "0.0.2502",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",
diff --git a/src/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag
index 7d88957849..c81eb929a8 100644
--- a/src/web/app/mobile/tags/user.tag
+++ b/src/web/app/mobile/tags/user.tag
@@ -250,7 +250,7 @@
 				> h2
 					margin 0
 					padding 8px 10px
-					font-size 16px
+					font-size 15px
 					font-weight normal
 					color #465258
 					background #fff
@@ -362,7 +362,8 @@
 						display inline-block
 						overflow hidden
 						width calc(100% - 45px)
-						margin-left 44px
+						margin 8px 0 0 44px
+						line-height 28px
 						white-space nowrap
 						text-overflow ellipsis
 
@@ -380,7 +381,7 @@
 						left 0
 						width 100%
 						height 20px
-						background linear-gradient(to bottom, transparent 0%, #fff 100%)
+						background linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, #fff 100%)
 
 				> mk-time
 					display inline-block

From c1510f90adae55ae4b3af117cce2b7267248c728 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 30 Aug 2017 02:03:07 +0900
Subject: [PATCH 075/364] v2503

---
 CHANGELOG.md                     | 4 ++++
 package.json                     | 2 +-
 src/web/app/mobile/tags/user.tag | 3 ++-
 3 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 09732dd66b..2cda49be49 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+2503 (2017/08/30)
+-----------------
+* デザインの調整
+
 2502 (2017/08/30)
 -----------------
 * デザインの修正・調整
diff --git a/package.json b/package.json
index 4139c94c60..bce252fa3f 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2502",
+  "version": "0.0.2503",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",
diff --git a/src/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag
index c81eb929a8..bd6bbad726 100644
--- a/src/web/app/mobile/tags/user.tag
+++ b/src/web/app/mobile/tags/user.tag
@@ -362,10 +362,11 @@
 						display inline-block
 						overflow hidden
 						width calc(100% - 45px)
-						margin 8px 0 0 44px
+						margin 8px 0 0 42px
 						line-height 28px
 						white-space nowrap
 						text-overflow ellipsis
+						font-size 12px
 
 				> div
 					padding 0 8px 8px 8px

From a1f355fc9d2fe646590f192e61aaa3bc0c6a1025 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 29 Aug 2017 21:54:50 +0000
Subject: [PATCH 076/364] chore(package): update @types/node to version 8.0.26

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

diff --git a/package.json b/package.json
index bce252fa3f..fdf3f0bd6b 100644
--- a/package.json
+++ b/package.json
@@ -53,7 +53,7 @@
     "@types/morgan": "1.7.32",
     "@types/ms": "0.7.30",
     "@types/multer": "1.3.2",
-    "@types/node": "8.0.25",
+    "@types/node": "8.0.26",
     "@types/ratelimiter": "2.1.28",
     "@types/redis": "2.6.0",
     "@types/request": "2.0.3",

From e89e40c80467418b8c15dc19cbb770f70844f175 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, 30 Aug 2017 10:59:50 +0900
Subject: [PATCH 077/364] Update DONORS.md

---
 DONORS.md | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/DONORS.md b/DONORS.md
index d022c4ef64..21610304e0 100644
--- a/DONORS.md
+++ b/DONORS.md
@@ -3,6 +3,9 @@ DONORS
 
 (no particular order)
 
+* らふぁ
+* 俺様
+* なぎうり
 * スルメ https://surume.tk/
 
 :heart: Thanks for donating, guys!

From 4c0fbaedfa4cd1cac6c4980b078330566eafaed5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 30 Aug 2017 11:38:35 +0900
Subject: [PATCH 078/364] =?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=82=A2=E3=82=AF=E3=83=86=E3=82=A3=E3=83=93?=
 =?UTF-8?q?=E3=83=86=E3=82=A3=E3=83=81=E3=83=A3=E3=83=BC=E3=83=88=E3=82=92?=
 =?UTF-8?q?=E5=A4=89=E6=9B=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 CHANGELOG.md                                  |  4 ++
 src/web/app/common/tags/index.js              |  1 +
 .../app/common/tags/weekly-activity-chart.tag | 49 +++++++++++++++++++
 src/web/app/mobile/tags/user.tag              |  2 +-
 4 files changed, 55 insertions(+), 1 deletion(-)
 create mode 100644 src/web/app/common/tags/weekly-activity-chart.tag

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2cda49be49..f6e78485c5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+unreleased
+----------
+* New: モバイル版のアクティビティチャートを変更
+
 2503 (2017/08/30)
 -----------------
 * デザインの調整
diff --git a/src/web/app/common/tags/index.js b/src/web/app/common/tags/index.js
index 1ee8dab42d..dd6ba75d7a 100644
--- a/src/web/app/common/tags/index.js
+++ b/src/web/app/common/tags/index.js
@@ -27,3 +27,4 @@ require('./activity-table.tag');
 require('./reaction-picker.tag');
 require('./reactions-viewer.tag');
 require('./reaction-icon.tag');
+require('./weekly-activity-chart.tag');
diff --git a/src/web/app/common/tags/weekly-activity-chart.tag b/src/web/app/common/tags/weekly-activity-chart.tag
new file mode 100644
index 0000000000..ae9f7071c5
--- /dev/null
+++ b/src/web/app/common/tags/weekly-activity-chart.tag
@@ -0,0 +1,49 @@
+<mk-weekly-activity-chart>
+	<svg if={ data } ref="canvas" viewBox="0 0 7 1" preserveAspectRatio="none">
+		<g each={ d, i in data.reverse() }>
+			<rect width="0.8" riot-height={ d.postsH }
+				riot-x={ i + 0.1 } y={ 1 - d.postsH - d.repliesH - d.repostsH }
+				fill="#41ddde"/>
+			<rect width="0.8" riot-height={ d.repliesH }
+				riot-x={ i + 0.1 } y={ 1 - d.repliesH - d.repostsH }
+				fill="#f7796c"/>
+			<rect width="0.8" riot-height={ d.repostsH }
+				riot-x={ i + 0.1 } y={ 1 - d.repostsH }
+				fill="#a1de41"/>
+			</g>
+	</svg>
+	<style>
+		:scope
+			display block
+			max-width 600px
+			margin 0 auto
+
+			> svg
+				display block
+
+				> rect
+					transform-origin center
+
+	</style>
+	<script>
+		this.mixin('api');
+
+		this.user = this.opts.user;
+
+		this.on('mount', () => {
+			this.api('aggregation/users/activity', {
+				user_id: this.user.id,
+				limit: 7
+			}).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-weekly-activity-chart>
diff --git a/src/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag
index bd6bbad726..3ed186de9e 100644
--- a/src/web/app/mobile/tags/user.tag
+++ b/src/web/app/mobile/tags/user.tag
@@ -230,7 +230,7 @@
 	<section class="activity">
 		<h2><i class="fa fa-bar-chart"></i>%i18n:mobile.tags.mk-user-overview.activity%</h2>
 		<div>
-			<mk-activity-table user={ user }/>
+			<mk-weekly-activity-chart user={ user }/>
 		</div>
 	</section>
 	<style>

From d86859b1ce72985f9eb2b065307fb9ef7ac3afad Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 30 Aug 2017 11:45:17 +0900
Subject: [PATCH 079/364] =?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=AB=E6=9C=80=E7=B5=82=E3=83=AD=E3=82=B0?=
 =?UTF-8?q?=E3=82=A4=E3=83=B3=E6=97=A5=E6=99=82=E3=82=92=E8=A1=A8=E7=A4=BA?=
 =?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

---
 CHANGELOG.md                     | 3 ++-
 locales/en.yml                   | 1 +
 locales/ja.yml                   | 1 +
 src/web/app/mobile/tags/user.tag | 7 +++++++
 4 files changed, 11 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f6e78485c5..72601b7dd2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,8 @@ ChangeLog (Release Notes)
 
 unreleased
 ----------
-* New: モバイル版のアクティビティチャートを変更
+* New: モバイル版のユーザーページのアクティビティチャートを変更
+* New: モバイル版のユーザーページに最終ログイン日時を表示するように
 
 2503 (2017/08/30)
 -----------------
diff --git a/locales/en.yml b/locales/en.yml
index 231fc640ec..29a6a764d3 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -480,6 +480,7 @@ mobile:
       recent-posts: "Recent posts"
       images: "Images"
       activity: "Activity"
+      last-used-at: "Latest used at"
 
     mk-user-overview-posts:
       loading: "Loading"
diff --git a/locales/ja.yml b/locales/ja.yml
index 651f529fb8..21a07640d0 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -481,6 +481,7 @@ mobile:
       recent-posts: "最近の投稿"
       images: "画像"
       activity: "アクティビティ"
+      last-used-at: "最終ログイン"
 
     mk-user-overview-posts:
       loading: "読み込み中"
diff --git a/src/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag
index 3ed186de9e..269acdaf3d 100644
--- a/src/web/app/mobile/tags/user.tag
+++ b/src/web/app/mobile/tags/user.tag
@@ -233,6 +233,7 @@
 			<mk-weekly-activity-chart 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>
 		:scope
 			display block
@@ -263,6 +264,12 @@
 				> div
 					padding 8px
 
+			> p
+				display block
+				margin 16px
+				text-align center
+				color #cad2da
+
 	</style>
 	<script>
 		this.user = this.opts.user;

From 4c7152f4ac923fff8470ac99d1cf195d6534ae5e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 30 Aug 2017 11:46:16 +0900
Subject: [PATCH 080/364] :art:

---
 CHANGELOG.md                               | 1 +
 src/web/app/mobile/tags/init-following.tag | 2 +-
 src/web/app/mobile/tags/user.tag           | 2 +-
 3 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 72601b7dd2..e9cbcdd42e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@ unreleased
 ----------
 * New: モバイル版のユーザーページのアクティビティチャートを変更
 * New: モバイル版のユーザーページに最終ログイン日時を表示するように
+* デザインの調整
 
 2503 (2017/08/30)
 -----------------
diff --git a/src/web/app/mobile/tags/init-following.tag b/src/web/app/mobile/tags/init-following.tag
index 6db9d89b61..d0b63ff5db 100644
--- a/src/web/app/mobile/tags/init-following.tag
+++ b/src/web/app/mobile/tags/init-following.tag
@@ -67,7 +67,7 @@
 
 					> .name
 						display block
-						margin 24px 0 2px 0
+						margin 24px 0 0 0
 						font-size 16px
 						color #555
 
diff --git a/src/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag
index 269acdaf3d..d85e3b51fd 100644
--- a/src/web/app/mobile/tags/user.tag
+++ b/src/web/app/mobile/tags/user.tag
@@ -376,7 +376,7 @@
 						font-size 12px
 
 				> div
-					padding 0 8px 8px 8px
+					padding 2px 8px 8px 8px
 					height 60px
 					overflow hidden
 					white-space normal

From 70688c029b6a47979b9a2e73031d98e5385d144f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 30 Aug 2017 11:46:41 +0900
Subject: [PATCH 081/364] v2358

---
 CHANGELOG.md | 4 ++--
 package.json | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e9cbcdd42e..c64a8ea04d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,8 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
-unreleased
-----------
+2508 (2017/08/30)
+-----------------
 * New: モバイル版のユーザーページのアクティビティチャートを変更
 * New: モバイル版のユーザーページに最終ログイン日時を表示するように
 * デザインの調整
diff --git a/package.json b/package.json
index bce252fa3f..964179d755 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2503",
+  "version": "0.0.2508",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",

From e7415dd42bf656a24d70c49776ff7c84a1838f9e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 30 Aug 2017 17:31:39 +0900
Subject: [PATCH 082/364] Implement #746

---
 CHANGELOG.md                             |   4 +
 locales/en.yml                           |   4 +
 locales/ja.yml                           |   4 +
 src/api/endpoints.ts                     |   4 +
 src/api/endpoints/i/pin.ts               |  44 ++++++++
 src/api/serializers/user.ts              |  41 ++++---
 src/web/app/common/tags/index.js         |   1 +
 src/web/app/common/tags/post-menu.tag    | 134 +++++++++++++++++++++++
 src/web/app/desktop/tags/post-detail.tag |  23 ++--
 src/web/app/desktop/tags/timeline.tag    |  21 ++--
 src/web/app/mobile/tags/page/post.tag    |  14 ++-
 src/web/app/mobile/tags/post-detail.tag  |  54 +++++----
 src/web/app/mobile/tags/timeline.tag     |  23 +++-
 src/web/app/mobile/tags/user.tag         |   4 +
 14 files changed, 315 insertions(+), 60 deletions(-)
 create mode 100644 src/api/endpoints/i/pin.ts
 create mode 100644 src/web/app/common/tags/post-menu.tag

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c64a8ea04d..2669bfee74 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+unreleased
+----------
+* New: 投稿のピン留め (#746)
+
 2508 (2017/08/30)
 -----------------
 * New: モバイル版のユーザーページのアクティビティチャートを変更
diff --git a/locales/en.yml b/locales/en.yml
index 29a6a764d3..15d278c370 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -77,6 +77,10 @@ common:
       show-result: "Show result"
       voted: "Voted"
 
+    mk-post-menu:
+      pin: "Pin"
+      pinned: "Pinned"
+
     mk-reaction-picker:
       choose-reaction: "Pick your reaction"
 
diff --git a/locales/ja.yml b/locales/ja.yml
index 21a07640d0..7ed4262f1c 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -77,6 +77,10 @@ common:
       show-result: "結果を見る"
       voted: "投票済み"
 
+    mk-post-menu:
+      pin: "ピン留め"
+      pinned: "ピン留めしました"
+
     mk-reaction-picker:
       choose-reaction: "リアクションを選択"
 
diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts
index c6661533e8..e5be68c096 100644
--- a/src/api/endpoints.ts
+++ b/src/api/endpoints.ts
@@ -167,6 +167,10 @@ const endpoints: Endpoint[] = [
 		name: 'i/regenerate_token',
 		withCredential: true
 	},
+	{
+		name: 'i/pin',
+		kind: 'account-write'
+	},
 	{
 		name: 'i/appdata/get',
 		withCredential: true
diff --git a/src/api/endpoints/i/pin.ts b/src/api/endpoints/i/pin.ts
new file mode 100644
index 0000000000..a94950d22b
--- /dev/null
+++ b/src/api/endpoints/i/pin.ts
@@ -0,0 +1,44 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import User from '../../models/user';
+import Post from '../../models/post';
+import serialize from '../../serializers/user';
+
+/**
+ * Pin post
+ *
+ * @param {any} params
+ * @param {any} 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');
+
+	// Fetch pinee
+	const post = await Post.findOne({
+		_id: postId,
+		user_id: user._id
+	});
+
+	if (post === null) {
+		return rej('post not found');
+	}
+
+	await User.update(user._id, {
+		$set: {
+			pinned_post_id: post._id
+		}
+	});
+
+	// Serialize
+	const iObj = await serialize(user, user, {
+		detail: true
+	});
+
+	// Send response
+	res(iObj);
+});
diff --git a/src/api/serializers/user.ts b/src/api/serializers/user.ts
index bdbc749589..c9189d9034 100644
--- a/src/api/serializers/user.ts
+++ b/src/api/serializers/user.ts
@@ -4,6 +4,7 @@
 import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
 import User from '../models/user';
+import serializePost from './post';
 import Following from '../models/following';
 import getFriends from '../common/get-friends';
 import config from '../../conf';
@@ -116,24 +117,32 @@ export default (
 		_user.is_followed = follow2 !== null;
 	}
 
-	if (me && !me.equals(_user.id) && opts.detail) {
-		const myFollowingIds = await getFriends(me);
+	if (opts.detail) {
+		if (_user.pinned_post_id) {
+			_user.pinned_post = await serializePost(_user.pinned_post_id, me, {
+				detail: true
+			});
+		}
 
-		// Get following you know count
-		const followingYouKnowCount = await Following.count({
-			followee_id: { $in: myFollowingIds },
-			follower_id: _user.id,
-			deleted_at: { $exists: false }
-		});
-		_user.following_you_know_count = followingYouKnowCount;
+		if (me && !me.equals(_user.id)) {
+			const myFollowingIds = await getFriends(me);
 
-		// Get followers you know count
-		const followersYouKnowCount = await Following.count({
-			followee_id: _user.id,
-			follower_id: { $in: myFollowingIds },
-			deleted_at: { $exists: false }
-		});
-		_user.followers_you_know_count = followersYouKnowCount;
+			// Get following you know count
+			const followingYouKnowCount = await Following.count({
+				followee_id: { $in: myFollowingIds },
+				follower_id: _user.id,
+				deleted_at: { $exists: false }
+			});
+			_user.following_you_know_count = followingYouKnowCount;
+
+			// Get followers you know count
+			const followersYouKnowCount = await Following.count({
+				followee_id: _user.id,
+				follower_id: { $in: myFollowingIds },
+				deleted_at: { $exists: false }
+			});
+			_user.followers_you_know_count = followersYouKnowCount;
+		}
 	}
 
 	resolve(_user);
diff --git a/src/web/app/common/tags/index.js b/src/web/app/common/tags/index.js
index dd6ba75d7a..6e6081da9b 100644
--- a/src/web/app/common/tags/index.js
+++ b/src/web/app/common/tags/index.js
@@ -28,3 +28,4 @@ require('./reaction-picker.tag');
 require('./reactions-viewer.tag');
 require('./reaction-icon.tag');
 require('./weekly-activity-chart.tag');
+require('./post-menu.tag');
diff --git a/src/web/app/common/tags/post-menu.tag b/src/web/app/common/tags/post-menu.tag
new file mode 100644
index 0000000000..33895212bc
--- /dev/null
+++ b/src/web/app/common/tags/post-menu.tag
@@ -0,0 +1,134 @@
+<mk-post-menu>
+	<div class="backdrop" ref="backdrop" onclick={ 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>
+	</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
+
+				> button
+					display block
+
+	</style>
+	<script>
+		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.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-post-menu>
diff --git a/src/web/app/desktop/tags/post-detail.tag b/src/web/app/desktop/tags/post-detail.tag
index 7a90dccf39..58343482d0 100644
--- a/src/web/app/desktop/tags/post-detail.tag
+++ b/src/web/app/desktop/tags/post-detail.tag
@@ -43,16 +43,18 @@
 			</div>
 			<footer>
 				<mk-reactions-viewer post={ p }/>
-				<button onclick={ reply } title="返信"><i class="fa fa-reply"></i>
-					<p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
+				<button onclick={ reply } title="返信">
+					<i class="fa fa-reply"></i><p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
 				</button>
-				<button onclick={ repost } title="Repost"><i class="fa fa-retweet"></i>
-					<p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p>
+				<button onclick={ repost } title="Repost">
+					<i class="fa fa-retweet"></i><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="リアクション"><i class="fa fa-plus"></i>
-					<p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
+				<button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton" title="リアクション">
+					<i class="fa fa-plus"></i><p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
+				</button>
+				<button onclick={ menu } ref="menuButton">
+					<i class="fa fa-ellipsis-h"></i>
 				</button>
-				<button><i class="fa fa-ellipsis-h"></i></button>
 			</footer>
 		</article>
 		<div class="replies">
@@ -315,6 +317,13 @@
 			});
 		};
 
+		this.menu = () => {
+			riot.mount(document.body.appendChild(document.createElement('mk-post-menu')), {
+				source: this.refs.menuButton,
+				post: this.p
+			});
+		};
+
 		this.loadContext = () => {
 			this.contextFetching = true;
 
diff --git a/src/web/app/desktop/tags/timeline.tag b/src/web/app/desktop/tags/timeline.tag
index bce27cd7f3..cd7ac7d207 100644
--- a/src/web/app/desktop/tags/timeline.tag
+++ b/src/web/app/desktop/tags/timeline.tag
@@ -128,16 +128,16 @@
 			</div>
 			<footer>
 				<mk-reactions-viewer post={ p } ref="reactionsViewer"/>
-				<button onclick={ reply } title="%i18n:desktop.tags.mk-timeline-post.reply%"><i class="fa fa-reply"></i>
-					<p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
+				<button onclick={ reply } title="%i18n:desktop.tags.mk-timeline-post.reply%">
+					<i class="fa fa-reply"></i><p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
 				</button>
-				<button onclick={ repost } title="%i18n:desktop.tags.mk-timeline-post.repost%"><i class="fa fa-retweet"></i>
-					<p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p>
+				<button onclick={ repost } title="%i18n:desktop.tags.mk-timeline-post.repost%">
+					<i class="fa fa-retweet"></i><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%"><i class="fa fa-plus"></i>
-					<p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
+				<button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton" title="%i18n:desktop.tags.mk-timeline-post.add-reaction%">
+					<i class="fa fa-plus"></i><p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
 				</button>
-				<button>
+				<button onclick={ menu } ref="menuButton">
 					<i class="fa fa-ellipsis-h"></i>
 				</button>
 				<button onclick={ toggleDetail } title="%i18n:desktop.tags.mk-timeline-post.detail">
@@ -525,6 +525,13 @@
 			});
 		};
 
+		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
diff --git a/src/web/app/mobile/tags/page/post.tag b/src/web/app/mobile/tags/page/post.tag
index 198acf1798..6888229f89 100644
--- a/src/web/app/mobile/tags/page/post.tag
+++ b/src/web/app/mobile/tags/page/post.tag
@@ -2,7 +2,9 @@
 	<mk-ui ref="ui">
 		<main if={ !parent.fetching }>
 			<a if={ parent.post.next } href={ parent.post.next }><i class="fa fa-angle-up"></i>%i18n:mobile.tags.mk-post-page.next%</a>
-			<mk-post-detail ref="post" post={ parent.post }/>
+			<div>
+				<mk-post-detail ref="post" post={ parent.post }/>
+			</div>
 			<a if={ parent.post.prev } href={ parent.post.prev }><i class="fa fa-angle-down"></i>%i18n:mobile.tags.mk-post-page.prev%</a>
 		</main>
 	</mk-ui>
@@ -13,6 +15,16 @@
 			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
 
diff --git a/src/web/app/mobile/tags/post-detail.tag b/src/web/app/mobile/tags/post-detail.tag
index 9215bafdbc..cf09434400 100644
--- a/src/web/app/mobile/tags/post-detail.tag
+++ b/src/web/app/mobile/tags/post-detail.tag
@@ -38,24 +38,26 @@
 			</div>
 			<mk-poll if={ p.poll } post={ p }/>
 		</div>
-		<a class="time" href={ url }>
+		<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 onclick={ reply } title="%i18n:mobile.tags.mk-post-detail.reply%"><i class="fa fa-reply"></i>
-				<p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
+			<button onclick={ reply } title="%i18n:mobile.tags.mk-post-detail.reply%">
+				<i class="fa fa-reply"></i><p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
 			</button>
-			<button onclick={ repost } title="Repost"><i class="fa fa-retweet"></i>
-				<p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p>
+			<button onclick={ repost } title="Repost">
+				<i class="fa fa-retweet"></i><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%"><i class="fa fa-plus"></i>
-				<p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
+			<button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton" title="%i18n:mobile.tags.mk-post-detail.reaction%">
+				<i class="fa fa-plus"></i><p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
+			</button>
+			<button onclick={ menu } ref="menuButton">
+				<i class="fa fa-ellipsis-h"></i>
 			</button>
-			<button><i class="fa fa-ellipsis-h"></i></button>
 		</footer>
 	</article>
-	<div class="replies">
+	<div class="replies" if={ !compact }>
 		<virtual each={ post in replies }>
 			<mk-post-detail-sub post={ post }/>
 		</virtual>
@@ -64,19 +66,14 @@
 		:scope
 			display block
 			overflow hidden
-			margin 8px auto
+			margin 0 auto
 			padding 0
-			max-width 500px
-			width calc(100% - 16px)
+			width 100%
 			text-align left
 			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)
-
 			> .fetching
 				padding 64px 0
 
@@ -269,6 +266,7 @@
 
 		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;
@@ -299,14 +297,16 @@
 			}
 
 			// Get replies
-			this.api('posts/replies', {
-				post_id: this.p.id,
-				limit: 8
-			}).then(replies => {
-				this.update({
-					replies: replies
+			if (!this.compact) {
+				this.api('posts/replies', {
+					post_id: this.p.id,
+					limit: 8
+				}).then(replies => {
+					this.update({
+						replies: replies
+					});
 				});
-			});
+			}
 		});
 
 		this.reply = () => {
@@ -332,6 +332,14 @@
 			});
 		};
 
+		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;
 
diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag
index 43470d197e..d8df8b2663 100644
--- a/src/web/app/mobile/tags/timeline.tag
+++ b/src/web/app/mobile/tags/timeline.tag
@@ -181,14 +181,17 @@
 			</div>
 			<footer>
 				<mk-reactions-viewer post={ p } ref="reactionsViewer"/>
-				<button onclick={ reply }><i class="fa fa-reply"></i>
-					<p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
+				<button onclick={ reply }>
+					<i class="fa fa-reply"></i><p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
 				</button>
-				<button onclick={ repost } title="Repost"><i class="fa fa-retweet"></i>
-					<p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p>
+				<button onclick={ repost } title="Repost">
+					<i class="fa fa-retweet"></i><p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p>
 				</button>
-				<button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton"><i class="fa fa-plus"></i>
-					<p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
+				<button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton">
+					<i class="fa fa-plus"></i><p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
+				</button>
+				<button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton">
+					<i class="fa fa-ellipsis-h"></i>
 				</button>
 			</footer>
 		</div>
@@ -558,6 +561,14 @@
 				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>
 
diff --git a/src/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag
index d85e3b51fd..0fe4055cf0 100644
--- a/src/web/app/mobile/tags/user.tag
+++ b/src/web/app/mobile/tags/user.tag
@@ -215,6 +215,7 @@
 </mk-user>
 
 <mk-user-overview>
+	<mk-post-detail if={ user.pinned_post } post={ user.pinned_post } compact={ true }/>
 	<section class="recent-posts">
 		<h2><i class="fa fa-comments-o"></i>%i18n:mobile.tags.mk-user-overview.recent-posts%</h2>
 		<div>
@@ -240,6 +241,9 @@
 			max-width 600px
 			margin 0 auto
 
+			> mk-post-detail
+				margin 0 0 8px 0
+
 			> section
 				background #eee
 				border-radius 8px

From 52138eb4c317a8a23ba5d12a9194937914713f35 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 30 Aug 2017 17:32:03 +0900
Subject: [PATCH 083/364] [Client] Fix bug

---
 CHANGELOG.md                                      | 1 +
 src/web/app/common/tags/weekly-activity-chart.tag | 6 +++---
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2669bfee74..a9a362dbef 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ ChangeLog (Release Notes)
 unreleased
 ----------
 * New: 投稿のピン留め (#746)
+* その他細かな修正
 
 2508 (2017/08/30)
 -----------------
diff --git a/src/web/app/common/tags/weekly-activity-chart.tag b/src/web/app/common/tags/weekly-activity-chart.tag
index ae9f7071c5..d9c6c4bd1c 100644
--- a/src/web/app/common/tags/weekly-activity-chart.tag
+++ b/src/web/app/common/tags/weekly-activity-chart.tag
@@ -2,13 +2,13 @@
 	<svg if={ data } ref="canvas" viewBox="0 0 7 1" preserveAspectRatio="none">
 		<g each={ d, i in data.reverse() }>
 			<rect width="0.8" riot-height={ d.postsH }
-				riot-x={ i + 0.1 } y={ 1 - d.postsH - d.repliesH - d.repostsH }
+				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 } y={ 1 - d.repliesH - d.repostsH }
+				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 } y={ 1 - d.repostsH }
+				riot-x={ i + 0.1 } riot-y={ 1 - d.repostsH }
 				fill="#a1de41"/>
 			</g>
 	</svg>

From 631ff6e49048c729c3d71db0e3170d1dec4196fe Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 30 Aug 2017 17:37:11 +0900
Subject: [PATCH 084/364] :v:

---
 src/web/app/common/tags/index.js              |  1 -
 .../app/common/tags/weekly-activity-chart.tag | 49 ----------------
 src/web/app/mobile/tags/user.tag              | 56 ++++++++++++++++++-
 3 files changed, 54 insertions(+), 52 deletions(-)
 delete mode 100644 src/web/app/common/tags/weekly-activity-chart.tag

diff --git a/src/web/app/common/tags/index.js b/src/web/app/common/tags/index.js
index 6e6081da9b..35a9f4586e 100644
--- a/src/web/app/common/tags/index.js
+++ b/src/web/app/common/tags/index.js
@@ -27,5 +27,4 @@ require('./activity-table.tag');
 require('./reaction-picker.tag');
 require('./reactions-viewer.tag');
 require('./reaction-icon.tag');
-require('./weekly-activity-chart.tag');
 require('./post-menu.tag');
diff --git a/src/web/app/common/tags/weekly-activity-chart.tag b/src/web/app/common/tags/weekly-activity-chart.tag
deleted file mode 100644
index d9c6c4bd1c..0000000000
--- a/src/web/app/common/tags/weekly-activity-chart.tag
+++ /dev/null
@@ -1,49 +0,0 @@
-<mk-weekly-activity-chart>
-	<svg if={ data } ref="canvas" viewBox="0 0 7 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>
-		:scope
-			display block
-			max-width 600px
-			margin 0 auto
-
-			> svg
-				display block
-
-				> rect
-					transform-origin center
-
-	</style>
-	<script>
-		this.mixin('api');
-
-		this.user = this.opts.user;
-
-		this.on('mount', () => {
-			this.api('aggregation/users/activity', {
-				user_id: this.user.id,
-				limit: 7
-			}).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-weekly-activity-chart>
diff --git a/src/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag
index 0fe4055cf0..5fc43269d6 100644
--- a/src/web/app/mobile/tags/user.tag
+++ b/src/web/app/mobile/tags/user.tag
@@ -231,7 +231,7 @@
 	<section class="activity">
 		<h2><i class="fa fa-bar-chart"></i>%i18n:mobile.tags.mk-user-overview.activity%</h2>
 		<div>
-			<mk-weekly-activity-chart user={ user }/>
+			<mk-user-overview-activity-chart user={ user }/>
 		</div>
 	</section>
 	<p>%i18n:mobile.tags.mk-user-overview.last-used-at%: <b><mk-time time={ user.last_used_at }/></b></p>
@@ -462,7 +462,7 @@
 			this.api('users/posts', {
 				user_id: this.user.id,
 				with_media: true,
-				limit: 9
+				limit: 6
 			}).then(posts => {
 				this.initializing = false;
 				posts.forEach(post => {
@@ -478,3 +478,55 @@
 		});
 	</script>
 </mk-user-overview-photos>
+
+<mk-user-overview-activity-chart>
+	<svg 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>
+		:scope
+			display block
+			max-width 600px
+			margin 0 auto
+
+			> svg
+				display block
+				width 100%
+				height 80px
+
+				> rect
+					transform-origin center
+
+	</style>
+	<script>
+		this.mixin('api');
+
+		this.user = this.opts.user;
+
+		this.on('mount', () => {
+			this.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>

From 7fe0abc5cec71759807a41f383606dd156f11d88 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 30 Aug 2017 17:45:23 +0900
Subject: [PATCH 085/364] Implement #745

---
 CHANGELOG.md                              |  1 +
 src/api/stream/home.ts                    | 10 ++++++++++
 src/web/app/common/scripts/home-stream.js |  5 +++++
 3 files changed, 16 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index a9a362dbef..cc5a6bc8a4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ ChangeLog (Release Notes)
 unreleased
 ----------
 * New: 投稿のピン留め (#746)
+* New: ホームストリームにメッセージを流すことでlast_used_atを更新できるようにする (#745)
 * その他細かな修正
 
 2508 (2017/08/30)
diff --git a/src/api/stream/home.ts b/src/api/stream/home.ts
index 2ab8d3025b..d5fe01c261 100644
--- a/src/api/stream/home.ts
+++ b/src/api/stream/home.ts
@@ -2,6 +2,7 @@ import * as websocket from 'websocket';
 import * as redis from 'redis';
 import * as debug from 'debug';
 
+import User from '../models/user';
 import serializePost from '../serializers/post';
 
 const log = debug('misskey');
@@ -35,6 +36,15 @@ export default function homeStream(request: websocket.request, connection: webso
 		const msg = JSON.parse(data.utf8Data);
 
 		switch (msg.type) {
+			case 'alive':
+				// Update lastUsedAt
+				User.update({ _id: user._id }, {
+					$set: {
+						last_used_at: new Date()
+					}
+				});
+				break;
+
 			case 'capture':
 				if (!msg.id) return;
 				const postId = msg.id;
diff --git a/src/web/app/common/scripts/home-stream.js b/src/web/app/common/scripts/home-stream.js
index c54cbd7f19..de9ceb3b51 100644
--- a/src/web/app/common/scripts/home-stream.js
+++ b/src/web/app/common/scripts/home-stream.js
@@ -12,6 +12,11 @@ class Connection extends Stream {
 			i: me.token
 		});
 
+		// 最終利用日時を更新するため定期的にaliveメッセージを送信
+		setInterval(() => {
+			this.send({ type: 'alive' });
+		}, 1000 * 60);
+
 		this.on('i_updated', me.update);
 
 		this.on('my_token_regenerated', () => {

From 5d9572cda22b89621cae4132b47ec2caaac07a22 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 30 Aug 2017 18:06:16 +0900
Subject: [PATCH 086/364] =?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=AB=E7=9F=A5=E3=82=8A=E5=90=88=E3=81=84?=
 =?UTF-8?q?=E3=81=AE=E3=83=95=E3=82=A9=E3=83=AD=E3=83=AF=E3=83=BC=E3=82=92?=
 =?UTF-8?q?=E8=A1=A8=E7=A4=BA=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

---
 CHANGELOG.md                     |  1 +
 locales/en.yml                   |  5 +++
 locales/ja.yml                   |  5 +++
 src/web/app/mobile/tags/user.tag | 65 ++++++++++++++++++++++++++++++++
 4 files changed, 76 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index cc5a6bc8a4..ab207126cd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ ChangeLog (Release Notes)
 unreleased
 ----------
 * New: 投稿のピン留め (#746)
+* New: モバイル版のユーザーページに知り合いのフォロワーを表示するように
 * New: ホームストリームにメッセージを流すことでlast_used_atを更新できるようにする (#745)
 * その他細かな修正
 
diff --git a/locales/en.yml b/locales/en.yml
index 15d278c370..a410bfb431 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -484,6 +484,7 @@ mobile:
       recent-posts: "Recent posts"
       images: "Images"
       activity: "Activity"
+      followers-you-know: "Followers you know"
       last-used-at: "Latest used at"
 
     mk-user-overview-posts:
@@ -494,6 +495,10 @@ mobile:
       loading: "Loading"
       no-photos: "No photos"
 
+    mk-user-overview-followers-you-know:
+      loading: "Loading"
+      no-users: "No users"
+
     mk-users-list:
       all: "All"
       known: "You know"
diff --git a/locales/ja.yml b/locales/ja.yml
index 7ed4262f1c..eb7e35c7d6 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -485,6 +485,7 @@ mobile:
       recent-posts: "最近の投稿"
       images: "画像"
       activity: "アクティビティ"
+      followers-you-know: "知り合いのフォロワー"
       last-used-at: "最終ログイン"
 
     mk-user-overview-posts:
@@ -495,6 +496,10 @@ mobile:
       loading: "読み込み中"
       no-photos: "写真はありません"
 
+    mk-user-overview-followers-you-know:
+      loading: "読み込み中"
+      no-users: "知り合いのユーザーはいません"
+
     mk-users-list:
       all: "すべて"
       known: "知り合い"
diff --git a/src/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag
index 5fc43269d6..83231f01d7 100644
--- a/src/web/app/mobile/tags/user.tag
+++ b/src/web/app/mobile/tags/user.tag
@@ -234,6 +234,12 @@
 			<mk-user-overview-activity-chart user={ user }/>
 		</div>
 	</section>
+	<section class="followers-you-know" if={ SIGNIN && I.id !== user.id }>
+		<h2><i class="fa fa-users"></i>%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>
 		:scope
@@ -276,6 +282,8 @@
 
 	</style>
 	<script>
+		this.mixin('i');
+
 		this.user = this.opts.user;
 	</script>
 </mk-user-overview>
@@ -530,3 +538,60 @@
 		});
 	</script>
 </mk-user-overview-activity-chart>
+
+<mk-user-overview-followers-you-know>
+	<p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:mobile.tags.mk-user-overview-followers-you-know.loading%<mk-ellipsis/></p>
+	<div 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>
+	<style>
+		: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>
+		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: 30
+			}).then(x => {
+				this.update({
+					users: x.users,
+					initializing: false
+				});
+			});
+		});
+	</script>
+</mk-user-overview-followers-you-know>

From 5d57b288007161a8f2d99bdaebcb5447d1fa6b11 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 30 Aug 2017 18:06:54 +0900
Subject: [PATCH 087/364] v2515

---
 CHANGELOG.md | 4 ++--
 package.json | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ab207126cd..066ce8cfee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,8 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
-unreleased
-----------
+2515 (2017/08/30)
+-----------------
 * New: 投稿のピン留め (#746)
 * New: モバイル版のユーザーページに知り合いのフォロワーを表示するように
 * New: ホームストリームにメッセージを流すことでlast_used_atを更新できるようにする (#745)
diff --git a/package.json b/package.json
index 964179d755..acb182c057 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2508",
+  "version": "0.0.2515",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",

From 659446c075f04f2a52faa5f51d01e14dd8d4aa08 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 30 Aug 2017 18:56:51 +0900
Subject: [PATCH 088/364] Fix bug

---
 CHANGELOG.md                         | 4 ++++
 src/web/app/mobile/tags/timeline.tag | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 066ce8cfee..3f7dadd948 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+unreleased
+----------
+* Fix: モバイル版のタイムラインからリアクションやメニューを開けない
+
 2515 (2017/08/30)
 -----------------
 * New: 投稿のピン留め (#746)
diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag
index d8df8b2663..bc01394554 100644
--- a/src/web/app/mobile/tags/timeline.tag
+++ b/src/web/app/mobile/tags/timeline.tag
@@ -190,7 +190,7 @@
 				<button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton">
 					<i class="fa fa-plus"></i><p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
 				</button>
-				<button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton">
+				<button onclick={ menu } ref="menuButton">
 					<i class="fa fa-ellipsis-h"></i>
 				</button>
 			</footer>

From d51ac13b237fc71cb6bb95aa006947cd0b1dfe5b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 30 Aug 2017 18:59:50 +0900
Subject: [PATCH 089/364] #748

---
 CHANGELOG.md                         | 1 +
 src/web/app/mobile/tags/timeline.tag | 6 +++++-
 2 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3f7dadd948..bca7815d24 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ ChangeLog (Release Notes)
 unreleased
 ----------
 * Fix: モバイル版のタイムラインからリアクションやメニューを開けない
+* デザインの調整
 
 2515 (2017/08/30)
 -----------------
diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag
index bc01394554..62d47678ac 100644
--- a/src/web/app/mobile/tags/timeline.tag
+++ b/src/web/app/mobile/tags/timeline.tag
@@ -190,7 +190,7 @@
 				<button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton">
 					<i class="fa fa-plus"></i><p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
 				</button>
-				<button onclick={ menu } ref="menuButton">
+				<button class="menu" onclick={ menu } ref="menuButton">
 					<i class="fa fa-ellipsis-h"></i>
 				</button>
 			</footer>
@@ -454,6 +454,10 @@
 							&.reacted
 								color $theme-color
 
+							&.menu
+								@media (max-width 350px)
+									display none
+
 	</style>
 	<script>
 		import compile from '../../common/scripts/text-compiler';

From 375336e42e8f829409c149b390bd90a9d21e1d47 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 30 Aug 2017 19:01:26 +0900
Subject: [PATCH 090/364] v2518

---
 CHANGELOG.md | 4 ++--
 package.json | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index bca7815d24..caba5640eb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,8 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
-unreleased
-----------
+2518 (2017/08/30)
+-----------------
 * Fix: モバイル版のタイムラインからリアクションやメニューを開けない
 * デザインの調整
 
diff --git a/package.json b/package.json
index acb182c057..ed872cca35 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2515",
+  "version": "0.0.2518",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",

From 85da2f925536568324bc70ae7c1b142533c79976 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 30 Aug 2017 19:06:15 +0900
Subject: [PATCH 091/364] :art:

---
 src/web/app/mobile/tags/post-detail.tag | 5 ++++-
 src/web/app/mobile/tags/timeline.tag    | 5 ++++-
 2 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/src/web/app/mobile/tags/post-detail.tag b/src/web/app/mobile/tags/post-detail.tag
index cf09434400..dc032fe964 100644
--- a/src/web/app/mobile/tags/post-detail.tag
+++ b/src/web/app/mobile/tags/post-detail.tag
@@ -234,7 +234,7 @@
 					font-size 1.2em
 
 					> button
-						margin 0 28px 0 0
+						margin 0
 						padding 8px
 						background transparent
 						border none
@@ -243,6 +243,9 @@
 						color #ddd
 						cursor pointer
 
+						&:not(:last-child)
+							margin-right 28px
+
 						&:hover
 							color #666
 
diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag
index 62d47678ac..2b0948ac34 100644
--- a/src/web/app/mobile/tags/timeline.tag
+++ b/src/web/app/mobile/tags/timeline.tag
@@ -434,7 +434,7 @@
 
 					> footer
 						> button
-							margin 0 28px 0 0
+							margin 0
 							padding 8px
 							background transparent
 							border none
@@ -443,6 +443,9 @@
 							color #ddd
 							cursor pointer
 
+							&:not(:last-child)
+								margin-right 28px
+
 							&:hover
 								color #666
 

From af35978687541dfa6455408a923af535d3ae1298 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 30 Aug 2017 19:06:27 +0900
Subject: [PATCH 092/364] v2520

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index caba5640eb..6c8e7d1132 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+2520 (2017/08/30)
+-----------------
+* デザインの調整
+
 2518 (2017/08/30)
 -----------------
 * Fix: モバイル版のタイムラインからリアクションやメニューを開けない
diff --git a/package.json b/package.json
index ed872cca35..ec706b5bb5 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2518",
+  "version": "0.0.2520",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",

From cb416ea1a544609c960b1949461bdc2a3b8c912b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 30 Aug 2017 21:49:15 +0900
Subject: [PATCH 093/364] Better English

---
 CHANGELOG.md                     | 4 ++++
 locales/en.yml                   | 2 +-
 locales/ja.yml                   | 2 +-
 src/web/app/mobile/tags/user.tag | 2 +-
 4 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6c8e7d1132..e20b305fd0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+unreleased
+----------
+* l10n
+
 2520 (2017/08/30)
 -----------------
 * デザインの調整
diff --git a/locales/en.yml b/locales/en.yml
index a410bfb431..d40896212b 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -473,7 +473,7 @@ mobile:
       no-posts-with-media: "There is no posts with media"
 
     mk-user:
-      is-followed: "Followed you"
+      follows-you: "Follows you"
       following: "Following"
       followers: "Followers"
       overview: "Overview"
diff --git a/locales/ja.yml b/locales/ja.yml
index eb7e35c7d6..b8e5cff412 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -473,7 +473,7 @@ mobile:
       no-posts-with-media: "メディア付き投稿はありません。"
 
     mk-user:
-      is-followed: "フォローされています"
+      follows-you: "フォローされています"
       following: "フォロー"
       followers: "フォロワー"
       overview: "概要"
diff --git a/src/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag
index 83231f01d7..ea431dcc53 100644
--- a/src/web/app/mobile/tags/user.tag
+++ b/src/web/app/mobile/tags/user.tag
@@ -12,7 +12,7 @@
 				<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.is-followed%</span>
+					<span class="followed" if={ user.is_followed }>%i18n:mobile.tags.mk-user.follows-you%</span>
 				</div>
 				<div class="description">{ user.description }</div>
 				<div class="info">

From 69cfec18ef6f8eb71277a3754b5cccdd546323a0 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 30 Aug 2017 21:32:58 +0000
Subject: [PATCH 094/364] chore(package): update css-loader to version 0.28.7

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

diff --git a/package.json b/package.json
index ec706b5bb5..874172cc40 100644
--- a/package.json
+++ b/package.json
@@ -66,7 +66,7 @@
     "@types/websocket": "0.0.34",
     "chai": "4.1.1",
     "chai-http": "3.0.0",
-    "css-loader": "0.28.5",
+    "css-loader": "0.28.7",
     "event-stream": "3.3.4",
     "gulp": "3.9.1",
     "gulp-cssnano": "2.1.2",

From 92a9fb282f35920566f018de2378a6151fbed7ec Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 31 Aug 2017 09:31:30 +0900
Subject: [PATCH 095/364] Improve usability

---
 CHANGELOG.md                       | 1 +
 src/web/app/mobile/tags/ui-nav.tag | 1 +
 2 files changed, 2 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e20b305fd0..6a86b02abb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ ChangeLog (Release Notes)
 unreleased
 ----------
 * l10n
+* ユーザビリティの向上
 
 2520 (2017/08/30)
 -----------------
diff --git a/src/web/app/mobile/tags/ui-nav.tag b/src/web/app/mobile/tags/ui-nav.tag
index 76c43ade66..34235ba4f1 100644
--- a/src/web/app/mobile/tags/ui-nav.tag
+++ b/src/web/app/mobile/tags/ui-nav.tag
@@ -44,6 +44,7 @@
 				width 240px
 				height 100%
 				overflow auto
+				-webkit-overflow-scrolling touch
 				color #777
 				background #fff
 

From 26a9a66a4d4277f3cee4286c9a5a232c7fea30b3 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 31 Aug 2017 13:58:17 +0000
Subject: [PATCH 096/364] fix(package): update inquirer to version 3.2.3

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

diff --git a/package.json b/package.json
index 43a05ae469..4263b4000f 100644
--- a/package.json
+++ b/package.json
@@ -116,7 +116,7 @@
     "file-type": "6.1.0",
     "fuckadblock": "3.2.1",
     "gm": "1.23.0",
-    "inquirer": "3.2.2",
+    "inquirer": "3.2.3",
     "is-root": "1.0.0",
     "is-url": "1.2.2",
     "js-yaml": "3.9.1",

From 5167434af8bb37ace196991a13b24f733f1a7e71 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 1 Sep 2017 00:13:14 +0900
Subject: [PATCH 097/364] Refactoring: Bundle related tags

---
 src/web/app/desktop/tags/index.js             |   8 -
 .../app/desktop/tags/ui-header-account.tag    | 214 -----
 src/web/app/desktop/tags/ui-header-clock.tag  |  86 --
 src/web/app/desktop/tags/ui-header-nav.tag    | 133 ---
 .../desktop/tags/ui-header-notifications.tag  | 108 ---
 .../desktop/tags/ui-header-post-button.tag    |  42 -
 src/web/app/desktop/tags/ui-header-search.tag |  42 -
 src/web/app/desktop/tags/ui-header.tag        |  86 --
 src/web/app/desktop/tags/ui-notification.tag  |  51 --
 src/web/app/desktop/tags/ui.tag               | 770 ++++++++++++++++++
 10 files changed, 770 insertions(+), 770 deletions(-)
 delete mode 100644 src/web/app/desktop/tags/ui-header-account.tag
 delete mode 100644 src/web/app/desktop/tags/ui-header-clock.tag
 delete mode 100644 src/web/app/desktop/tags/ui-header-nav.tag
 delete mode 100644 src/web/app/desktop/tags/ui-header-notifications.tag
 delete mode 100644 src/web/app/desktop/tags/ui-header-post-button.tag
 delete mode 100644 src/web/app/desktop/tags/ui-header-search.tag
 delete mode 100644 src/web/app/desktop/tags/ui-header.tag
 delete mode 100644 src/web/app/desktop/tags/ui-notification.tag

diff --git a/src/web/app/desktop/tags/index.js b/src/web/app/desktop/tags/index.js
index 98bfc68804..4e286013a1 100644
--- a/src/web/app/desktop/tags/index.js
+++ b/src/web/app/desktop/tags/index.js
@@ -16,13 +16,6 @@ require('./crop-window.tag');
 require('./settings.tag');
 require('./settings-window.tag');
 require('./analog-clock.tag');
-require('./ui-header.tag');
-require('./ui-header-account.tag');
-require('./ui-header-notifications.tag');
-require('./ui-header-clock.tag');
-require('./ui-header-nav.tag');
-require('./ui-header-post-button.tag');
-require('./ui-header-search.tag');
 require('./notifications.tag');
 require('./post-form-window.tag');
 require('./post-form.tag');
@@ -88,5 +81,4 @@ require('./user-followers.tag');
 require('./user-following-window.tag');
 require('./user-followers-window.tag');
 require('./list-user.tag');
-require('./ui-notification.tag');
 require('./detailed-post-window.tag');
diff --git a/src/web/app/desktop/tags/ui-header-account.tag b/src/web/app/desktop/tags/ui-header-account.tag
deleted file mode 100644
index 23c4fdbbf9..0000000000
--- a/src/web/app/desktop/tags/ui-header-account.tag
+++ /dev/null
@@ -1,214 +0,0 @@
-<mk-ui-header-account>
-	<button class="header" data-active={ isOpen.toString() } onclick={ toggle }>
-		<span class="username">{ I.username }<i class="fa fa-angle-down" if={ !isOpen }></i><i class="fa fa-angle-up" if={ isOpen }></i></span>
-		<img class="avatar" src={ I.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
-	</button>
-	<div class="menu" if={ isOpen }>
-		<ul>
-			<li>
-				<a href={ '/' + I.username }><i class="fa fa-user"></i>%i18n:desktop.tags.mk-ui-header-account.profile%<i class="fa fa-angle-right"></i></a>
-			</li>
-			<li onclick={ drive }>
-				<p><i class="fa fa-cloud"></i>%i18n:desktop.tags.mk-ui-header-account.drive%<i class="fa fa-angle-right"></i></p>
-			</li>
-			<li>
-				<a href="/i>mentions"><i class="fa fa-at"></i>%i18n:desktop.tags.mk-ui-header-account.mentions%<i class="fa fa-angle-right"></i></a>
-			</li>
-		</ul>
-		<ul>
-			<li onclick={ settings }>
-				<p><i class="fa fa-cog"></i>%i18n:desktop.tags.mk-ui-header-account.settings%<i class="fa fa-angle-right"></i></p>
-			</li>
-		</ul>
-		<ul>
-			<li onclick={ signout }>
-				<p><i class="fa fa-power-off"></i>%i18n:desktop.tags.mk-ui-header-account.signout%<i class="fa fa-angle-right"></i></p>
-			</li>
-		</ul>
-	</div>
-	<style>
-		: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
-
-					i
-						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
-
-							> i:first-of-type
-								margin-right 6px
-
-							> i: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>
-		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>
diff --git a/src/web/app/desktop/tags/ui-header-clock.tag b/src/web/app/desktop/tags/ui-header-clock.tag
deleted file mode 100644
index b8cb078497..0000000000
--- a/src/web/app/desktop/tags/ui-header-clock.tag
+++ /dev/null
@@ -1,86 +0,0 @@
-<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>
-		: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>
-		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>
diff --git a/src/web/app/desktop/tags/ui-header-nav.tag b/src/web/app/desktop/tags/ui-header-nav.tag
deleted file mode 100644
index c36ce65798..0000000000
--- a/src/web/app/desktop/tags/ui-header-nav.tag
+++ /dev/null
@@ -1,133 +0,0 @@
-<mk-ui-header-nav>
-	<ul if={ SIGNIN }>
-		<li class="home { active: page == 'home' }">
-			<a href={ CONFIG.url }>
-				<i class="fa fa-home"></i>
-				<p>%i18n:desktop.tags.mk-ui-header-nav.home%</p>
-			</a>
-		</li>
-		<li class="messaging">
-			<a onclick={ messaging }>
-				<i class="fa fa-comments"></i>
-				<p>%i18n:desktop.tags.mk-ui-header-nav.messaging%</p>
-				<i class="fa fa-circle" if={ hasUnreadMessagingMessages }></i>
-			</a>
-		</li>
-		<li class="info">
-			<a href="https://twitter.com/misskey_xyz" target="_blank">
-				<i class="fa fa-info"></i>
-				<p>%i18n:desktop.tags.mk-ui-header-nav.info%</p>
-			</a>
-		</li>
-	</ul>
-	<style>
-		: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
-
-						> i:first-child
-							margin-right 8px
-
-						> i:last-child
-							margin-left 5px
-							vertical-align super
-							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>
-		this.mixin('i');
-		this.mixin('api');
-		this.mixin('stream');
-
-		this.page = this.opts.page;
-
-		this.on('mount', () => {
-			this.stream.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
-			this.stream.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', () => {
-			this.stream.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
-			this.stream.off('unread_messaging_message', this.onUnreadMessagingMessage);
-		});
-
-		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>
diff --git a/src/web/app/desktop/tags/ui-header-notifications.tag b/src/web/app/desktop/tags/ui-header-notifications.tag
deleted file mode 100644
index 3cd8d1e3df..0000000000
--- a/src/web/app/desktop/tags/ui-header-notifications.tag
+++ /dev/null
@@ -1,108 +0,0 @@
-<mk-ui-header-notifications>
-	<button class="header" data-active={ isOpen } onclick={ toggle }><i class="fa fa-bell-o"></i></button>
-	<div class="notifications" if={ isOpen }>
-		<mk-notifications/>
-	</div>
-	<style>
-		:scope
-			display block
-			float left
-
-			> .header
-				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%)
-
-				> i
-					font-size 1.2em
-					line-height 48px
-
-			> .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>
-		import contains from '../../common/scripts/contains';
-
-		this.isOpen = false;
-
-		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>
diff --git a/src/web/app/desktop/tags/ui-header-post-button.tag b/src/web/app/desktop/tags/ui-header-post-button.tag
deleted file mode 100644
index ca380b06ea..0000000000
--- a/src/web/app/desktop/tags/ui-header-post-button.tag
+++ /dev/null
@@ -1,42 +0,0 @@
-<mk-ui-header-post-button>
-	<button onclick={ post } title="新規投稿"><i class="fa fa-pencil-square-o"></i></button>
-	<style>
-		: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 2px
-				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>
-		this.post = e => {
-			this.parent.parent.openPostForm();
-		};
-	</script>
-</mk-ui-header-post-button>
diff --git a/src/web/app/desktop/tags/ui-header-search.tag b/src/web/app/desktop/tags/ui-header-search.tag
deleted file mode 100644
index 616476f42c..0000000000
--- a/src/web/app/desktop/tags/ui-header-search.tag
+++ /dev/null
@@ -1,42 +0,0 @@
-<mk-ui-header-search>
-	<form class="search" onsubmit={ onsubmit }>
-		<input ref="q" type="search" placeholder="&#xf002; %i18n:desktop.tags.mk-ui-header-search.placeholder%"/>
-		<div class="result"></div>
-	</form>
-	<style>
-		:scope
-
-			> form
-				display block
-				float left
-
-				> input
-					user-select text
-					cursor auto
-					margin 0
-					padding 6px 18px
-					width 14em
-					height 48px
-					font-size 1em
-					line-height calc(48px - 12px)
-					background transparent
-					outline none
-					//border solid 1px #ddd
-					border none
-					border-radius 0
-					transition color 0.5s ease, border 0.5s ease
-					font-family FontAwesome, sans-serif
-
-					&::-webkit-input-placeholder
-						color #9eaba8
-
-	</style>
-	<script>
-		this.mixin('page');
-
-		this.onsubmit = e => {
-			e.preventDefault();
-			this.page('/search:' + this.refs.q.value);
-		};
-	</script>
-</mk-ui-header-search>
diff --git a/src/web/app/desktop/tags/ui-header.tag b/src/web/app/desktop/tags/ui-header.tag
deleted file mode 100644
index fa7f2cb2ac..0000000000
--- a/src/web/app/desktop/tags/ui-header.tag
+++ /dev/null
@@ -1,86 +0,0 @@
-<mk-ui-header>
-	<mk-donation if={ SIGNIN && I.data.no_donation != 'true' }/>
-	<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 if={ SIGNIN }/>
-					<mk-ui-header-notifications if={ SIGNIN }/>
-					<mk-ui-header-post-button if={ SIGNIN }/>
-					<mk-ui-header-clock/>
-				</div>
-			</div>
-		</div>
-	</div>
-	<style>
-		: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-color rgba(255, 255, 255, 0.75)
-					background #fff
-
-					&: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>this.mixin('i');</script>
-</mk-ui-header>
diff --git a/src/web/app/desktop/tags/ui-notification.tag b/src/web/app/desktop/tags/ui-notification.tag
deleted file mode 100644
index f39d766d8c..0000000000
--- a/src/web/app/desktop/tags/ui-notification.tag
+++ /dev/null
@@ -1,51 +0,0 @@
-<mk-ui-notification>
-	<p>{ opts.message }</p>
-	<style>
-		: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>
-		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.unmount()
-				});
-			}, 6000);
-		});
-	</script>
-</mk-ui-notification>
diff --git a/src/web/app/desktop/tags/ui.tag b/src/web/app/desktop/tags/ui.tag
index 788fb56131..fce0743ff7 100644
--- a/src/web/app/desktop/tags/ui.tag
+++ b/src/web/app/desktop/tags/ui.tag
@@ -35,3 +35,773 @@
 		};
 	</script>
 </mk-ui>
+
+<mk-ui-header>
+	<mk-donation if={ SIGNIN && I.data.no_donation != 'true' }/>
+	<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 if={ SIGNIN }/>
+					<mk-ui-header-notifications if={ SIGNIN }/>
+					<mk-ui-header-post-button if={ SIGNIN }/>
+					<mk-ui-header-clock/>
+				</div>
+			</div>
+		</div>
+	</div>
+	<style>
+		: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-color rgba(255, 255, 255, 0.75)
+					background #1d2429
+
+					&: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>this.mixin('i');</script>
+</mk-ui-header>
+
+<mk-ui-header-search>
+	<form class="search" onsubmit={ onsubmit }>
+		<input ref="q" type="search" placeholder="&#xf002; %i18n:desktop.tags.mk-ui-header-search.placeholder%"/>
+		<div class="result"></div>
+	</form>
+	<style>
+		:scope
+
+			> form
+				display block
+				float left
+
+				> input
+					user-select text
+					cursor auto
+					margin 0
+					padding 6px 18px
+					width 14em
+					height 48px
+					font-size 1em
+					line-height calc(48px - 12px)
+					background transparent
+					outline none
+					//border solid 1px #ddd
+					border none
+					border-radius 0
+					transition color 0.5s ease, border 0.5s ease
+					font-family FontAwesome, sans-serif
+
+					&::-webkit-input-placeholder
+						color #9eaba8
+
+	</style>
+	<script>
+		this.mixin('page');
+
+		this.onsubmit = e => {
+			e.preventDefault();
+			this.page('/search:' + this.refs.q.value);
+		};
+	</script>
+</mk-ui-header-search>
+
+<mk-ui-header-post-button>
+	<button onclick={ post } title="新規投稿"><i class="fa fa-pencil-square-o"></i></button>
+	<style>
+		: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 2px
+				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>
+		this.post = e => {
+			this.parent.parent.openPostForm();
+		};
+	</script>
+</mk-ui-header-post-button>
+
+<mk-ui-header-notifications>
+	<button class="header" data-active={ isOpen } onclick={ toggle }><i class="fa fa-bell-o"></i></button>
+	<div class="notifications" if={ isOpen }>
+		<mk-notifications/>
+	</div>
+	<style>
+		:scope
+			display block
+			float left
+
+			> .header
+				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%)
+
+				> i
+					font-size 1.2em
+					line-height 48px
+
+			> .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>
+		import contains from '../../common/scripts/contains';
+
+		this.isOpen = false;
+
+		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 if={ SIGNIN }>
+		<li class="home { active: page == 'home' }">
+			<a href={ CONFIG.url }>
+				<i class="fa fa-home"></i>
+				<p>%i18n:desktop.tags.mk-ui-header-nav.home%</p>
+			</a>
+		</li>
+		<li class="messaging">
+			<a onclick={ messaging }>
+				<i class="fa fa-comments"></i>
+				<p>%i18n:desktop.tags.mk-ui-header-nav.messaging%</p>
+				<i class="fa fa-circle" if={ hasUnreadMessagingMessages }></i>
+			</a>
+		</li>
+		<li class="info">
+			<a href="https://twitter.com/misskey_xyz" target="_blank">
+				<i class="fa fa-info"></i>
+				<p>%i18n:desktop.tags.mk-ui-header-nav.info%</p>
+			</a>
+		</li>
+	</ul>
+	<style>
+		: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
+
+						> i:first-child
+							margin-right 8px
+
+						> i:last-child
+							margin-left 5px
+							vertical-align super
+							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>
+		this.mixin('i');
+		this.mixin('api');
+		this.mixin('stream');
+
+		this.page = this.opts.page;
+
+		this.on('mount', () => {
+			this.stream.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
+			this.stream.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', () => {
+			this.stream.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
+			this.stream.off('unread_messaging_message', this.onUnreadMessagingMessage);
+		});
+
+		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>
+		: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>
+		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() } onclick={ toggle }>
+		<span class="username">{ I.username }<i class="fa fa-angle-down" if={ !isOpen }></i><i class="fa fa-angle-up" if={ isOpen }></i></span>
+		<img class="avatar" src={ I.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
+	</button>
+	<div class="menu" if={ isOpen }>
+		<ul>
+			<li>
+				<a href={ '/' + I.username }><i class="fa fa-user"></i>%i18n:desktop.tags.mk-ui-header-account.profile%<i class="fa fa-angle-right"></i></a>
+			</li>
+			<li onclick={ drive }>
+				<p><i class="fa fa-cloud"></i>%i18n:desktop.tags.mk-ui-header-account.drive%<i class="fa fa-angle-right"></i></p>
+			</li>
+			<li>
+				<a href="/i>mentions"><i class="fa fa-at"></i>%i18n:desktop.tags.mk-ui-header-account.mentions%<i class="fa fa-angle-right"></i></a>
+			</li>
+		</ul>
+		<ul>
+			<li onclick={ settings }>
+				<p><i class="fa fa-cog"></i>%i18n:desktop.tags.mk-ui-header-account.settings%<i class="fa fa-angle-right"></i></p>
+			</li>
+		</ul>
+		<ul>
+			<li onclick={ signout }>
+				<p><i class="fa fa-power-off"></i>%i18n:desktop.tags.mk-ui-header-account.signout%<i class="fa fa-angle-right"></i></p>
+			</li>
+		</ul>
+	</div>
+	<style>
+		: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
+
+					i
+						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
+
+							> i:first-of-type
+								margin-right 6px
+
+							> i: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>
+		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>
+		: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>
+		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.unmount()
+				});
+			}, 6000);
+		});
+	</script>
+</mk-ui-notification>

From 03f1515f694b415b55269ba14afd2bfa381523d3 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 31 Aug 2017 16:52:23 +0000
Subject: [PATCH 098/364] fix(package): update typescript to version 2.5.2

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

diff --git a/package.json b/package.json
index 43a05ae469..e137681230 100644
--- a/package.json
+++ b/package.json
@@ -146,7 +146,7 @@
     "tcp-port-used": "0.1.2",
     "textarea-caret": "3.0.2",
     "ts-node": "3.3.0",
-    "typescript": "2.4.2",
+    "typescript": "2.5.2",
     "uuid": "3.1.0",
     "vhost": "3.0.2",
     "websocket": "1.0.24",

From 6e28f5f970a6707733f59dec73d768ec7ddf080d Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 31 Aug 2017 22:14:40 +0000
Subject: [PATCH 099/364] chore(package): update chai to version 4.1.2

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

diff --git a/package.json b/package.json
index 43a05ae469..1e6586a630 100644
--- a/package.json
+++ b/package.json
@@ -64,7 +64,7 @@
     "@types/webpack": "3.0.10",
     "@types/webpack-stream": "3.2.7",
     "@types/websocket": "0.0.34",
-    "chai": "4.1.1",
+    "chai": "4.1.2",
     "chai-http": "3.0.0",
     "css-loader": "0.28.7",
     "event-stream": "3.3.4",

From c52f6803399aa47e083a5bbce9ce2c831a63c617 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sat, 2 Sep 2017 17:26:37 +0000
Subject: [PATCH 100/364] fix(package): update riot to version 3.7.0

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

diff --git a/package.json b/package.json
index 43a05ae469..5c9c902b15 100644
--- a/package.json
+++ b/package.json
@@ -137,7 +137,7 @@
     "redis": "2.8.0",
     "request": "2.81.0",
     "rimraf": "2.6.1",
-    "riot": "3.6.3",
+    "riot": "3.7.0",
     "rndstr": "1.0.0",
     "s-age": "1.1.0",
     "serve-favicon": "2.4.3",

From 825d9b9b04234a0ff40590ea89c91a92175994c8 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sun, 3 Sep 2017 13:22:07 +0000
Subject: [PATCH 101/364] fix(package): update cropperjs to version 1.0.0

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

diff --git a/package.json b/package.json
index 5c9c902b15..f48c7fd72c 100644
--- a/package.json
+++ b/package.json
@@ -103,7 +103,7 @@
     "chalk": "2.1.0",
     "compression": "1.7.0",
     "cors": "2.8.4",
-    "cropperjs": "1.0.0-rc.3",
+    "cropperjs": "1.0.0",
     "crypto": "1.0.1",
     "debug": "3.0.1",
     "deep-equal": "1.0.1",

From b8777bd58723f6e93d3e476fd4314a5e2ae0026c 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, 4 Sep 2017 02:32:06 +0900
Subject: [PATCH 102/364] Update DONORS.md

---
 DONORS.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/DONORS.md b/DONORS.md
index 21610304e0..da71c043ac 100644
--- a/DONORS.md
+++ b/DONORS.md
@@ -12,7 +12,7 @@ DONORS
 
 ---
 
-You donated, but you are not listed here? please contact to us!
+Although you donated, you are not listed here? please contact to us!
 
 If you want to donate to Misskey, please get in touch with [@syuilo][syuilo-link].
 

From 016dda27bdc9cf854c6f26309faf09f6f778f904 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, 4 Sep 2017 02:41:32 +0900
Subject: [PATCH 103/364] Update mocha.opts

---
 test/mocha.opts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/mocha.opts b/test/mocha.opts
index cf80ee74bc..907011807d 100644
--- a/test/mocha.opts
+++ b/test/mocha.opts
@@ -1 +1 @@
---timeout 5000
+--timeout 10000

From f33571f2f42cf9d5313a32195fbe147941a95f87 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 6 Sep 2017 19:41:36 +0900
Subject: [PATCH 104/364] wip

---
 package.json                |  2 +
 src/tools/ai/categorizer.ts | 89 +++++++++++++++++++++++++++++++++++++
 2 files changed, 91 insertions(+)
 create mode 100644 src/tools/ai/categorizer.ts

diff --git a/package.json b/package.json
index a2896f4c70..ae959d1b13 100644
--- a/package.json
+++ b/package.json
@@ -97,6 +97,7 @@
     "accesses": "2.5.0",
     "animejs": "2.0.2",
     "autwh": "0.0.1",
+    "bayes": "0.0.7",
     "bcryptjs": "2.4.3",
     "body-parser": "1.17.2",
     "cafy": "2.4.0",
@@ -120,6 +121,7 @@
     "is-root": "1.0.0",
     "is-url": "1.2.2",
     "js-yaml": "3.9.1",
+    "mecab-async": "^0.1.0",
     "mongodb": "2.2.31",
     "monk": "6.0.3",
     "morgan": "1.8.2",
diff --git a/src/tools/ai/categorizer.ts b/src/tools/ai/categorizer.ts
new file mode 100644
index 0000000000..f70ce1b7d4
--- /dev/null
+++ b/src/tools/ai/categorizer.ts
@@ -0,0 +1,89 @@
+import * as fs from 'fs';
+const bayes = require('bayes');
+const MeCab = require('mecab-async');
+import Post from '../../api/models/post';
+
+export default class Categorizer {
+	classifier: any;
+	categorizerDbFilePath: string;
+	mecab: any;
+
+	constructor(categorizerDbFilePath: string, mecabCommand: string = 'mecab -d /usr/share/mecab/dic/mecab-ipadic-neologd') {
+		this.categorizerDbFilePath = categorizerDbFilePath;
+
+		this.mecab = new MeCab();
+		this.mecab.command = mecabCommand;
+
+		// BIND -----------------------------------
+		this.tokenizer = this.tokenizer.bind(this);
+	}
+
+	tokenizer(text: string) {
+		return this.mecab.wakachiSync(text);
+	}
+
+	async init() {
+		try {
+			const db = fs.readFileSync(this.categorizerDbFilePath, {
+				encoding: 'utf8'
+			});
+
+			this.classifier = bayes.fromJson(db);
+			this.classifier.tokenizer = this.tokenizer;
+		} catch(e) {
+			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);
+			});
+
+			this.save();
+		}
+	}
+
+	async learn(id, category) {
+		const post = await Post.findOne({ _id: id });
+
+		Post.update({ _id: id }, {
+			$set: {
+				category: category,
+				is_category_verified: true
+			}
+		});
+
+		this.classifier.learn(post.text, category);
+
+		this.save();
+	}
+
+	async categorize(id) {
+		const post = await Post.findOne({ _id: id });
+
+		const category = this.classifier.categorize(post.text);
+
+		Post.update({ _id: id }, {
+			$set: {
+				category: category
+			}
+		});
+	}
+
+	async test(text) {
+		return this.classifier.categorize(text);
+	}
+
+	save() {
+		fs.writeFileSync(this.categorizerDbFilePath, this.classifier.toJson(), {
+			encoding: 'utf8'
+		});
+	}
+}
+

From cf7b1c0c5d86a20328dd7a44f27cff739f4126b3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 6 Sep 2017 21:29:56 +0900
Subject: [PATCH 105/364] wip

---
 .gitignore                  |   1 +
 package.json                |   3 +-
 src/config.ts               |   3 +
 src/tools/ai/categorizer.ts |  48 +++---
 src/tools/ai/naive-bayes.js | 302 ++++++++++++++++++++++++++++++++++++
 5 files changed, 334 insertions(+), 23 deletions(-)
 create mode 100644 src/tools/ai/naive-bayes.js

diff --git a/.gitignore b/.gitignore
index 42b1bde94f..2ae0f98c5e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
 /node_modules
 /built
 /uploads
+/data
 npm-debug.log
 *.pem
 run.bat
diff --git a/package.json b/package.json
index ae959d1b13..31cf7a02cd 100644
--- a/package.json
+++ b/package.json
@@ -64,6 +64,7 @@
     "@types/webpack": "3.0.10",
     "@types/webpack-stream": "3.2.7",
     "@types/websocket": "0.0.34",
+    "@types/msgpack-lite": "^0.1.5",
     "chai": "4.1.2",
     "chai-http": "3.0.0",
     "css-loader": "0.28.7",
@@ -97,7 +98,6 @@
     "accesses": "2.5.0",
     "animejs": "2.0.2",
     "autwh": "0.0.1",
-    "bayes": "0.0.7",
     "bcryptjs": "2.4.3",
     "body-parser": "1.17.2",
     "cafy": "2.4.0",
@@ -126,6 +126,7 @@
     "monk": "6.0.3",
     "morgan": "1.8.2",
     "ms": "2.0.0",
+    "msgpack-lite": "^0.1.26",
     "multer": "1.3.0",
     "nprogress": "0.2.0",
     "os-utils": "0.0.14",
diff --git a/src/config.ts b/src/config.ts
index 8f4ada5af9..f333a1f5a9 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -68,6 +68,9 @@ type Source = {
 		hook_secret: string;
 		username: string;
 	};
+	categorizer?: {
+		mecab_command?: string;
+	};
 };
 
 /**
diff --git a/src/tools/ai/categorizer.ts b/src/tools/ai/categorizer.ts
index f70ce1b7d4..c13374161d 100644
--- a/src/tools/ai/categorizer.ts
+++ b/src/tools/ai/categorizer.ts
@@ -1,36 +1,42 @@
 import * as fs from 'fs';
-const bayes = require('bayes');
+
+const bayes = require('./naive-bayes.js');
 const MeCab = require('mecab-async');
+import * as msgpack from 'msgpack-lite';
+
 import Post from '../../api/models/post';
+import config from '../../conf';
 
+/**
+ * 投稿を学習したり与えられた投稿のカテゴリを予測します
+ */
 export default class Categorizer {
-	classifier: any;
-	categorizerDbFilePath: string;
-	mecab: any;
+	private classifier: any;
+	private categorizerDbFilePath: string;
+	private mecab: any;
 
-	constructor(categorizerDbFilePath: string, mecabCommand: string = 'mecab -d /usr/share/mecab/dic/mecab-ipadic-neologd') {
-		this.categorizerDbFilePath = categorizerDbFilePath;
+	constructor() {
+		this.categorizerDbFilePath = `${__dirname}/../../../data/category`;
 
 		this.mecab = new MeCab();
-		this.mecab.command = mecabCommand;
+		if (config.categorizer.mecab_command) this.mecab.command = config.categorizer.mecab_command;
 
 		// BIND -----------------------------------
 		this.tokenizer = this.tokenizer.bind(this);
 	}
 
-	tokenizer(text: string) {
+	private tokenizer(text: string) {
 		return this.mecab.wakachiSync(text);
 	}
 
-	async init() {
+	public async init() {
 		try {
-			const db = fs.readFileSync(this.categorizerDbFilePath, {
-				encoding: 'utf8'
-			});
+			const buffer = fs.readFileSync(this.categorizerDbFilePath);
+			const db = msgpack.decode(buffer);
 
-			this.classifier = bayes.fromJson(db);
+			this.classifier = bayes.import(db);
 			this.classifier.tokenizer = this.tokenizer;
-		} catch(e) {
+		} catch (e) {
 			this.classifier = bayes({
 				tokenizer: this.tokenizer
 			});
@@ -49,7 +55,7 @@ export default class Categorizer {
 		}
 	}
 
-	async learn(id, category) {
+	public async learn(id, category) {
 		const post = await Post.findOne({ _id: id });
 
 		Post.update({ _id: id }, {
@@ -64,7 +70,7 @@ export default class Categorizer {
 		this.save();
 	}
 
-	async categorize(id) {
+	public async categorize(id) {
 		const post = await Post.findOne({ _id: id });
 
 		const category = this.classifier.categorize(post.text);
@@ -76,14 +82,12 @@ export default class Categorizer {
 		});
 	}
 
-	async test(text) {
+	public async test(text) {
 		return this.classifier.categorize(text);
 	}
 
-	save() {
-		fs.writeFileSync(this.categorizerDbFilePath, this.classifier.toJson(), {
-			encoding: 'utf8'
-		});
+	private save() {
+		const buffer = msgpack.encode(this.classifier.export());
+		fs.writeFileSync(this.categorizerDbFilePath, buffer);
 	}
 }
-
diff --git a/src/tools/ai/naive-bayes.js b/src/tools/ai/naive-bayes.js
new file mode 100644
index 0000000000..78f07153cf
--- /dev/null
+++ b/src/tools/ai/naive-bayes.js
@@ -0,0 +1,302 @@
+// 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
+}

From c6b0bf42a112f0d9afa8920d6497cc76205ecaf4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 6 Sep 2017 23:19:58 +0900
Subject: [PATCH 106/364] wip

---
 locales/en.yml                            | 12 +++
 locales/ja.yml                            | 12 +++
 src/api/endpoints.ts                      |  4 +
 src/api/endpoints/posts/categorize.ts     | 52 +++++++++++++
 src/tools/ai/categorizer.ts               | 93 -----------------------
 src/tools/ai/predict-all-post-category.ts | 57 ++++++++++++++
 src/tools/ai/predict-user-interst.ts      | 45 +++++++++++
 src/web/app/common/tags/post-menu.tag     | 23 ++++++
 8 files changed, 205 insertions(+), 93 deletions(-)
 create mode 100644 src/api/endpoints/posts/categorize.ts
 delete mode 100644 src/tools/ai/categorizer.ts
 create mode 100644 src/tools/ai/predict-all-post-category.ts
 create mode 100644 src/tools/ai/predict-user-interst.ts

diff --git a/locales/en.yml b/locales/en.yml
index d40896212b..3b87ea758d 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -22,6 +22,14 @@ common:
     confused: "Confused"
     pudding: "Pudding"
 
+  post_categories:
+    music: "Music"
+    game: "Video Game"
+    anime: "Anime"
+    it: "IT"
+    gadgets: "Gadgets"
+    photography: "Photography"
+
   input-message-here: "Enter message here"
   send: "Send"
   delete: "Delete"
@@ -80,6 +88,9 @@ common:
     mk-post-menu:
       pin: "Pin"
       pinned: "Pinned"
+      select: "Select category"
+      categorize: "Accept"
+      categorized: "Category reported. Thank you!"
 
     mk-reaction-picker:
       choose-reaction: "Pick your reaction"
@@ -375,6 +386,7 @@ mobile:
       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 b8e5cff412..13d451b6d8 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -22,6 +22,14 @@ common:
     confused: "こまこまのこまり"
     pudding: "Pudding"
 
+  post_categories:
+    music: "音楽"
+    game: "ゲーム"
+    anime: "アニメ"
+    it: "IT"
+    gadgets: "ガジェット"
+    photography: "写真"
+
   input-message-here: "ここにメッセージを入力"
   send: "送信"
   delete: "削除"
@@ -80,6 +88,9 @@ common:
     mk-post-menu:
       pin: "ピン留め"
       pinned: "ピン留めしました"
+      select: "カテゴリを選択"
+      categorize: "決定"
+      categorized: "カテゴリを報告しました。これによりMisskeyが賢くなり、投稿の自動カテゴライズに役立てられます。ご協力ありがとうございました。"
 
     mk-reaction-picker:
       choose-reaction: "リアクションを選択"
@@ -375,6 +386,7 @@ mobile:
       twitter-integration: "Twitter連携"
       signin-history: "ログイン履歴"
       api: "API"
+      link: "Misskeyリンク"
       settings: "設定"
       signout: "サインアウト"
 
diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts
index e5be68c096..97b98895b8 100644
--- a/src/api/endpoints.ts
+++ b/src/api/endpoints.ts
@@ -394,6 +394,10 @@ const endpoints: Endpoint[] = [
 		name: 'posts/trend',
 		withCredential: true
 	},
+	{
+		name: 'posts/categorize',
+		withCredential: true
+	},
 	{
 		name: 'posts/reactions',
 		withCredential: true
diff --git a/src/api/endpoints/posts/categorize.ts b/src/api/endpoints/posts/categorize.ts
new file mode 100644
index 0000000000..3530ba6bc4
--- /dev/null
+++ b/src/api/endpoints/posts/categorize.ts
@@ -0,0 +1,52 @@
+/**
+ * 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.is_pro) {
+		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 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/tools/ai/categorizer.ts b/src/tools/ai/categorizer.ts
deleted file mode 100644
index c13374161d..0000000000
--- a/src/tools/ai/categorizer.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-import * as fs from 'fs';
-
-const bayes = require('./naive-bayes.js');
-const MeCab = require('mecab-async');
-import * as msgpack from 'msgpack-lite';
-
-import Post from '../../api/models/post';
-import config from '../../conf';
-
-/**
- * 投稿を学習したり与えられた投稿のカテゴリを予測します
- */
-export default class Categorizer {
-	private classifier: any;
-	private categorizerDbFilePath: string;
-	private mecab: any;
-
-	constructor() {
-		this.categorizerDbFilePath = `${__dirname}/../../../data/category`;
-
-		this.mecab = new MeCab();
-		if (config.categorizer.mecab_command) this.mecab.command = config.categorizer.mecab_command;
-
-		// BIND -----------------------------------
-		this.tokenizer = this.tokenizer.bind(this);
-	}
-
-	private tokenizer(text: string) {
-		return this.mecab.wakachiSync(text);
-	}
-
-	public async init() {
-		try {
-			const buffer = fs.readFileSync(this.categorizerDbFilePath);
-			const db = msgpack.decode(buffer);
-
-			this.classifier = bayes.import(db);
-			this.classifier.tokenizer = this.tokenizer;
-		} catch (e) {
-			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);
-			});
-
-			this.save();
-		}
-	}
-
-	public async learn(id, category) {
-		const post = await Post.findOne({ _id: id });
-
-		Post.update({ _id: id }, {
-			$set: {
-				category: category,
-				is_category_verified: true
-			}
-		});
-
-		this.classifier.learn(post.text, category);
-
-		this.save();
-	}
-
-	public async categorize(id) {
-		const post = await Post.findOne({ _id: id });
-
-		const category = this.classifier.categorize(post.text);
-
-		Post.update({ _id: id }, {
-			$set: {
-				category: category
-			}
-		});
-	}
-
-	public async test(text) {
-		return this.classifier.categorize(text);
-	}
-
-	private save() {
-		const buffer = msgpack.encode(this.classifier.export());
-		fs.writeFileSync(this.categorizerDbFilePath, buffer);
-	}
-}
diff --git a/src/tools/ai/predict-all-post-category.ts b/src/tools/ai/predict-all-post-category.ts
new file mode 100644
index 0000000000..87e198b39b
--- /dev/null
+++ b/src/tools/ai/predict-all-post-category.ts
@@ -0,0 +1,57 @@
+const bayes = require('./naive-bayes.js');
+const MeCab = require('mecab-async');
+
+import Post from '../../api/models/post';
+import config from '../../conf';
+
+const classifier = bayes({
+	tokenizer: this.tokenizer
+});
+
+const mecab = new MeCab();
+if (config.categorizer.mecab_command) mecab.command = config.categorizer.mecab_command;
+
+// 訓練データ取得
+Post.find({
+	is_category_verified: true
+}, {
+	fields: {
+		_id: false,
+		text: true,
+		category: true
+	}
+}).then(verifiedPosts => {
+	// 学習
+	verifiedPosts.forEach(post => {
+		classifier.learn(post.text, post.category);
+	});
+
+	// 全ての(人間によって証明されていない)投稿を取得
+	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 = classifier.categorize(post.text);
+
+			Post.update({ _id: post._id }, {
+				$set: {
+					category: category
+				}
+			});
+		});
+	});
+});
diff --git a/src/tools/ai/predict-user-interst.ts b/src/tools/ai/predict-user-interst.ts
new file mode 100644
index 0000000000..99bdfa4206
--- /dev/null
+++ b/src/tools/ai/predict-user-interst.ts
@@ -0,0 +1,45 @@
+import Post from '../../api/models/post';
+import User from '../../api/models/user';
+
+export async function predictOne(id) {
+	console.log(`predict interest of ${id} ...`);
+
+	// TODO: repostなども含める
+	const recentPosts = await Post.find({
+		user_id: 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/src/web/app/common/tags/post-menu.tag b/src/web/app/common/tags/post-menu.tag
index 33895212bc..be4468a214 100644
--- a/src/web/app/common/tags/post-menu.tag
+++ b/src/web/app/common/tags/post-menu.tag
@@ -2,6 +2,18 @@
 	<div class="backdrop" ref="backdrop" onclick={ 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>
+		<div 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 onclick={ categorize }>%i18n:common.tags.mk-post-menu.categorize%</button>
+		</div>
 	</div>
 	<style>
 		$border-color = rgba(27, 31, 35, 0.15)
@@ -111,6 +123,17 @@
 			});
 		};
 
+		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.unmount();
+			});
+		};
+
 		this.close = () => {
 			this.refs.backdrop.style.pointerEvents = 'none';
 			anime({

From a1bcfd39da40844e2e63938a1df0eeea62efc950 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 6 Sep 2017 23:25:28 +0900
Subject: [PATCH 107/364] v2544

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6a86b02abb..7f75549fcc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,9 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
-unreleased
-----------
+2544 (2017/09/06)
+-----------------
+* 投稿のカテゴリに関する実験的な実装
 * l10n
 * ユーザビリティの向上
 
diff --git a/package.json b/package.json
index 31cf7a02cd..e2b7922b1b 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2520",
+  "version": "0.0.2544",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",

From ed3e188bfc1102bf6deb3a39b1a2bd0de4553490 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 6 Sep 2017 23:58:28 +0900
Subject: [PATCH 108/364] Refactoring

---
 src/tools/ai/core.ts                      | 45 +++++++++++++++++++++++
 src/tools/ai/predict-all-post-category.ts | 30 ++-------------
 2 files changed, 49 insertions(+), 26 deletions(-)
 create mode 100644 src/tools/ai/core.ts

diff --git a/src/tools/ai/core.ts b/src/tools/ai/core.ts
new file mode 100644
index 0000000000..25bb807bba
--- /dev/null
+++ b/src/tools/ai/core.ts
@@ -0,0 +1,45 @@
+const bayes = require('./naive-bayes.js');
+const MeCab = require('mecab-async');
+
+import Post from '../../api/models/post';
+import config from '../../conf';
+
+/**
+ * 投稿を学習したり与えられた投稿のカテゴリを予測します
+ */
+export default class Categorizer {
+	private classifier: any;
+	private mecab: any;
+
+	constructor() {
+		this.mecab = new MeCab();
+		if (config.categorizer.mecab_command) this.mecab.command = config.categorizer.mecab_command;
+
+		// BIND -----------------------------------
+		this.tokenizer = this.tokenizer.bind(this);
+	}
+
+	private tokenizer(text: string) {
+		return this.mecab.wakachiSync(text);
+	}
+
+	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/ai/predict-all-post-category.ts b/src/tools/ai/predict-all-post-category.ts
index 87e198b39b..058c4f99ef 100644
--- a/src/tools/ai/predict-all-post-category.ts
+++ b/src/tools/ai/predict-all-post-category.ts
@@ -1,31 +1,9 @@
-const bayes = require('./naive-bayes.js');
-const MeCab = require('mecab-async');
-
 import Post from '../../api/models/post';
-import config from '../../conf';
+import Core from './core';
 
-const classifier = bayes({
-	tokenizer: this.tokenizer
-});
-
-const mecab = new MeCab();
-if (config.categorizer.mecab_command) mecab.command = config.categorizer.mecab_command;
-
-// 訓練データ取得
-Post.find({
-	is_category_verified: true
-}, {
-	fields: {
-		_id: false,
-		text: true,
-		category: true
-	}
-}).then(verifiedPosts => {
-	// 学習
-	verifiedPosts.forEach(post => {
-		classifier.learn(post.text, post.category);
-	});
+const c = new Core();
 
+c.init().then(() => {
 	// 全ての(人間によって証明されていない)投稿を取得
 	Post.find({
 		text: {
@@ -45,7 +23,7 @@ Post.find({
 	}).then(posts => {
 		posts.forEach(post => {
 			console.log(`predicting... ${post._id}`);
-			const category = classifier.categorize(post.text);
+			const category = c.predict(post.text);
 
 			Post.update({ _id: post._id }, {
 				$set: {

From a447b876c08006111fc6b3fd16f852f29073bead Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 6 Sep 2017 15:07:35 +0000
Subject: [PATCH 109/364] chore(package): update @types/chai-http to version
 3.0.3

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

diff --git a/package.json b/package.json
index e2b7922b1b..8ab4dcdef7 100644
--- a/package.json
+++ b/package.json
@@ -24,7 +24,7 @@
     "@types/bcryptjs": "2.4.0",
     "@types/body-parser": "1.16.5",
     "@types/chai": "4.0.4",
-    "@types/chai-http": "3.0.2",
+    "@types/chai-http": "3.0.3",
     "@types/chalk": "0.4.31",
     "@types/compression": "0.0.34",
     "@types/cors": "2.8.1",

From 7587b7f0d7b5742f84d16ca2de9456c14ff0d69c Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 6 Sep 2017 15:21:50 +0000
Subject: [PATCH 110/364] chore(package): update @types/uuid to version 3.4.2

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

diff --git a/package.json b/package.json
index e2b7922b1b..c3af8e1eba 100644
--- a/package.json
+++ b/package.json
@@ -60,7 +60,7 @@
     "@types/rimraf": "2.0.0",
     "@types/riot": "3.6.0",
     "@types/serve-favicon": "2.2.28",
-    "@types/uuid": "3.4.1",
+    "@types/uuid": "3.4.2",
     "@types/webpack": "3.0.10",
     "@types/webpack-stream": "3.2.7",
     "@types/websocket": "0.0.34",

From 52671d3fc29fe5e6e97b8a97b911abc34be43830 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 6 Sep 2017 15:32:02 +0000
Subject: [PATCH 111/364] fix(package): update pug to version 2.0.0-rc.4

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

diff --git a/package.json b/package.json
index e2b7922b1b..4faa9f89e3 100644
--- a/package.json
+++ b/package.json
@@ -133,7 +133,7 @@
     "page": "1.7.1",
     "pictograph": "2.0.4",
     "prominence": "0.2.0",
-    "pug": "2.0.0-rc.3",
+    "pug": "2.0.0-rc.4",
     "ratelimiter": "3.0.3",
     "recaptcha-promise": "0.1.3",
     "reconnecting-websocket": "3.2.1",

From 7585f0854d72b521e9396469f4c2e5d3f66056d6 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 6 Sep 2017 16:24:03 +0000
Subject: [PATCH 112/364] chore(package): update webpack to version 3.5.6

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

diff --git a/package.json b/package.json
index e2b7922b1b..46625d686f 100644
--- a/package.json
+++ b/package.json
@@ -92,7 +92,7 @@
     "uglify-es": "3.0.27",
     "uglify-es-webpack-plugin": "0.10.0",
     "uglify-js": "git+https://github.com/mishoo/UglifyJS2.git#harmony",
-    "webpack": "3.5.5"
+    "webpack": "3.5.6"
   },
   "dependencies": {
     "accesses": "2.5.0",

From 47db3684a281c3b7e5b347ce6c37d82ac0adeca1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 7 Sep 2017 02:59:33 +0900
Subject: [PATCH 113/364] :+1:

---
 src/tools/ai/core.ts | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/tools/ai/core.ts b/src/tools/ai/core.ts
index 25bb807bba..5dcce26264 100644
--- a/src/tools/ai/core.ts
+++ b/src/tools/ai/core.ts
@@ -20,7 +20,13 @@ export default class Categorizer {
 	}
 
 	private tokenizer(text: string) {
-		return this.mecab.wakachiSync(text);
+		const tokens = this.mecab.parseSync(text)
+			// 名詞だけに制限
+			.filter(token => token[1] === '名詞')
+			// 取り出し
+			.map(token => token[0]);
+
+		return tokens;
 	}
 
 	public async init() {

From 469bcf1e47c4741cbfaf62ed4c8b11439c90ea70 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 6 Sep 2017 22:15:13 +0000
Subject: [PATCH 114/364] chore(package): update @types/node to version 8.0.27

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

diff --git a/package.json b/package.json
index e2b7922b1b..a46fae8ae4 100644
--- a/package.json
+++ b/package.json
@@ -53,7 +53,7 @@
     "@types/morgan": "1.7.32",
     "@types/ms": "0.7.30",
     "@types/multer": "1.3.2",
-    "@types/node": "8.0.26",
+    "@types/node": "8.0.27",
     "@types/ratelimiter": "2.1.28",
     "@types/redis": "2.6.0",
     "@types/request": "2.0.3",

From 41214073fdcfa9ef1962ce7ffed8b449fc372c3b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 7 Sep 2017 13:19:03 +0900
Subject: [PATCH 115/364] Clean up

---
 package.json | 2 --
 1 file changed, 2 deletions(-)

diff --git a/package.json b/package.json
index e2b7922b1b..2e37f647ad 100644
--- a/package.json
+++ b/package.json
@@ -64,7 +64,6 @@
     "@types/webpack": "3.0.10",
     "@types/webpack-stream": "3.2.7",
     "@types/websocket": "0.0.34",
-    "@types/msgpack-lite": "^0.1.5",
     "chai": "4.1.2",
     "chai-http": "3.0.0",
     "css-loader": "0.28.7",
@@ -126,7 +125,6 @@
     "monk": "6.0.3",
     "morgan": "1.8.2",
     "ms": "2.0.0",
-    "msgpack-lite": "^0.1.26",
     "multer": "1.3.0",
     "nprogress": "0.2.0",
     "os-utils": "0.0.14",

From c5bb7dabf92ba92015b24bb3f03c4788d6707222 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 7 Sep 2017 13:19:14 +0900
Subject: [PATCH 116/364] Add analysis script

---
 src/tools/ai/extract-user-keywords.ts | 94 +++++++++++++++++++++++++++
 1 file changed, 94 insertions(+)
 create mode 100644 src/tools/ai/extract-user-keywords.ts

diff --git a/src/tools/ai/extract-user-keywords.ts b/src/tools/ai/extract-user-keywords.ts
new file mode 100644
index 0000000000..9f21ae2e17
--- /dev/null
+++ b/src/tools/ai/extract-user-keywords.ts
@@ -0,0 +1,94 @@
+const MeCab = require('mecab-async');
+
+import Post from '../../api/models/post';
+import User from '../../api/models/user';
+import config from '../../conf';
+
+const mecab = new MeCab();
+if (config.categorizer.mecab_command) mecab.command = config.categorizer.mecab_command;
+
+function tokenize(text: string) {
+	const tokens = this.mecab.parseSync(text)
+		// キーワードのみ
+		.filter(token => token[1] == '名詞' && (token[2] == '固有名詞' || token[2] == '一般'))
+		// 取り出し
+		.map(token => token[0]);
+
+	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, () => x(cb));
+	};
+
+	x(() => {
+		console.log('complete');
+	});
+});
+
+async function extractKeywordsOne(id, cb) {
+	console.log(`extract keywords of ${id} ...`);
+
+	// Fetch recent posts
+	const recentPosts = await Post.find({
+		user_id: id,
+		text: {
+			$exists: true
+		}
+	}, {
+		sort: {
+			_id: -1
+		},
+		limit: 1000,
+		fields: {
+			_id: false,
+			text: true
+		}
+	});
+
+	// 投稿が少なかったら中断
+	if (recentPosts.length < 10) {
+		return cb();
+	}
+
+	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(' '));
+
+	// Save
+	User.update({ _id: id }, {
+		$set: {
+			keywords: topKeywords
+		}
+	}).then(() => {
+		cb();
+	});
+}

From e891b34d6160ff3e3357e75cbe065812be636982 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 7 Sep 2017 13:19:28 +0900
Subject: [PATCH 117/364] Rename

---
 src/tools/{ai => analysis}/core.ts                      | 0
 src/tools/{ai => analysis}/extract-user-keywords.ts     | 0
 src/tools/{ai => analysis}/naive-bayes.js               | 0
 src/tools/{ai => analysis}/predict-all-post-category.ts | 0
 src/tools/{ai => analysis}/predict-user-interst.ts      | 0
 5 files changed, 0 insertions(+), 0 deletions(-)
 rename src/tools/{ai => analysis}/core.ts (100%)
 rename src/tools/{ai => analysis}/extract-user-keywords.ts (100%)
 rename src/tools/{ai => analysis}/naive-bayes.js (100%)
 rename src/tools/{ai => analysis}/predict-all-post-category.ts (100%)
 rename src/tools/{ai => analysis}/predict-user-interst.ts (100%)

diff --git a/src/tools/ai/core.ts b/src/tools/analysis/core.ts
similarity index 100%
rename from src/tools/ai/core.ts
rename to src/tools/analysis/core.ts
diff --git a/src/tools/ai/extract-user-keywords.ts b/src/tools/analysis/extract-user-keywords.ts
similarity index 100%
rename from src/tools/ai/extract-user-keywords.ts
rename to src/tools/analysis/extract-user-keywords.ts
diff --git a/src/tools/ai/naive-bayes.js b/src/tools/analysis/naive-bayes.js
similarity index 100%
rename from src/tools/ai/naive-bayes.js
rename to src/tools/analysis/naive-bayes.js
diff --git a/src/tools/ai/predict-all-post-category.ts b/src/tools/analysis/predict-all-post-category.ts
similarity index 100%
rename from src/tools/ai/predict-all-post-category.ts
rename to src/tools/analysis/predict-all-post-category.ts
diff --git a/src/tools/ai/predict-user-interst.ts b/src/tools/analysis/predict-user-interst.ts
similarity index 100%
rename from src/tools/ai/predict-user-interst.ts
rename to src/tools/analysis/predict-user-interst.ts

From 4d01acffdc92d9a9c7e7aa2344c43f6d79635ff2 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, 7 Sep 2017 13:23:47 +0900
Subject: [PATCH 118/364] Create backup.md

---
 docs/backup.md | 6 ++++++
 1 file changed, 6 insertions(+)
 create mode 100644 docs/backup.md

diff --git a/docs/backup.md b/docs/backup.md
new file mode 100644
index 0000000000..155170c36f
--- /dev/null
+++ b/docs/backup.md
@@ -0,0 +1,6 @@
+In your shell:
+``` shell
+$ mongodump --archive=db-backup
+```
+
+Make sure **mongodb-tools** installed.

From 8c355b4223eb97447f71994fc50ebbaafc4584e2 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, 7 Sep 2017 13:24:32 +0900
Subject: [PATCH 119/364] Update backup.md

---
 docs/backup.md | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/docs/backup.md b/docs/backup.md
index 155170c36f..61b05764a7 100644
--- a/docs/backup.md
+++ b/docs/backup.md
@@ -1,3 +1,6 @@
+How to backup your Misskey
+==========================
+
 In your shell:
 ``` shell
 $ mongodump --archive=db-backup

From 6e8f309b5d761a01a42a89311b772db2fe0c1186 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, 7 Sep 2017 13:25:47 +0900
Subject: [PATCH 120/364] Update backup.md

---
 docs/backup.md | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/docs/backup.md b/docs/backup.md
index 61b05764a7..baf4b23621 100644
--- a/docs/backup.md
+++ b/docs/backup.md
@@ -7,3 +7,10 @@ $ mongodump --archive=db-backup
 ```
 
 Make sure **mongodb-tools** installed.
+
+Restore
+-------
+
+``` shell
+$ mongorestore --archive=db-backup
+```

From 2b3c4cf80e2c1b0f72dae28cb88f3b0621415715 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, 7 Sep 2017 13:26:15 +0900
Subject: [PATCH 121/364] Update backup.md

---
 docs/backup.md | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/docs/backup.md b/docs/backup.md
index baf4b23621..9c02d98551 100644
--- a/docs/backup.md
+++ b/docs/backup.md
@@ -1,13 +1,15 @@
 How to backup your Misskey
 ==========================
 
+Make sure **mongodb-tools** installed.
+
+--
+
 In your shell:
 ``` shell
 $ mongodump --archive=db-backup
 ```
 
-Make sure **mongodb-tools** installed.
-
 Restore
 -------
 

From ac97cb358d715a3b039ee67dd0b9826dfc43aad1 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, 7 Sep 2017 13:26:22 +0900
Subject: [PATCH 122/364] 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 9c02d98551..56054455ed 100644
--- a/docs/backup.md
+++ b/docs/backup.md
@@ -3,7 +3,7 @@ How to backup your Misskey
 
 Make sure **mongodb-tools** installed.
 
---
+---
 
 In your shell:
 ``` shell

From 8b60f7c6a26fdf1862d723164801aca3ca8f41f5 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, 7 Sep 2017 13:28:57 +0900
Subject: [PATCH 123/364] Update backup.md

---
 docs/backup.md | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/docs/backup.md b/docs/backup.md
index 56054455ed..484564b314 100644
--- a/docs/backup.md
+++ b/docs/backup.md
@@ -10,9 +10,13 @@ In your shell:
 $ mongodump --archive=db-backup
 ```
 
+For details, plese see [mongodump docs](https://docs.mongodb.com/manual/reference/program/mongodump/).
+
 Restore
 -------
 
 ``` shell
 $ mongorestore --archive=db-backup
 ```
+
+For details, please see [mongorestore docs](https://docs.mongodb.com/manual/reference/program/mongorestore/).

From 1ed468911ed8a268c38362218564050217ede07f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 7 Sep 2017 13:31:31 +0900
Subject: [PATCH 124/364] Rename

---
 src/config.ts                               | 2 +-
 src/tools/analysis/extract-user-keywords.ts | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/config.ts b/src/config.ts
index f333a1f5a9..f8facdee2e 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -68,7 +68,7 @@ type Source = {
 		hook_secret: string;
 		username: string;
 	};
-	categorizer?: {
+	analysis?: {
 		mecab_command?: string;
 	};
 };
diff --git a/src/tools/analysis/extract-user-keywords.ts b/src/tools/analysis/extract-user-keywords.ts
index 9f21ae2e17..49fc9a5c9b 100644
--- a/src/tools/analysis/extract-user-keywords.ts
+++ b/src/tools/analysis/extract-user-keywords.ts
@@ -5,7 +5,7 @@ import User from '../../api/models/user';
 import config from '../../conf';
 
 const mecab = new MeCab();
-if (config.categorizer.mecab_command) mecab.command = config.categorizer.mecab_command;
+if (config.analysis.mecab_command) mecab.command = config.analysis.mecab_command;
 
 function tokenize(text: string) {
 	const tokens = this.mecab.parseSync(text)

From 5b36fe0e3af3eb993cac0b577b2812ec3654313f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 7 Sep 2017 13:32:07 +0900
Subject: [PATCH 125/364] Fix bug

---
 src/tools/analysis/extract-user-keywords.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/tools/analysis/extract-user-keywords.ts b/src/tools/analysis/extract-user-keywords.ts
index 49fc9a5c9b..d34bb7cd6f 100644
--- a/src/tools/analysis/extract-user-keywords.ts
+++ b/src/tools/analysis/extract-user-keywords.ts
@@ -8,7 +8,7 @@ const mecab = new MeCab();
 if (config.analysis.mecab_command) mecab.command = config.analysis.mecab_command;
 
 function tokenize(text: string) {
-	const tokens = this.mecab.parseSync(text)
+	const tokens = mecab.parseSync(text)
 		// キーワードのみ
 		.filter(token => token[1] == '名詞' && (token[2] == '固有名詞' || token[2] == '一般'))
 		// 取り出し

From af0dd2622412a1e55b68f52628c71b61d5adf1aa Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 7 Sep 2017 13:33:39 +0900
Subject: [PATCH 126/364] :v:

---
 src/tools/analysis/extract-user-keywords.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/tools/analysis/extract-user-keywords.ts b/src/tools/analysis/extract-user-keywords.ts
index d34bb7cd6f..9ef7784390 100644
--- a/src/tools/analysis/extract-user-keywords.ts
+++ b/src/tools/analysis/extract-user-keywords.ts
@@ -36,7 +36,7 @@ User.find({}, {
 });
 
 async function extractKeywordsOne(id, cb) {
-	console.log(`extract keywords of ${id} ...`);
+	console.log(`extracting keywords of ${id} ...`);
 
 	// Fetch recent posts
 	const recentPosts = await Post.find({

From 1e3f93d68ee5db3739084039be57279c14d0a62b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 7 Sep 2017 14:52:07 +0900
Subject: [PATCH 127/364] :v:

---
 src/tools/analysis/core.ts                  |  4 +-
 src/tools/analysis/extract-user-keywords.ts | 59 +++++++++++---
 src/tools/analysis/mecab.js                 | 85 +++++++++++++++++++++
 3 files changed, 135 insertions(+), 13 deletions(-)
 create mode 100644 src/tools/analysis/mecab.js

diff --git a/src/tools/analysis/core.ts b/src/tools/analysis/core.ts
index 5dcce26264..20e5fa6c51 100644
--- a/src/tools/analysis/core.ts
+++ b/src/tools/analysis/core.ts
@@ -1,8 +1,7 @@
 const bayes = require('./naive-bayes.js');
-const MeCab = require('mecab-async');
 
+const MeCab = require('./mecab');
 import Post from '../../api/models/post';
-import config from '../../conf';
 
 /**
  * 投稿を学習したり与えられた投稿のカテゴリを予測します
@@ -13,7 +12,6 @@ export default class Categorizer {
 
 	constructor() {
 		this.mecab = new MeCab();
-		if (config.categorizer.mecab_command) this.mecab.command = config.categorizer.mecab_command;
 
 		// BIND -----------------------------------
 		this.tokenizer = this.tokenizer.bind(this);
diff --git a/src/tools/analysis/extract-user-keywords.ts b/src/tools/analysis/extract-user-keywords.ts
index 9ef7784390..5251a0d1d0 100644
--- a/src/tools/analysis/extract-user-keywords.ts
+++ b/src/tools/analysis/extract-user-keywords.ts
@@ -1,18 +1,56 @@
-const MeCab = require('mecab-async');
-
+const MeCab = require('./mecab');
 import Post from '../../api/models/post';
 import User from '../../api/models/user';
-import config from '../../conf';
+import parse from '../../api/common/text';
+
+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();
-if (config.analysis.mecab_command) mecab.command = config.analysis.mecab_command;
 
 function tokenize(text: string) {
-	const tokens = mecab.parseSync(text)
+	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 => token[0]);
+		.map(token => token[0].toLowerCase())
+		// ストップワード
+		.filter(word => stopwords.indexOf(word) === -1 && word.length > 1);
 
 	return tokens;
 }
@@ -36,7 +74,7 @@ User.find({}, {
 });
 
 async function extractKeywordsOne(id, cb) {
-	console.log(`extracting keywords of ${id} ...`);
+	process.stdout.write(`extracting keywords of ${id} ...`);
 
 	// Fetch recent posts
 	const recentPosts = await Post.find({
@@ -48,7 +86,7 @@ async function extractKeywordsOne(id, cb) {
 		sort: {
 			_id: -1
 		},
-		limit: 1000,
+		limit: 10000,
 		fields: {
 			_id: false,
 			text: true
@@ -56,7 +94,8 @@ async function extractKeywordsOne(id, cb) {
 	});
 
 	// 投稿が少なかったら中断
-	if (recentPosts.length < 10) {
+	if (recentPosts.length < 300) {
+		process.stdout.write(' >>> -\n');
 		return cb();
 	}
 
@@ -81,7 +120,7 @@ async function extractKeywordsOne(id, cb) {
 	// Lookup top 10 keywords
 	const topKeywords = keywordsSorted.slice(0, 10);
 
-	process.stdout.write(' >>> ' + topKeywords.join(' '));
+	process.stdout.write(' >>> ' + topKeywords.join(', ') + '\n');
 
 	// Save
 	User.update({ _id: id }, {
diff --git a/src/tools/analysis/mecab.js b/src/tools/analysis/mecab.js
new file mode 100644
index 0000000000..82f7d6d529
--- /dev/null
+++ b/src/tools/analysis/mecab.js
@@ -0,0 +1,85 @@
+// 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;

From 9a0a2fdd854ba2a0ca0ce29cbdae4a1a29b2f5ba Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 7 Sep 2017 16:15:40 +0900
Subject: [PATCH 128/364] :v:

---
 package.json                                |   1 +
 src/tools/analysis/extract-user-keywords.ts | 135 +++++++++++---------
 2 files changed, 79 insertions(+), 57 deletions(-)

diff --git a/package.json b/package.json
index 2e37f647ad..726956140d 100644
--- a/package.json
+++ b/package.json
@@ -121,6 +121,7 @@
     "is-url": "1.2.2",
     "js-yaml": "3.9.1",
     "mecab-async": "^0.1.0",
+    "moji": "^0.5.1",
     "mongodb": "2.2.31",
     "monk": "6.0.3",
     "morgan": "1.8.2",
diff --git a/src/tools/analysis/extract-user-keywords.ts b/src/tools/analysis/extract-user-keywords.ts
index 5251a0d1d0..38a9073f7b 100644
--- a/src/tools/analysis/extract-user-keywords.ts
+++ b/src/tools/analysis/extract-user-keywords.ts
@@ -1,8 +1,12 @@
+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';
 
+process.on('unhandledRejection', console.dir);
+
 const stopwords = [
 	'ー',
 
@@ -16,7 +20,8 @@ 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',
@@ -47,10 +52,16 @@ function tokenize(text: string) {
 	const tokens = mecab.parseSync(plain)
 		// キーワードのみ
 		.filter(token => token[1] == '名詞' && (token[2] == '固有名詞' || token[2] == '一般'))
-		// 取り出し
-		.map(token => token[0].toLowerCase())
-		// ストップワード
-		.filter(word => stopwords.indexOf(word) === -1 && word.length > 1);
+		// 取り出し(&整形(全角を半角にしたり大文字を小文字で統一したり))
+		.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;
 }
@@ -65,7 +76,13 @@ User.find({}, {
 
 	const x = cb => {
 		if (++i == users.length) return cb();
-		extractKeywordsOne(users[i]._id, () => x(cb));
+		extractKeywordsOne(users[i]._id).then(() => x(cb), err => {
+			console.error(err);
+			setTimeout(() => {
+				i--;
+				x(cb);
+			}, 1000);
+		});
 	};
 
 	x(() => {
@@ -73,61 +90,65 @@ User.find({}, {
 	});
 });
 
-async function extractKeywordsOne(id, cb) {
-	process.stdout.write(`extracting keywords of ${id} ...`);
+function extractKeywordsOne(id) {
+	return new Promise(async (resolve, reject) => {
+		process.stdout.write(`extracting keywords of ${id} ...`);
 
-	// Fetch recent posts
-	const recentPosts = await Post.find({
-		user_id: id,
-		text: {
-			$exists: true
-		}
-	}, {
-		sort: {
-			_id: -1
-		},
-		limit: 10000,
-		fields: {
-			_id: false,
-			text: true
-		}
-	});
-
-	// 投稿が少なかったら中断
-	if (recentPosts.length < 300) {
-		process.stdout.write(' >>> -\n');
-		return cb();
-	}
-
-	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;
+		// Fetch recent posts
+		const recentPosts = await Post.find({
+			user_id: id,
+			text: {
+				$exists: true
+			}
+		}, {
+			sort: {
+				_id: -1
+			},
+			limit: 10000,
+			fields: {
+				_id: false,
+				text: true
 			}
 		});
-	});
 
-	// 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
+		// 投稿が少なかったら中断
+		if (recentPosts.length < 300) {
+			process.stdout.write(' >>> -\n');
+			return resolve();
 		}
-	}).then(() => {
-		cb();
+
+		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);
+		});
 	});
 }

From 0f6b78df5804a41e4616383e7131605355411ca7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 7 Sep 2017 18:15:24 +0900
Subject: [PATCH 129/364] :v:

---
 src/tools/analysis/extract-user-keywords.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/tools/analysis/extract-user-keywords.ts b/src/tools/analysis/extract-user-keywords.ts
index 38a9073f7b..b99ca93211 100644
--- a/src/tools/analysis/extract-user-keywords.ts
+++ b/src/tools/analysis/extract-user-keywords.ts
@@ -21,7 +21,7 @@ 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',

From 54b007f1ac4c7f564cf677b87a2efe2474723788 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 7 Sep 2017 18:15:53 +0900
Subject: [PATCH 130/364] =?UTF-8?q?#768=E3=81=A8=E3=81=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 locales/en.yml                   |  4 ++-
 locales/ja.yml                   |  5 ++--
 src/web/app/mobile/tags/user.tag | 47 ++++++++++++++++++++++++++++++--
 3 files changed, 50 insertions(+), 6 deletions(-)

diff --git a/locales/en.yml b/locales/en.yml
index 3b87ea758d..0af748aa69 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -488,14 +488,16 @@ mobile:
       follows-you: "Follows you"
       following: "Following"
       followers: "Followers"
+      posts: "Posts"
       overview: "Overview"
-      posts: "Timeline"
+      timeline: "Timeline"
       media: "Media"
 
     mk-user-overview:
       recent-posts: "Recent posts"
       images: "Images"
       activity: "Activity"
+      keywords: "Keywords"
       followers-you-know: "Followers you know"
       last-used-at: "Latest used at"
 
diff --git a/locales/ja.yml b/locales/ja.yml
index 13d451b6d8..15f9e9db90 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -488,15 +488,16 @@ mobile:
       follows-you: "フォローされています"
       following: "フォロー"
       followers: "フォロワー"
+      posts: "投稿"
       overview: "概要"
-      posts: "タイムライン"
-      posts-count: "ポスト"
+      timeline: "タイムライン"
       media: "メディア"
 
     mk-user-overview:
       recent-posts: "最近の投稿"
       images: "画像"
       activity: "アクティビティ"
+      keywords: "キーワード"
       followers-you-know: "知り合いのフォロワー"
       last-used-at: "最終ログイン"
 
diff --git a/src/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag
index ea431dcc53..06d4da9d3f 100644
--- a/src/web/app/mobile/tags/user.tag
+++ b/src/web/app/mobile/tags/user.tag
@@ -26,7 +26,7 @@
 				<div class="status">
 				  <a>
 				    <b>{ user.posts_count }</b>
-						<i>%i18n:mobile.tags.mk-user.posts-count%</i>
+						<i>%i18n:mobile.tags.mk-user.posts%</i>
 					</a>
 					<a href="{ user.username }/following">
 						<b>{ user.following_count }</b>
@@ -40,7 +40,7 @@
 			</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.posts%</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>
 			</nav>
 		</header>
@@ -143,7 +143,7 @@
 							> a
 								color #657786
 
-								&:first-child
+								&:not(:last-child)
 									margin-right 16px
 
 								> b
@@ -234,6 +234,12 @@
 			<mk-user-overview-activity-chart user={ user }/>
 		</div>
 	</section>
+	<section class="keywords">
+		<h2><i class="fa fa-comment-o"></i>%i18n:mobile.tags.mk-user-overview.keywords%</h2>
+		<div>
+			<mk-user-overview-keywords user={ user }/>
+		</div>
+	</section>
 	<section class="followers-you-know" if={ SIGNIN && I.id !== user.id }>
 		<h2><i class="fa fa-users"></i>%i18n:mobile.tags.mk-user-overview.followers-you-know%</h2>
 		<div>
@@ -539,6 +545,41 @@
 	</script>
 </mk-user-overview-activity-chart>
 
+
+<mk-user-overview-keywords>
+	<div if={ user.keywords != null && user.keywords.length > 1 }>
+		<virtual each={ keyword in user.keywords }>
+			<a>{ keyword }</a>
+		</virtual>
+	</div>
+	<p class="empty" if={ !initializing && users.length == 0 }>%i18n:mobile.tags.mk-user-overview-followers-you-know.no-users%</p>
+	<style>
+		: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>
+		this.user = this.opts.user;
+	</script>
+</mk-user-overview-keywords>
+
 <mk-user-overview-followers-you-know>
 	<p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:mobile.tags.mk-user-overview-followers-you-know.loading%<mk-ellipsis/></p>
 	<div if={ !initializing && users.length > 0 }>

From 98411aa64cf278d77b6ffe5eba19011a208212d8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 7 Sep 2017 18:16:38 +0900
Subject: [PATCH 131/364] Update CHANGELOG

---
 CHANGELOG.md | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7f75549fcc..c19b889325 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+unreleased
+----------
+* New: 投稿することの多いキーワードをユーザーページに表示する (#768)
+
 2544 (2017/09/06)
 -----------------
 * 投稿のカテゴリに関する実験的な実装

From 24983b6269f4ac07a4dec7e8812e5d726f7d1c70 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 7 Sep 2017 18:21:15 +0900
Subject: [PATCH 132/364] Fix

---
 locales/en.yml                   | 3 +++
 locales/ja.yml                   | 3 +++
 src/web/app/mobile/tags/user.tag | 2 +-
 3 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/locales/en.yml b/locales/en.yml
index 0af748aa69..05b4663c16 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -509,6 +509,9 @@ mobile:
       loading: "Loading"
       no-photos: "No photos"
 
+    mk-user-overview-keywords:
+      no-keywords: "No keywords"
+
     mk-user-overview-followers-you-know:
       loading: "Loading"
       no-users: "No users"
diff --git a/locales/ja.yml b/locales/ja.yml
index 15f9e9db90..86da082723 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -509,6 +509,9 @@ mobile:
       loading: "読み込み中"
       no-photos: "写真はありません"
 
+    mk-user-overview-keywords:
+      no-keywords: "キーワードはありません(十分な数の投稿をしていない可能性があります)"
+
     mk-user-overview-followers-you-know:
       loading: "読み込み中"
       no-users: "知り合いのユーザーはいません"
diff --git a/src/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag
index 06d4da9d3f..928e0188a9 100644
--- a/src/web/app/mobile/tags/user.tag
+++ b/src/web/app/mobile/tags/user.tag
@@ -552,7 +552,7 @@
 			<a>{ keyword }</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" if={ user.keywords == null || user.keywords.length == 0 }>%i18n:mobile.tags.mk-user-overview-keywords.no-keywords%</p>
 	<style>
 		:scope
 			display block

From bc2dbb8df43d59e37f59b9700f23ae2e547af69b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 7 Sep 2017 18:29:26 +0900
Subject: [PATCH 133/364] v2566

---
 CHANGELOG.md                     | 6 ++++--
 README.md                        | 2 +-
 package.json                     | 2 +-
 src/web/app/mobile/tags/user.tag | 1 -
 4 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c19b889325..fc7e2c7db8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,9 +2,11 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
-unreleased
-----------
+2566 (2017/09/07)
+-----------------
 * New: 投稿することの多いキーワードをユーザーページに表示する (#768)
+* l10n
+* デザインの修正
 
 2544 (2017/09/06)
 -----------------
diff --git a/README.md b/README.md
index 7d256993ee..2e05298e1d 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ Key features
 * Automatically updated timeline
 * Private messages
 * Free 1GB storage for each all users
-* Mobile device support (smartphone, tablet, etc)
+* Machine learning
 * Web API for third-party applications
 * No ads
 
diff --git a/package.json b/package.json
index 726956140d..75e674afd6 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2544",
+  "version": "0.0.2566",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",
diff --git a/src/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag
index 928e0188a9..ca777b8fd1 100644
--- a/src/web/app/mobile/tags/user.tag
+++ b/src/web/app/mobile/tags/user.tag
@@ -545,7 +545,6 @@
 	</script>
 </mk-user-overview-activity-chart>
 
-
 <mk-user-overview-keywords>
 	<div if={ user.keywords != null && user.keywords.length > 1 }>
 		<virtual each={ keyword in user.keywords }>

From bc23ec0a95779d27eb62e74e536a18795bbad077 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 7 Sep 2017 20:22:36 +0900
Subject: [PATCH 134/364] wip dark

---
 src/web/app/desktop/style.styl | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/web/app/desktop/style.styl b/src/web/app/desktop/style.styl
index fa50f6ce31..88adb68b2b 100644
--- a/src/web/app/desktop/style.styl
+++ b/src/web/app/desktop/style.styl
@@ -39,7 +39,8 @@
 		background rgba(0, 0, 0, 0.2)
 
 html
-	background #fdfdfd
+	//background #2f3e42
+	background #313a42
 
 	// ↓ workaround of https://github.com/riot/riot/issues/2134
 	&[data-page='entrance']

From 3feeaccf5955ebf17c26ccc06f32bb39624b89f6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 8 Sep 2017 04:13:01 +0900
Subject: [PATCH 135/364] Add type definition

---
 src/api/endpoints/posts/create.ts |  4 ++--
 src/api/models/post.ts            | 14 +++++++++++
 src/api/models/user.ts            | 40 +++++++++++++++++++++++++++++--
 src/utils/type.ts                 |  3 +++
 4 files changed, 57 insertions(+), 4 deletions(-)
 create mode 100644 src/utils/type.ts

diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index eb979402c4..805dba7f83 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -6,7 +6,7 @@ import deepEqual = require('deep-equal');
 import parse from '../../common/text';
 import Post from '../../models/post';
 import { isValidText } from '../../models/post';
-import User from '../../models/user';
+import { default as User, IUser } from '../../models/user';
 import Following from '../../models/following';
 import DriveFile from '../../models/drive-file';
 import Watching from '../../models/post-watching';
@@ -24,7 +24,7 @@ import config from '../../../conf';
  * @param {any} app
  * @return {Promise<any>}
  */
-module.exports = (params, user, app) => new Promise(async (res, rej) => {
+module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 	// Get 'text' parameter
 	const [text, textErr] = $(params.text).optional.string().pipe(isValidText).$;
 	if (textErr) return rej('invalid text');
diff --git a/src/api/models/post.ts b/src/api/models/post.ts
index baab63f991..8b9f7f5ef6 100644
--- a/src/api/models/post.ts
+++ b/src/api/models/post.ts
@@ -1,3 +1,5 @@
+import * as mongo from 'mongodb';
+
 import db from '../../db/mongodb';
 
 export default db.get('posts') as any; // fuck type definition
@@ -5,3 +7,15 @@ export default db.get('posts') as any; // fuck type definition
 export function isValidText(text: string): boolean {
 	return text.length <= 1000 && text.trim() != '';
 }
+
+export type IPost = {
+	_id: mongo.ObjectID;
+	created_at: Date;
+	media_ids: mongo.ObjectID[];
+	reply_to_id: mongo.ObjectID;
+	repost_id: mongo.ObjectID;
+	poll: {}; // todo
+	text: string;
+	user_id: mongo.ObjectID;
+	app_id: mongo.ObjectID;
+};
diff --git a/src/api/models/user.ts b/src/api/models/user.ts
index 9f8cf0161d..1591b339bc 100644
--- a/src/api/models/user.ts
+++ b/src/api/models/user.ts
@@ -1,4 +1,7 @@
+import * as mongo from 'mongodb';
+
 import db from '../../db/mongodb';
+import { IPost } from './post';
 
 const collection = db.get('users');
 
@@ -31,6 +34,39 @@ export function isValidBirthday(birthday: string): boolean {
 	return typeof birthday == 'string' && /^([0-9]{4})\-([0-9]{2})-([0-9]{2})$/.test(birthday);
 }
 
-export interface IUser {
+export type IUser = {
+	_id: mongo.ObjectID;
+	created_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;
+	};
+	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_pro: boolean;
+	is_suspended: boolean;
+	keywords: string[];
+};
diff --git a/src/utils/type.ts b/src/utils/type.ts
new file mode 100644
index 0000000000..ba6ea0be77
--- /dev/null
+++ b/src/utils/type.ts
@@ -0,0 +1,3 @@
+// https://github.com/Microsoft/TypeScript/issues/12215
+export type Diff<T extends string, U extends string> = ({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T];
+export type Omit<T, K extends keyof T> = { [P in Diff<keyof T, K>]: T[P] };

From 69ab594ac73a8b977152c67d72ff6eabad6e624c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 8 Sep 2017 17:24:11 +0900
Subject: [PATCH 136/364] Refactoring

---
 src/api/serializers/post.ts | 38 ++++++++++++++++++++-------------
 src/api/serializers/user.ts | 42 ++++++++++++++++++-------------------
 2 files changed, 45 insertions(+), 35 deletions(-)

diff --git a/src/api/serializers/post.ts b/src/api/serializers/post.ts
index 13773bda9e..86016d6696 100644
--- a/src/api/serializers/post.ts
+++ b/src/api/serializers/post.ts
@@ -3,8 +3,9 @@
  */
 import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
-import Post from '../models/post';
+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 serializeUser from './user';
@@ -14,14 +15,14 @@ import parse from '../common/text';
 /**
  * Serialize a post
  *
- * @param {any} post
- * @param {any} me?
- * @param {any} options?
- * @return {Promise<any>}
+ * @param post target
+ * @param me? serializee
+ * @param options? serialize options
+ * @return response
  */
 const self = (
-	post: any,
-	me?: any,
+	post: string | mongo.ObjectID | IPost,
+	me?: string | mongo.ObjectID | IUser,
 	options?: {
 		detail: boolean
 	}
@@ -30,6 +31,15 @@ const self = (
 		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
@@ -59,7 +69,7 @@ const self = (
 	}
 
 	// Populate user
-	_post.user = await serializeUser(_post.user_id, me);
+	_post.user = await serializeUser(_post.user_id, meId);
 
 	// Populate app
 	if (_post.app_id) {
@@ -109,23 +119,23 @@ const self = (
 
 		if (_post.reply_to_id) {
 			// Populate reply to post
-			_post.reply_to = await self(_post.reply_to_id, me, {
+			_post.reply_to = await self(_post.reply_to_id, meId, {
 				detail: false
 			});
 		}
 
 		if (_post.repost_id) {
 			// Populate repost
-			_post.repost = await self(_post.repost_id, me, {
+			_post.repost = await self(_post.repost_id, meId, {
 				detail: _post.text == null
 			});
 		}
 
 		// Poll
-		if (me && _post.poll) {
+		if (meId && _post.poll) {
 			const vote = await Vote
 				.findOne({
-					user_id: me._id,
+					user_id: meId,
 					post_id: id
 				});
 
@@ -135,10 +145,10 @@ const self = (
 		}
 
 		// Fetch my reaction
-		if (me) {
+		if (meId) {
 			const reaction = await Reaction
 				.findOne({
-					user_id: me._id,
+					user_id: meId,
 					post_id: id,
 					deleted_at: { $exists: false }
 				});
diff --git a/src/api/serializers/user.ts b/src/api/serializers/user.ts
index c9189d9034..57599fe85c 100644
--- a/src/api/serializers/user.ts
+++ b/src/api/serializers/user.ts
@@ -3,7 +3,7 @@
  */
 import * as mongo from 'mongodb';
 import deepcopy = require('deepcopy');
-import User from '../models/user';
+import { default as User, IUser } from '../models/user';
 import serializePost from './post';
 import Following from '../models/following';
 import getFriends from '../common/get-friends';
@@ -12,14 +12,14 @@ import config from '../../conf';
 /**
  * Serialize a user
  *
- * @param {any} user
- * @param {any} me?
- * @param {any} options?
- * @return {Promise<any>}
+ * @param user target
+ * @param me? serializee
+ * @param options? serialize options
+ * @return response
  */
 export default (
-	user: any,
-	me?: any,
+	user: string | mongo.ObjectID | IUser,
+	me?: string | mongo.ObjectID | IUser,
 	options?: {
 		detail?: boolean,
 		includeSecrets?: boolean
@@ -54,13 +54,13 @@ export default (
 	}
 
 	// Me
-	if (me && !mongo.ObjectID.prototype.isPrototypeOf(me)) {
-		if (typeof me === 'string') {
-			me = new mongo.ObjectID(me);
-		} else {
-			me = me._id;
-		}
-	}
+	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;
@@ -92,17 +92,17 @@ export default (
 		? `${config.drive_url}/${_user.banner_id}`
 		: null;
 
-	if (!me || !me.equals(_user.id) || !opts.detail) {
+	if (!meId || !meId.equals(_user.id) || !opts.detail) {
 		delete _user.avatar_id;
 		delete _user.banner_id;
 
 		delete _user.drive_capacity;
 	}
 
-	if (me && !me.equals(_user.id)) {
+	if (meId && !meId.equals(_user.id)) {
 		// If the user is following
 		const follow = await Following.findOne({
-			follower_id: me,
+			follower_id: meId,
 			followee_id: _user.id,
 			deleted_at: { $exists: false }
 		});
@@ -111,7 +111,7 @@ export default (
 		// If the user is followed
 		const follow2 = await Following.findOne({
 			follower_id: _user.id,
-			followee_id: me,
+			followee_id: meId,
 			deleted_at: { $exists: false }
 		});
 		_user.is_followed = follow2 !== null;
@@ -119,13 +119,13 @@ export default (
 
 	if (opts.detail) {
 		if (_user.pinned_post_id) {
-			_user.pinned_post = await serializePost(_user.pinned_post_id, me, {
+			_user.pinned_post = await serializePost(_user.pinned_post_id, meId, {
 				detail: true
 			});
 		}
 
-		if (me && !me.equals(_user.id)) {
-			const myFollowingIds = await getFriends(me);
+		if (meId && !meId.equals(_user.id)) {
+			const myFollowingIds = await getFriends(meId);
 
 			// Get following you know count
 			const followingYouKnowCount = await Following.count({

From e1aea70154545bcf398b138e5c651c240ee036c8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 8 Sep 2017 20:49:53 +0900
Subject: [PATCH 137/364] Refactoring

---
 src/api/serializers/post.ts | 17 ++++++++++-------
 1 file changed, 10 insertions(+), 7 deletions(-)

diff --git a/src/api/serializers/post.ts b/src/api/serializers/post.ts
index 86016d6696..df917a8595 100644
--- a/src/api/serializers/post.ts
+++ b/src/api/serializers/post.ts
@@ -33,12 +33,12 @@ const self = (
 
 	// 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;
+		? mongo.ObjectID.prototype.isPrototypeOf(me)
+			? me as mongo.ObjectID
+			: typeof me === 'string'
+				? new mongo.ObjectID(me)
+				: (me as IUser)._id
+		: null;
 
 	let _post: any;
 
@@ -140,7 +140,10 @@ const self = (
 				});
 
 			if (vote != null) {
-				_post.poll.choices.filter(c => c.id == vote.choice)[0].is_voted = true;
+				const myChoice = _post.poll.choices
+					.filter(c => c.id == vote.choice)[0];
+
+				myChoice.is_voted = true;
 			}
 		}
 

From 5c37b9cef307bf795c4bc49dc7ab21de0c8ba97b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 8 Sep 2017 22:10:25 +0900
Subject: [PATCH 138/364] Implement #771

---
 CHANGELOG.md                               |   4 +
 locales/en.yml                             |   4 +
 locales/ja.yml                             |   4 +
 src/tools/analysis/extract-user-domains.ts | 120 +++++++++++++++++++++
 src/web/app/mobile/tags/user.tag           |  40 +++++++
 5 files changed, 172 insertions(+)
 create mode 100644 src/tools/analysis/extract-user-domains.ts

diff --git a/CHANGELOG.md b/CHANGELOG.md
index fc7e2c7db8..b071274d36 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+unreleased
+----------
+* New: ユーザーページによく使うドメインを表示 (#771)
+
 2566 (2017/09/07)
 -----------------
 * New: 投稿することの多いキーワードをユーザーページに表示する (#768)
diff --git a/locales/en.yml b/locales/en.yml
index 05b4663c16..fd07be2aed 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -498,6 +498,7 @@ mobile:
       images: "Images"
       activity: "Activity"
       keywords: "Keywords"
+      domains: "Domains"
       followers-you-know: "Followers you know"
       last-used-at: "Latest used at"
 
@@ -512,6 +513,9 @@ mobile:
     mk-user-overview-keywords:
       no-keywords: "No keywords"
 
+    mk-user-overview-domains:
+      no-domains: "No domains"
+
     mk-user-overview-followers-you-know:
       loading: "Loading"
       no-users: "No users"
diff --git a/locales/ja.yml b/locales/ja.yml
index 86da082723..832390f5fe 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -498,6 +498,7 @@ mobile:
       images: "画像"
       activity: "アクティビティ"
       keywords: "キーワード"
+      domains: "頻出ドメイン"
       followers-you-know: "知り合いのフォロワー"
       last-used-at: "最終ログイン"
 
@@ -512,6 +513,9 @@ mobile:
     mk-user-overview-keywords:
       no-keywords: "キーワードはありません(十分な数の投稿をしていない可能性があります)"
 
+    mk-user-overview-domains:
+      no-domains: "よく表れるドメインは検出されませんでした"
+
     mk-user-overview-followers-you-know:
       loading: "読み込み中"
       no-users: "知り合いのユーザーはいません"
diff --git a/src/tools/analysis/extract-user-domains.ts b/src/tools/analysis/extract-user-domains.ts
new file mode 100644
index 0000000000..bc120f5c17
--- /dev/null
+++ b/src/tools/analysis/extract-user-domains.ts
@@ -0,0 +1,120 @@
+import * as URL from 'url';
+
+import Post from '../../api/models/post';
+import User from '../../api/models/user';
+import parse from '../../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({
+			user_id: 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/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag
index ca777b8fd1..a323559218 100644
--- a/src/web/app/mobile/tags/user.tag
+++ b/src/web/app/mobile/tags/user.tag
@@ -240,6 +240,12 @@
 			<mk-user-overview-keywords user={ user }/>
 		</div>
 	</section>
+	<section class="domains">
+		<h2><i class="fa fa-globe"></i>%i18n:mobile.tags.mk-user-overview.domains%</h2>
+		<div>
+			<mk-user-overview-domains user={ user }/>
+		</div>
+	</section>
 	<section class="followers-you-know" if={ SIGNIN && I.id !== user.id }>
 		<h2><i class="fa fa-users"></i>%i18n:mobile.tags.mk-user-overview.followers-you-know%</h2>
 		<div>
@@ -579,6 +585,40 @@
 	</script>
 </mk-user-overview-keywords>
 
+<mk-user-overview-domains>
+	<div 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>
+	<style>
+		: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>
+		this.user = this.opts.user;
+	</script>
+</mk-user-overview-domains>
+
 <mk-user-overview-followers-you-know>
 	<p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:mobile.tags.mk-user-overview-followers-you-know.loading%<mk-ellipsis/></p>
 	<div if={ !initializing && users.length > 0 }>

From 13a568889cabd36539b6205acbb0f9ce42b6419b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 8 Sep 2017 23:29:33 +0900
Subject: [PATCH 139/364] Implement #770

---
 CHANGELOG.md                                  |  1 +
 locales/en.yml                                |  5 +
 locales/ja.yml                                |  5 +
 src/api/endpoints.ts                          |  3 +
 .../users/get_frequently_replied_users.ts     | 96 +++++++++++++++++++
 src/web/app/mobile/tags/index.js              |  1 +
 src/web/app/mobile/tags/init-following.tag    | 54 +----------
 src/web/app/mobile/tags/user-card.tag         | 55 +++++++++++
 src/web/app/mobile/tags/user.tag              | 58 +++++++++++
 9 files changed, 228 insertions(+), 50 deletions(-)
 create mode 100644 src/api/endpoints/users/get_frequently_replied_users.ts
 create mode 100644 src/web/app/mobile/tags/user-card.tag

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b071274d36..cf3d82f701 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ ChangeLog (Release Notes)
 unreleased
 ----------
 * New: ユーザーページによく使うドメインを表示 (#771)
+* New: よくリプライするユーザーをユーザーページに表示 (#770)
 
 2566 (2017/09/07)
 -----------------
diff --git a/locales/en.yml b/locales/en.yml
index fd07be2aed..a7dd3aea23 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -499,6 +499,7 @@ mobile:
       activity: "Activity"
       keywords: "Keywords"
       domains: "Domains"
+      frequently-replied-users: "Frequently talking users"
       followers-you-know: "Followers you know"
       last-used-at: "Latest used at"
 
@@ -516,6 +517,10 @@ mobile:
     mk-user-overview-domains:
       no-domains: "No domains"
 
+    mk-user-overview-frequently-replied-users:
+      loading: "Loading"
+      no-users: "No users"
+
     mk-user-overview-followers-you-know:
       loading: "Loading"
       no-users: "No users"
diff --git a/locales/ja.yml b/locales/ja.yml
index 832390f5fe..451650ef76 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -499,6 +499,7 @@ mobile:
       activity: "アクティビティ"
       keywords: "キーワード"
       domains: "頻出ドメイン"
+      frequently-replied-users: "よく会話するユーザー"
       followers-you-know: "知り合いのフォロワー"
       last-used-at: "最終ログイン"
 
@@ -516,6 +517,10 @@ mobile:
     mk-user-overview-domains:
       no-domains: "よく表れるドメインは検出されませんでした"
 
+    mk-user-overview-frequently-replied-users:
+      loading: "読み込み中"
+      no-users: "よく会話するユーザーはいません"
+
     mk-user-overview-followers-you-know:
       loading: "読み込み中"
       no-users: "知り合いのユーザーはいません"
diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts
index 97b98895b8..f05762340c 100644
--- a/src/api/endpoints.ts
+++ b/src/api/endpoints.ts
@@ -326,6 +326,9 @@ const endpoints: Endpoint[] = [
 		withCredential: true,
 		kind: 'account-read'
 	},
+	{
+		name: 'users/get_frequently_replied_users'
+	},
 
 	{
 		name: 'following/create',
diff --git a/src/api/endpoints/users/get_frequently_replied_users.ts b/src/api/endpoints/users/get_frequently_replied_users.ts
new file mode 100644
index 0000000000..2e0e2e40a7
--- /dev/null
+++ b/src/api/endpoints/users/get_frequently_replied_users.ts
@@ -0,0 +1,96 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import Post from '../../models/post';
+import User from '../../models/user';
+import serialize from '../../serializers/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');
+
+	// Lookup user
+	const user = await User.findOne({
+		_id: userId
+	}, {
+		fields: {
+			_id: true
+		}
+	});
+
+	if (user === null) {
+		return rej('user not found');
+	}
+
+	// Fetch recent posts
+	const recentPosts = await Post.find({
+		user_id: user._id,
+		reply_to_id: {
+			$exists: true,
+			$ne: null
+		}
+	}, {
+		sort: {
+			_id: -1
+		},
+		limit: 1000,
+		fields: {
+			_id: false,
+			reply_to_id: true
+		}
+	});
+
+	// 投稿が少なかったら中断
+	if (recentPosts.length === 0) {
+		return res([]);
+	}
+
+	const replyTargetPosts = await Post.find({
+		_id: {
+			$in: recentPosts.map(p => p.reply_to_id)
+		},
+		user_id: {
+			$ne: user._id
+		}
+	}, {
+		fields: {
+			_id: false,
+			user_id: true
+		}
+	});
+
+	const repliedUsers = {};
+
+	// Extract replies from recent posts
+	replyTargetPosts.forEach(post => {
+		const userId = post.user_id.toString();
+		if (repliedUsers[userId]) {
+			repliedUsers[userId]++;
+		} else {
+			repliedUsers[userId] = 1;
+		}
+	});
+
+	// Calc peak
+	let peak = 0;
+	Object.keys(repliedUsers).forEach(user => {
+		if (repliedUsers[user] > peak) peak = repliedUsers[user];
+	});
+
+	// Sort replies by frequency
+	const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]);
+
+	// Lookup top 10 replies
+	const topRepliedUsers = repliedUsersSorted.slice(0, 10);
+
+	// Make replies object (includes weights)
+	const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({
+		user: await serialize(user, me, { detail: true }),
+		weight: repliedUsers[user] / peak
+	})));
+
+	// Response
+	res(repliesObj);
+});
diff --git a/src/web/app/mobile/tags/index.js b/src/web/app/mobile/tags/index.js
index 6f985a91fd..c5aafd20ba 100644
--- a/src/web/app/mobile/tags/index.js
+++ b/src/web/app/mobile/tags/index.js
@@ -49,3 +49,4 @@ require('./users-list.tag');
 require('./user-following.tag');
 require('./user-followers.tag');
 require('./init-following.tag');
+require('./user-card.tag');
diff --git a/src/web/app/mobile/tags/init-following.tag b/src/web/app/mobile/tags/init-following.tag
index d0b63ff5db..6357f86a29 100644
--- a/src/web/app/mobile/tags/init-following.tag
+++ b/src/web/app/mobile/tags/init-following.tag
@@ -1,16 +1,9 @@
 <mk-init-following>
 	<p class="title">気になるユーザーをフォロー:</p>
 	<div class="users" if={ !fetching && users.length > 0 }>
-		<div class="user" each={ users }>
-			<header style={ banner_url ? 'background-image: url(' + banner_url + '?thumbnail&size=1024)' : '' }>
-				<a href={ '/' + username }>
-					<img src={ avatar_url + '?thumbnail&size=200' } alt="avatar"/>
-				</a>
-			</header>
-			<a class="name" href={ '/' + username } target="_blank">{ name }</a>
-			<p class="username">@{ username }</p>
-			<mk-follow-button user={ this }/>
-		</div>
+		<virtual each={ users }>
+			<mk-user-card user={ this } />
+		</virtual>
 	</div>
 	<p class="empty" if={ !fetching && users.length == 0 }>おすすめのユーザーは見つかりませんでした。</p>
 	<p class="fetching" if={ fetching }><i class="fa fa-spinner fa-pulse fa-fw"></i>読み込んでいます<mk-ellipsis/></p>
@@ -37,49 +30,10 @@
 				padding 16px
 				background #eee
 
-				> .user
-					display inline-block
-					width 200px
-					text-align center
-					border-radius 8px
-					background #fff
-
+				> mk-user-card
 					&:not(:last-child)
 						margin-right 16px
 
-					> 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
-
 			> .empty
 				margin 0
 				padding 16px
diff --git a/src/web/app/mobile/tags/user-card.tag b/src/web/app/mobile/tags/user-card.tag
new file mode 100644
index 0000000000..d0c79698c5
--- /dev/null
+++ b/src/web/app/mobile/tags/user-card.tag
@@ -0,0 +1,55 @@
+<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>
+		: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>
+		this.user = this.opts.user;
+	</script>
+</mk-user-card>
diff --git a/src/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag
index a323559218..f29f0a0c86 100644
--- a/src/web/app/mobile/tags/user.tag
+++ b/src/web/app/mobile/tags/user.tag
@@ -246,6 +246,12 @@
 			<mk-user-overview-domains user={ user }/>
 		</div>
 	</section>
+	<section class="frequently-replied-users">
+		<h2><i class="fa fa-users"></i>%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" if={ SIGNIN && I.id !== user.id }>
 		<h2><i class="fa fa-users"></i>%i18n:mobile.tags.mk-user-overview.followers-you-know%</h2>
 		<div>
@@ -619,6 +625,58 @@
 	</script>
 </mk-user-overview-domains>
 
+<mk-user-overview-frequently-replied-users>
+	<p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:mobile.tags.mk-user-overview-frequently-replied-users.loading%<mk-ellipsis/></p>
+	<div 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>
+	<style>
+		: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>
+		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
+			}).then(x => {
+				this.update({
+					users: x,
+					initializing: false
+				});
+			});
+		});
+	</script>
+</mk-user-overview-frequently-replied-users>
+
 <mk-user-overview-followers-you-know>
 	<p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:mobile.tags.mk-user-overview-followers-you-know.loading%<mk-ellipsis/></p>
 	<div if={ !initializing && users.length > 0 }>

From c3ab70b021393435b403b69e30628e03002bffad Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 8 Sep 2017 23:30:40 +0900
Subject: [PATCH 140/364] v2584

---
 CHANGELOG.md | 4 ++--
 package.json | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index cf3d82f701..c45400f884 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,8 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
-unreleased
-----------
+2584 (2017/09/08)
+-----------------
 * New: ユーザーページによく使うドメインを表示 (#771)
 * New: よくリプライするユーザーをユーザーページに表示 (#770)
 
diff --git a/package.json b/package.json
index e3a5ae6395..0dbfa1a10c 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2566",
+  "version": "0.0.2584",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",

From 1323a08a5f1d09986dc834d6f83ed9201f6188a5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 9 Sep 2017 02:58:36 +0900
Subject: [PATCH 141/364] Fix English

---
 locales/en.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/locales/en.yml b/locales/en.yml
index a7dd3aea23..1df3001e5a 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -501,7 +501,7 @@ mobile:
       domains: "Domains"
       frequently-replied-users: "Frequently talking users"
       followers-you-know: "Followers you know"
-      last-used-at: "Latest used at"
+      last-used-at: "Last used at"
 
     mk-user-overview-posts:
       loading: "Loading"

From e22a6f2913a51f93ce4681f12407070de40f1797 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, 9 Sep 2017 04:39:23 +0900
Subject: [PATCH 142/364] Update user.ts

---
 src/api/serializers/user.ts | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/api/serializers/user.ts b/src/api/serializers/user.ts
index 57599fe85c..23a176096a 100644
--- a/src/api/serializers/user.ts
+++ b/src/api/serializers/user.ts
@@ -37,7 +37,9 @@ export default (
 		data: false
 	} : {
 		data: false,
-		profile: false
+		profile: false,
+		keywords: false,
+		domains: false
 	};
 
 	// Populate the user if 'user' is ID
@@ -119,6 +121,7 @@ export default (
 
 	if (opts.detail) {
 		if (_user.pinned_post_id) {
+			// Populate pinned post
 			_user.pinned_post = await serializePost(_user.pinned_post_id, meId, {
 				detail: true
 			});

From b89b950ec3b7d00340b6d336d22bc9f5a6f07563 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Fri, 8 Sep 2017 21:38:17 +0000
Subject: [PATCH 143/364] chore(package): update @types/mocha to version 2.2.43

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

diff --git a/package.json b/package.json
index 0dbfa1a10c..a3f048d27f 100644
--- a/package.json
+++ b/package.json
@@ -47,7 +47,7 @@
     "@types/is-root": "1.0.0",
     "@types/is-url": "1.2.28",
     "@types/js-yaml": "3.9.0",
-    "@types/mocha": "2.2.42",
+    "@types/mocha": "2.2.43",
     "@types/mongodb": "2.2.11",
     "@types/monk": "1.0.6",
     "@types/morgan": "1.7.32",

From 44b405a8a30c53a3f9d27574233babaad0bd47e1 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Fri, 8 Sep 2017 21:51:57 +0000
Subject: [PATCH 144/364] chore(package): update @types/node to version 8.0.28

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

diff --git a/package.json b/package.json
index 0dbfa1a10c..036edc41a0 100644
--- a/package.json
+++ b/package.json
@@ -53,7 +53,7 @@
     "@types/morgan": "1.7.32",
     "@types/ms": "0.7.30",
     "@types/multer": "1.3.2",
-    "@types/node": "8.0.27",
+    "@types/node": "8.0.28",
     "@types/ratelimiter": "2.1.28",
     "@types/redis": "2.6.0",
     "@types/request": "2.0.3",

From 462e6c6d1cf6a6138d1e75f4a3193d33291e49e2 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sat, 9 Sep 2017 04:26:19 +0000
Subject: [PATCH 145/364] fix(package): update body-parser to version 1.18.0

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

diff --git a/package.json b/package.json
index c2bad0d687..900307cb7b 100644
--- a/package.json
+++ b/package.json
@@ -98,7 +98,7 @@
     "animejs": "2.0.2",
     "autwh": "0.0.1",
     "bcryptjs": "2.4.3",
-    "body-parser": "1.17.2",
+    "body-parser": "1.18.0",
     "cafy": "2.4.0",
     "chalk": "2.1.0",
     "compression": "1.7.0",

From dfa0dcd7a54833804635e24a816ba38bba89b171 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sun, 10 Sep 2017 23:46:16 +0000
Subject: [PATCH 146/364] chore(package): update mocha to version 3.5.2

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

diff --git a/package.json b/package.json
index 900307cb7b..78dc1ed04e 100644
--- a/package.json
+++ b/package.json
@@ -80,7 +80,7 @@
     "gulp-typescript": "3.2.2",
     "gulp-uglify": "3.0.0",
     "gulp-util": "3.0.8",
-    "mocha": "3.5.0",
+    "mocha": "3.5.2",
     "riot-tag-loader": "1.0.0",
     "string-replace-webpack-plugin": "0.1.3",
     "style-loader": "0.18.2",

From 45382dac27f40ae769df8d3cab04c54d66d9b021 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 11 Sep 2017 08:24:10 +0000
Subject: [PATCH 147/364] fix(package): update js-yaml to version 3.10.0

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

diff --git a/package.json b/package.json
index 900307cb7b..cdf69a9071 100644
--- a/package.json
+++ b/package.json
@@ -119,7 +119,7 @@
     "inquirer": "3.2.3",
     "is-root": "1.0.0",
     "is-url": "1.2.2",
-    "js-yaml": "3.9.1",
+    "js-yaml": "3.10.0",
     "mecab-async": "^0.1.0",
     "moji": "^0.5.1",
     "mongodb": "2.2.31",

From d3d9b260ba1684f2820ab1ff079eda32f52d2479 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 11 Sep 2017 10:45:20 +0000
Subject: [PATCH 148/364] fix(package): update monk to version 6.0.4

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

diff --git a/package.json b/package.json
index 900307cb7b..fc98a7245c 100644
--- a/package.json
+++ b/package.json
@@ -123,7 +123,7 @@
     "mecab-async": "^0.1.0",
     "moji": "^0.5.1",
     "mongodb": "2.2.31",
-    "monk": "6.0.3",
+    "monk": "6.0.4",
     "morgan": "1.8.2",
     "ms": "2.0.0",
     "multer": "1.3.0",

From ea98c8fce0c7595dc6af2e5ccde63945688d2430 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 11 Sep 2017 17:03:57 +0000
Subject: [PATCH 149/364] fix(package): update rimraf to version 2.6.2

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

diff --git a/package.json b/package.json
index 6d346a2b87..2fb7da68dd 100644
--- a/package.json
+++ b/package.json
@@ -138,7 +138,7 @@
     "reconnecting-websocket": "3.2.1",
     "redis": "2.8.0",
     "request": "2.81.0",
-    "rimraf": "2.6.1",
+    "rimraf": "2.6.2",
     "riot": "3.7.0",
     "rndstr": "1.0.0",
     "s-age": "1.1.0",

From 4ad141cfbaf21148d697786929da8718f4e85468 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 11 Sep 2017 22:19:29 +0000
Subject: [PATCH 150/364] chore(package): update mocha to version 3.5.3

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

diff --git a/package.json b/package.json
index 2fb7da68dd..258521ff0c 100644
--- a/package.json
+++ b/package.json
@@ -80,7 +80,7 @@
     "gulp-typescript": "3.2.2",
     "gulp-uglify": "3.0.0",
     "gulp-util": "3.0.8",
-    "mocha": "3.5.2",
+    "mocha": "3.5.3",
     "riot-tag-loader": "1.0.0",
     "string-replace-webpack-plugin": "0.1.3",
     "style-loader": "0.18.2",

From 579d0bf4723064538d8d1b9eb043f471531fc1fc Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 12 Sep 2017 04:09:21 +0000
Subject: [PATCH 151/364] fix(package): update serve-favicon to version 2.4.4

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

diff --git a/package.json b/package.json
index 2fb7da68dd..e81832f47a 100644
--- a/package.json
+++ b/package.json
@@ -142,7 +142,7 @@
     "riot": "3.7.0",
     "rndstr": "1.0.0",
     "s-age": "1.1.0",
-    "serve-favicon": "2.4.3",
+    "serve-favicon": "2.4.4",
     "summaly": "2.0.3",
     "syuilo-password-strength": "0.0.1",
     "tcp-port-used": "0.1.2",

From ceb446765d8524f15bccfe07f656a58ec6577d55 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 12 Sep 2017 16:10:28 +0000
Subject: [PATCH 152/364] fix(package): update body-parser to version 1.18.1

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

diff --git a/package.json b/package.json
index 2fb7da68dd..f3e85788c9 100644
--- a/package.json
+++ b/package.json
@@ -98,7 +98,7 @@
     "animejs": "2.0.2",
     "autwh": "0.0.1",
     "bcryptjs": "2.4.3",
-    "body-parser": "1.18.0",
+    "body-parser": "1.18.1",
     "cafy": "2.4.0",
     "chalk": "2.1.0",
     "compression": "1.7.0",

From dd1379e2316be64a957df7fba97f1a3aadaa1c8c Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 13 Sep 2017 20:56:46 +0000
Subject: [PATCH 153/364] fix(package): update riot to version 3.7.1

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

diff --git a/package.json b/package.json
index 2fb7da68dd..a0f8f44308 100644
--- a/package.json
+++ b/package.json
@@ -139,7 +139,7 @@
     "redis": "2.8.0",
     "request": "2.81.0",
     "rimraf": "2.6.2",
-    "riot": "3.7.0",
+    "riot": "3.7.1",
     "rndstr": "1.0.0",
     "s-age": "1.1.0",
     "serve-favicon": "2.4.3",

From 7462daa12691ddbc8f04704058f2d1210f0427ef Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Fri, 15 Sep 2017 09:15:33 +0000
Subject: [PATCH 154/364] chore(package): update webpack to version 3.6.0

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

diff --git a/package.json b/package.json
index b1ab86d0df..76642c0b42 100644
--- a/package.json
+++ b/package.json
@@ -91,7 +91,7 @@
     "uglify-es": "3.0.27",
     "uglify-es-webpack-plugin": "0.10.0",
     "uglify-js": "git+https://github.com/mishoo/UglifyJS2.git#harmony",
-    "webpack": "3.5.6"
+    "webpack": "3.6.0"
   },
   "dependencies": {
     "accesses": "2.5.0",

From 86858586cf0ff993735dc289836e896f14456982 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, 16 Sep 2017 10:19:07 +0900
Subject: [PATCH 155/364] fix

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

diff --git a/package.json b/package.json
index 76642c0b42..d72a10ee92 100644
--- a/package.json
+++ b/package.json
@@ -139,7 +139,7 @@
     "redis": "2.8.0",
     "request": "2.81.0",
     "rimraf": "2.6.2",
-    "riot": "3.7.1",
+    "riot": "3.7.0",
     "rndstr": "1.0.0",
     "s-age": "1.1.0",
     "serve-favicon": "2.4.4",

From bbfac657fb95536f2e942fbd02343bb1185fc68b 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, 16 Sep 2017 14:30:44 +0900
Subject: [PATCH 156/364] Refactoring

---
 src/api/authenticate.ts | 18 +++++++++++++-----
 src/api/streaming.ts    | 22 ++++++++++------------
 2 files changed, 23 insertions(+), 17 deletions(-)

diff --git a/src/api/authenticate.ts b/src/api/authenticate.ts
index d4cc3fc41f..e044d4ae8b 100644
--- a/src/api/authenticate.ts
+++ b/src/api/authenticate.ts
@@ -1,6 +1,6 @@
 import * as express from 'express';
 import App from './models/app';
-import User from './models/user';
+import{ default as User, IUser } from './models/user';
 import AccessToken from './models/access-token';
 import isNativeToken from './common/is-native-token';
 
@@ -13,7 +13,7 @@ export interface IAuthContext {
 	/**
 	 * Authenticated user
 	 */
-	user: any;
+	user: IUser;
 
 	/**
 	 * Weather if the request is via the User-Native Token or not
@@ -25,11 +25,15 @@ export default (req: express.Request) => new Promise<IAuthContext>(async (resolv
 	const token = req.body['i'] as string;
 
 	if (token == null) {
-		return resolve({ app: null, user: null, isSecure: false });
+		return resolve({
+			app: null,
+			user: null,
+			isSecure: false
+		});
 	}
 
 	if (isNativeToken(token)) {
-		const user = await User
+		const user: IUser = await User
 			.findOne({ token: token });
 
 		if (user === null) {
@@ -56,6 +60,10 @@ export default (req: express.Request) => new Promise<IAuthContext>(async (resolv
 		const user = await User
 			.findOne({ _id: accessToken.user_id });
 
-		return resolve({ app: app, user: user, isSecure: false });
+		return resolve({
+			app: app,
+			user: user,
+			isSecure: false
+		});
 	}
 });
diff --git a/src/api/streaming.ts b/src/api/streaming.ts
index c71132100c..db600013b9 100644
--- a/src/api/streaming.ts
+++ b/src/api/streaming.ts
@@ -2,7 +2,7 @@ import * as http from 'http';
 import * as websocket from 'websocket';
 import * as redis from 'redis';
 import config from '../conf';
-import User from './models/user';
+import { default as User, IUser } from './models/user';
 import AccessToken from './models/access-token';
 import isNativeToken from './common/is-native-token';
 
@@ -26,7 +26,7 @@ module.exports = (server: http.Server) => {
 			return;
 		}
 
-		const user = await authenticate(connection, request.resourceURL.query.i);
+		const user = await authenticate(request.resourceURL.query.i);
 
 		if (user == null) {
 			connection.send('authentication-failed');
@@ -56,7 +56,11 @@ module.exports = (server: http.Server) => {
 	});
 };
 
-function authenticate(connection: websocket.connection, token: string): Promise<any> {
+/**
+ * 接続してきたユーザーを取得します
+ * @param token 送信されてきたトークン
+ */
+function authenticate(token: string): Promise<IUser> {
 	if (token == null) {
 		return Promise.resolve(null);
 	}
@@ -64,8 +68,7 @@ function authenticate(connection: websocket.connection, token: string): Promise<
 	return new Promise(async (resolve, reject) => {
 		if (isNativeToken(token)) {
 			// Fetch user
-			// SELECT _id
-			const user = await User
+			const user: IUser = await User
 				.findOne({
 					token: token
 				});
@@ -81,13 +84,8 @@ function authenticate(connection: websocket.connection, token: string): Promise<
 			}
 
 			// Fetch user
-			// SELECT _id
-			const user = await User
-				.findOne({ _id: accessToken.user_id }, {
-					fields: {
-						_id: true
-					}
-				});
+			const user: IUser = await User
+				.findOne({ _id: accessToken.user_id });
 
 			resolve(user);
 		}

From b2e28869cc9410070b689517993f6cdbfd73e0f8 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, 16 Sep 2017 14:31:24 +0900
Subject: [PATCH 157/364] oops

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

diff --git a/src/api/authenticate.ts b/src/api/authenticate.ts
index e044d4ae8b..6de91c16e8 100644
--- a/src/api/authenticate.ts
+++ b/src/api/authenticate.ts
@@ -1,6 +1,6 @@
 import * as express from 'express';
 import App from './models/app';
-import{ default as User, IUser } from './models/user';
+import { default as User, IUser } from './models/user';
 import AccessToken from './models/access-token';
 import isNativeToken from './common/is-native-token';
 

From 0b3cee9057ec4c0e9e640c72103d9dd229f0bd82 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, 16 Sep 2017 14:38:33 +0900
Subject: [PATCH 158/364] Refactor: Better English

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

diff --git a/src/api/authenticate.ts b/src/api/authenticate.ts
index 6de91c16e8..48de748e90 100644
--- a/src/api/authenticate.ts
+++ b/src/api/authenticate.ts
@@ -16,7 +16,7 @@ export interface IAuthContext {
 	user: IUser;
 
 	/**
-	 * Weather if the request is via the User-Native Token or not
+	 * Weather requested with a User-Native Token
 	 */
 	isSecure: boolean;
 }

From 76c0f67def857cabfcc70c01be28318e3175de97 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, 16 Sep 2017 14:38:58 +0900
Subject: [PATCH 159/364] typo

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

diff --git a/src/api/authenticate.ts b/src/api/authenticate.ts
index 48de748e90..b289959ac1 100644
--- a/src/api/authenticate.ts
+++ b/src/api/authenticate.ts
@@ -16,7 +16,7 @@ export interface IAuthContext {
 	user: IUser;
 
 	/**
-	 * Weather requested with a User-Native Token
+	 * Whether requested with a User-Native Token
 	 */
 	isSecure: boolean;
 }

From a12cc1a1b067745695bd6f55defe5eb2d7400da0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 16 Sep 2017 17:31:37 +0900
Subject: [PATCH 160/364] Refactoring

---
 src/api/private/signin.ts | 4 ++--
 src/api/private/signup.ts | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/api/private/signin.ts b/src/api/private/signin.ts
index afa83e50c3..c7dc243980 100644
--- a/src/api/private/signin.ts
+++ b/src/api/private/signin.ts
@@ -1,6 +1,6 @@
 import * as express from 'express';
 import * as bcrypt from 'bcryptjs';
-import User from '../models/user';
+import { default as User, IUser } from '../models/user';
 import Signin from '../models/signin';
 import serialize from '../serializers/signin';
 import event from '../event';
@@ -23,7 +23,7 @@ export default async (req: express.Request, res: express.Response) => {
 	}
 
 	// Fetch user
-	const user = await User.findOne({
+	const user: IUser = await User.findOne({
 		username_lower: username.toLowerCase()
 	}, {
 		fields: {
diff --git a/src/api/private/signup.ts b/src/api/private/signup.ts
index 899fa88472..bcc17a876d 100644
--- a/src/api/private/signup.ts
+++ b/src/api/private/signup.ts
@@ -1,7 +1,7 @@
 import * as express from 'express';
 import * as bcrypt from 'bcryptjs';
 import recaptcha = require('recaptcha-promise');
-import User from '../models/user';
+import { default as User, IUser } from '../models/user';
 import { validateUsername, validatePassword } from '../models/user';
 import serialize from '../serializers/user';
 import generateUserToken from '../common/generate-native-user-token';
@@ -61,7 +61,7 @@ export default async (req: express.Request, res: express.Response) => {
 	const secret = generateUserToken();
 
 	// Create account
-	const account = await User.insert({
+	const account: IUser = await User.insert({
 		token: secret,
 		avatar_id: null,
 		banner_id: null,

From 6c5960811a616d0ef230471d91a716a9464498ac Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sun, 17 Sep 2017 15:09:51 +0000
Subject: [PATCH 161/364] fix(package): update animejs to version 2.1.0

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

diff --git a/package.json b/package.json
index d72a10ee92..675bc2a8c9 100644
--- a/package.json
+++ b/package.json
@@ -95,7 +95,7 @@
   },
   "dependencies": {
     "accesses": "2.5.0",
-    "animejs": "2.0.2",
+    "animejs": "2.1.0",
     "autwh": "0.0.1",
     "bcryptjs": "2.4.3",
     "body-parser": "1.18.1",

From df340e6aba8f40736d37dea5553443da451ac58a Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 18 Sep 2017 14:15:58 +0000
Subject: [PATCH 162/364] chore(package): update @types/morgan to version
 1.7.33

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

diff --git a/package.json b/package.json
index 675bc2a8c9..2e39cfeca7 100644
--- a/package.json
+++ b/package.json
@@ -50,7 +50,7 @@
     "@types/mocha": "2.2.43",
     "@types/mongodb": "2.2.11",
     "@types/monk": "1.0.6",
-    "@types/morgan": "1.7.32",
+    "@types/morgan": "1.7.33",
     "@types/ms": "0.7.30",
     "@types/multer": "1.3.2",
     "@types/node": "8.0.28",

From 731c6d22a0da2ffbf6eb3883f98677e26840d575 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 18 Sep 2017 15:06:23 +0000
Subject: [PATCH 163/364] fix(package): update inquirer to version 3.3.0

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

diff --git a/package.json b/package.json
index 675bc2a8c9..f094ae73fe 100644
--- a/package.json
+++ b/package.json
@@ -116,7 +116,7 @@
     "file-type": "6.1.0",
     "fuckadblock": "3.2.1",
     "gm": "1.23.0",
-    "inquirer": "3.2.3",
+    "inquirer": "3.3.0",
     "is-root": "1.0.0",
     "is-url": "1.2.2",
     "js-yaml": "3.10.0",

From 9ae042f9dd5ff924687493e9a3728f2cb983aef4 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 18 Sep 2017 23:35:10 +0000
Subject: [PATCH 164/364] fix(package): update cafy to version 3.0.0

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

diff --git a/package.json b/package.json
index 675bc2a8c9..54ad789558 100644
--- a/package.json
+++ b/package.json
@@ -99,7 +99,7 @@
     "autwh": "0.0.1",
     "bcryptjs": "2.4.3",
     "body-parser": "1.18.1",
-    "cafy": "2.4.0",
+    "cafy": "3.0.0",
     "chalk": "2.1.0",
     "compression": "1.7.0",
     "cors": "2.8.4",

From 5ad34defa7bddffbd450bf0632860d27bee40e4c Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 19 Sep 2017 18:19:06 +0000
Subject: [PATCH 165/364] chore(package): update @types/webpack to version
 3.0.11

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

diff --git a/package.json b/package.json
index 54ad789558..fe08fdb823 100644
--- a/package.json
+++ b/package.json
@@ -61,7 +61,7 @@
     "@types/riot": "3.6.0",
     "@types/serve-favicon": "2.2.28",
     "@types/uuid": "3.4.2",
-    "@types/webpack": "3.0.10",
+    "@types/webpack": "3.0.11",
     "@types/webpack-stream": "3.2.7",
     "@types/websocket": "0.0.34",
     "chai": "4.1.2",

From 6417dd19347e7f443ec7c39d83bba69d685155f5 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 19 Sep 2017 20:25:22 +0000
Subject: [PATCH 166/364] fix(package): update request to version 2.82.0

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

diff --git a/package.json b/package.json
index 54ad789558..c58bdca414 100644
--- a/package.json
+++ b/package.json
@@ -137,7 +137,7 @@
     "recaptcha-promise": "0.1.3",
     "reconnecting-websocket": "3.2.1",
     "redis": "2.8.0",
-    "request": "2.81.0",
+    "request": "2.82.0",
     "rimraf": "2.6.2",
     "riot": "3.7.0",
     "rndstr": "1.0.0",

From bd1e09713ebb65d19f20cdb129b22fe81ea389f1 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 21 Sep 2017 21:35:51 +0000
Subject: [PATCH 167/364] chore(package): update @types/node to version 8.0.29

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

diff --git a/package.json b/package.json
index a22decef9b..d8b505505e 100644
--- a/package.json
+++ b/package.json
@@ -53,7 +53,7 @@
     "@types/morgan": "1.7.32",
     "@types/ms": "0.7.30",
     "@types/multer": "1.3.2",
-    "@types/node": "8.0.28",
+    "@types/node": "8.0.29",
     "@types/ratelimiter": "2.1.28",
     "@types/redis": "2.6.0",
     "@types/request": "2.0.3",

From 5cf72dcf9b7c04175c398ab01e9b4b0dc4b763ea Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Fri, 22 Sep 2017 17:04:18 +0000
Subject: [PATCH 168/364] fix(package): update body-parser to version 1.18.2

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

diff --git a/package.json b/package.json
index d8b505505e..ddda43cf23 100644
--- a/package.json
+++ b/package.json
@@ -98,7 +98,7 @@
     "animejs": "2.1.0",
     "autwh": "0.0.1",
     "bcryptjs": "2.4.3",
-    "body-parser": "1.18.1",
+    "body-parser": "1.18.2",
     "cafy": "3.0.0",
     "chalk": "2.1.0",
     "compression": "1.7.0",

From e90cc4eb0a5a6901108394f6f2c58df3a33fcc1b Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Fri, 22 Sep 2017 18:07:47 +0000
Subject: [PATCH 169/364] chore(package): update @types/node to version 8.0.30

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

diff --git a/package.json b/package.json
index d8b505505e..3c8c0f92a8 100644
--- a/package.json
+++ b/package.json
@@ -53,7 +53,7 @@
     "@types/morgan": "1.7.32",
     "@types/ms": "0.7.30",
     "@types/multer": "1.3.2",
-    "@types/node": "8.0.29",
+    "@types/node": "8.0.30",
     "@types/ratelimiter": "2.1.28",
     "@types/redis": "2.6.0",
     "@types/request": "2.0.3",

From 6846dab586b7ec30d623820a23767f3898941bcb Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sat, 23 Sep 2017 10:32:59 +0000
Subject: [PATCH 170/364] fix(package): update file-type to version 6.2.0

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

diff --git a/package.json b/package.json
index d8b505505e..171998c9a7 100644
--- a/package.json
+++ b/package.json
@@ -113,7 +113,7 @@
     "elasticsearch": "13.3.1",
     "escape-regexp": "0.0.1",
     "express": "4.15.4",
-    "file-type": "6.1.0",
+    "file-type": "6.2.0",
     "fuckadblock": "3.2.1",
     "gm": "1.23.0",
     "inquirer": "3.3.0",

From 03166c8f2d63841ac811b199066db4eccd9a44cd Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sat, 23 Sep 2017 14:29:19 +0000
Subject: [PATCH 171/364] fix(package): update riot to version 3.7.2

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

diff --git a/package.json b/package.json
index 4d67256106..0df1c99433 100644
--- a/package.json
+++ b/package.json
@@ -139,7 +139,7 @@
     "redis": "2.8.0",
     "request": "2.82.0",
     "rimraf": "2.6.2",
-    "riot": "3.7.0",
+    "riot": "3.7.2",
     "rndstr": "1.0.0",
     "s-age": "1.1.0",
     "serve-favicon": "2.4.4",

From 9d345a68c3a28f9ff09770d9b01630f9d93f162c Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sat, 23 Sep 2017 17:21:57 +0000
Subject: [PATCH 172/364] fix(package): update reconnecting-websocket to
 version 3.2.2

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

diff --git a/package.json b/package.json
index 0df1c99433..7e517a8096 100644
--- a/package.json
+++ b/package.json
@@ -135,7 +135,7 @@
     "pug": "2.0.0-rc.4",
     "ratelimiter": "3.0.3",
     "recaptcha-promise": "0.1.3",
-    "reconnecting-websocket": "3.2.1",
+    "reconnecting-websocket": "3.2.2",
     "redis": "2.8.0",
     "request": "2.82.0",
     "rimraf": "2.6.2",

From 2431b503bf00f18dc125537abe2b28d905b182e9 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 25 Sep 2017 17:10:52 +0000
Subject: [PATCH 173/364] fix(package): update animejs to version 2.2.0

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

diff --git a/package.json b/package.json
index 7e517a8096..4d51475609 100644
--- a/package.json
+++ b/package.json
@@ -95,7 +95,7 @@
   },
   "dependencies": {
     "accesses": "2.5.0",
-    "animejs": "2.1.0",
+    "animejs": "2.2.0",
     "autwh": "0.0.1",
     "bcryptjs": "2.4.3",
     "body-parser": "1.18.2",

From 3fb047e23937e85fde5a04d4b9b705c13a2e71a6 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 25 Sep 2017 19:42:45 +0000
Subject: [PATCH 174/364] chore(package): update @types/webpack to version
 3.0.12

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

diff --git a/package.json b/package.json
index 4d51475609..89b5a54b9d 100644
--- a/package.json
+++ b/package.json
@@ -61,7 +61,7 @@
     "@types/riot": "3.6.0",
     "@types/serve-favicon": "2.2.28",
     "@types/uuid": "3.4.2",
-    "@types/webpack": "3.0.11",
+    "@types/webpack": "3.0.12",
     "@types/webpack-stream": "3.2.7",
     "@types/websocket": "0.0.34",
     "chai": "4.1.2",

From 47ad9599752e06382ac58cde8b4c90de4e91e409 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 25 Sep 2017 21:27:51 +0000
Subject: [PATCH 175/364] chore(package): update @types/node to version 8.0.31

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

diff --git a/package.json b/package.json
index 4d51475609..f50c9c8b23 100644
--- a/package.json
+++ b/package.json
@@ -53,7 +53,7 @@
     "@types/morgan": "1.7.32",
     "@types/ms": "0.7.30",
     "@types/multer": "1.3.2",
-    "@types/node": "8.0.30",
+    "@types/node": "8.0.31",
     "@types/ratelimiter": "2.1.28",
     "@types/redis": "2.6.0",
     "@types/request": "2.0.3",

From 4c5f9cc83012f62ed9e92f6b185b9c62769dee96 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 26 Sep 2017 17:46:57 +0000
Subject: [PATCH 176/364] fix(package): update serve-favicon to version 2.4.5

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

diff --git a/package.json b/package.json
index 4d51475609..0a437eefb4 100644
--- a/package.json
+++ b/package.json
@@ -142,7 +142,7 @@
     "riot": "3.7.2",
     "rndstr": "1.0.0",
     "s-age": "1.1.0",
-    "serve-favicon": "2.4.4",
+    "serve-favicon": "2.4.5",
     "summaly": "2.0.3",
     "syuilo-password-strength": "0.0.1",
     "tcp-port-used": "0.1.2",

From 7b559ac8d4142b9c1ca3c5c37bcd00ae739858fd Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 26 Sep 2017 20:35:17 +0000
Subject: [PATCH 177/364] fix(package): update debug to version 3.1.0

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

diff --git a/package.json b/package.json
index 0a437eefb4..88b111fcd8 100644
--- a/package.json
+++ b/package.json
@@ -105,7 +105,7 @@
     "cors": "2.8.4",
     "cropperjs": "1.0.0",
     "crypto": "1.0.1",
-    "debug": "3.0.1",
+    "debug": "3.1.0",
     "deep-equal": "1.0.1",
     "deepcopy": "0.6.3",
     "diskusage": "^0.2.2",

From c321dac1edf1ac86fb549a1169869846bce4ef2f Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 27 Sep 2017 00:17:46 +0000
Subject: [PATCH 178/364] fix(package): update typescript to version 2.5.3

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

diff --git a/package.json b/package.json
index 0a437eefb4..4cb3ed3545 100644
--- a/package.json
+++ b/package.json
@@ -148,7 +148,7 @@
     "tcp-port-used": "0.1.2",
     "textarea-caret": "3.0.2",
     "ts-node": "3.3.0",
-    "typescript": "2.5.2",
+    "typescript": "2.5.3",
     "uuid": "3.1.0",
     "vhost": "3.0.2",
     "websocket": "1.0.24",

From 10a2e981e943777d21508ed493808c6c48353258 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 27 Sep 2017 02:57:50 +0000
Subject: [PATCH 179/364] fix(package): update morgan to version 1.9.0

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

diff --git a/package.json b/package.json
index 0a437eefb4..14d1f84c3e 100644
--- a/package.json
+++ b/package.json
@@ -124,7 +124,7 @@
     "moji": "^0.5.1",
     "mongodb": "2.2.31",
     "monk": "6.0.4",
-    "morgan": "1.8.2",
+    "morgan": "1.9.0",
     "ms": "2.0.0",
     "multer": "1.3.0",
     "nprogress": "0.2.0",

From 589ff5c01de0a69af70b9ab2f54f5ebd6859fab5 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 27 Sep 2017 05:03:33 +0000
Subject: [PATCH 180/364] fix(package): update request to version 2.83.0

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

diff --git a/package.json b/package.json
index 0a437eefb4..fd1592c6a9 100644
--- a/package.json
+++ b/package.json
@@ -137,7 +137,7 @@
     "recaptcha-promise": "0.1.3",
     "reconnecting-websocket": "3.2.2",
     "redis": "2.8.0",
-    "request": "2.82.0",
+    "request": "2.83.0",
     "rimraf": "2.6.2",
     "riot": "3.7.2",
     "rndstr": "1.0.0",

From 1fe1e8d8bfabf46bc69f8bdf1efa19e1b18c1d5c Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 27 Sep 2017 05:46:08 +0000
Subject: [PATCH 181/364] fix(package): update compression to version 1.7.1

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

diff --git a/package.json b/package.json
index 0a437eefb4..74dca61224 100644
--- a/package.json
+++ b/package.json
@@ -101,7 +101,7 @@
     "body-parser": "1.18.2",
     "cafy": "3.0.0",
     "chalk": "2.1.0",
-    "compression": "1.7.0",
+    "compression": "1.7.1",
     "cors": "2.8.4",
     "cropperjs": "1.0.0",
     "crypto": "1.0.1",

From 98e6ce6d6a1ebf31ccb2b1748deae671f8222a70 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 28 Sep 2017 07:55:56 +0000
Subject: [PATCH 182/364] chore(package): update @types/elasticsearch to
 version 5.0.17

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

diff --git a/package.json b/package.json
index 0a437eefb4..4552041115 100644
--- a/package.json
+++ b/package.json
@@ -30,7 +30,7 @@
     "@types/cors": "2.8.1",
     "@types/debug": "0.0.30",
     "@types/deep-equal": "1.0.1",
-    "@types/elasticsearch": "5.0.14",
+    "@types/elasticsearch": "5.0.17",
     "@types/event-stream": "3.3.32",
     "@types/express": "4.0.37",
     "@types/gm": "1.17.32",

From 93b3c7cd0c0ce020183ed3ba112984b184215680 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sun, 1 Oct 2017 09:40:22 +0000
Subject: [PATCH 183/364] fix(package): update riot to version 3.7.3

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

diff --git a/package.json b/package.json
index 2fb0987b25..22bf817929 100644
--- a/package.json
+++ b/package.json
@@ -139,7 +139,7 @@
     "redis": "2.8.0",
     "request": "2.83.0",
     "rimraf": "2.6.2",
-    "riot": "3.7.2",
+    "riot": "3.7.3",
     "rndstr": "1.0.0",
     "s-age": "1.1.0",
     "serve-favicon": "2.4.5",

From 5daf490224b3d4fa69718e065d3739ef0b8c0d7a Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 3 Oct 2017 12:17:24 +0000
Subject: [PATCH 184/364] chore(package): update style-loader to version 0.19.0

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

diff --git a/package.json b/package.json
index 2fb0987b25..0f34c09fbd 100644
--- a/package.json
+++ b/package.json
@@ -83,7 +83,7 @@
     "mocha": "3.5.3",
     "riot-tag-loader": "1.0.0",
     "string-replace-webpack-plugin": "0.1.3",
-    "style-loader": "0.18.2",
+    "style-loader": "0.19.0",
     "stylus": "0.54.5",
     "stylus-loader": "3.0.1",
     "swagger-jsdoc": "1.9.7",

From c3fcdd2c9a4726933babf7ef56e4665ecd813e3b Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 3 Oct 2017 14:28:12 +0000
Subject: [PATCH 185/364] chore(package): update @types/mongodb to version
 2.2.12

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

diff --git a/package.json b/package.json
index 2fb0987b25..69aaa8ff2f 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,7 @@
     "@types/is-url": "1.2.28",
     "@types/js-yaml": "3.9.0",
     "@types/mocha": "2.2.43",
-    "@types/mongodb": "2.2.11",
+    "@types/mongodb": "2.2.12",
     "@types/monk": "1.0.6",
     "@types/morgan": "1.7.33",
     "@types/ms": "0.7.30",

From 8473a627838b3c2ff1ebaaa90ecf47cd64efd229 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 3 Oct 2017 15:36:39 +0000
Subject: [PATCH 186/364] chore(package): update @types/node to version 8.0.32

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

diff --git a/package.json b/package.json
index 2fb0987b25..08b35dd2e9 100644
--- a/package.json
+++ b/package.json
@@ -53,7 +53,7 @@
     "@types/morgan": "1.7.33",
     "@types/ms": "0.7.30",
     "@types/multer": "1.3.2",
-    "@types/node": "8.0.31",
+    "@types/node": "8.0.32",
     "@types/ratelimiter": "2.1.28",
     "@types/redis": "2.6.0",
     "@types/request": "2.0.3",

From 132c14d93eaec80d7fca4e866774c71fe226299d Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 3 Oct 2017 18:26:58 +0000
Subject: [PATCH 187/364] chore(package): update @types/webpack to version
 3.0.13

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

diff --git a/package.json b/package.json
index 2fb0987b25..30401757e7 100644
--- a/package.json
+++ b/package.json
@@ -61,7 +61,7 @@
     "@types/riot": "3.6.0",
     "@types/serve-favicon": "2.2.28",
     "@types/uuid": "3.4.2",
-    "@types/webpack": "3.0.12",
+    "@types/webpack": "3.0.13",
     "@types/webpack-stream": "3.2.7",
     "@types/websocket": "0.0.34",
     "chai": "4.1.2",

From a8a91c4be761d5a62bc26519b16b021e895aa775 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 4 Oct 2017 18:28:21 +0000
Subject: [PATCH 188/364] chore(package): update @types/request to version
 2.0.4

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

diff --git a/package.json b/package.json
index 2fb0987b25..bb7cbc3a65 100644
--- a/package.json
+++ b/package.json
@@ -56,7 +56,7 @@
     "@types/node": "8.0.31",
     "@types/ratelimiter": "2.1.28",
     "@types/redis": "2.6.0",
-    "@types/request": "2.0.3",
+    "@types/request": "2.0.4",
     "@types/rimraf": "2.0.0",
     "@types/riot": "3.6.0",
     "@types/serve-favicon": "2.2.28",

From 852a302ce76be2b2b0535cc35c1364ebb57f1237 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Fri, 6 Oct 2017 10:29:37 +0000
Subject: [PATCH 189/364] chore(package): update gulp-imagemin to version 3.4.0

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

diff --git a/package.json b/package.json
index b56f97c797..cb23269479 100644
--- a/package.json
+++ b/package.json
@@ -70,7 +70,7 @@
     "event-stream": "3.3.4",
     "gulp": "3.9.1",
     "gulp-cssnano": "2.1.2",
-    "gulp-imagemin": "3.3.0",
+    "gulp-imagemin": "3.4.0",
     "gulp-htmlmin": "3.0.0",
     "gulp-mocha": "4.3.1",
     "gulp-pug": "3.3.0",

From aa5ca06830e8848fb213cd179c304990f01561a3 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Fri, 6 Oct 2017 17:26:20 +0000
Subject: [PATCH 190/364] chore(package): update @types/mongodb to version
 2.2.13

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

diff --git a/package.json b/package.json
index cb23269479..0336725bda 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,7 @@
     "@types/is-url": "1.2.28",
     "@types/js-yaml": "3.9.0",
     "@types/mocha": "2.2.43",
-    "@types/mongodb": "2.2.12",
+    "@types/mongodb": "2.2.13",
     "@types/monk": "1.0.6",
     "@types/morgan": "1.7.33",
     "@types/ms": "0.7.30",

From 6a5c6280ffd3ffe820beb23294f1c2c1f5deb9cf Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Oct 2017 03:36:46 +0900
Subject: [PATCH 191/364] :v:

---
 src/api/bot/core.ts                           | 88 +++++++++++++++++++
 src/api/bot/interfaces/line.ts                | 37 ++++++++
 src/api/endpoints/i/appdata/set.ts            |  2 +-
 src/api/models/user.ts                        |  3 +
 src/api/serializers/user.ts                   |  1 +
 src/api/server.ts                             |  2 +
 .../scripts => common}/get-post-summary.js    |  4 +
 src/config.ts                                 |  3 +
 src/web/app/desktop/script.js                 |  2 +-
 src/web/app/desktop/tags/notifications.tag    |  2 +-
 src/web/app/desktop/tags/pages/home.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/page/home.tag         |  2 +-
 src/web/app/mobile/tags/post-detail.tag       |  2 +-
 src/web/app/mobile/tags/timeline.tag          |  2 +-
 src/web/app/mobile/tags/user.tag              |  2 +-
 tslint.json                                   |  1 +
 19 files changed, 150 insertions(+), 11 deletions(-)
 create mode 100644 src/api/bot/core.ts
 create mode 100644 src/api/bot/interfaces/line.ts
 rename src/{web/app/common/scripts => common}/get-post-summary.js (88%)

diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
new file mode 100644
index 0000000000..002ac1b06e
--- /dev/null
+++ b/src/api/bot/core.ts
@@ -0,0 +1,88 @@
+import * as EventEmitter from 'events';
+import * as bcrypt from 'bcryptjs';
+
+import User, { IUser } from '../models/user';
+
+export default class BotCore extends EventEmitter {
+	public user: IUser;
+
+	private context: Context = null;
+
+	constructor(user: IUser) {
+		super();
+
+		this.user = user;
+	}
+
+	public async q(query: string): Promise<string> {
+		if (this.context != null) {
+			return await this.context.q(query);
+		}
+
+		switch (query) {
+			case 'ping':
+				return 'PONG';
+			case 'ログイン':
+			case 'サインイン':
+				this.context = new SigninContext(this);
+				return await this.context.greet();
+			default:
+				return '?';
+		}
+	}
+
+	public setUser(user: IUser) {
+		this.user = user;
+		this.emit('set-user', user);
+	}
+}
+
+abstract class Context {
+	protected core: BotCore;
+
+	public abstract async greet(): Promise<string>;
+	public abstract async q(query: string): Promise<string>;
+
+	constructor(core: BotCore) {
+		this.core = core;
+	}
+}
+
+class SigninContext extends Context {
+	private temporaryUser: IUser;
+
+	public async greet(): Promise<string> {
+		return 'まずユーザー名を教えてください:';
+	}
+
+	public async q(query: string): Promise<string> {
+		if (this.temporaryUser == null) {
+			// Fetch user
+			const user: IUser = await User.findOne({
+				username_lower: query.toLowerCase()
+			}, {
+				fields: {
+					data: false,
+					profile: false
+				}
+			});
+
+			if (user === null) {
+				return `${query}というユーザーは存在しませんでした... もう一度教えてください:`;
+			} else {
+				this.temporaryUser = user;
+				return `パスワードを教えてください:`;
+			}
+		} else {
+			// Compare password
+			const same = bcrypt.compareSync(query, this.temporaryUser.password);
+
+			if (same) {
+				this.core.setUser(this.temporaryUser);
+				return `${this.temporaryUser.name}さん、おかえりなさい!`;
+			} else {
+				return `パスワードが違います... もう一度教えてください:`;
+			}
+		}
+	}
+}
diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts
new file mode 100644
index 0000000000..4bee844c12
--- /dev/null
+++ b/src/api/bot/interfaces/line.ts
@@ -0,0 +1,37 @@
+import * as EventEmitter from 'events';
+import * as express from 'express';
+import * as crypto from 'crypto';
+//import User from '../../models/user';
+import config from '../../../conf';
+/*import BotCore from '../core';
+
+const sessions: {
+	userId: string;
+	session: BotCore;
+}[] = [];
+*/
+module.exports = async (app: express.Application) => {
+	if (config.line_bot == null) return;
+
+	const handler = new EventEmitter();
+
+	app.post('/hooks/line', (req, res, next) => {
+		// req.headers['X-Line-Signature'] は常に string ですが、型定義の都合上
+		// string | string[] になっているので string を明示しています
+		const sig1 = req.headers['X-Line-Signature'] as string;
+
+		const hash = crypto.createHmac('sha256', config.line_bot.channel_secret)
+			.update(JSON.stringify(req.body));
+
+		const sig2 = hash.digest('base64');
+
+		// シグネチャ比較
+		if (sig1 === sig2) {
+			console.log(req.body);
+			handler.emit(req.body.type);
+			res.sendStatus(200);
+		} else {
+			res.sendStatus(400);
+		}
+	});
+};
diff --git a/src/api/endpoints/i/appdata/set.ts b/src/api/endpoints/i/appdata/set.ts
index 24f192de6b..9c3dbe185b 100644
--- a/src/api/endpoints/i/appdata/set.ts
+++ b/src/api/endpoints/i/appdata/set.ts
@@ -21,7 +21,7 @@ module.exports = (params, user, app, isSecure) => new Promise(async (res, rej) =
 	const [data, dataError] = $(params.data).optional.object()
 		.pipe(obj => {
 			const hasInvalidData = Object.entries(obj).some(([k, v]) =>
-				$(k).string().match(/^[a-z_]+$/).isNg() && $(v).string().isNg());
+				$(k).string().match(/^[a-z_]+$/).nok() && $(v).string().nok());
 			return !hasInvalidData;
 		}).$;
 	if (dataError) return rej('invalid data param');
diff --git a/src/api/models/user.ts b/src/api/models/user.ts
index 1591b339bc..4f8086d42b 100644
--- a/src/api/models/user.ts
+++ b/src/api/models/user.ts
@@ -57,6 +57,9 @@ export type IUser = {
 		user_id: string;
 		screen_name: string;
 	};
+	line: {
+		user_id: string;
+	};
 	description: string;
 	profile: {
 		location: string;
diff --git a/src/api/serializers/user.ts b/src/api/serializers/user.ts
index 23a176096a..3deff2d003 100644
--- a/src/api/serializers/user.ts
+++ b/src/api/serializers/user.ts
@@ -79,6 +79,7 @@ export default (
 		delete _user.twitter.access_token;
 		delete _user.twitter.access_token_secret;
 	}
+	delete _user.line;
 
 	// Visible via only the official client
 	if (!opts.includeSecrets) {
diff --git a/src/api/server.ts b/src/api/server.ts
index c98167eb3e..fdff0c7546 100644
--- a/src/api/server.ts
+++ b/src/api/server.ts
@@ -54,4 +54,6 @@ app.use((req, res, next) => {
 require('./service/github')(app);
 require('./service/twitter')(app);
 
+require('./bot/interfaces/line')(app);
+
 module.exports = app;
diff --git a/src/web/app/common/scripts/get-post-summary.js b/src/common/get-post-summary.js
similarity index 88%
rename from src/web/app/common/scripts/get-post-summary.js
rename to src/common/get-post-summary.js
index 83eda8f6b4..f7a481a164 100644
--- a/src/web/app/common/scripts/get-post-summary.js
+++ b/src/common/get-post-summary.js
@@ -1,3 +1,7 @@
+/**
+ * 投稿を表す文字列を取得します。
+ * @param {*} post 投稿
+ */
 const summarize = post => {
 	let summary = post.text ? post.text : '';
 
diff --git a/src/config.ts b/src/config.ts
index f8facdee2e..0ea332f67d 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -68,6 +68,9 @@ type Source = {
 		hook_secret: string;
 		username: string;
 	};
+	line_bot?: {
+		channel_secret: string;
+	};
 	analysis?: {
 		mecab_command?: string;
 	};
diff --git a/src/web/app/desktop/script.js b/src/web/app/desktop/script.js
index 2e81147943..e3dc8b7d96 100644
--- a/src/web/app/desktop/script.js
+++ b/src/web/app/desktop/script.js
@@ -11,7 +11,7 @@ import * as riot from 'riot';
 import init from '../init';
 import route from './router';
 import fuckAdBlock from './scripts/fuck-ad-block';
-import getPostSummary from '../common/scripts/get-post-summary';
+import getPostSummary from '../../../common/get-post-summary';
 
 /**
  * init
diff --git a/src/web/app/desktop/tags/notifications.tag b/src/web/app/desktop/tags/notifications.tag
index 21e4fe7fa5..4747d1c0f4 100644
--- a/src/web/app/desktop/tags/notifications.tag
+++ b/src/web/app/desktop/tags/notifications.tag
@@ -207,7 +207,7 @@
 
 	</style>
 	<script>
-		import getPostSummary from '../../common/scripts/get-post-summary';
+		import getPostSummary from '../../../../common/get-post-summary';
 		this.getPostSummary = getPostSummary;
 
 		this.mixin('i');
diff --git a/src/web/app/desktop/tags/pages/home.tag b/src/web/app/desktop/tags/pages/home.tag
index 124a2eefa3..a56c546059 100644
--- a/src/web/app/desktop/tags/pages/home.tag
+++ b/src/web/app/desktop/tags/pages/home.tag
@@ -8,7 +8,7 @@
 	</style>
 	<script>
 		import Progress from '../../../common/scripts/loading';
-		import getPostSummary from '../../../common/scripts/get-post-summary';
+		import getPostSummary from '../../../../../common/get-post-summary';
 
 		this.mixin('i');
 		this.mixin('api');
diff --git a/src/web/app/mobile/tags/notification-preview.tag b/src/web/app/mobile/tags/notification-preview.tag
index 077ae78463..36b4f5eda7 100644
--- a/src/web/app/mobile/tags/notification-preview.tag
+++ b/src/web/app/mobile/tags/notification-preview.tag
@@ -110,7 +110,7 @@
 
 	</style>
 	<script>
-		import getPostSummary from '../../common/scripts/get-post-summary';
+		import getPostSummary from '../../../../common/get-post-summary';
 		this.getPostSummary = getPostSummary;
 		this.notification = this.opts.notification;
 	</script>
diff --git a/src/web/app/mobile/tags/notification.tag b/src/web/app/mobile/tags/notification.tag
index 3663709525..416493ee23 100644
--- a/src/web/app/mobile/tags/notification.tag
+++ b/src/web/app/mobile/tags/notification.tag
@@ -163,7 +163,7 @@
 
 	</style>
 	<script>
-		import getPostSummary from '../../common/scripts/get-post-summary';
+		import getPostSummary from '../../../../common/get-post-summary';
 		this.getPostSummary = getPostSummary;
 		this.notification = this.opts.notification;
 	</script>
diff --git a/src/web/app/mobile/tags/notifications.tag b/src/web/app/mobile/tags/notifications.tag
index 2f314769db..9985b3351c 100644
--- a/src/web/app/mobile/tags/notifications.tag
+++ b/src/web/app/mobile/tags/notifications.tag
@@ -78,7 +78,7 @@
 
 	</style>
 	<script>
-		import getPostSummary from '../../common/scripts/get-post-summary';
+		import getPostSummary from '../../../../common/get-post-summary';
 		this.getPostSummary = getPostSummary;
 
 		this.mixin('api');
diff --git a/src/web/app/mobile/tags/page/home.tag b/src/web/app/mobile/tags/page/home.tag
index efb5068a57..6f7369798e 100644
--- a/src/web/app/mobile/tags/page/home.tag
+++ b/src/web/app/mobile/tags/page/home.tag
@@ -9,7 +9,7 @@
 	<script>
 		import ui from '../../scripts/ui-event';
 		import Progress from '../../../common/scripts/loading';
-		import getPostSummary from '../../../common/scripts/get-post-summary';
+		import getPostSummary from '../../../../../common/get-post-summary';
 		import openPostForm from '../../scripts/open-post-form';
 
 		this.mixin('i');
diff --git a/src/web/app/mobile/tags/post-detail.tag b/src/web/app/mobile/tags/post-detail.tag
index dc032fe964..4be1a8080a 100644
--- a/src/web/app/mobile/tags/post-detail.tag
+++ b/src/web/app/mobile/tags/post-detail.tag
@@ -264,7 +264,7 @@
 	</style>
 	<script>
 		import compile from '../../common/scripts/text-compiler';
-		import getPostSummary from '../../common/scripts/get-post-summary';
+		import getPostSummary from '../../../../common/get-post-summary';
 		import openPostForm from '../scripts/open-post-form';
 
 		this.mixin('api');
diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag
index 2b0948ac34..80debbf66e 100644
--- a/src/web/app/mobile/tags/timeline.tag
+++ b/src/web/app/mobile/tags/timeline.tag
@@ -464,7 +464,7 @@
 	</style>
 	<script>
 		import compile from '../../common/scripts/text-compiler';
-		import getPostSummary from '../../common/scripts/get-post-summary';
+		import getPostSummary from '../../../../common/get-post-summary';
 		import openPostForm from '../scripts/open-post-form';
 
 		this.mixin('api');
diff --git a/src/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag
index f29f0a0c86..cc34074218 100644
--- a/src/web/app/mobile/tags/user.tag
+++ b/src/web/app/mobile/tags/user.tag
@@ -428,7 +428,7 @@
 
 	</style>
 	<script>
-		import summary from '../../common/scripts/get-post-summary';
+		import summary from '../../../../common/get-post-summary';
 
 		this.post = this.opts.post;
 		this.text = summary(this.post);
diff --git a/tslint.json b/tslint.json
index dfd8309675..33704ca43b 100644
--- a/tslint.json
+++ b/tslint.json
@@ -23,6 +23,7 @@
 		"comment-format": [false],
 		"interface-over-type-literal": false,
 		"max-line-length": [false],
+		"max-classes-per-file": false,
 		"member-ordering": [false],
 		"ban-types": [
 			"Object"

From a2abb9b2080161122a6bd46396e6c16c53cff54e Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Fri, 6 Oct 2017 19:25:08 +0000
Subject: [PATCH 192/364] chore(package): update @types/node to version 8.0.33

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

diff --git a/package.json b/package.json
index 0336725bda..1ffe6a6717 100644
--- a/package.json
+++ b/package.json
@@ -53,7 +53,7 @@
     "@types/morgan": "1.7.33",
     "@types/ms": "0.7.30",
     "@types/multer": "1.3.2",
-    "@types/node": "8.0.32",
+    "@types/node": "8.0.33",
     "@types/ratelimiter": "2.1.28",
     "@types/redis": "2.6.0",
     "@types/request": "2.0.4",

From fffea98462b7ba3250118b4bdf4e5678cf6e4ba7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Oct 2017 04:30:57 +0900
Subject: [PATCH 193/364] :v:

---
 src/api/bot/core.ts            |  4 +-
 src/api/bot/interfaces/line.ts | 69 +++++++++++++++++++++++++++-------
 src/api/server.ts              |  7 +++-
 src/config.ts                  |  1 +
 tslint.json                    |  1 +
 5 files changed, 66 insertions(+), 16 deletions(-)

diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
index 002ac1b06e..47989dbaa2 100644
--- a/src/api/bot/core.ts
+++ b/src/api/bot/core.ts
@@ -4,11 +4,11 @@ import * as bcrypt from 'bcryptjs';
 import User, { IUser } from '../models/user';
 
 export default class BotCore extends EventEmitter {
-	public user: IUser;
+	public user: IUser = null;
 
 	private context: Context = null;
 
-	constructor(user: IUser) {
+	constructor(user?: IUser) {
 		super();
 
 		this.user = user;
diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts
index 4bee844c12..8c7d6acfd5 100644
--- a/src/api/bot/interfaces/line.ts
+++ b/src/api/bot/interfaces/line.ts
@@ -1,34 +1,77 @@
 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 config from '../../../conf';
-/*import BotCore from '../core';
+import BotCore from '../core';
 
-const sessions: {
-	userId: string;
+const sessions: Array<{
+	sourceId: string;
 	session: BotCore;
-}[] = [];
-*/
+}> = [];
+
 module.exports = async (app: express.Application) => {
 	if (config.line_bot == null) return;
 
 	const handler = new EventEmitter();
 
-	app.post('/hooks/line', (req, res, next) => {
-		// req.headers['X-Line-Signature'] は常に string ですが、型定義の都合上
-		// string | string[] になっているので string を明示しています
-		const sig1 = req.headers['X-Line-Signature'] as string;
+	handler.on('message', async (ev) => {
+		// テキスト以外(スタンプなど)は無視
+		if (ev.message.type !== 'text') return;
 
-		const hash = crypto.createHmac('sha256', config.line_bot.channel_secret)
-			.update(JSON.stringify(req.body));
+		const sourceId = ev.source.userId;
+		let session = sessions.find(s => {
+			return s.sourceId === sourceId;
+		});
+
+		if (!session) {
+			session = {
+				sourceId: sourceId,
+				session: new BotCore()
+			};
+
+			sessions.push(session);
+		}
+
+		const res = await session.session.q(ev.message.text);
+
+		request({
+			url: 'https://api.line.me/v2/bot/message/reply',
+			headers: {
+				'Authorization': `Bearer ${config.line_bot.channel_access_token}`
+			},
+			json: {
+				replyToken: ev.replyToken,
+				messages: [{
+					type: 'text',
+					text: res
+				}]
+			}
+		}, (err, res, body) => {
+			if (err) {
+				console.error(err);
+				return;
+			}
+		});
+	});
+
+	app.post('/hooks/line', (req, res, next) => {
+		// req.headers['x-line-signature'] は常に string ですが、型定義の都合上
+		// string | string[] になっているので string を明示しています
+		const sig1 = req.headers['x-line-signature'] as string;
+
+		const hash = crypto.createHmac('SHA256', config.line_bot.channel_secret)
+			.update((req as any).rawBody);
 
 		const sig2 = hash.digest('base64');
 
 		// シグネチャ比較
 		if (sig1 === sig2) {
-			console.log(req.body);
-			handler.emit(req.body.type);
+			req.body.events.forEach(ev => {
+				handler.emit(ev.type, ev);
+			});
+
 			res.sendStatus(200);
 		} else {
 			res.sendStatus(400);
diff --git a/src/api/server.ts b/src/api/server.ts
index fdff0c7546..3de32d9eab 100644
--- a/src/api/server.ts
+++ b/src/api/server.ts
@@ -19,7 +19,12 @@ app.disable('x-powered-by');
 app.set('etag', false);
 app.use(bodyParser.urlencoded({ extended: true }));
 app.use(bodyParser.json({
-	type: ['application/json', 'text/plain']
+	type: ['application/json', 'text/plain'],
+	verify: (req, res, buf, encoding) => {
+		if (buf && buf.length) {
+			(req as any).rawBody = buf.toString(encoding || 'utf8');
+		}
+	}
 }));
 app.use(cors({
 	origin: true
diff --git a/src/config.ts b/src/config.ts
index 0ea332f67d..46a93f5fef 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -70,6 +70,7 @@ type Source = {
 	};
 	line_bot?: {
 		channel_secret: string;
+		channel_access_token: string;
 	};
 	analysis?: {
 		mecab_command?: string;
diff --git a/tslint.json b/tslint.json
index 33704ca43b..1c44579512 100644
--- a/tslint.json
+++ b/tslint.json
@@ -16,6 +16,7 @@
 		"ordered-imports": [false],
 		"arrow-parens": false,
 		"object-literal-shorthand": false,
+		"object-literal-key-quotes": false,
 		"triple-equals": [false],
 		"no-shadowed-variable": false,
 		"no-string-literal": false,

From 0b20c3caa69b438c4d551fe1fc1a09364c9650eb Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Oct 2017 04:48:56 +0900
Subject: [PATCH 194/364] :v:

---
 src/api/bot/interfaces/line.ts | 37 ++++++++++++++++++++++++++--------
 1 file changed, 29 insertions(+), 8 deletions(-)

diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts
index 8c7d6acfd5..9e1c813570 100644
--- a/src/api/bot/interfaces/line.ts
+++ b/src/api/bot/interfaces/line.ts
@@ -2,13 +2,13 @@ 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';
 
 const sessions: Array<{
 	sourceId: string;
-	session: BotCore;
+	core: BotCore;
 }> = [];
 
 module.exports = async (app: express.Application) => {
@@ -21,22 +21,43 @@ module.exports = async (app: express.Application) => {
 		if (ev.message.type !== 'text') return;
 
 		const sourceId = ev.source.userId;
-		let session = sessions.find(s => {
-			return s.sourceId === sourceId;
-		});
+		let session = sessions.find(s => s.sourceId === sourceId);
 
 		if (!session) {
+			const user = await User.findOne({
+				line: {
+					user_id: sourceId
+				}
+			});
+
+			let core: BotCore;
+
+			if (user) {
+				core = new BotCore(user);
+			} else {
+				core = new BotCore();
+				core.on('set-user', user => {
+					User.update(user._id, {
+						$set: {
+							line: {
+								user_id: sourceId
+							}
+						}
+					});
+				});
+			}
+
 			session = {
 				sourceId: sourceId,
-				session: new BotCore()
+				core: core
 			};
 
 			sessions.push(session);
 		}
 
-		const res = await session.session.q(ev.message.text);
+		const res = await session.core.q(ev.message.text);
 
-		request({
+		request.post({
 			url: 'https://api.line.me/v2/bot/message/reply',
 			headers: {
 				'Authorization': `Bearer ${config.line_bot.channel_access_token}`

From 0f2b503f2f545ce11fcca8c2c17a1084fd91df5e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Oct 2017 05:50:01 +0900
Subject: [PATCH 195/364] :v:

---
 src/api/bot/core.ts            | 52 ++++++++++++++++++++++++++++++++--
 src/api/bot/interfaces/line.ts | 38 +++++++++++++------------
 2 files changed, 70 insertions(+), 20 deletions(-)

diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
index 47989dbaa2..1f624c5f0a 100644
--- a/src/api/bot/core.ts
+++ b/src/api/bot/core.ts
@@ -14,6 +14,31 @@ export default class BotCore extends EventEmitter {
 		this.user = user;
 	}
 
+	private setContect(context: Context) {
+		this.context = context;
+		this.emit('updated');
+
+		if (context) {
+			context.on('updated', () => {
+				this.emit('updated');
+			});
+		}
+	}
+
+	public export() {
+		return {
+			user: this.user,
+			context: this.context ? this.context.export() : null
+		};
+	}
+
+	public static import(data) {
+		const core = new BotCore();
+		core.user = data.user;
+		core.setContect(data.context ? Context.import(core, data.context) : null);
+		return core;
+	}
+
 	public async q(query: string): Promise<string> {
 		if (this.context != null) {
 			return await this.context.q(query);
@@ -22,9 +47,11 @@ export default class BotCore extends EventEmitter {
 		switch (query) {
 			case 'ping':
 				return 'PONG';
+			case 'me':
+				return this.user ? `${this.user.name}としてサインインしています` : 'サインインしていません';
 			case 'ログイン':
 			case 'サインイン':
-				this.context = new SigninContext(this);
+				this.setContect(new SigninContext(this));
 				return await this.context.greet();
 			default:
 				return '?';
@@ -34,18 +61,26 @@ export default class BotCore extends EventEmitter {
 	public setUser(user: IUser) {
 		this.user = user;
 		this.emit('set-user', user);
+		this.emit('updated');
 	}
 }
 
-abstract class Context {
+abstract class Context extends EventEmitter {
 	protected core: BotCore;
 
 	public abstract async greet(): Promise<string>;
 	public abstract async q(query: string): Promise<string>;
+	public abstract export(): any;
 
 	constructor(core: BotCore) {
+		super();
 		this.core = core;
 	}
+
+	public static import(core: BotCore, data: any) {
+		if (data.type == 'signin') return SigninContext.import(core, data.content);
+		return null;
+	}
 }
 
 class SigninContext extends Context {
@@ -71,6 +106,7 @@ class SigninContext extends Context {
 				return `${query}というユーザーは存在しませんでした... もう一度教えてください:`;
 			} else {
 				this.temporaryUser = user;
+				this.emit('updated');
 				return `パスワードを教えてください:`;
 			}
 		} else {
@@ -85,4 +121,16 @@ class SigninContext extends Context {
 			}
 		}
 	}
+
+	public export() {
+		return {
+			temporaryUser: this.temporaryUser
+		};
+	}
+
+	public static import(core: BotCore, data: any) {
+		const context = new SigninContext(core);
+		context.temporaryUser = data.temporaryUser;
+		return context;
+	}
 }
diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts
index 9e1c813570..61aa728121 100644
--- a/src/api/bot/interfaces/line.ts
+++ b/src/api/bot/interfaces/line.ts
@@ -5,11 +5,10 @@ import * as crypto from 'crypto';
 import User from '../../models/user';
 import config from '../../../conf';
 import BotCore from '../core';
+import _redis from '../../../db/redis';
+import prominence = require('prominence');
 
-const sessions: Array<{
-	sourceId: string;
-	core: BotCore;
-}> = [];
+const redis = prominence(_redis);
 
 module.exports = async (app: express.Application) => {
 	if (config.line_bot == null) return;
@@ -21,22 +20,23 @@ module.exports = async (app: express.Application) => {
 		if (ev.message.type !== 'text') return;
 
 		const sourceId = ev.source.userId;
-		let session = sessions.find(s => s.sourceId === sourceId);
+		const sessionId = `line-bot-sessions:${sourceId}`;
 
-		if (!session) {
+		const _session = await redis.get(sessionId);
+		let session: BotCore;
+
+		if (_session == null) {
 			const user = await User.findOne({
 				line: {
 					user_id: sourceId
 				}
 			});
 
-			let core: BotCore;
-
 			if (user) {
-				core = new BotCore(user);
+				session = new BotCore(user);
 			} else {
-				core = new BotCore();
-				core.on('set-user', user => {
+				session = new BotCore();
+				session.on('set-user', user => {
 					User.update(user._id, {
 						$set: {
 							line: {
@@ -47,16 +47,18 @@ module.exports = async (app: express.Application) => {
 				});
 			}
 
-			session = {
-				sourceId: sourceId,
-				core: core
-			};
-
-			sessions.push(session);
+			redis.set(sessionId, JSON.stringify(session.export()));
+		} else {
+			session = BotCore.import(JSON.parse(_session));
 		}
 
-		const res = await session.core.q(ev.message.text);
+		session.on('updated', () => {
+			redis.set(sessionId, JSON.stringify(session.export()));
+		});
 
+		const res = await session.q(ev.message.text);
+
+		// 返信
 		request.post({
 			url: 'https://api.line.me/v2/bot/message/reply',
 			headers: {

From f5fe36825b0c83d8770e0f3cdf5c2d477eb2b995 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Oct 2017 06:03:16 +0900
Subject: [PATCH 196/364] :v:

---
 src/api/bot/core.ts            |  5 +++--
 src/api/bot/interfaces/line.ts | 20 ++++++++------------
 2 files changed, 11 insertions(+), 14 deletions(-)

diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
index 1f624c5f0a..4109519ca5 100644
--- a/src/api/bot/core.ts
+++ b/src/api/bot/core.ts
@@ -34,7 +34,7 @@ export default class BotCore extends EventEmitter {
 
 	public static import(data) {
 		const core = new BotCore();
-		core.user = data.user;
+		core.user = data.user ? data.user : null;
 		core.setContect(data.context ? Context.import(core, data.context) : null);
 		return core;
 	}
@@ -84,7 +84,7 @@ abstract class Context extends EventEmitter {
 }
 
 class SigninContext extends Context {
-	private temporaryUser: IUser;
+	private temporaryUser: IUser = null;
 
 	public async greet(): Promise<string> {
 		return 'まずユーザー名を教えてください:';
@@ -124,6 +124,7 @@ class SigninContext extends Context {
 
 	public export() {
 		return {
+			type: 'signin',
 			temporaryUser: this.temporaryUser
 		};
 	}
diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts
index 61aa728121..52559eaeff 100644
--- a/src/api/bot/interfaces/line.ts
+++ b/src/api/bot/interfaces/line.ts
@@ -32,20 +32,16 @@ module.exports = async (app: express.Application) => {
 				}
 			});
 
-			if (user) {
-				session = new BotCore(user);
-			} else {
-				session = new BotCore();
-				session.on('set-user', user => {
-					User.update(user._id, {
-						$set: {
-							line: {
-								user_id: sourceId
-							}
+			session = new BotCore(user);
+			session.on('set-user', user => {
+				User.update(user._id, {
+					$set: {
+						line: {
+							user_id: sourceId
 						}
-					});
+					}
 				});
-			}
+			});
 
 			redis.set(sessionId, JSON.stringify(session.export()));
 		} else {

From 1d4f9378ca57dc9a19d4ff13e676669e01814612 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Oct 2017 06:43:36 +0900
Subject: [PATCH 197/364] :v:

---
 src/api/bot/core.ts            | 99 +++++++++++++++++++++++++++++++---
 src/api/bot/interfaces/line.ts | 13 ++++-
 src/tsconfig.json              |  1 +
 tsconfig.json                  |  1 +
 4 files changed, 105 insertions(+), 9 deletions(-)

diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
index 4109519ca5..cf2bdef1dc 100644
--- a/src/api/bot/core.ts
+++ b/src/api/bot/core.ts
@@ -3,6 +3,8 @@ import * as bcrypt from 'bcryptjs';
 
 import User, { IUser } from '../models/user';
 
+import getPostSummary from '../../common/get-post-summary.js';
+
 export default class BotCore extends EventEmitter {
 	public user: IUser = null;
 
@@ -14,7 +16,7 @@ export default class BotCore extends EventEmitter {
 		this.user = user;
 	}
 
-	private setContect(context: Context) {
+	public setContext(context: Context) {
 		this.context = context;
 		this.emit('updated');
 
@@ -35,7 +37,7 @@ export default class BotCore extends EventEmitter {
 	public static import(data) {
 		const core = new BotCore();
 		core.user = data.user ? data.user : null;
-		core.setContect(data.context ? Context.import(core, data.context) : null);
+		core.setContext(data.context ? Context.import(core, data.context) : null);
 		return core;
 	}
 
@@ -47,22 +49,74 @@ export default class BotCore extends EventEmitter {
 		switch (query) {
 			case 'ping':
 				return 'PONG';
+
+			case 'help':
+			case 'ヘルプ':
+				return 'コマンド一覧です:' +
+					'help: これです\n' +
+					'me: アカウント情報を見ます\n' +
+					'login, signin: サインインします\n' +
+					'logout, signout: サインアウトします\n' +
+					'post: 投稿します\n' +
+					'tl: タイムラインを見ます\n';
+
 			case 'me':
 				return this.user ? `${this.user.name}としてサインインしています` : 'サインインしていません';
+
+			case 'login':
+			case 'signin':
 			case 'ログイン':
 			case 'サインイン':
-				this.setContect(new SigninContext(this));
+				this.setContext(new SigninContext(this));
 				return await this.context.greet();
-			default:
+
+			case 'logout':
+			case 'signout':
+			case 'ログアウト':
+			case 'サインアウト':
+				if (this.user == null) return '今はサインインしてないですよ!';
+				this.signout();
+				return 'ご利用ありがとうございました <3';
+
+			case 'post':
+			case '投稿':
+				if (this.user == null) return 'まずサインインしてください。';
+				this.setContext(new PostContext(this));
+				return await this.context.greet();
+
+			case 'tl':
+			case 'タイムライン':
+				return await this.getTl();
+
+				default:
 				return '?';
 		}
 	}
 
-	public setUser(user: IUser) {
+	public signin(user: IUser) {
 		this.user = user;
-		this.emit('set-user', user);
+		this.emit('signin', user);
 		this.emit('updated');
 	}
+
+	public signout() {
+		const user = this.user;
+		this.user = null;
+		this.emit('signout', user);
+		this.emit('updated');
+	}
+
+	public async getTl() {
+		if (this.user == null) return 'まずサインインしてください。';
+
+		const tl = await require('../endpoints/posts/timeline')({}, this.user);
+
+		const text = tl
+			.map(post => getPostSummary(post))
+			.join('\n-----\n');
+
+		return text;
+	}
 }
 
 abstract class Context extends EventEmitter {
@@ -78,6 +132,7 @@ abstract class Context extends EventEmitter {
 	}
 
 	public static import(core: BotCore, data: any) {
+		if (data.type == 'post') return PostContext.import(core, data.content);
 		if (data.type == 'signin') return SigninContext.import(core, data.content);
 		return null;
 	}
@@ -114,7 +169,8 @@ class SigninContext extends Context {
 			const same = bcrypt.compareSync(query, this.temporaryUser.password);
 
 			if (same) {
-				this.core.setUser(this.temporaryUser);
+				this.core.signin(this.temporaryUser);
+				this.core.setContext(null);
 				return `${this.temporaryUser.name}さん、おかえりなさい!`;
 			} else {
 				return `パスワードが違います... もう一度教えてください:`;
@@ -125,7 +181,9 @@ class SigninContext extends Context {
 	public export() {
 		return {
 			type: 'signin',
-			temporaryUser: this.temporaryUser
+			content: {
+				temporaryUser: this.temporaryUser
+			}
 		};
 	}
 
@@ -135,3 +193,28 @@ class SigninContext extends Context {
 		return context;
 	}
 }
+
+class PostContext extends Context {
+	public async greet(): Promise<string> {
+		return '内容:';
+	}
+
+	public async q(query: string): Promise<string> {
+		await require('../endpoints/posts/create')({
+			text: query
+		}, this.core.user);
+		this.core.setContext(null);
+		return '投稿しましたよ!';
+	}
+
+	public export() {
+		return {
+			type: 'post'
+		};
+	}
+
+	public static import(core: BotCore, data: any) {
+		const context = new PostContext(core);
+		return context;
+	}
+}
diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts
index 52559eaeff..437f29cb3c 100644
--- a/src/api/bot/interfaces/line.ts
+++ b/src/api/bot/interfaces/line.ts
@@ -33,7 +33,8 @@ module.exports = async (app: express.Application) => {
 			});
 
 			session = new BotCore(user);
-			session.on('set-user', user => {
+
+			session.on('signin', user => {
 				User.update(user._id, {
 					$set: {
 						line: {
@@ -43,6 +44,16 @@ module.exports = async (app: express.Application) => {
 				});
 			});
 
+			session.on('signout', user => {
+				User.update(user._id, {
+					$set: {
+						line: {
+							user_id: null
+						}
+					}
+				});
+			});
+
 			redis.set(sessionId, JSON.stringify(session.export()));
 		} else {
 			session = BotCore.import(JSON.parse(_session));
diff --git a/src/tsconfig.json b/src/tsconfig.json
index ecff047a74..36600eed2b 100644
--- a/src/tsconfig.json
+++ b/src/tsconfig.json
@@ -1,5 +1,6 @@
 {
   "compilerOptions": {
+    "allowJs": true,
     "noEmitOnError": false,
     "noImplicitAny": false,
     "noImplicitReturns": true,
diff --git a/tsconfig.json b/tsconfig.json
index 064a04e4d2..a38ff220b2 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,5 +1,6 @@
 {
   "compilerOptions": {
+    "allowJs": true,
     "noEmitOnError": false,
     "noImplicitAny": false,
     "noImplicitReturns": true,

From fe98dd927de6906671dfd3aa9671080ab6a065c6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Oct 2017 06:58:50 +0900
Subject: [PATCH 198/364] :v:

---
 package.json                                            | 3 ++-
 src/api/bot/core.ts                                     | 6 ++++--
 src/common/{get-post-summary.js => get-post-summary.ts} | 2 +-
 src/web/app/desktop/script.js                           | 2 +-
 src/web/app/desktop/tags/notifications.tag              | 2 +-
 src/web/app/desktop/tags/pages/home.tag                 | 2 +-
 src/web/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/page/home.tag                   | 2 +-
 src/web/app/mobile/tags/post-detail.tag                 | 2 +-
 src/web/app/mobile/tags/timeline.tag                    | 2 +-
 src/web/app/mobile/tags/user.tag                        | 2 +-
 webpack/module/rules/index.ts                           | 4 +++-
 webpack/module/rules/typescript.ts                      | 8 ++++++++
 15 files changed, 28 insertions(+), 15 deletions(-)
 rename src/common/{get-post-summary.js => get-post-summary.ts} (94%)
 create mode 100644 webpack/module/rules/typescript.ts

diff --git a/package.json b/package.json
index 0336725bda..6a9e7e26f0 100644
--- a/package.json
+++ b/package.json
@@ -64,14 +64,15 @@
     "@types/webpack": "3.0.13",
     "@types/webpack-stream": "3.2.7",
     "@types/websocket": "0.0.34",
+    "awesome-typescript-loader": "^3.2.3",
     "chai": "4.1.2",
     "chai-http": "3.0.0",
     "css-loader": "0.28.7",
     "event-stream": "3.3.4",
     "gulp": "3.9.1",
     "gulp-cssnano": "2.1.2",
-    "gulp-imagemin": "3.4.0",
     "gulp-htmlmin": "3.0.0",
+    "gulp-imagemin": "3.4.0",
     "gulp-mocha": "4.3.1",
     "gulp-pug": "3.3.0",
     "gulp-rename": "1.2.2",
diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
index cf2bdef1dc..57330e67eb 100644
--- a/src/api/bot/core.ts
+++ b/src/api/bot/core.ts
@@ -3,7 +3,7 @@ import * as bcrypt from 'bcryptjs';
 
 import User, { IUser } from '../models/user';
 
-import getPostSummary from '../../common/get-post-summary.js';
+import getPostSummary from '../../common/get-post-summary';
 
 export default class BotCore extends EventEmitter {
 	public user: IUser = null;
@@ -109,7 +109,9 @@ export default class BotCore extends EventEmitter {
 	public async getTl() {
 		if (this.user == null) return 'まずサインインしてください。';
 
-		const tl = await require('../endpoints/posts/timeline')({}, this.user);
+		const tl = await require('../endpoints/posts/timeline')({
+			limit: 5
+		}, this.user);
 
 		const text = tl
 			.map(post => getPostSummary(post))
diff --git a/src/common/get-post-summary.js b/src/common/get-post-summary.ts
similarity index 94%
rename from src/common/get-post-summary.js
rename to src/common/get-post-summary.ts
index f7a481a164..f628a32b41 100644
--- a/src/common/get-post-summary.js
+++ b/src/common/get-post-summary.ts
@@ -2,7 +2,7 @@
  * 投稿を表す文字列を取得します。
  * @param {*} post 投稿
  */
-const summarize = post => {
+const summarize = (post: any): string => {
 	let summary = post.text ? post.text : '';
 
 	// メディアが添付されているとき
diff --git a/src/web/app/desktop/script.js b/src/web/app/desktop/script.js
index e3dc8b7d96..46a7fce700 100644
--- a/src/web/app/desktop/script.js
+++ b/src/web/app/desktop/script.js
@@ -11,7 +11,7 @@ import * as riot from 'riot';
 import init from '../init';
 import route from './router';
 import fuckAdBlock from './scripts/fuck-ad-block';
-import getPostSummary from '../../../common/get-post-summary';
+import getPostSummary from '../../../common/get-post-summary.ts';
 
 /**
  * init
diff --git a/src/web/app/desktop/tags/notifications.tag b/src/web/app/desktop/tags/notifications.tag
index 4747d1c0f4..1046358ce9 100644
--- a/src/web/app/desktop/tags/notifications.tag
+++ b/src/web/app/desktop/tags/notifications.tag
@@ -207,7 +207,7 @@
 
 	</style>
 	<script>
-		import getPostSummary from '../../../../common/get-post-summary';
+		import getPostSummary from '../../../../common/get-post-summary.ts';
 		this.getPostSummary = getPostSummary;
 
 		this.mixin('i');
diff --git a/src/web/app/desktop/tags/pages/home.tag b/src/web/app/desktop/tags/pages/home.tag
index a56c546059..e8ba4023de 100644
--- a/src/web/app/desktop/tags/pages/home.tag
+++ b/src/web/app/desktop/tags/pages/home.tag
@@ -8,7 +8,7 @@
 	</style>
 	<script>
 		import Progress from '../../../common/scripts/loading';
-		import getPostSummary from '../../../../../common/get-post-summary';
+		import getPostSummary from '../../../../../common/get-post-summary.ts';
 
 		this.mixin('i');
 		this.mixin('api');
diff --git a/src/web/app/mobile/tags/notification-preview.tag b/src/web/app/mobile/tags/notification-preview.tag
index 36b4f5eda7..1fdcc57641 100644
--- a/src/web/app/mobile/tags/notification-preview.tag
+++ b/src/web/app/mobile/tags/notification-preview.tag
@@ -110,7 +110,7 @@
 
 	</style>
 	<script>
-		import getPostSummary from '../../../../common/get-post-summary';
+		import getPostSummary from '../../../../common/get-post-summary.ts';
 		this.getPostSummary = getPostSummary;
 		this.notification = this.opts.notification;
 	</script>
diff --git a/src/web/app/mobile/tags/notification.tag b/src/web/app/mobile/tags/notification.tag
index 416493ee23..53222b9dbe 100644
--- a/src/web/app/mobile/tags/notification.tag
+++ b/src/web/app/mobile/tags/notification.tag
@@ -163,7 +163,7 @@
 
 	</style>
 	<script>
-		import getPostSummary from '../../../../common/get-post-summary';
+		import getPostSummary from '../../../../common/get-post-summary.ts';
 		this.getPostSummary = getPostSummary;
 		this.notification = this.opts.notification;
 	</script>
diff --git a/src/web/app/mobile/tags/notifications.tag b/src/web/app/mobile/tags/notifications.tag
index 9985b3351c..7370aa84d3 100644
--- a/src/web/app/mobile/tags/notifications.tag
+++ b/src/web/app/mobile/tags/notifications.tag
@@ -78,7 +78,7 @@
 
 	</style>
 	<script>
-		import getPostSummary from '../../../../common/get-post-summary';
+		import getPostSummary from '../../../../common/get-post-summary.ts';
 		this.getPostSummary = getPostSummary;
 
 		this.mixin('api');
diff --git a/src/web/app/mobile/tags/page/home.tag b/src/web/app/mobile/tags/page/home.tag
index 6f7369798e..3b0255b293 100644
--- a/src/web/app/mobile/tags/page/home.tag
+++ b/src/web/app/mobile/tags/page/home.tag
@@ -9,7 +9,7 @@
 	<script>
 		import ui from '../../scripts/ui-event';
 		import Progress from '../../../common/scripts/loading';
-		import getPostSummary from '../../../../../common/get-post-summary';
+		import getPostSummary from '../../../../../common/get-post-summary.ts';
 		import openPostForm from '../../scripts/open-post-form';
 
 		this.mixin('i');
diff --git a/src/web/app/mobile/tags/post-detail.tag b/src/web/app/mobile/tags/post-detail.tag
index 4be1a8080a..ed275749ec 100644
--- a/src/web/app/mobile/tags/post-detail.tag
+++ b/src/web/app/mobile/tags/post-detail.tag
@@ -264,7 +264,7 @@
 	</style>
 	<script>
 		import compile from '../../common/scripts/text-compiler';
-		import getPostSummary from '../../../../common/get-post-summary';
+		import getPostSummary from '../../../../common/get-post-summary.ts';
 		import openPostForm from '../scripts/open-post-form';
 
 		this.mixin('api');
diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag
index 80debbf66e..5ecc2df9d1 100644
--- a/src/web/app/mobile/tags/timeline.tag
+++ b/src/web/app/mobile/tags/timeline.tag
@@ -464,7 +464,7 @@
 	</style>
 	<script>
 		import compile from '../../common/scripts/text-compiler';
-		import getPostSummary from '../../../../common/get-post-summary';
+		import getPostSummary from '../../../../common/get-post-summary.ts';
 		import openPostForm from '../scripts/open-post-form';
 
 		this.mixin('api');
diff --git a/src/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag
index cc34074218..a332e930e2 100644
--- a/src/web/app/mobile/tags/user.tag
+++ b/src/web/app/mobile/tags/user.tag
@@ -428,7 +428,7 @@
 
 	</style>
 	<script>
-		import summary from '../../../../common/get-post-summary';
+		import summary from '../../../../common/get-post-summary.ts';
 
 		this.post = this.opts.post;
 		this.text = summary(this.post);
diff --git a/webpack/module/rules/index.ts b/webpack/module/rules/index.ts
index 2308f4e535..2707a9c2f1 100644
--- a/webpack/module/rules/index.ts
+++ b/webpack/module/rules/index.ts
@@ -2,10 +2,12 @@ import i18n from './i18n';
 import themeColor from './theme-color';
 import tag from './tag';
 import stylus from './stylus';
+import typescript from './typescript';
 
 export default (lang, locale) => [
 	i18n(lang, locale),
 	themeColor(),
 	tag(),
-	stylus()
+	stylus(),
+	typescript()
 ];
diff --git a/webpack/module/rules/typescript.ts b/webpack/module/rules/typescript.ts
new file mode 100644
index 0000000000..eb2b279a55
--- /dev/null
+++ b/webpack/module/rules/typescript.ts
@@ -0,0 +1,8 @@
+/**
+ * TypeScript
+ */
+
+export default () => ({
+	test: /\.ts$/,
+	use: 'awesome-typescript-loader'
+});

From 91251916ce750c8c361668fd7969e4a9dec91ee3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Oct 2017 07:23:00 +0900
Subject: [PATCH 199/364] :v:

---
 src/api/bot/core.ts    | 6 +++---
 src/api/models/user.ts | 7 +++++++
 2 files changed, 10 insertions(+), 3 deletions(-)

diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
index 57330e67eb..25190b6d65 100644
--- a/src/api/bot/core.ts
+++ b/src/api/bot/core.ts
@@ -1,7 +1,7 @@
 import * as EventEmitter from 'events';
 import * as bcrypt from 'bcryptjs';
 
-import User, { IUser } from '../models/user';
+import User, { IUser, init as initUser } from '../models/user';
 
 import getPostSummary from '../../common/get-post-summary';
 
@@ -36,7 +36,7 @@ export default class BotCore extends EventEmitter {
 
 	public static import(data) {
 		const core = new BotCore();
-		core.user = data.user ? data.user : null;
+		core.user = data.user ? initUser(data.user) : null;
 		core.setContext(data.context ? Context.import(core, data.context) : null);
 		return core;
 	}
@@ -52,7 +52,7 @@ export default class BotCore extends EventEmitter {
 
 			case 'help':
 			case 'ヘルプ':
-				return 'コマンド一覧です:' +
+				return 'コマンド一覧です:\n' +
 					'help: これです\n' +
 					'me: アカウント情報を見ます\n' +
 					'login, signin: サインインします\n' +
diff --git a/src/api/models/user.ts b/src/api/models/user.ts
index 4f8086d42b..08ffe4a109 100644
--- a/src/api/models/user.ts
+++ b/src/api/models/user.ts
@@ -73,3 +73,10 @@ export type IUser = {
 	is_suspended: boolean;
 	keywords: string[];
 };
+
+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);
+}

From 3795b88277fdb765148d83869fef3b480976bd1c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Oct 2017 07:34:35 +0900
Subject: [PATCH 200/364] :v:

---
 src/api/models/user.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/api/models/user.ts b/src/api/models/user.ts
index 08ffe4a109..b2f3af09fa 100644
--- a/src/api/models/user.ts
+++ b/src/api/models/user.ts
@@ -79,4 +79,5 @@ export function init(user): IUser {
 	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);
+	return user;
 }

From 88e8dffb5bf8a21b46495b8f7ff2f68562e432d6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Oct 2017 07:50:47 +0900
Subject: [PATCH 201/364] :v:

---
 src/api/bot/core.ts | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
index 25190b6d65..b1a745a760 100644
--- a/src/api/bot/core.ts
+++ b/src/api/bot/core.ts
@@ -5,6 +5,13 @@ import User, { IUser, init as initUser } from '../models/user';
 
 import getPostSummary from '../../common/get-post-summary';
 
+function getUserSummary(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.description}」`;
+}
+
 export default class BotCore extends EventEmitter {
 	public user: IUser = null;
 
@@ -52,7 +59,7 @@ export default class BotCore extends EventEmitter {
 
 			case 'help':
 			case 'ヘルプ':
-				return 'コマンド一覧です:\n' +
+				return '利用可能なコマンド一覧です:\n' +
 					'help: これです\n' +
 					'me: アカウント情報を見ます\n' +
 					'login, signin: サインインします\n' +
@@ -61,7 +68,7 @@ export default class BotCore extends EventEmitter {
 					'tl: タイムラインを見ます\n';
 
 			case 'me':
-				return this.user ? `${this.user.name}としてサインインしています` : 'サインインしていません';
+				return this.user ? `${this.user.name}としてサインインしています。\n\n${getUserSummary(this.user)}` : 'サインインしていません';
 
 			case 'login':
 			case 'signin':

From 83c3490426cd041c2a6dc7da894123f6a533d40f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Oct 2017 08:04:55 +0900
Subject: [PATCH 202/364] :v:

---
 src/api/bot/core.ts | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
index b1a745a760..5038665573 100644
--- a/src/api/bot/core.ts
+++ b/src/api/bot/core.ts
@@ -161,8 +161,7 @@ class SigninContext extends Context {
 				username_lower: query.toLowerCase()
 			}, {
 				fields: {
-					data: false,
-					profile: false
+					data: false
 				}
 			});
 

From edb2dae7fc47c8382d0c899671b0f95d5f257b7f Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sat, 7 Oct 2017 08:07:48 +0000
Subject: [PATCH 203/364] fix(package): update monk to version 6.0.5

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

diff --git a/package.json b/package.json
index 6a9e7e26f0..9c5c4ed9f9 100644
--- a/package.json
+++ b/package.json
@@ -124,7 +124,7 @@
     "mecab-async": "^0.1.0",
     "moji": "^0.5.1",
     "mongodb": "2.2.31",
-    "monk": "6.0.4",
+    "monk": "6.0.5",
     "morgan": "1.9.0",
     "ms": "2.0.0",
     "multer": "1.3.0",

From d1f5d62251526c01b23e741ec363152cc21cc58c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Oct 2017 17:20:47 +0900
Subject: [PATCH 204/364] Refactor

---
 src/api/bot/core.ts            | 18 ++++++++++--------
 src/common/get-user-summary.ts | 12 ++++++++++++
 2 files changed, 22 insertions(+), 8 deletions(-)
 create mode 100644 src/common/get-user-summary.ts

diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
index 5038665573..4970f24469 100644
--- a/src/api/bot/core.ts
+++ b/src/api/bot/core.ts
@@ -4,14 +4,12 @@ import * as bcrypt from 'bcryptjs';
 import User, { IUser, init as initUser } from '../models/user';
 
 import getPostSummary from '../../common/get-post-summary';
+import getUserSummary from '../../common/get-user-summary';
 
-function getUserSummary(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.description}」`;
-}
 
+/**
+ * Botの頭脳
+ */
 export default class BotCore extends EventEmitter {
 	public user: IUser = null;
 
@@ -23,6 +21,10 @@ export default class BotCore extends EventEmitter {
 		this.user = user;
 	}
 
+	public clearContext() {
+		this.setContext(null);
+	}
+
 	public setContext(context: Context) {
 		this.context = context;
 		this.emit('updated');
@@ -178,7 +180,7 @@ class SigninContext extends Context {
 
 			if (same) {
 				this.core.signin(this.temporaryUser);
-				this.core.setContext(null);
+				this.core.clearContext();
 				return `${this.temporaryUser.name}さん、おかえりなさい!`;
 			} else {
 				return `パスワードが違います... もう一度教えてください:`;
@@ -211,7 +213,7 @@ class PostContext extends Context {
 		await require('../endpoints/posts/create')({
 			text: query
 		}, this.core.user);
-		this.core.setContext(null);
+		this.core.clearContext();
 		return '投稿しましたよ!';
 	}
 
diff --git a/src/common/get-user-summary.ts b/src/common/get-user-summary.ts
new file mode 100644
index 0000000000..1bec2f9a26
--- /dev/null
+++ b/src/common/get-user-summary.ts
@@ -0,0 +1,12 @@
+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.profile.location}、誕生日: ${user.profile.birthday}\n` +
+		`「${user.description}」`;
+}

From 96b6ef4d9ba73dc04eb601ffedee52d6b6ab580a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Oct 2017 18:30:04 +0900
Subject: [PATCH 205/364] :v:

---
 src/api/bot/core.ts            |  72 ++++++++++++++--------
 src/api/bot/interfaces/line.ts | 105 +++++++++++++++++++++++----------
 2 files changed, 122 insertions(+), 55 deletions(-)

diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
index 4970f24469..4e168a6054 100644
--- a/src/api/bot/core.ts
+++ b/src/api/bot/core.ts
@@ -43,18 +43,26 @@ export default class BotCore extends EventEmitter {
 		};
 	}
 
-	public static import(data) {
-		const core = new BotCore();
-		core.user = data.user ? initUser(data.user) : null;
-		core.setContext(data.context ? Context.import(core, data.context) : null);
-		return core;
+	protected _import(data) {
+		this.user = data.user ? initUser(data.user) : null;
+		this.setContext(data.context ? Context.import(this, data.context) : null);
 	}
 
-	public async q(query: string): Promise<string> {
+	public static import(data) {
+		const bot = new BotCore();
+		bot._import(data);
+		return bot;
+	}
+
+	public async q(query: string): Promise<string | void> {
 		if (this.context != null) {
 			return await this.context.q(query);
 		}
 
+		if (/^@[a-zA-Z0-9-]+$/.test(query)) {
+			return await this.showUserCommand(query);
+		}
+
 		switch (query) {
 			case 'ping':
 				return 'PONG';
@@ -67,7 +75,8 @@ export default class BotCore extends EventEmitter {
 					'login, signin: サインインします\n' +
 					'logout, signout: サインアウトします\n' +
 					'post: 投稿します\n' +
-					'tl: タイムラインを見ます\n';
+					'tl: タイムラインを見ます\n' +
+					'@<ユーザー名>: ユーザーを表示します';
 
 			case 'me':
 				return this.user ? `${this.user.name}としてサインインしています。\n\n${getUserSummary(this.user)}` : 'サインインしていません';
@@ -76,6 +85,7 @@ export default class BotCore extends EventEmitter {
 			case 'signin':
 			case 'ログイン':
 			case 'サインイン':
+				if (this.user != null) return '既にサインインしていますよ!';
 				this.setContext(new SigninContext(this));
 				return await this.context.greet();
 
@@ -95,9 +105,9 @@ export default class BotCore extends EventEmitter {
 
 			case 'tl':
 			case 'タイムライン':
-				return await this.getTl();
+				return await this.tlCommand();
 
-				default:
+			default:
 				return '?';
 		}
 	}
@@ -115,7 +125,7 @@ export default class BotCore extends EventEmitter {
 		this.emit('updated');
 	}
 
-	public async getTl() {
+	public async tlCommand(): Promise<string | void> {
 		if (this.user == null) return 'まずサインインしてください。';
 
 		const tl = await require('../endpoints/posts/timeline')({
@@ -128,23 +138,37 @@ export default class BotCore extends EventEmitter {
 
 		return text;
 	}
+
+	public async showUserCommand(q: string): Promise<string | void> {
+		try {
+			const user = await require('../endpoints/users/show')({
+				username: q.substr(1)
+			}, this.user);
+
+			const text = getUserSummary(user);
+
+			return text;
+		} catch (e) {
+			return `問題が発生したようです...: ${e}`;
+		}
+	}
 }
 
 abstract class Context extends EventEmitter {
-	protected core: BotCore;
+	protected bot: BotCore;
 
 	public abstract async greet(): Promise<string>;
 	public abstract async q(query: string): Promise<string>;
 	public abstract export(): any;
 
-	constructor(core: BotCore) {
+	constructor(bot: BotCore) {
 		super();
-		this.core = core;
+		this.bot = bot;
 	}
 
-	public static import(core: BotCore, data: any) {
-		if (data.type == 'post') return PostContext.import(core, data.content);
-		if (data.type == 'signin') return SigninContext.import(core, data.content);
+	public static import(bot: BotCore, data: any) {
+		if (data.type == 'post') return PostContext.import(bot, data.content);
+		if (data.type == 'signin') return SigninContext.import(bot, data.content);
 		return null;
 	}
 }
@@ -179,8 +203,8 @@ class SigninContext extends Context {
 			const same = bcrypt.compareSync(query, this.temporaryUser.password);
 
 			if (same) {
-				this.core.signin(this.temporaryUser);
-				this.core.clearContext();
+				this.bot.signin(this.temporaryUser);
+				this.bot.clearContext();
 				return `${this.temporaryUser.name}さん、おかえりなさい!`;
 			} else {
 				return `パスワードが違います... もう一度教えてください:`;
@@ -197,8 +221,8 @@ class SigninContext extends Context {
 		};
 	}
 
-	public static import(core: BotCore, data: any) {
-		const context = new SigninContext(core);
+	public static import(bot: BotCore, data: any) {
+		const context = new SigninContext(bot);
 		context.temporaryUser = data.temporaryUser;
 		return context;
 	}
@@ -212,8 +236,8 @@ class PostContext extends Context {
 	public async q(query: string): Promise<string> {
 		await require('../endpoints/posts/create')({
 			text: query
-		}, this.core.user);
-		this.core.clearContext();
+		}, this.bot.user);
+		this.bot.clearContext();
 		return '投稿しましたよ!';
 	}
 
@@ -223,8 +247,8 @@ class PostContext extends Context {
 		};
 	}
 
-	public static import(core: BotCore, data: any) {
-		const context = new PostContext(core);
+	public static import(bot: BotCore, data: any) {
+		const context = new PostContext(bot);
 		return context;
 	}
 }
diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts
index 437f29cb3c..03dc2a85ba 100644
--- a/src/api/bot/interfaces/line.ts
+++ b/src/api/bot/interfaces/line.ts
@@ -10,20 +10,83 @@ import prominence = require('prominence');
 
 const redis = prominence(_redis);
 
+class LineBot extends BotCore {
+	private replyToken: string;
+
+	private reply(messages: any[]) {
+		request.post({
+			url: 'https://api.line.me/v2/bot/message/reply',
+			headers: {
+				'Authorization': `Bearer ${config.line_bot.channel_access_token}`
+			},
+			json: {
+				replyToken: this.replyToken,
+				messages: messages
+			}
+		}, (err, res, body) => {
+			if (err) {
+				console.error(err);
+				return;
+			}
+		});
+	}
+
+	public async react(ev: any): Promise<void> {
+		// テキスト以外(スタンプなど)は無視
+		if (ev.message.type !== 'text') return;
+
+		const res = await this.q(ev.message.text);
+
+		if (res == null) return;
+
+		// 返信
+		this.reply([{
+			type: 'text',
+			text: res
+		}]);
+	}
+
+	public static import(data) {
+		const bot = new LineBot();
+		bot._import(data);
+		return bot;
+	}
+
+	public async showUserCommand(q: string) {
+		const user = await require('../endpoints/users/show')({
+			username: q.substr(1)
+		}, this.user);
+
+		this.reply([{
+			type: 'template',
+			altText: await super.showUserCommand(q),
+			template: {
+				type: 'buttons',
+				thumbnailImageUrl: `${user.avatar_url}?thumbnail&size=1024`,
+				title: `${user.name} (@${user.username})`,
+				text: user.description || '(no description)',
+				actions: [{
+					type: 'uri',
+					label: 'Webで見る',
+					uri: `${config.url}/${user.username}`
+				}]
+			}
+		}]);
+	}
+}
+
 module.exports = async (app: express.Application) => {
 	if (config.line_bot == null) return;
 
 	const handler = new EventEmitter();
 
 	handler.on('message', async (ev) => {
-		// テキスト以外(スタンプなど)は無視
-		if (ev.message.type !== 'text') return;
 
 		const sourceId = ev.source.userId;
 		const sessionId = `line-bot-sessions:${sourceId}`;
 
 		const _session = await redis.get(sessionId);
-		let session: BotCore;
+		let bot: LineBot;
 
 		if (_session == null) {
 			const user = await User.findOne({
@@ -32,9 +95,9 @@ module.exports = async (app: express.Application) => {
 				}
 			});
 
-			session = new BotCore(user);
+			bot = new LineBot(user);
 
-			session.on('signin', user => {
+			bot.on('signin', user => {
 				User.update(user._id, {
 					$set: {
 						line: {
@@ -44,7 +107,7 @@ module.exports = async (app: express.Application) => {
 				});
 			});
 
-			session.on('signout', user => {
+			bot.on('signout', user => {
 				User.update(user._id, {
 					$set: {
 						line: {
@@ -54,36 +117,16 @@ module.exports = async (app: express.Application) => {
 				});
 			});
 
-			redis.set(sessionId, JSON.stringify(session.export()));
+			redis.set(sessionId, JSON.stringify(bot.export()));
 		} else {
-			session = BotCore.import(JSON.parse(_session));
+			bot = LineBot.import(JSON.parse(_session));
 		}
 
-		session.on('updated', () => {
-			redis.set(sessionId, JSON.stringify(session.export()));
+		bot.on('updated', () => {
+			redis.set(sessionId, JSON.stringify(bot.export()));
 		});
 
-		const res = await session.q(ev.message.text);
-
-		// 返信
-		request.post({
-			url: 'https://api.line.me/v2/bot/message/reply',
-			headers: {
-				'Authorization': `Bearer ${config.line_bot.channel_access_token}`
-			},
-			json: {
-				replyToken: ev.replyToken,
-				messages: [{
-					type: 'text',
-					text: res
-				}]
-			}
-		}, (err, res, body) => {
-			if (err) {
-				console.error(err);
-				return;
-			}
-		});
+		bot.react(ev);
 	});
 
 	app.post('/hooks/line', (req, res, next) => {

From 64db80662706fedbf1c0103187de413dcb2d1360 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Oct 2017 18:31:55 +0900
Subject: [PATCH 206/364] :v:

---
 src/api/bot/interfaces/line.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts
index 03dc2a85ba..c5e8293b92 100644
--- a/src/api/bot/interfaces/line.ts
+++ b/src/api/bot/interfaces/line.ts
@@ -32,6 +32,8 @@ class LineBot extends BotCore {
 	}
 
 	public async react(ev: any): Promise<void> {
+		this.replyToken = ev.replyToken;
+
 		// テキスト以外(スタンプなど)は無視
 		if (ev.message.type !== 'text') return;
 

From d6c11e31f38b83e9bed502d43bf8d543e956365b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Oct 2017 18:37:19 +0900
Subject: [PATCH 207/364] oops

---
 src/api/bot/core.ts | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
index 4e168a6054..84ac9a9b6e 100644
--- a/src/api/bot/core.ts
+++ b/src/api/bot/core.ts
@@ -6,7 +6,6 @@ import User, { IUser, init as initUser } from '../models/user';
 import getPostSummary from '../../common/get-post-summary';
 import getUserSummary from '../../common/get-user-summary';
 
-
 /**
  * Botの頭脳
  */

From 844c62e2578f15c728163bd924f164da6e25b957 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Oct 2017 18:50:44 +0900
Subject: [PATCH 208/364] :v:

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

diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
index 84ac9a9b6e..bc5818d976 100644
--- a/src/api/bot/core.ts
+++ b/src/api/bot/core.ts
@@ -140,7 +140,7 @@ export default class BotCore extends EventEmitter {
 
 	public async showUserCommand(q: string): Promise<string | void> {
 		try {
-			const user = await require('../endpoints/users/show')({
+			const user = await require('../../endpoints/users/show')({
 				username: q.substr(1)
 			}, this.user);
 

From 041499814a955b514970bbd3cb2d89480e6802a3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Oct 2017 19:09:10 +0900
Subject: [PATCH 209/364] :v:

---
 src/api/bot/interfaces/line.ts | 74 +++++++++++++++++++++++++++++-----
 1 file changed, 64 insertions(+), 10 deletions(-)

diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts
index c5e8293b92..cc634ca89c 100644
--- a/src/api/bot/interfaces/line.ts
+++ b/src/api/bot/interfaces/line.ts
@@ -7,9 +7,25 @@ import config from '../../../conf';
 import BotCore from '../core';
 import _redis from '../../../db/redis';
 import prominence = require('prominence');
+import getPostSummary from '../../../common/get-post-summary';
 
 const redis = prominence(_redis);
 
+// SEE: https://developers.line.me/media/messaging-api/messages/sticker_list.pdf
+const stickers = [
+	'297',
+	'298',
+	'299',
+	'300',
+	'301',
+	'302',
+	'303',
+	'304',
+	'305',
+	'306',
+	'307'
+];
+
 class LineBot extends BotCore {
 	private replyToken: string;
 
@@ -34,18 +50,36 @@ class LineBot extends BotCore {
 	public async react(ev: any): Promise<void> {
 		this.replyToken = ev.replyToken;
 
-		// テキスト以外(スタンプなど)は無視
-		if (ev.message.type !== 'text') return;
+		// テキスト
+		if (ev.message.type == 'text') {
+			const res = await this.q(ev.message.text);
 
-		const res = await this.q(ev.message.text);
+			if (res == null) return;
 
-		if (res == null) return;
-
-		// 返信
-		this.reply([{
-			type: 'text',
-			text: res
-		}]);
+			// 返信
+			this.reply([{
+				type: 'text',
+				text: res
+			}]);
+		// スタンプ
+		} else if (ev.message.type == 'sticker') {
+			// スタンプで返信
+			this.reply([{
+				type: 'sticker',
+				packageId: '4',
+				stickerId: stickers[Math.floor(Math.random() * stickers.length)]
+			}]);
+		// postback
+		} else if (ev.message.type == 'postback') {
+			const data = ev.message.postback.data;
+			const cmd = data.split('|')[0];
+			const arg = data.split('|')[1];
+			switch (cmd) {
+				case 'showtl':
+					this.showUserTimelinePostback(arg);
+					break;
+			}
+		}
 	}
 
 	public static import(data) {
@@ -68,6 +102,10 @@ class LineBot extends BotCore {
 				title: `${user.name} (@${user.username})`,
 				text: user.description || '(no description)',
 				actions: [{
+					type: 'postback',
+					label: 'タイムラインを見る',
+					data: `showtl|${user._id}`
+				}, {
 					type: 'uri',
 					label: 'Webで見る',
 					uri: `${config.url}/${user.username}`
@@ -75,6 +113,22 @@ class LineBot extends BotCore {
 			}
 		}]);
 	}
+
+	public async showUserTimelinePostback(userId: string) {
+		const tl = await require('../../endpoints/users/posts')({
+			user_id: userId,
+			limit: 5
+		}, this.user);
+
+		const text = tl
+			.map(post => getPostSummary(post))
+			.join('\n-----\n');
+
+		this.reply([{
+			type: 'text',
+			text: text
+		}]);
+	}
 }
 
 module.exports = async (app: express.Application) => {

From b1990bc8477ebf44158bfb3aea8a4d93a0f2821a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Oct 2017 19:15:32 +0900
Subject: [PATCH 210/364] :v:

---
 src/api/bot/interfaces/line.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts
index cc634ca89c..91240a325f 100644
--- a/src/api/bot/interfaces/line.ts
+++ b/src/api/bot/interfaces/line.ts
@@ -89,7 +89,7 @@ class LineBot extends BotCore {
 	}
 
 	public async showUserCommand(q: string) {
-		const user = await require('../endpoints/users/show')({
+		const user = await require('../../endpoints/users/show')({
 			username: q.substr(1)
 		}, this.user);
 

From a931bcbf8d325a1fb7346954b20b64065052727b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Oct 2017 19:29:32 +0900
Subject: [PATCH 211/364] :v:

---
 src/api/bot/interfaces/line.ts | 41 ++++++++++++++++++----------------
 1 file changed, 22 insertions(+), 19 deletions(-)

diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts
index 91240a325f..06b46c953b 100644
--- a/src/api/bot/interfaces/line.ts
+++ b/src/api/bot/interfaces/line.ts
@@ -50,25 +50,28 @@ class LineBot extends BotCore {
 	public async react(ev: any): Promise<void> {
 		this.replyToken = ev.replyToken;
 
-		// テキスト
-		if (ev.message.type == 'text') {
-			const res = await this.q(ev.message.text);
+		// メッセージ
+		if (ev.type == 'message') {
+			// テキスト
+			if (ev.message.type == 'text') {
+				const res = await this.q(ev.message.text);
 
-			if (res == null) return;
+				if (res == null) return;
 
-			// 返信
-			this.reply([{
-				type: 'text',
-				text: res
-			}]);
-		// スタンプ
-		} else if (ev.message.type == 'sticker') {
-			// スタンプで返信
-			this.reply([{
-				type: 'sticker',
-				packageId: '4',
-				stickerId: stickers[Math.floor(Math.random() * stickers.length)]
-			}]);
+				// 返信
+				this.reply([{
+					type: 'text',
+					text: res
+				}]);
+			// スタンプ
+			} else if (ev.message.type == 'sticker') {
+				// スタンプで返信
+				this.reply([{
+					type: 'sticker',
+					packageId: '4',
+					stickerId: stickers[Math.floor(Math.random() * stickers.length)]
+				}]);
+			}
 		// postback
 		} else if (ev.message.type == 'postback') {
 			const data = ev.message.postback.data;
@@ -136,7 +139,7 @@ module.exports = async (app: express.Application) => {
 
 	const handler = new EventEmitter();
 
-	handler.on('message', async (ev) => {
+	handler.on('event', async (ev) => {
 
 		const sourceId = ev.source.userId;
 		const sessionId = `line-bot-sessions:${sourceId}`;
@@ -198,7 +201,7 @@ module.exports = async (app: express.Application) => {
 		// シグネチャ比較
 		if (sig1 === sig2) {
 			req.body.events.forEach(ev => {
-				handler.emit(ev.type, ev);
+				handler.emit('event', ev);
 			});
 
 			res.sendStatus(200);

From 103979dd6ff567ca72a08411b9c0859a0f8bb6fc Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Oct 2017 19:37:51 +0900
Subject: [PATCH 212/364] :v:

---
 src/api/bot/interfaces/line.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts
index 06b46c953b..956dcc6574 100644
--- a/src/api/bot/interfaces/line.ts
+++ b/src/api/bot/interfaces/line.ts
@@ -73,8 +73,8 @@ class LineBot extends BotCore {
 				}]);
 			}
 		// postback
-		} else if (ev.message.type == 'postback') {
-			const data = ev.message.postback.data;
+		} else if (ev.type == 'postback') {
+			const data = ev.postback.data;
 			const cmd = data.split('|')[0];
 			const arg = data.split('|')[1];
 			switch (cmd) {

From d4d110a5c269bf799f2486fcd6a1782d10155481 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 7 Oct 2017 19:39:36 +0900
Subject: [PATCH 213/364] :v:

---
 src/api/bot/interfaces/line.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts
index 956dcc6574..2bf62c1f6c 100644
--- a/src/api/bot/interfaces/line.ts
+++ b/src/api/bot/interfaces/line.ts
@@ -107,7 +107,7 @@ class LineBot extends BotCore {
 				actions: [{
 					type: 'postback',
 					label: 'タイムラインを見る',
-					data: `showtl|${user._id}`
+					data: `showtl|${user.id}`
 				}, {
 					type: 'uri',
 					label: 'Webで見る',

From b18e4fea9851ce0692b3f1a627c1d220d5f16b9c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Oct 2017 03:24:10 +0900
Subject: [PATCH 214/364] :v:

---
 src/api/bot/core.ts            |  51 ++++++
 src/api/bot/interfaces/line.ts | 108 +++++++------
 src/common/othello.ts          | 275 +++++++++++++++++++++++++++++++++
 3 files changed, 391 insertions(+), 43 deletions(-)
 create mode 100644 src/common/othello.ts

diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
index bc5818d976..6042862d39 100644
--- a/src/api/bot/core.ts
+++ b/src/api/bot/core.ts
@@ -6,6 +6,8 @@ import User, { IUser, init as initUser } from '../models/user';
 import getPostSummary from '../../common/get-post-summary';
 import getUserSummary from '../../common/get-user-summary';
 
+import Othello, { ai as othelloAi } from '../../common/othello';
+
 /**
  * Botの頭脳
  */
@@ -106,6 +108,11 @@ export default class BotCore extends EventEmitter {
 			case 'タイムライン':
 				return await this.tlCommand();
 
+			case 'othello':
+			case 'オセロ':
+				this.setContext(new OthelloContext(this));
+				return await this.context.greet();
+
 			default:
 				return '?';
 		}
@@ -124,6 +131,18 @@ export default class BotCore extends EventEmitter {
 		this.emit('updated');
 	}
 
+	public async refreshUser() {
+		this.user = await User.findOne({
+			_id: this.user._id
+		}, {
+			fields: {
+				data: false
+			}
+		});
+
+		this.emit('updated');
+	}
+
 	public async tlCommand(): Promise<string | void> {
 		if (this.user == null) return 'まずサインインしてください。';
 
@@ -166,6 +185,7 @@ abstract class Context extends EventEmitter {
 	}
 
 	public static import(bot: BotCore, data: any) {
+		if (data.type == 'othello') return OthelloContext.import(bot, data.content);
 		if (data.type == 'post') return PostContext.import(bot, data.content);
 		if (data.type == 'signin') return SigninContext.import(bot, data.content);
 		return null;
@@ -251,3 +271,34 @@ class PostContext extends Context {
 		return context;
 	}
 }
+
+class OthelloContext extends Context {
+	private othello: Othello = null;
+
+	public async greet(): Promise<string> {
+		this.othello = new Othello();
+		return this.othello.toPatternString('black');
+	}
+
+	public async q(query: string): Promise<string> {
+		this.othello.setByNumber('black', parseInt(query, 10));
+		othelloAi('white', this.othello);
+		return 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;
+	}
+}
diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts
index 2bf62c1f6c..0caa71ed2b 100644
--- a/src/api/bot/interfaces/line.ts
+++ b/src/api/bot/interfaces/line.ts
@@ -50,38 +50,44 @@ class LineBot extends BotCore {
 	public async react(ev: any): Promise<void> {
 		this.replyToken = ev.replyToken;
 
-		// メッセージ
-		if (ev.type == 'message') {
-			// テキスト
-			if (ev.message.type == 'text') {
-				const res = await this.q(ev.message.text);
+		switch (ev.type) {
+			// メッセージ
+			case 'message':
+				switch (ev.message.type) {
+					// テキスト
+					case 'text':
+						const res = await this.q(ev.message.text);
+						if (res == null) return;
+						// 返信
+						this.reply([{
+							type: 'text',
+							text: res
+						}]);
+						break;
 
-				if (res == null) return;
+					// スタンプ
+					case 'sticker':
+						// スタンプで返信
+						this.reply([{
+							type: 'sticker',
+							packageId: '4',
+							stickerId: stickers[Math.floor(Math.random() * stickers.length)]
+						}]);
+						break;
+				}
+				break;
 
-				// 返信
-				this.reply([{
-					type: 'text',
-					text: res
-				}]);
-			// スタンプ
-			} else if (ev.message.type == 'sticker') {
-				// スタンプで返信
-				this.reply([{
-					type: 'sticker',
-					packageId: '4',
-					stickerId: stickers[Math.floor(Math.random() * stickers.length)]
-				}]);
-			}
-		// postback
-		} else if (ev.type == 'postback') {
-			const data = ev.postback.data;
-			const cmd = data.split('|')[0];
-			const arg = data.split('|')[1];
-			switch (cmd) {
-				case 'showtl':
-					this.showUserTimelinePostback(arg);
-					break;
-			}
+			// postback
+			case 'postback':
+				const data = ev.postback.data;
+				const cmd = data.split('|')[0];
+				const arg = data.split('|')[1];
+				switch (cmd) {
+					case 'showtl':
+						this.showUserTimelinePostback(arg);
+						break;
+				}
+				break;
 		}
 	}
 
@@ -96,6 +102,28 @@ class LineBot extends BotCore {
 			username: q.substr(1)
 		}, this.user);
 
+		const actions = [];
+
+		actions.push({
+			type: 'postback',
+			label: 'タイムラインを見る',
+			data: `showtl|${user.id}`
+		});
+
+		if (user.twitter) {
+			actions.push({
+				type: 'uri',
+				label: 'Twitterアカウントを見る',
+				uri: `https://twitter.com/${user.twitter.screen_name}`
+			});
+		}
+
+		actions.push({
+			type: 'uri',
+			label: 'Webで見る',
+			uri: `${config.url}/${user.username}`
+		});
+
 		this.reply([{
 			type: 'template',
 			altText: await super.showUserCommand(q),
@@ -104,15 +132,7 @@ class LineBot extends BotCore {
 				thumbnailImageUrl: `${user.avatar_url}?thumbnail&size=1024`,
 				title: `${user.name} (@${user.username})`,
 				text: user.description || '(no description)',
-				actions: [{
-					type: 'postback',
-					label: 'タイムラインを見る',
-					data: `showtl|${user.id}`
-				}, {
-					type: 'uri',
-					label: 'Webで見る',
-					uri: `${config.url}/${user.username}`
-				}]
+				actions: actions
 			}
 		}]);
 	}
@@ -123,7 +143,7 @@ class LineBot extends BotCore {
 			limit: 5
 		}, this.user);
 
-		const text = tl
+		const text = `${tl[0].user.name}さんのタイムラインはこちらです:\n\n` + tl
 			.map(post => getPostSummary(post))
 			.join('\n-----\n');
 
@@ -144,10 +164,10 @@ module.exports = async (app: express.Application) => {
 		const sourceId = ev.source.userId;
 		const sessionId = `line-bot-sessions:${sourceId}`;
 
-		const _session = await redis.get(sessionId);
+		const session = await redis.get(sessionId);
 		let bot: LineBot;
 
-		if (_session == null) {
+		if (session == null) {
 			const user = await User.findOne({
 				line: {
 					user_id: sourceId
@@ -178,13 +198,15 @@ module.exports = async (app: express.Application) => {
 
 			redis.set(sessionId, JSON.stringify(bot.export()));
 		} else {
-			bot = LineBot.import(JSON.parse(_session));
+			bot = LineBot.import(JSON.parse(session));
 		}
 
 		bot.on('updated', () => {
 			redis.set(sessionId, JSON.stringify(bot.export()));
 		});
 
+		if (session != null) bot.refreshUser();
+
 		bot.react(ev);
 	});
 
diff --git a/src/common/othello.ts b/src/common/othello.ts
new file mode 100644
index 0000000000..0060401976
--- /dev/null
+++ b/src/common/othello.ts
@@ -0,0 +1,275 @@
+import * as EventEmitter from 'events';
+
+export default class Othello extends EventEmitter {
+	public board: Array<Array<'black' | 'white'>>;
+
+	/**
+	 * ゲームを初期化します
+	 */
+	constructor() {
+		super();
+
+		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]
+		];
+	}
+
+	public setByNumber(color, n) {
+		const ps = this.getPattern(color);
+		this.set(color, ps[n][0], ps[n][1]);
+	}
+
+	/**
+	 * 石を配置します
+	 */
+	public set(color, x, y) {
+		this.board[y][x] = color;
+
+		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.board[x][_y] = color;
+					}
+					break;
+
+				case 1: // 右上
+					for (let c = 0, i = 1; c < r[1]; c++, i++) {
+						this.board[x + i][y - i] = color;
+					}
+					break;
+
+				case 2: // 右
+					for (let c = 0, _x = x + 1; c < r[1]; c++, _x++) {
+						this.board[_x][y] = color;
+					}
+					break;
+
+				case 3: // 右下
+					for (let c = 0, i = 1; c < r[1]; c++, i++) {
+						this.board[x + i][y + i] = color;
+					}
+					break;
+
+				case 4: // 下
+					for (let c = 0, _y = y + 1; c < r[1]; c++, _y++) {
+						this.board[x][_y] = color;
+					}
+					break;
+
+				case 5: // 左下
+					for (let c = 0, i = 1; c < r[1]; c++, i++) {
+						this.board[x - i][y + i] = color;
+					}
+					break;
+
+				case 6: // 左
+					for (let c = 0, _x = x - 1; c < r[1]; c++, _x--) {
+						this.board[_x][y] = color;
+					}
+					break;
+
+				case 7: // 左上
+					for (let c = 0, i = 1; c < r[1]; c++, i++) {
+						this.board[x - i][y - i] = color;
+					}
+					break;
+				}
+		});
+
+		this.emit('set:' + color, x, y);
+	}
+
+	/**
+	 * 打つことができる場所を取得します
+	 */
+	public getPattern(myColor): number[][] {
+		const result = [];
+		this.board.forEach((stones, y) => stones.forEach((stone, x) => {
+			if (stone != null) return;
+			if (this.canReverse(myColor, x, y)) result.push([x, y]);
+		}));
+		return result;
+	}
+
+	/**
+	 * 指定の位置に石を打つことができるかどうか(相手の石を1つでも反転させられるか)を取得します
+	 */
+	public canReverse(myColor, targetx, targety): boolean {
+		return this.getReverse(myColor, targetx, targety) !== 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.board[x][y] == myColor && opponentStoneFound) {
+					return true;
+				} else if (this.board[x][y] == myColor && !opponentStoneFound) {
+					breaked = true;
+				} else if (this.board[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(8 - 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 < 8; c++, x++) {
+			if (iterate(x, targety)) {
+				res.push([2, c]);
+				break;
+			}
+		}
+
+		// 右下
+		iterate = createIterater();
+		for (let c = 0, i = 1; i < Math.min(8 - targetx, 8 - targety); c++, i++) {
+			if (iterate(targetx + i, targety + i)) {
+				res.push([3, c]);
+				break;
+			}
+		}
+
+		// 下
+		iterate = createIterater();
+		for (let c = 0, y = targety + 1; y < 8; c++, y++) {
+			if (iterate(targetx, y)) {
+				res.push([4, c]);
+				break;
+			}
+		}
+
+		// 左下
+		iterate = createIterater();
+		for (let c = 0, i = 1; i < Math.min(targetx, 8 - 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');
+	}
+
+	public toPatternString(color): string {
+		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] : '🔹';
+		}).join('')).join('\n');
+	}
+}
+/*
+export class Ai {
+	private othello: Othello;
+	private color: string;
+	private opponentColor: string;
+
+	constructor(color: string, othello: Othello) {
+		this.othello = othello;
+		this.color = color;
+		this.opponentColor = this.color == 'black' ? 'white' : 'black';
+
+		this.othello.on('set:' + this.opponentColor, () => {
+			this.turn();
+		});
+
+		if (this.color == 'black') {
+			this.turn();
+		}
+	}
+
+	public turn() {
+		const ps = this.othello.getPattern(this.color);
+		if (ps.length > 0) {
+			const p = ps[Math.floor(Math.random() * ps.length)];
+			this.othello.set(this.color, p[0], p[1]);
+
+			// 相手の打つ場所がない場合続けてAIのターン
+			if (this.othello.getPattern(this.opponentColor).length === 0) {
+				this.turn();
+			}
+		}
+	}
+}
+*/
+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 p = ps[Math.floor(Math.random() * ps.length)];
+			othello.set(color, p[0], p[1]);
+
+			// 相手の打つ場所がない場合続けてAIのターン
+			if (othello.getPattern(opponentColor).length === 0) {
+				think();
+			}
+		}
+	}
+}

From 0f7a7a4ebb1cbb8b3c1d983b9bf7db5e0d8362e5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Oct 2017 03:36:34 +0900
Subject: [PATCH 215/364] :v:

---
 src/api/bot/core.ts | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
index 6042862d39..ca5f5b89eb 100644
--- a/src/api/bot/core.ts
+++ b/src/api/bot/core.ts
@@ -275,8 +275,13 @@ class PostContext extends Context {
 class OthelloContext extends Context {
 	private othello: Othello = null;
 
-	public async greet(): Promise<string> {
+	constructor(bot: BotCore) {
+		super(bot);
+
 		this.othello = new Othello();
+	}
+
+	public async greet(): Promise<string> {
 		return this.othello.toPatternString('black');
 	}
 

From c14f946bd493d0d57d0928ccb1e6b823b44cc98f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Oct 2017 03:56:03 +0900
Subject: [PATCH 216/364] :v:

---
 src/common/othello.ts | 25 ++++++++++++++-----------
 1 file changed, 14 insertions(+), 11 deletions(-)

diff --git a/src/common/othello.ts b/src/common/othello.ts
index 0060401976..ef11514dff 100644
--- a/src/common/othello.ts
+++ b/src/common/othello.ts
@@ -38,49 +38,49 @@ export default class Othello extends EventEmitter {
 			switch (r[0]) {
 				case 0: // 上
 					for (let c = 0, _y = y - 1; c < r[1]; c++, _y--) {
-						this.board[x][_y] = color;
+						this.board[_y][x] = color;
 					}
 					break;
 
 				case 1: // 右上
 					for (let c = 0, i = 1; c < r[1]; c++, i++) {
-						this.board[x + i][y - i] = color;
+						this.board[y - i][x + i] = color;
 					}
 					break;
 
 				case 2: // 右
 					for (let c = 0, _x = x + 1; c < r[1]; c++, _x++) {
-						this.board[_x][y] = color;
+						this.board[y][_x] = color;
 					}
 					break;
 
 				case 3: // 右下
 					for (let c = 0, i = 1; c < r[1]; c++, i++) {
-						this.board[x + i][y + i] = color;
+						this.board[y + i][x + i] = color;
 					}
 					break;
 
 				case 4: // 下
 					for (let c = 0, _y = y + 1; c < r[1]; c++, _y++) {
-						this.board[x][_y] = color;
+						this.board[_y][x] = color;
 					}
 					break;
 
 				case 5: // 左下
 					for (let c = 0, i = 1; c < r[1]; c++, i++) {
-						this.board[x - i][y + i] = color;
+						this.board[y + i][x - i] = color;
 					}
 					break;
 
 				case 6: // 左
 					for (let c = 0, _x = x - 1; c < r[1]; c++, _x--) {
-						this.board[_x][y] = color;
+						this.board[y][_x] = color;
 					}
 					break;
 
 				case 7: // 左上
 					for (let c = 0, i = 1; c < r[1]; c++, i++) {
-						this.board[x - i][y - i] = color;
+						this.board[y - i][x - i] = color;
 					}
 					break;
 				}
@@ -117,11 +117,11 @@ export default class Othello extends EventEmitter {
 			return (x, y): any => {
 				if (breaked) {
 					return;
-				} else if (this.board[x][y] == myColor && opponentStoneFound) {
+				} else if (this.board[y][x] == myColor && opponentStoneFound) {
 					return true;
-				} else if (this.board[x][y] == myColor && !opponentStoneFound) {
+				} else if (this.board[y][x] == myColor && !opponentStoneFound) {
 					breaked = true;
-				} else if (this.board[x][y] == opponentColor) {
+				} else if (this.board[y][x] == opponentColor) {
 					opponentStoneFound = true;
 				} else {
 					breaked = true;
@@ -209,16 +209,19 @@ export default class Othello extends EventEmitter {
 	}
 
 	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');
 	}
 
 	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');
 	}

From e05144b93ee671e03739640868d32a4d31b91b35 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Oct 2017 04:13:38 +0900
Subject: [PATCH 217/364] :v:

---
 src/api/bot/core.ts   | 9 ++++++++-
 src/common/othello.ts | 2 ++
 2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
index ca5f5b89eb..207de71619 100644
--- a/src/api/bot/core.ts
+++ b/src/api/bot/core.ts
@@ -287,8 +287,15 @@ class OthelloContext extends Context {
 
 	public async q(query: string): Promise<string> {
 		this.othello.setByNumber('black', parseInt(query, 10));
+		const s = this.othello.toString() + '\n\n...(AI)...\n\n';
 		othelloAi('white', this.othello);
-		return this.othello.toPatternString('black');
+		if (this.othello.getPattern('black').length === 0) {
+			this.bot.clearContext();
+			return '~終了~';
+		} else {
+			this.emit('updated');
+			return s + this.othello.toPatternString('black');
+		}
 	}
 
 	public export() {
diff --git a/src/common/othello.ts b/src/common/othello.ts
index ef11514dff..0b377ff5d3 100644
--- a/src/common/othello.ts
+++ b/src/common/othello.ts
@@ -275,4 +275,6 @@ export function ai(color: string, othello: Othello) {
 			}
 		}
 	}
+
+	think();
 }

From 27102094c8fd0afb834ff545735a647095d02c0f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Oct 2017 12:01:57 +0900
Subject: [PATCH 218/364] :v:

---
 src/api/bot/core.ts | 65 ++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 64 insertions(+), 1 deletion(-)

diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
index 207de71619..d7655f8175 100644
--- a/src/api/bot/core.ts
+++ b/src/api/bot/core.ts
@@ -108,6 +108,11 @@ export default class BotCore extends EventEmitter {
 			case 'タイムライン':
 				return await this.tlCommand();
 
+			case 'guessing-game':
+			case '数当てゲーム':
+				this.setContext(new GuessingGameContext(this));
+				return await this.context.greet();
+
 			case 'othello':
 			case 'オセロ':
 				this.setContext(new OthelloContext(this));
@@ -185,6 +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 == 'othello') return OthelloContext.import(bot, data.content);
 		if (data.type == 'post') return PostContext.import(bot, data.content);
 		if (data.type == 'signin') return SigninContext.import(bot, data.content);
@@ -272,6 +278,56 @@ class PostContext extends Context {
 	}
 }
 
+class GuessingGameContext extends Context {
+	private secret: number;
+	private try: number;
+
+	public async greet(): Promise<string> {
+		this.secret = Math.floor(Math.random() * 100);
+		this.try = 0;
+		this.emit('updated');
+		return '0~100の秘密の数を当ててみてください:';
+	}
+
+	public async q(query: string): Promise<string> {
+		if (query == 'やめる') {
+			this.bot.clearContext();
+			return 'やめました。';
+		}
+
+		this.try++;
+		this.emit('updated');
+
+		const guess = parseInt(query, 10);
+
+		if (this.secret < guess) {
+			return `${guess}よりも小さいですね`;
+		} else if (this.secret > guess) {
+			return `${guess}よりも大きいですね`;
+		} else {
+			this.bot.clearContext();
+			return `正解です🎉 (${this.try}回目で当てました)`;
+		}
+	}
+
+	public export() {
+		return {
+			type: 'guessing-game',
+			content: {
+				secret: this.secret,
+				try: this.try
+			}
+		};
+	}
+
+	public static import(bot: BotCore, data: any) {
+		const context = new GuessingGameContext(bot);
+		context.secret = data.secret;
+		context.try = data.try;
+		return context;
+	}
+}
+
 class OthelloContext extends Context {
 	private othello: Othello = null;
 
@@ -286,12 +342,19 @@ class OthelloContext extends Context {
 	}
 
 	public async q(query: string): Promise<string> {
+		if (query == 'やめる') {
+			this.bot.clearContext();
+			return 'オセロをやめました。';
+		}
 		this.othello.setByNumber('black', parseInt(query, 10));
 		const s = this.othello.toString() + '\n\n...(AI)...\n\n';
 		othelloAi('white', this.othello);
 		if (this.othello.getPattern('black').length === 0) {
 			this.bot.clearContext();
-			return '~終了~';
+			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 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');

From 9f6f616ecc70f065c9739e89cc555ce55980e4ec Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Oct 2017 12:09:10 +0900
Subject: [PATCH 219/364] :v:

---
 src/api/bot/core.ts | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
index d7655f8175..b13402dce9 100644
--- a/src/api/bot/core.ts
+++ b/src/api/bot/core.ts
@@ -295,11 +295,15 @@ class GuessingGameContext extends Context {
 			return 'やめました。';
 		}
 
+		const guess = parseInt(query, 10);
+
+		if (isNaN(guess)) {
+			return '整数で推測してください。「やめる」と言うとゲームをやめます。';
+		}
+
 		this.try++;
 		this.emit('updated');
 
-		const guess = parseInt(query, 10);
-
 		if (this.secret < guess) {
 			return `${guess}よりも小さいですね`;
 		} else if (this.secret > guess) {

From a4ffd9c3214e856971d6c199d92203005ce86114 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Oct 2017 18:30:37 +0900
Subject: [PATCH 220/364] :v:

---
 src/api/bot/core.ts | 35 ++++++++++++++++++++++++-----------
 1 file changed, 24 insertions(+), 11 deletions(-)

diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
index b13402dce9..12d1b639e9 100644
--- a/src/api/bot/core.ts
+++ b/src/api/bot/core.ts
@@ -8,6 +8,13 @@ import getUserSummary from '../../common/get-user-summary';
 
 import Othello, { ai as othelloAi } from '../../common/othello';
 
+const hmm = [
+	'?',
+	'ふぅ~む...?',
+	'ちょっと何言ってるかわからないです',
+	'「ヘルプ」と言うと利用可能な操作が確認できますよ'
+];
+
 /**
  * Botの頭脳
  */
@@ -119,7 +126,7 @@ export default class BotCore extends EventEmitter {
 				return await this.context.greet();
 
 			default:
-				return '?';
+				return hmm[Math.floor(Math.random() * hmm.length)];
 		}
 	}
 
@@ -164,7 +171,7 @@ export default class BotCore extends EventEmitter {
 
 	public async showUserCommand(q: string): Promise<string | void> {
 		try {
-			const user = await require('../../endpoints/users/show')({
+			const user = await require('../endpoints/users/show')({
 				username: q.substr(1)
 			}, this.user);
 
@@ -280,11 +287,10 @@ class PostContext extends Context {
 
 class GuessingGameContext extends Context {
 	private secret: number;
-	private try: number;
+	private history: number[] = [];
 
 	public async greet(): Promise<string> {
 		this.secret = Math.floor(Math.random() * 100);
-		this.try = 0;
 		this.emit('updated');
 		return '0~100の秘密の数を当ててみてください:';
 	}
@@ -301,16 +307,16 @@ class GuessingGameContext extends Context {
 			return '整数で推測してください。「やめる」と言うとゲームをやめます。';
 		}
 
-		this.try++;
+		this.history.push(guess);
 		this.emit('updated');
 
 		if (this.secret < guess) {
-			return `${guess}よりも小さいですね`;
+			return this.history.indexOf(guess) === -1 ? `${guess}よりも小さいですね` : `もう一度言いますが${guess}より小さいですよ`;
 		} else if (this.secret > guess) {
-			return `${guess}よりも大きいですね`;
+			return this.history.indexOf(guess) === -1 ? `${guess}よりも大きいですね` : `もう一度言いますが${guess}より大きいですよ`;
 		} else {
 			this.bot.clearContext();
-			return `正解です🎉 (${this.try}回目で当てました)`;
+			return `正解です🎉 (${this.history.length}回目で当てました)`;
 		}
 	}
 
@@ -319,7 +325,7 @@ class GuessingGameContext extends Context {
 			type: 'guessing-game',
 			content: {
 				secret: this.secret,
-				try: this.try
+				history: this.history
 			}
 		};
 	}
@@ -327,7 +333,7 @@ class GuessingGameContext extends Context {
 	public static import(bot: BotCore, data: any) {
 		const context = new GuessingGameContext(bot);
 		context.secret = data.secret;
-		context.try = data.try;
+		context.history = data.history;
 		return context;
 	}
 }
@@ -350,7 +356,14 @@ class OthelloContext extends Context {
 			this.bot.clearContext();
 			return 'オセロをやめました。';
 		}
-		this.othello.setByNumber('black', parseInt(query, 10));
+
+		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) {

From e6474cf23905e839708f5d48c760c46e53f179d8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 8 Oct 2017 18:42:08 +0900
Subject: [PATCH 221/364] :v:

---
 src/api/bot/core.ts | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
index 12d1b639e9..53fb18119e 100644
--- a/src/api/bot/core.ts
+++ b/src/api/bot/core.ts
@@ -307,13 +307,15 @@ class GuessingGameContext extends Context {
 			return '整数で推測してください。「やめる」と言うとゲームをやめます。';
 		}
 
+		const firsttime = this.history.indexOf(guess) === -1;
+
 		this.history.push(guess);
 		this.emit('updated');
 
 		if (this.secret < guess) {
-			return this.history.indexOf(guess) === -1 ? `${guess}よりも小さいですね` : `もう一度言いますが${guess}より小さいですよ`;
+			return firsttime ? `${guess}よりも小さいですね` : `もう一度言いますが${guess}より小さいですよ`;
 		} else if (this.secret > guess) {
-			return this.history.indexOf(guess) === -1 ? `${guess}よりも大きいですね` : `もう一度言いますが${guess}より大きいですよ`;
+			return firsttime ? `${guess}よりも大きいですね` : `もう一度言いますが${guess}より大きいですよ`;
 		} else {
 			this.bot.clearContext();
 			return `正解です🎉 (${this.history.length}回目で当てました)`;

From 06a4cb93dfb6bd8d610949fb574be24915f88117 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Oct 2017 00:23:17 +0900
Subject: [PATCH 222/364] Refactor

---
 src/common/othello.ts | 63 ++++++++++---------------------------------
 1 file changed, 14 insertions(+), 49 deletions(-)

diff --git a/src/common/othello.ts b/src/common/othello.ts
index 0b377ff5d3..5f7000019a 100644
--- a/src/common/othello.ts
+++ b/src/common/othello.ts
@@ -1,14 +1,10 @@
-import * as EventEmitter from 'events';
-
-export default class Othello extends EventEmitter {
+export default class Othello {
 	public board: Array<Array<'black' | 'white'>>;
 
 	/**
 	 * ゲームを初期化します
 	 */
 	constructor() {
-		super();
-
 		this.board = [
 			[null, null, null, null, null, null, null, null],
 			[null, null, null, null, null, null, null, null],
@@ -26,11 +22,15 @@ export default class Othello extends EventEmitter {
 		this.set(color, ps[n][0], ps[n][1]);
 	}
 
+	private write(color, x, y) {
+		this.board[y][x] = color;
+	}
+
 	/**
 	 * 石を配置します
 	 */
 	public set(color, x, y) {
-		this.board[y][x] = color;
+		this.write(color, x, y);
 
 		const reverses = this.getReverse(color, x, y);
 
@@ -38,55 +38,53 @@ export default class Othello extends EventEmitter {
 			switch (r[0]) {
 				case 0: // 上
 					for (let c = 0, _y = y - 1; c < r[1]; c++, _y--) {
-						this.board[_y][x] = color;
+						this.write(color, x, _y);
 					}
 					break;
 
 				case 1: // 右上
 					for (let c = 0, i = 1; c < r[1]; c++, i++) {
-						this.board[y - i][x + i] = color;
+						this.write(color, x + i, y - i);
 					}
 					break;
 
 				case 2: // 右
 					for (let c = 0, _x = x + 1; c < r[1]; c++, _x++) {
-						this.board[y][_x] = color;
+						this.write(color, _x, y);
 					}
 					break;
 
 				case 3: // 右下
 					for (let c = 0, i = 1; c < r[1]; c++, i++) {
-						this.board[y + i][x + i] = color;
+						this.write(color, x + i, y + i);
 					}
 					break;
 
 				case 4: // 下
 					for (let c = 0, _y = y + 1; c < r[1]; c++, _y++) {
-						this.board[_y][x] = color;
+						this.write(color, x, _y);
 					}
 					break;
 
 				case 5: // 左下
 					for (let c = 0, i = 1; c < r[1]; c++, i++) {
-						this.board[y + i][x - i] = color;
+						this.write(color, x - i, y + i);
 					}
 					break;
 
 				case 6: // 左
 					for (let c = 0, _x = x - 1; c < r[1]; c++, _x--) {
-						this.board[y][_x] = color;
+						this.write(color, _x, y);
 					}
 					break;
 
 				case 7: // 左上
 					for (let c = 0, i = 1; c < r[1]; c++, i++) {
-						this.board[y - i][x - i] = color;
+						this.write(color, x - i, y - i);
 					}
 					break;
 				}
 		});
-
-		this.emit('set:' + color, x, y);
 	}
 
 	/**
@@ -226,40 +224,7 @@ export default class Othello extends EventEmitter {
 		}).join('')).join('\n');
 	}
 }
-/*
-export class Ai {
-	private othello: Othello;
-	private color: string;
-	private opponentColor: string;
 
-	constructor(color: string, othello: Othello) {
-		this.othello = othello;
-		this.color = color;
-		this.opponentColor = this.color == 'black' ? 'white' : 'black';
-
-		this.othello.on('set:' + this.opponentColor, () => {
-			this.turn();
-		});
-
-		if (this.color == 'black') {
-			this.turn();
-		}
-	}
-
-	public turn() {
-		const ps = this.othello.getPattern(this.color);
-		if (ps.length > 0) {
-			const p = ps[Math.floor(Math.random() * ps.length)];
-			this.othello.set(this.color, p[0], p[1]);
-
-			// 相手の打つ場所がない場合続けてAIのターン
-			if (this.othello.getPattern(this.opponentColor).length === 0) {
-				this.turn();
-			}
-		}
-	}
-}
-*/
 export function ai(color: string, othello: Othello) {
 	const opponentColor = color == 'black' ? 'white' : 'black';
 

From 1316f17dca5ac024c91ba93d0522a073f2c546be Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Oct 2017 00:43:53 +0900
Subject: [PATCH 223/364] Improve the othello AI

---
 src/common/othello.ts | 27 ++++++++++++++++++++++++---
 1 file changed, 24 insertions(+), 3 deletions(-)

diff --git a/src/common/othello.ts b/src/common/othello.ts
index 5f7000019a..15dc571ced 100644
--- a/src/common/othello.ts
+++ b/src/common/othello.ts
@@ -229,10 +229,31 @@ 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 p = ps[Math.floor(Math.random() * ps.length)];
-			othello.set(color, p[0], p[1]);
+
+		if (ps.length > 0) { // 打てる場所がある場合
+			// 角を取得
+			const corners = ps.filter(p =>
+				// 左上
+				(p[0] == 0 && p[1] == 0) ||
+				// 右上
+				(p[0] == 7 && p[1] == 0) ||
+				// 右下
+				(p[0] == 7 && p[1] == 7) ||
+				// 左下
+				(p[0] == 0 && p[1] == 7)
+			);
+
+			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) {

From c8dbf5776e20229a68ad623f9f05edc578185329 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Oct 2017 00:49:48 +0900
Subject: [PATCH 224/364] :v:

---
 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 15dc571ced..0f96d477ee 100644
--- a/src/common/othello.ts
+++ b/src/common/othello.ts
@@ -213,7 +213,7 @@ export default class Othello {
 
 	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);
 

From b717e575eacfd446d1daedc4c3297b985a8f69d9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Oct 2017 19:52:18 +0900
Subject: [PATCH 225/364] Refactor: Remove the magick numbers

---
 src/common/othello.ts | 18 ++++++++++--------
 1 file changed, 10 insertions(+), 8 deletions(-)

diff --git a/src/common/othello.ts b/src/common/othello.ts
index 0f96d477ee..aa53dee465 100644
--- a/src/common/othello.ts
+++ b/src/common/othello.ts
@@ -1,3 +1,5 @@
+const BOARD_SIZE = 8;
+
 export default class Othello {
 	public board: Array<Array<'black' | 'white'>>;
 
@@ -142,7 +144,7 @@ export default class Othello {
 
 		// 右上
 		iterate = createIterater();
-		for (let c = 0, i = 1; i < Math.min(8 - targetx, targety); c++, i++) {
+		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;
@@ -151,7 +153,7 @@ export default class Othello {
 
 		// 右
 		iterate = createIterater();
-		for (let c = 0, x = targetx + 1; x < 8; c++, x++) {
+		for (let c = 0, x = targetx + 1; x < BOARD_SIZE; c++, x++) {
 			if (iterate(x, targety)) {
 				res.push([2, c]);
 				break;
@@ -160,7 +162,7 @@ export default class Othello {
 
 		// 右下
 		iterate = createIterater();
-		for (let c = 0, i = 1; i < Math.min(8 - targetx, 8 - 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;
@@ -169,7 +171,7 @@ export default class Othello {
 
 		// 下
 		iterate = createIterater();
-		for (let c = 0, y = targety + 1; y < 8; c++, y++) {
+		for (let c = 0, y = targety + 1; y < BOARD_SIZE; c++, y++) {
 			if (iterate(targetx, y)) {
 				res.push([4, c]);
 				break;
@@ -178,7 +180,7 @@ export default class Othello {
 
 		// 左下
 		iterate = createIterater();
-		for (let c = 0, i = 1; i < Math.min(targetx, 8 - 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;
@@ -238,11 +240,11 @@ export function ai(color: string, othello: Othello) {
 				// 左上
 				(p[0] == 0 && p[1] == 0) ||
 				// 右上
-				(p[0] == 7 && p[1] == 0) ||
+				(p[0] == (BOARD_SIZE - 1) && p[1] == 0) ||
 				// 右下
-				(p[0] == 7 && p[1] == 7) ||
+				(p[0] == (BOARD_SIZE - 1) && p[1] == (BOARD_SIZE - 1)) ||
 				// 左下
-				(p[0] == 0 && p[1] == 7)
+				(p[0] == 0 && p[1] == (BOARD_SIZE - 1))
 			);
 
 			if (corners.length > 0) { // どこかしらの角に打てる場合

From e77b017edf487f3d88cda4f127c41579b65cb479 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Oct 2017 20:12:06 +0900
Subject: [PATCH 226/364] Fix bug

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

diff --git a/src/common/othello.ts b/src/common/othello.ts
index aa53dee465..858fc33158 100644
--- a/src/common/othello.ts
+++ b/src/common/othello.ts
@@ -144,7 +144,7 @@ export default class Othello {
 
 		// 右上
 		iterate = createIterater();
-		for (let c = 0, i = 1; i < Math.min(BOARD_SIZE - targetx, targety); c++, i++) {
+		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;
@@ -198,7 +198,7 @@ export default class Othello {
 
 		// 左上
 		iterate = createIterater();
-		for (let c = 0, i = 1; i < Math.min(targetx, targety); c++, i++) {
+		for (let c = 0, i = 1; i <= Math.min(targetx, targety); c++, i++) {
 			if (iterate(targetx - i, targety - i)) {
 				res.push([7, c]);
 				break;

From f54014bc988a9b0b7c4231a9c21cda709cd36e72 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 11 Oct 2017 13:46:43 +0000
Subject: [PATCH 227/364] fix(package): update cropperjs to version 1.1.1

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

diff --git a/package.json b/package.json
index e68d18c11a..9bbe96e00f 100644
--- a/package.json
+++ b/package.json
@@ -104,7 +104,7 @@
     "chalk": "2.1.0",
     "compression": "1.7.1",
     "cors": "2.8.4",
-    "cropperjs": "1.0.0",
+    "cropperjs": "1.1.1",
     "crypto": "1.0.1",
     "debug": "3.1.0",
     "deep-equal": "1.0.1",

From eabb61d8ed2731d405d670e6789f09eb2f40d782 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 11 Oct 2017 18:36:32 +0000
Subject: [PATCH 228/364] chore(package): update webpack to version 3.7.0

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

diff --git a/package.json b/package.json
index e68d18c11a..1373628c54 100644
--- a/package.json
+++ b/package.json
@@ -92,7 +92,7 @@
     "uglify-es": "3.0.27",
     "uglify-es-webpack-plugin": "0.10.0",
     "uglify-js": "git+https://github.com/mishoo/UglifyJS2.git#harmony",
-    "webpack": "3.6.0"
+    "webpack": "3.7.0"
   },
   "dependencies": {
     "accesses": "2.5.0",

From d113860e44190a46156d2752461babe0f59eda9d Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 11 Oct 2017 20:08:22 +0000
Subject: [PATCH 229/364] chore(package): update webpack to version 3.7.1

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

diff --git a/package.json b/package.json
index 0171e4167c..cbfe3f6377 100644
--- a/package.json
+++ b/package.json
@@ -92,7 +92,7 @@
     "uglify-es": "3.0.27",
     "uglify-es-webpack-plugin": "0.10.0",
     "uglify-js": "git+https://github.com/mishoo/UglifyJS2.git#harmony",
-    "webpack": "3.7.0"
+    "webpack": "3.7.1"
   },
   "dependencies": {
     "accesses": "2.5.0",

From 48255b7c15118826b16b716df33cb17d0e3b81bb Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 12 Oct 2017 19:27:54 +0000
Subject: [PATCH 230/364] fix(package): update mongodb to version 2.2.33

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

diff --git a/package.json b/package.json
index 0171e4167c..30617055a4 100644
--- a/package.json
+++ b/package.json
@@ -123,7 +123,7 @@
     "js-yaml": "3.10.0",
     "mecab-async": "^0.1.0",
     "moji": "^0.5.1",
-    "mongodb": "2.2.31",
+    "mongodb": "2.2.33",
     "monk": "6.0.5",
     "morgan": "1.9.0",
     "ms": "2.0.0",

From c5b9e01288c42c6411f8c36aa042002e3d4199be Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 17 Oct 2017 00:15:25 +0000
Subject: [PATCH 231/364] chore(package): update @types/gm to version 1.17.33

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

diff --git a/package.json b/package.json
index 688664fa66..0c6d3ff3d8 100644
--- a/package.json
+++ b/package.json
@@ -33,7 +33,7 @@
     "@types/elasticsearch": "5.0.14",
     "@types/event-stream": "3.3.32",
     "@types/express": "4.0.37",
-    "@types/gm": "1.17.32",
+    "@types/gm": "1.17.33",
     "@types/gulp": "4.0.3",
     "@types/gulp-htmlmin": "1.3.30",
     "@types/gulp-mocha": "0.0.30",

From 97bebddbc03b4a28aa0ba52e79e9fe67879e615c Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 17 Oct 2017 07:08:45 +0000
Subject: [PATCH 232/364] fix(package): update file-type to version 7.2.0

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

diff --git a/package.json b/package.json
index 688664fa66..a7f52d0649 100644
--- a/package.json
+++ b/package.json
@@ -114,7 +114,7 @@
     "elasticsearch": "13.3.1",
     "escape-regexp": "0.0.1",
     "express": "4.15.4",
-    "file-type": "6.2.0",
+    "file-type": "7.2.0",
     "fuckadblock": "3.2.1",
     "gm": "1.23.0",
     "inquirer": "3.3.0",

From 1dba7be55c34224f865f98b930fc1cbc6064ca5c Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 17 Oct 2017 15:59:18 +0000
Subject: [PATCH 233/364] chore(package): update webpack to version 3.8.1

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

diff --git a/package.json b/package.json
index 688664fa66..67908ef56b 100644
--- a/package.json
+++ b/package.json
@@ -92,7 +92,7 @@
     "uglify-es": "3.0.27",
     "uglify-es-webpack-plugin": "0.10.0",
     "uglify-js": "git+https://github.com/mishoo/UglifyJS2.git#harmony",
-    "webpack": "3.7.1"
+    "webpack": "3.8.1"
   },
   "dependencies": {
     "accesses": "2.5.0",

From 7dc81a3bda40fed661be414ee45ef6e161998977 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 17 Oct 2017 18:37:46 +0000
Subject: [PATCH 234/364] chore(package): update @types/uuid to version 3.4.3

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

diff --git a/package.json b/package.json
index 688664fa66..daf3e3dc03 100644
--- a/package.json
+++ b/package.json
@@ -60,7 +60,7 @@
     "@types/rimraf": "2.0.0",
     "@types/riot": "3.6.0",
     "@types/serve-favicon": "2.2.28",
-    "@types/uuid": "3.4.2",
+    "@types/uuid": "3.4.3",
     "@types/webpack": "3.0.13",
     "@types/webpack-stream": "3.2.7",
     "@types/websocket": "0.0.34",

From 878d7c37cb883ce8e29f07570348c18d0fa8666e Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 18 Oct 2017 13:32:52 +0000
Subject: [PATCH 235/364] fix(package): update cropperjs to version 1.1.2

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

diff --git a/package.json b/package.json
index 688664fa66..fadbbc087a 100644
--- a/package.json
+++ b/package.json
@@ -104,7 +104,7 @@
     "chalk": "2.1.0",
     "compression": "1.7.1",
     "cors": "2.8.4",
-    "cropperjs": "1.1.1",
+    "cropperjs": "1.1.2",
     "crypto": "1.0.1",
     "debug": "3.1.0",
     "deep-equal": "1.0.1",

From 6a7c4e54dbfc9537441f8f3566293dd4022ec2d3 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 19 Oct 2017 00:31:54 +0000
Subject: [PATCH 236/364] fix(package): update websocket to version 1.0.25

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

diff --git a/package.json b/package.json
index dd1622a37b..cb3b59ed72 100644
--- a/package.json
+++ b/package.json
@@ -152,7 +152,7 @@
     "typescript": "2.5.3",
     "uuid": "3.1.0",
     "vhost": "3.0.2",
-    "websocket": "1.0.24",
+    "websocket": "1.0.25",
     "xev": "2.0.0"
   }
 }

From 231a209cf3298cc4b74ee67251884036c3234733 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sat, 21 Oct 2017 10:14:02 +0000
Subject: [PATCH 237/364] fix(package): update cropperjs to version 1.1.3

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

diff --git a/package.json b/package.json
index dd1622a37b..ceb7da756d 100644
--- a/package.json
+++ b/package.json
@@ -104,7 +104,7 @@
     "chalk": "2.1.0",
     "compression": "1.7.1",
     "cors": "2.8.4",
-    "cropperjs": "1.1.2",
+    "cropperjs": "1.1.3",
     "crypto": "1.0.1",
     "debug": "3.1.0",
     "deep-equal": "1.0.1",

From 5f40e7eaa46fd8a3f60d2306a21364e38db73012 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, 22 Oct 2017 14:11:28 +0900
Subject: [PATCH 238/364] =?UTF-8?q?=E3=82=AA=E3=83=95=E3=83=A9=E3=82=A4?=
 =?UTF-8?q?=E3=83=B3=E3=81=A7=E3=82=82=E7=94=BB=E5=83=8F=E3=82=92=E8=A1=A8?=
 =?UTF-8?q?=E7=A4=BA=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?=
 =?UTF-8?q?base64=E3=81=AB=E3=81=97=E3=81=A6=E5=9F=8B=E3=82=81=E8=BE=BC?=
 =?UTF-8?q?=E3=82=80=E3=82=88=E3=81=86=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/web/app/common/tags/error.tag |  2 +-
 webpack/module/rules/base64.ts    | 19 +++++++++++++++++++
 webpack/module/rules/index.ts     |  2 ++
 3 files changed, 22 insertions(+), 1 deletion(-)
 create mode 100644 webpack/module/rules/base64.ts

diff --git a/src/web/app/common/tags/error.tag b/src/web/app/common/tags/error.tag
index e4e0272a49..a06f17cd1c 100644
--- a/src/web/app/common/tags/error.tag
+++ b/src/web/app/common/tags/error.tag
@@ -1,5 +1,5 @@
 <mk-error>
-	<img src="/assets/error.jpg" alt=""/>
+	<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%</p>
 	<p class="thanks">%i18n:common.tags.mk-error.thanks%</p>
diff --git a/webpack/module/rules/base64.ts b/webpack/module/rules/base64.ts
new file mode 100644
index 0000000000..529816bd20
--- /dev/null
+++ b/webpack/module/rules/base64.ts
@@ -0,0 +1,19 @@
+/**
+ * Replace base64 symbols
+ */
+
+import * as fs from 'fs';
+const StringReplacePlugin = require('string-replace-webpack-plugin');
+
+export default () => ({
+	enforce: 'pre',
+	test: /\.(tag|js)$/,
+	exclude: /node_modules/,
+	loader: StringReplacePlugin.replace({
+		replacements: [{
+			pattern: /%base64:(.+?)%/g, replacement: (_, key) => {
+				return fs.readFileSync(__dirname + '/../../../src/web/' + key, 'base64');
+			}
+		}]
+	})
+});
diff --git a/webpack/module/rules/index.ts b/webpack/module/rules/index.ts
index 2707a9c2f1..9c1262b3d6 100644
--- a/webpack/module/rules/index.ts
+++ b/webpack/module/rules/index.ts
@@ -1,4 +1,5 @@
 import i18n from './i18n';
+import base64 from './base64';
 import themeColor from './theme-color';
 import tag from './tag';
 import stylus from './stylus';
@@ -6,6 +7,7 @@ import typescript from './typescript';
 
 export default (lang, locale) => [
 	i18n(lang, locale),
+	base64(),
 	themeColor(),
 	tag(),
 	stylus(),

From 866d5428bcb06b4978f8039fe7197c560559d1aa Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 22 Oct 2017 15:01:02 +0900
Subject: [PATCH 239/364] Use uglifyjs-webpack-plugin instead of
 uglify-es-webpack-plugin

Because uglify-es-webpack-plugin is now deprecated
---
 package.json              | 8 ++++----
 webpack/plugins/index.ts  | 8 +++-----
 webpack/plugins/minify.ts | 4 ++--
 3 files changed, 9 insertions(+), 11 deletions(-)

diff --git a/package.json b/package.json
index e6fc742913..befa8b2b4e 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2584",
+  "version": "0.0.2732",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",
@@ -64,7 +64,7 @@
     "@types/webpack": "3.0.13",
     "@types/webpack-stream": "3.2.7",
     "@types/websocket": "0.0.34",
-    "awesome-typescript-loader": "^3.2.3",
+    "awesome-typescript-loader": "3.2.3",
     "chai": "4.1.2",
     "chai-http": "3.0.0",
     "css-loader": "0.28.7",
@@ -90,8 +90,8 @@
     "swagger-jsdoc": "1.9.7",
     "tslint": "5.7.0",
     "uglify-es": "3.0.27",
-    "uglify-es-webpack-plugin": "0.10.0",
     "uglify-js": "git+https://github.com/mishoo/UglifyJS2.git#harmony",
+    "uglifyjs-webpack-plugin": "1.0.0-beta.2",
     "webpack": "3.8.1"
   },
   "dependencies": {
@@ -109,7 +109,7 @@
     "debug": "3.1.0",
     "deep-equal": "1.0.1",
     "deepcopy": "0.6.3",
-    "diskusage": "^0.2.2",
+    "diskusage": "0.2.2",
     "download": "6.2.5",
     "elasticsearch": "13.3.1",
     "escape-regexp": "0.0.1",
diff --git a/webpack/plugins/index.ts b/webpack/plugins/index.ts
index 99b16c2b05..d5191f1555 100644
--- a/webpack/plugins/index.ts
+++ b/webpack/plugins/index.ts
@@ -2,13 +2,11 @@ const StringReplacePlugin = require('string-replace-webpack-plugin');
 
 import constant from './const';
 import hoist from './hoist';
-//import minify from './minify';
+import minify from './minify';
 import banner from './banner';
 
-/*
 const env = process.env.NODE_ENV;
 const isProduction = env === 'production';
-*/
 
 export default version => {
 	const plugins = [
@@ -16,11 +14,11 @@ export default version => {
 		new StringReplacePlugin(),
 		hoist()
 	];
-/*
+
 	if (isProduction) {
 		plugins.push(minify());
 	}
-*/
+
 	plugins.push(banner(version));
 
 	return plugins;
diff --git a/webpack/plugins/minify.ts b/webpack/plugins/minify.ts
index ec4c9b3405..e46d4c5a10 100644
--- a/webpack/plugins/minify.ts
+++ b/webpack/plugins/minify.ts
@@ -1,3 +1,3 @@
-const UglifyEsPlugin = require('uglify-es-webpack-plugin');
+const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
 
-export default () => new UglifyEsPlugin();
+export default () => new UglifyJsPlugin();

From 03800077ede279e82a0bb89ebc09e1c779cc5af8 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, 22 Oct 2017 15:10:14 +0900
Subject: [PATCH 240/364] Update README.md

---
 README.md | 12 ------------
 1 file changed, 12 deletions(-)

diff --git a/README.md b/README.md
index 2e05298e1d..3aeac828e3 100644
--- a/README.md
+++ b/README.md
@@ -42,14 +42,6 @@ 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 displayed 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] |
-
-[List of all contributors](https://github.com/syuilo/misskey/graphs/contributors)
-
 Copyright
 ----------------------------------------------------------------
 Misskey is an open-source software licensed under [The MIT License](LICENSE).
@@ -67,7 +59,3 @@ Misskey is an open-source software licensed under [The MIT License](LICENSE).
 <!-- Collaborators 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

From afc338de83140ed2653671f4bd25fe836812faf5 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, 22 Oct 2017 15:17:38 +0900
Subject: [PATCH 241/364] Update CHANGELOG.md

---
 CHANGELOG.md | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c45400f884..58ec237093 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+2732 (2017/10/22)
+-----------------
+* 依存関係の更新など
+
 2584 (2017/09/08)
 -----------------
 * New: ユーザーページによく使うドメインを表示 (#771)

From c964abcf754578407719e2af7c12e776562510ea Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 22 Oct 2017 15:22:28 +0900
Subject: [PATCH 242/364] =?UTF-8?q?=E3=83=A2=E3=83=90=E3=82=A4=E3=83=AB?=
 =?UTF-8?q?=E7=89=88=E3=81=8B=E3=82=89=E3=81=A7=E3=82=82=E3=82=AF=E3=83=A9?=
 =?UTF-8?q?=E3=82=A4=E3=82=A2=E3=83=B3=E3=83=88=E3=83=90=E3=83=BC=E3=82=B8?=
 =?UTF-8?q?=E3=83=A7=E3=83=B3=E3=82=92=E7=A2=BA=E8=AA=8D=E3=81=A7=E3=81=8D?=
 =?UTF-8?q?=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/desktop/tags/home-widgets/version.tag | 2 +-
 src/web/app/mobile/tags/page/settings.tag         | 3 +++
 2 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/web/app/desktop/tags/home-widgets/version.tag b/src/web/app/desktop/tags/home-widgets/version.tag
index 079e4e86b8..fa92afc49f 100644
--- a/src/web/app/desktop/tags/home-widgets/version.tag
+++ b/src/web/app/desktop/tags/home-widgets/version.tag
@@ -1,5 +1,5 @@
 <mk-version-home-widget>
-	<p>ver{ version }</p>
+	<p>ver { version }</p>
 	<style>
 		:scope
 			display block
diff --git a/src/web/app/mobile/tags/page/settings.tag b/src/web/app/mobile/tags/page/settings.tag
index b129b97bd1..b366d3a16a 100644
--- a/src/web/app/mobile/tags/page/settings.tag
+++ b/src/web/app/mobile/tags/page/settings.tag
@@ -29,6 +29,7 @@
 	<ul>
 		<li><a onclick={ signout }><i class="fa fa-power-off"></i>%i18n:mobile.tags.mk-settings-page.signout%</a></li>
 	</ul>
+	<p><small>ver { version }</small></p>
 	<style>
 		:scope
 			display block
@@ -96,5 +97,7 @@
 		this.signout = signout;
 
 		this.mixin('i');
+
+		this.version = VERSION;
 	</script>
 </mk-settings>

From ce471edf2d01ad270635701529096861995d0024 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 22 Oct 2017 15:23:23 +0900
Subject: [PATCH 243/364] v2735

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 58ec237093..ba307ece92 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+2735 (2017/10/22)
+-----------------
+* モバイル版からでもクライアントバージョンを確認できるように
+
 2732 (2017/10/22)
 -----------------
 * 依存関係の更新など
diff --git a/package.json b/package.json
index befa8b2b4e..4ddb3cb451 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2732",
+  "version": "0.0.2735",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",

From 28938b40a4940f8c09619289b64cc75bca2947f0 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, 22 Oct 2017 16:36:58 +0900
Subject: [PATCH 244/364] Update README.md

---
 README.md | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 3aeac828e3..4eeac44f5e 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,8 @@ and more! You can touch with your own eyes at https://misskey.xyz/.
 
 Setup and Installation
 ----------------------------------------------------------------
-Please see [Setup and installation guide](./docs/setup.en.md).
+If you want to create your Misskey instance,
+please see [Setup and installation guide](./docs/setup.en.md).
 
 Contribution
 ----------------------------------------------------------------

From 8bbf2c98e2590f314cee6e459be601797273df9d Mon Sep 17 00:00:00 2001
From: Aya Morisawa <AyaMorisawa4869@gmail.com>
Date: Mon, 23 Oct 2017 02:38:32 +0000
Subject: [PATCH 245/364] Update README.md

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 4eeac44f5e..b777618f46 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,7 @@ and more! You can touch with your own eyes at https://misskey.xyz/.
 
 Setup and Installation
 ----------------------------------------------------------------
-If you want to create your Misskey instance,
+If you want to run your own instance of Misskey,
 please see [Setup and installation guide](./docs/setup.en.md).
 
 Contribution

From fdcff509d48081bdf58291f9c383b0ab8273c280 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 24 Oct 2017 02:40:07 +0000
Subject: [PATCH 246/364] chore(package): update uglifyjs-webpack-plugin to
 version 1.0.1

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

diff --git a/package.json b/package.json
index 4ddb3cb451..0b990ea40f 100644
--- a/package.json
+++ b/package.json
@@ -91,7 +91,7 @@
     "tslint": "5.7.0",
     "uglify-es": "3.0.27",
     "uglify-js": "git+https://github.com/mishoo/UglifyJS2.git#harmony",
-    "uglifyjs-webpack-plugin": "1.0.0-beta.2",
+    "uglifyjs-webpack-plugin": "1.0.1",
     "webpack": "3.8.1"
   },
   "dependencies": {

From 7d95989fa03021fbaf163d661afb144ba580161e Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 24 Oct 2017 04:31:04 +0000
Subject: [PATCH 247/364] fix(package): update chalk to version 2.3.0

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

diff --git a/package.json b/package.json
index 4ddb3cb451..2284bb01b0 100644
--- a/package.json
+++ b/package.json
@@ -101,7 +101,7 @@
     "bcryptjs": "2.4.3",
     "body-parser": "1.18.2",
     "cafy": "3.0.0",
-    "chalk": "2.1.0",
+    "chalk": "2.3.0",
     "compression": "1.7.1",
     "cors": "2.8.4",
     "cropperjs": "1.1.3",

From 27f98578c527a1a73c45be228eb979436d2e5ec3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 25 Oct 2017 08:01:20 +0900
Subject: [PATCH 248/364] :v:

---
 locales/en.yml                    |  2 +-
 locales/ja.yml                    |  2 +-
 src/web/app/common/tags/error.tag | 12 +++++++++++-
 3 files changed, 13 insertions(+), 3 deletions(-)

diff --git a/locales/en.yml b/locales/en.yml
index 1df3001e5a..d4dfbf76bf 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -64,7 +64,7 @@ common:
 
     mk-error:
       title: "Unable to connect to the server"
-      description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから再度お試しください。"
+      description: "There is a problem with Internet connection, or the server may be down or maintaining. Please {try again} later."
       thanks: "Thank you for using Misskey."
 
     mk-forkit:
diff --git a/locales/ja.yml b/locales/ja.yml
index 451650ef76..9a8490deca 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -64,7 +64,7 @@ common:
 
     mk-error:
       title: "サーバーに接続できません"
-      description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから再度お試しください。"
+      description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから{再度お試し}ください。"
       thanks: "いつもMisskeyをご利用いただきありがとうございます。"
 
     mk-forkit:
diff --git a/src/web/app/common/tags/error.tag b/src/web/app/common/tags/error.tag
index a06f17cd1c..7a2976541d 100644
--- a/src/web/app/common/tags/error.tag
+++ b/src/web/app/common/tags/error.tag
@@ -1,7 +1,13 @@
 <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%</p>
+	<p class="text">{
+		'%i18n:common.tags.mk-error.description%'.substr(0, '%i18n:common.tags.mk-error.description%'.indexOf('{'))
+	}<a onclick={ 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>
 	<p class="thanks">%i18n:common.tags.mk-error.thanks%</p>
 	<style>
 		:scope
@@ -53,5 +59,9 @@
 			document.title = 'Oops!';
 			document.documentElement.style.background = '#f8f8f8';
 		});
+
+		this.reload = () => {
+			location.reload();
+		};
 	</script>
 </mk-error>

From 51476ed4c89b0d4f507cd5aa92ef32cf5e4cfa5d Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 25 Oct 2017 00:30:04 +0000
Subject: [PATCH 249/364] chore(package): update @types/gulp to version 4.0.5

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

diff --git a/package.json b/package.json
index 4ddb3cb451..7be9e49d63 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,7 @@
     "@types/event-stream": "3.3.32",
     "@types/express": "4.0.37",
     "@types/gm": "1.17.32",
-    "@types/gulp": "4.0.3",
+    "@types/gulp": "4.0.5",
     "@types/gulp-htmlmin": "1.3.30",
     "@types/gulp-mocha": "0.0.30",
     "@types/gulp-rename": "0.0.32",

From 58b097620a4e6638dc288dc9e1effb238baf913a Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 25 Oct 2017 01:30:35 +0000
Subject: [PATCH 250/364] chore(package): update @types/mongodb to version
 2.2.15

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

diff --git a/package.json b/package.json
index 4ddb3cb451..5105ea7273 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,7 @@
     "@types/is-url": "1.2.28",
     "@types/js-yaml": "3.9.0",
     "@types/mocha": "2.2.43",
-    "@types/mongodb": "2.2.13",
+    "@types/mongodb": "2.2.15",
     "@types/monk": "1.0.6",
     "@types/morgan": "1.7.33",
     "@types/ms": "0.7.30",

From ee9ff7edd8234482c1f5f168169117647276e59d Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 25 Oct 2017 02:13:31 +0000
Subject: [PATCH 251/364] chore(package): update @types/node to version 8.0.47

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

diff --git a/package.json b/package.json
index 4ddb3cb451..0f63f1f0af 100644
--- a/package.json
+++ b/package.json
@@ -53,7 +53,7 @@
     "@types/morgan": "1.7.33",
     "@types/ms": "0.7.30",
     "@types/multer": "1.3.2",
-    "@types/node": "8.0.33",
+    "@types/node": "8.0.47",
     "@types/ratelimiter": "2.1.28",
     "@types/redis": "2.6.0",
     "@types/request": "2.0.4",

From 47fb9879a78e96012ff1ef8f025999dd909473ad Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 25 Oct 2017 17:56:28 +0900
Subject: [PATCH 252/364] [Client] Implement troubleshooting

---
 locales/en.yml                    |  19 ++++
 locales/ja.yml                    |  19 ++++
 src/web/app/common/tags/error.tag | 150 ++++++++++++++++++++++++++++++
 3 files changed, 188 insertions(+)

diff --git a/locales/en.yml b/locales/en.yml
index d4dfbf76bf..fbe8a22fbb 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -66,6 +66,25 @@ common:
       title: "Unable to connect to the server"
       description: "There is a problem with Internet connection, or the server may be down or maintaining. Please {try again} later."
       thanks: "Thank you for using Misskey."
+      troubleshoot: "Troubleshoot"
+
+      troubleshooter:
+        title: "TroubleShooting"
+        network: "Network connection"
+        checking-network: "Checking network connection"
+        internet: "Internet connection"
+        checking-internet: "Checking internet connection"
+        server: "Server connection"
+        checking-server: "Checking server connection"
+        finding: "Finding a problem"
+        no-network: "No network connection"
+        no-network-desc: "Please make sure you are connected to the Network."
+        no-internet: "No internet connection"
+        no-internet-desc: "Please make sure you are connected to the Internet."
+        no-server: "Unable to connect to the server"
+        no-server-desc: "The network connection of your PC is normal, but you could not connect to Misskey's server. There is a possibility that the server is down or maintaining, please try to access it again after a while."
+        success: "Successfully connect to the Misskey's server"
+        success-desc: "It seems to be able to connect normally. Please reload the page."
 
     mk-forkit:
       open-github-link: "View source on Github"
diff --git a/locales/ja.yml b/locales/ja.yml
index 9a8490deca..023bac4d47 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -66,6 +66,25 @@ common:
       title: "サーバーに接続できません"
       description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから{再度お試し}ください。"
       thanks: "いつもMisskeyをご利用いただきありがとうございます。"
+      troubleshoot: "トラブルシュート"
+
+      troubleshooter:
+        title: "トラブルシューティング"
+        network: "ネットワーク接続"
+        checking-network: "ネットワーク接続を確認中"
+        internet: "インターネット接続"
+        checking-internet: "インターネット接続を確認中"
+        server: "サーバー接続"
+        checking-server: "サーバー接続を確認中"
+        finding: "問題を調べています"
+        no-network: "ネットワークに接続されていません"
+        no-network-desc: "お使いのPCのネットワーク接続が正常か確認してください。"
+        no-internet: "インターネットに接続されていません"
+        no-internet-desc: "ネットワークには接続されていますが、インターネットには接続されていないようです。お使いのPCのインターネット接続が正常か確認してください。"
+        no-server: "Misskeyのサーバーに接続できません"
+        no-server-desc: "お使いのPCのネットワーク接続は正常ですが、Misskeyのサーバーには接続できませんでした。サーバーがダウンまたはメンテナンスしている可能性があるので、しばらくしてから再度御アクセスください。"
+        success: "Misskeyのサーバーに接続できました"
+        success-desc: "正常に接続できるようです。ページを再度読み込みしてください。"
 
     mk-forkit:
       open-github-link: "View source on Github"
diff --git a/src/web/app/common/tags/error.tag b/src/web/app/common/tags/error.tag
index 7a2976541d..62f4563e5c 100644
--- a/src/web/app/common/tags/error.tag
+++ b/src/web/app/common/tags/error.tag
@@ -8,6 +8,8 @@
 	}</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>
+	<mk-troubleshooter if={ troubleshooting }/>
 	<p class="thanks">%i18n:common.tags.mk-error.thanks%</p>
 	<style>
 		:scope
@@ -36,6 +38,25 @@
 				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
@@ -55,6 +76,8 @@
 
 	</style>
 	<script>
+		this.troubleshooting = false;
+
 		this.on('mount', () => {
 			document.title = 'Oops!';
 			document.documentElement.style.background = '#f8f8f8';
@@ -63,5 +86,132 @@
 		this.reload = () => {
 			location.reload();
 		};
+
+		this.troubleshoot = () => {
+			this.update({
+				troubleshooting: true
+			});
+		};
 	</script>
 </mk-error>
+
+<mk-troubleshooter>
+	<h1><i class="fa fa-wrench"></i>%i18n:common.tags.mk-error.troubleshooter.title%</h1>
+	<div>
+		<p data-wip={ network == null }><i if={ network != null } class="fa fa-{ network ? 'check' : 'times' }"></i>{ 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 }><i if={ internet != null } class="fa fa-{ internet ? 'check' : 'times' }"></i>{ 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 }><i if={ server != null } class="fa fa-{ server ? 'check' : 'times' }"></i>{ server == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-server%' : '%i18n:common.tags.mk-error.troubleshooter.server%' }<mk-ellipsis if={ server == null }/></p>
+	</div>
+	<p if={ !end }>%i18n:common.tags.mk-error.troubleshooter.finding%<mk-ellipsis/></p>
+	<p if={ network === false }><b><i class="fa fa-exclamation-triangle"></i>%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><i class="fa fa-exclamation-triangle"></i>%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><i class="fa fa-exclamation-triangle"></i>%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><i class="fa fa-info-circle"></i>%i18n:common.tags.mk-error.troubleshooter.success%</b><br>%i18n:common.tags.mk-error.troubleshooter.success-desc%</p>
+
+	<style>
+		: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
+
+				> i
+					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
+
+					> i
+						margin-right 0.25em
+
+						&.fa-times
+							color #e03524
+
+						&.fa-check
+							color #84c32f
+
+			> p
+				margin 0
+				padding 0.6em 1.2em
+				font-size 1em
+				color #444
+				border-top solid 1px #eee
+
+				> b
+					> i
+						margin-right 0.25em
+
+				&.success
+					> b
+						color #39adad
+
+				&:not(.success)
+					> b
+						color #ad4339
+
+	</style>
+	<script>
+		import CONFIG from '../../common/scripts/config';
+
+		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(`${CONFIG.apiUrl}/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>

From cc1dbf771249c709805f48271ab7c165bfea1da3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 25 Oct 2017 17:57:30 +0900
Subject: [PATCH 253/364] v2742

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ba307ece92..675ffe62ba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+2742 (2017/10/25)
+-----------------
+* トラブルシューティングを実装するなど
+
 2735 (2017/10/22)
 -----------------
 * モバイル版からでもクライアントバージョンを確認できるように
diff --git a/package.json b/package.json
index 4ddb3cb451..1f604b9ce1 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2735",
+  "version": "0.0.2742",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",

From e581ce3014d52579877743b29b348e220f4c734a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 25 Oct 2017 20:27:24 +0900
Subject: [PATCH 254/364] =?UTF-8?q?=E3=83=A1=E3=82=B8=E3=83=A3=E3=83=BC?=
 =?UTF-8?q?=E3=83=90=E3=83=BC=E3=82=B8=E3=83=A7=E3=83=B3=E3=82=92=E6=98=8E?=
 =?UTF-8?q?=E8=A8=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/web/app/desktop/tags/home-widgets/version.tag | 2 +-
 src/web/app/mobile/tags/page/settings.tag         | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/web/app/desktop/tags/home-widgets/version.tag b/src/web/app/desktop/tags/home-widgets/version.tag
index fa92afc49f..ea5307061c 100644
--- a/src/web/app/desktop/tags/home-widgets/version.tag
+++ b/src/web/app/desktop/tags/home-widgets/version.tag
@@ -1,5 +1,5 @@
 <mk-version-home-widget>
-	<p>ver { version }</p>
+	<p>ver { version } (葵 aoi)</p>
 	<style>
 		:scope
 			display block
diff --git a/src/web/app/mobile/tags/page/settings.tag b/src/web/app/mobile/tags/page/settings.tag
index b366d3a16a..b6501142ee 100644
--- a/src/web/app/mobile/tags/page/settings.tag
+++ b/src/web/app/mobile/tags/page/settings.tag
@@ -29,7 +29,7 @@
 	<ul>
 		<li><a onclick={ signout }><i class="fa fa-power-off"></i>%i18n:mobile.tags.mk-settings-page.signout%</a></li>
 	</ul>
-	<p><small>ver { version }</small></p>
+	<p><small>ver { version } (葵 aoi)</small></p>
 	<style>
 		:scope
 			display block

From 4cac6316f77e4c31d8caa1e7bb74ea9a063e02c7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 25 Oct 2017 20:29:14 +0900
Subject: [PATCH 255/364] [Client] Fix bug

---
 webpack/module/rules/i18n.ts | 72 +++++++++++++++++++++---------------
 1 file changed, 42 insertions(+), 30 deletions(-)

diff --git a/webpack/module/rules/i18n.ts b/webpack/module/rules/i18n.ts
index 3023253cab..9a4acde686 100644
--- a/webpack/module/rules/i18n.ts
+++ b/webpack/module/rules/i18n.ts
@@ -4,34 +4,46 @@
 
 const StringReplacePlugin = require('string-replace-webpack-plugin');
 
-export default (lang, locale) => ({
-	enforce: 'pre',
-	test: /\.(tag|js)$/,
-	exclude: /node_modules/,
-	loader: StringReplacePlugin.replace({
-		replacements: [
-			{
-				pattern: /%i18n:(.+?)%/g, replacement: (_, key) => {
-					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.replace(/'/g, '\\\'').replace(/"/g, '\\"');
-					}
-				}
+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;
+		}
+	}
+
+	return {
+		enforce: 'pre',
+		test: /\.(tag|js)$/,
+		exclude: /node_modules/,
+		loader: StringReplacePlugin.replace({
+			replacements: [{
+				pattern: /"%i18n:(.+?)%"/g, replacement: (_, key) => {
+					return '"' + get(key).replace(/"/g, '\\"') + '"';
+				}
+			}, {
+				pattern: /'%i18n:(.+?)%'/g, replacement: (_, key) => {
+					return '\'' + get(key).replace(/'/g, '\\\'') + '\'';
+				}
+			}, {
+				pattern: /%i18n:(.+?)%/g, replacement: (_, key) => {
+					return get(key);
+				}
+			}]
+		})
+	};
+};

From 5a9b2a12551bb98652d503a0123d75c16a68cf9f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 25 Oct 2017 20:34:33 +0900
Subject: [PATCH 256/364] Better English

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

diff --git a/locales/en.yml b/locales/en.yml
index fbe8a22fbb..0d66c5c548 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -77,9 +77,9 @@ common:
         server: "Server connection"
         checking-server: "Checking server connection"
         finding: "Finding a problem"
-        no-network: "No network connection"
+        no-network: "There is no Network connection"
         no-network-desc: "Please make sure you are connected to the Network."
-        no-internet: "No internet connection"
+        no-internet: "There is no Internet connection"
         no-internet-desc: "Please make sure you are connected to the Internet."
         no-server: "Unable to connect to the server"
         no-server-desc: "The network connection of your PC is normal, but you could not connect to Misskey's server. There is a possibility that the server is down or maintaining, please try to access it again after a while."

From a41a05de317403cdf155bf8df7862aefc8f5c56e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 25 Oct 2017 20:48:45 +0900
Subject: [PATCH 257/364] Fix #89

---
 CHANGELOG.md                          |  8 +++++--
 src/web/app/desktop/tags/timeline.tag | 30 +++++++++++++++++----------
 src/web/app/desktop/tags/ui.tag       | 30 +++++++++++++++------------
 src/web/app/mobile/tags/timeline.tag  | 30 +++++++++++++++++----------
 src/web/app/mobile/tags/ui.tag        |  2 +-
 5 files changed, 62 insertions(+), 38 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 675ffe62ba..cb68046abb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,13 +2,17 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+unreleased
+----------
+* Fix: 非ログイン状態ですべてのページが致命的な問題を発生させる (#89)
+
 2742 (2017/10/25)
 -----------------
-* トラブルシューティングを実装するなど
+* New: トラブルシューティングを実装するなど
 
 2735 (2017/10/22)
 -----------------
-* モバイル版からでもクライアントバージョンを確認できるように
+* New: モバイル版からでもクライアントバージョンを確認できるように
 
 2732 (2017/10/22)
 -----------------
diff --git a/src/web/app/desktop/tags/timeline.tag b/src/web/app/desktop/tags/timeline.tag
index cd7ac7d207..2d6b439e38 100644
--- a/src/web/app/desktop/tags/timeline.tag
+++ b/src/web/app/desktop/tags/timeline.tag
@@ -424,6 +424,7 @@
 		import compile from '../../common/scripts/text-compiler';
 		import dateStringify from '../../common/scripts/date-stringify';
 
+		this.mixin('i');
 		this.mixin('api');
 		this.mixin('stream');
 		this.mixin('user-preview');
@@ -462,24 +463,31 @@
 		};
 
 		this.capture = withHandler => {
-			this.stream.send({
-				type: 'capture',
-				id: this.post.id
-			});
-			if (withHandler) this.stream.on('post-updated', this.onStreamPostUpdated);
+			if (this.SIGNIN) {
+				this.stream.send({
+					type: 'capture',
+					id: this.post.id
+				});
+				if (withHandler) this.stream.on('post-updated', this.onStreamPostUpdated);
+			}
 		};
 
 		this.decapture = withHandler => {
-			this.stream.send({
-				type: 'decapture',
-				id: this.post.id
-			});
-			if (withHandler) this.stream.off('post-updated', this.onStreamPostUpdated);
+			if (this.SIGNIN) {
+				this.stream.send({
+					type: 'decapture',
+					id: this.post.id
+				});
+				if (withHandler) this.stream.off('post-updated', this.onStreamPostUpdated);
+			}
 		};
 
 		this.on('mount', () => {
 			this.capture(true);
-			this.stream.on('_connected_', this.onStreamConnected);
+
+			if (this.SIGNIN) {
+				this.stream.on('_connected_', this.onStreamConnected);
+			}
 
 			if (this.p.text) {
 				const tokens = this.p.ast;
diff --git a/src/web/app/desktop/tags/ui.tag b/src/web/app/desktop/tags/ui.tag
index fce0743ff7..e0d7393b08 100644
--- a/src/web/app/desktop/tags/ui.tag
+++ b/src/web/app/desktop/tags/ui.tag
@@ -5,7 +5,7 @@
 	<div class="content">
 		<yield />
 	</div>
-	<mk-stream-indicator/>
+	<mk-stream-indicator if={ SIGNIN }/>
 	<style>
 		:scope
 			display block
@@ -416,22 +416,26 @@
 		this.page = this.opts.page;
 
 		this.on('mount', () => {
-			this.stream.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
-			this.stream.on('unread_messaging_message', this.onUnreadMessagingMessage);
+			if (this.SIGNIN) {
+				this.stream.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
+				this.stream.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
-					});
-				}
-			});
+				// Fetch count of unread messaging messages
+				this.api('messaging/unread').then(res => {
+					if (res.count > 0) {
+						this.update({
+							hasUnreadMessagingMessages: true
+						});
+					}
+				});
+			}
 		});
 
 		this.on('unmount', () => {
-			this.stream.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
-			this.stream.off('unread_messaging_message', this.onUnreadMessagingMessage);
+			if (this.SIGNIN) {
+				this.stream.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
+				this.stream.off('unread_messaging_message', this.onUnreadMessagingMessage);
+			}
 		});
 
 		this.onReadAllMessagingMessages = () => {
diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag
index 5ecc2df9d1..c7f5bfd681 100644
--- a/src/web/app/mobile/tags/timeline.tag
+++ b/src/web/app/mobile/tags/timeline.tag
@@ -467,6 +467,7 @@
 		import getPostSummary from '../../../../common/get-post-summary.ts';
 		import openPostForm from '../scripts/open-post-form';
 
+		this.mixin('i');
 		this.mixin('api');
 		this.mixin('stream');
 
@@ -502,24 +503,31 @@
 		};
 
 		this.capture = withHandler => {
-			this.stream.send({
-				type: 'capture',
-				id: this.post.id
-			});
-			if (withHandler) this.stream.on('post-updated', this.onStreamPostUpdated);
+			if (this.SIGNIN) {
+				this.stream.send({
+					type: 'capture',
+					id: this.post.id
+				});
+				if (withHandler) this.stream.on('post-updated', this.onStreamPostUpdated);
+			}
 		};
 
 		this.decapture = withHandler => {
-			this.stream.send({
-				type: 'decapture',
-				id: this.post.id
-			});
-			if (withHandler) this.stream.off('post-updated', this.onStreamPostUpdated);
+			if (this.SIGNIN) {
+				this.stream.send({
+					type: 'decapture',
+					id: this.post.id
+				});
+				if (withHandler) this.stream.off('post-updated', this.onStreamPostUpdated);
+			}
 		};
 
 		this.on('mount', () => {
 			this.capture(true);
-			this.stream.on('_connected_', this.onStreamConnected);
+
+			if (this.SIGNIN) {
+				this.stream.on('_connected_', this.onStreamConnected);
+			}
 
 			if (this.p.text) {
 				const tokens = this.p.ast;
diff --git a/src/web/app/mobile/tags/ui.tag b/src/web/app/mobile/tags/ui.tag
index b2f738dc2e..9d9cd4d74a 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/>
+	<mk-stream-indicator if={ SIGNIN }/>
 	<style>
 		:scope
 			display block

From d5930462b90817e9ec9d251764abd7b791f5bd8d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 25 Oct 2017 20:48:52 +0900
Subject: [PATCH 258/364] :v:

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

diff --git a/src/web/app/init.js b/src/web/app/init.js
index 44391b8fcb..2ddb66b3e7 100644
--- a/src/web/app/init.js
+++ b/src/web/app/init.js
@@ -19,7 +19,7 @@ require('./common/tags');
  * APP ENTRY POINT!
  */
 
-console.info(`Misskey v${VERSION}`);
+console.info(`Misskey v${VERSION} (葵 aoi)`);
 
 document.domain = CONFIG.host;
 

From 25a81484608630b14a8f302f223338d05f643c46 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 25 Oct 2017 20:49:39 +0900
Subject: [PATCH 259/364] v2747

---
 CHANGELOG.md | 4 ++--
 package.json | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index cb68046abb..ca41d016c1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,8 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
-unreleased
-----------
+2747 (2017/10/25)
+-----------------
 * Fix: 非ログイン状態ですべてのページが致命的な問題を発生させる (#89)
 
 2742 (2017/10/25)
diff --git a/package.json b/package.json
index 1f604b9ce1..43a0159619 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2742",
+  "version": "0.0.2747",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",

From e830dd71e1644b8627d3833cb663faa90af9aae2 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 25 Oct 2017 16:48:59 +0000
Subject: [PATCH 260/364] chore(package): update @types/riot to version 3.6.1

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

diff --git a/package.json b/package.json
index 43a0159619..1eb0e94cda 100644
--- a/package.json
+++ b/package.json
@@ -58,7 +58,7 @@
     "@types/redis": "2.6.0",
     "@types/request": "2.0.4",
     "@types/rimraf": "2.0.0",
-    "@types/riot": "3.6.0",
+    "@types/riot": "3.6.1",
     "@types/serve-favicon": "2.2.28",
     "@types/uuid": "3.4.2",
     "@types/webpack": "3.0.13",

From 781ca218e7760c2d3ce33d5169779e303f2a658a Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 25 Oct 2017 18:05:35 +0000
Subject: [PATCH 261/364] chore(package): update @types/webpack to version
 3.0.14

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

diff --git a/package.json b/package.json
index 43a0159619..d15c25a289 100644
--- a/package.json
+++ b/package.json
@@ -61,7 +61,7 @@
     "@types/riot": "3.6.0",
     "@types/serve-favicon": "2.2.28",
     "@types/uuid": "3.4.2",
-    "@types/webpack": "3.0.13",
+    "@types/webpack": "3.0.14",
     "@types/webpack-stream": "3.2.7",
     "@types/websocket": "0.0.34",
     "awesome-typescript-loader": "3.2.3",

From c8c16c8190751589144f350d8658ab57adaa735a Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 25 Oct 2017 19:26:49 +0000
Subject: [PATCH 262/364] chore(package): update @types/webpack-stream to
 version 3.2.8

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

diff --git a/package.json b/package.json
index 43a0159619..f83f7ff0be 100644
--- a/package.json
+++ b/package.json
@@ -62,7 +62,7 @@
     "@types/serve-favicon": "2.2.28",
     "@types/uuid": "3.4.2",
     "@types/webpack": "3.0.13",
-    "@types/webpack-stream": "3.2.7",
+    "@types/webpack-stream": "3.2.8",
     "@types/websocket": "0.0.34",
     "awesome-typescript-loader": "3.2.3",
     "chai": "4.1.2",

From e25ae9ad6e921f6a5c65b11553b26df924006bfe Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 25 Oct 2017 20:20:52 +0000
Subject: [PATCH 263/364] chore(package): update @types/request to version
 2.0.7

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

diff --git a/package.json b/package.json
index 43a0159619..473878e394 100644
--- a/package.json
+++ b/package.json
@@ -56,7 +56,7 @@
     "@types/node": "8.0.33",
     "@types/ratelimiter": "2.1.28",
     "@types/redis": "2.6.0",
-    "@types/request": "2.0.4",
+    "@types/request": "2.0.7",
     "@types/rimraf": "2.0.0",
     "@types/riot": "3.6.0",
     "@types/serve-favicon": "2.2.28",

From 58af4365e95dc1a86bbabdf46c9e4bd833f943fa Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 26 Oct 2017 09:02:40 +0900
Subject: [PATCH 264/364] [Client] Set description meta tag

---
 locales/en.yml      |  2 ++
 locales/ja.yml      |  2 ++
 src/web/app/init.js | 10 +++++++++-
 3 files changed, 13 insertions(+), 1 deletion(-)

diff --git a/locales/en.yml b/locales/en.yml
index 0d66c5c548..03d5306d3e 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -1,4 +1,6 @@
 common:
+  misskey: "Note everything and share it others using Misskey."
+
   time:
     unknown: "unknown"
     future: "future"
diff --git a/locales/ja.yml b/locales/ja.yml
index 023bac4d47..b640f0f248 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -1,4 +1,6 @@
 common:
+  misskey: "Misskeyに何でも投稿して皆と共有しましょう。"
+
   time:
     unknown: "なぞのじかん"
     future: "未来"
diff --git a/src/web/app/init.js b/src/web/app/init.js
index 2ddb66b3e7..cb661c2595 100644
--- a/src/web/app/init.js
+++ b/src/web/app/init.js
@@ -2,7 +2,7 @@
  * App initializer
  */
 
-"use strict";
+'use strict';
 
 import * as riot from 'riot';
 import api from './common/scripts/api';
@@ -21,6 +21,14 @@ require('./common/tags');
 
 console.info(`Misskey v${VERSION} (葵 aoi)`);
 
+{ // 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);
+}
+
 document.domain = CONFIG.host;
 
 // Set global configuration

From a2621f40d7f1f311089975c4b255f108fad0fdee Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 26 Oct 2017 15:09:38 +0000
Subject: [PATCH 265/364] chore(package): update @types/chalk to version 2.2.0

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

diff --git a/package.json b/package.json
index 43a0159619..0deb7dbb07 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,7 @@
     "@types/body-parser": "1.16.5",
     "@types/chai": "4.0.4",
     "@types/chai-http": "3.0.3",
-    "@types/chalk": "0.4.31",
+    "@types/chalk": "2.2.0",
     "@types/compression": "0.0.34",
     "@types/cors": "2.8.1",
     "@types/debug": "0.0.30",

From cdff672a8354ee119a8b704075ff69b3ff760c25 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 26 Oct 2017 18:06:50 +0000
Subject: [PATCH 266/364] chore(package): update @types/mocha to version 2.2.44

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

diff --git a/package.json b/package.json
index 43a0159619..163c2da574 100644
--- a/package.json
+++ b/package.json
@@ -47,7 +47,7 @@
     "@types/is-root": "1.0.0",
     "@types/is-url": "1.2.28",
     "@types/js-yaml": "3.9.0",
-    "@types/mocha": "2.2.43",
+    "@types/mocha": "2.2.44",
     "@types/mongodb": "2.2.13",
     "@types/monk": "1.0.6",
     "@types/morgan": "1.7.33",

From c4806958fc86c2e65351ce23bba2688ac81a2ddf Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 26 Oct 2017 19:34:24 +0000
Subject: [PATCH 267/364] chore(package): update @types/express to version
 4.0.39

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

diff --git a/package.json b/package.json
index 43a0159619..c6da419d2f 100644
--- a/package.json
+++ b/package.json
@@ -32,7 +32,7 @@
     "@types/deep-equal": "1.0.1",
     "@types/elasticsearch": "5.0.14",
     "@types/event-stream": "3.3.32",
-    "@types/express": "4.0.37",
+    "@types/express": "4.0.39",
     "@types/gm": "1.17.32",
     "@types/gulp": "4.0.3",
     "@types/gulp-htmlmin": "1.3.30",

From cccf267f70bbe8c033c9b6cf3ab5053d4b3f9ddd Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 26 Oct 2017 19:35:00 +0000
Subject: [PATCH 268/364] chore(package): update @types/morgan to version
 1.7.35

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

diff --git a/package.json b/package.json
index 43a0159619..d2c8ac38a3 100644
--- a/package.json
+++ b/package.json
@@ -50,7 +50,7 @@
     "@types/mocha": "2.2.43",
     "@types/mongodb": "2.2.13",
     "@types/monk": "1.0.6",
-    "@types/morgan": "1.7.33",
+    "@types/morgan": "1.7.35",
     "@types/ms": "0.7.30",
     "@types/multer": "1.3.2",
     "@types/node": "8.0.33",

From b6733c57d15d1767acbb40c1a9b0fc05618d3259 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 26 Oct 2017 19:35:25 +0000
Subject: [PATCH 269/364] chore(package): update @types/multer to version 1.3.5

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

diff --git a/package.json b/package.json
index 43a0159619..7e1d481a4e 100644
--- a/package.json
+++ b/package.json
@@ -52,7 +52,7 @@
     "@types/monk": "1.0.6",
     "@types/morgan": "1.7.33",
     "@types/ms": "0.7.30",
-    "@types/multer": "1.3.2",
+    "@types/multer": "1.3.5",
     "@types/node": "8.0.33",
     "@types/ratelimiter": "2.1.28",
     "@types/redis": "2.6.0",

From 9aed3d21c900acefe78d456bd1614bc862764b8c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 27 Oct 2017 08:04:18 +0900
Subject: [PATCH 270/364] i18n

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

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 550d7e76de..e9b740762e 100644
--- a/src/web/app/desktop/tags/home-widgets/rss-reader.tag
+++ b/src/web/app/desktop/tags/home-widgets/rss-reader.tag
@@ -4,7 +4,7 @@
 	<div class="feed" if={ !initializing }>
 		<virtual each={ item in items }><a href={ item.link } target="_blank">{ item.title }</a></virtual>
 	</div>
-	<p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>読み込んでいます<mk-ellipsis/></p>
+	<p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:common.loading%<mk-ellipsis/></p>
 	<style>
 		:scope
 			display block

From 4da5fd57c7e49111e9116f0990ad081fcd4d7963 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sat, 28 Oct 2017 00:55:09 +0000
Subject: [PATCH 271/364] chore(package): update @types/redis to version 2.8.1

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

diff --git a/package.json b/package.json
index 43a0159619..e278cadd8a 100644
--- a/package.json
+++ b/package.json
@@ -55,7 +55,7 @@
     "@types/multer": "1.3.2",
     "@types/node": "8.0.33",
     "@types/ratelimiter": "2.1.28",
-    "@types/redis": "2.6.0",
+    "@types/redis": "2.8.1",
     "@types/request": "2.0.4",
     "@types/rimraf": "2.0.0",
     "@types/riot": "3.6.0",

From 77528f022d2e9f76298331b55303cfc42359c7af Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 30 Oct 2017 17:30:32 +0900
Subject: [PATCH 272/364] wip

---
 locales/en.yml                          |  6 ++++
 locales/ja.yml                          |  6 ++++
 src/api/endpoints/bbs/threads/create.ts | 29 ++++++++++++++++
 src/api/models/bbs-thread.ts            | 13 ++++++++
 src/api/serializers/bbs-thread.ts       | 44 +++++++++++++++++++++++++
 src/web/app/desktop/tags/index.js       |  1 +
 src/web/app/desktop/tags/pages/bbs.tag  | 30 +++++++++++++++++
 src/web/app/desktop/tags/ui.tag         | 32 +++++++++++-------
 8 files changed, 149 insertions(+), 12 deletions(-)
 create mode 100644 src/api/endpoints/bbs/threads/create.ts
 create mode 100644 src/api/models/bbs-thread.ts
 create mode 100644 src/api/serializers/bbs-thread.ts
 create mode 100644 src/web/app/desktop/tags/pages/bbs.tag

diff --git a/locales/en.yml b/locales/en.yml
index 03d5306d3e..6c763886df 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -241,6 +241,7 @@ desktop:
     mk-ui-header-nav:
       home: "Home"
       messaging: "Messages"
+      bbs: "BBS"
       info: "News"
 
     mk-ui-header-search:
@@ -351,6 +352,11 @@ desktop:
     mk-repost-form-window:
       title: "Are you sure you want to repost this post?"
 
+    mk-bbs-page:
+      title: "Misskey BBS"
+      new: "Create new thread"
+      thread-title: "Thread title"
+
 mobile:
   tags:
     mk-drive-file-viewer:
diff --git a/locales/ja.yml b/locales/ja.yml
index b640f0f248..1e243fb8d6 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -241,6 +241,7 @@ desktop:
     mk-ui-header-nav:
       home: "ホーム"
       messaging: "メッセージ"
+      bbs: "掲示板"
       info: "お知らせ"
 
     mk-ui-header-search:
@@ -351,6 +352,11 @@ desktop:
     mk-repost-form-window:
       title: "この投稿をRepostしますか?"
 
+    mk-bbs-page:
+      title: "Misskey掲示板"
+      new: "スレッドを作成"
+      thread-title: "スレッドのタイトル"
+
 mobile:
   tags:
     mk-drive-file-viewer:
diff --git a/src/api/endpoints/bbs/threads/create.ts b/src/api/endpoints/bbs/threads/create.ts
new file mode 100644
index 0000000000..71d61d8711
--- /dev/null
+++ b/src/api/endpoints/bbs/threads/create.ts
@@ -0,0 +1,29 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import Thread from '../../../models/bbs-thread';
+import serialize from '../../../serializers/bbs-thread';
+
+/**
+ * Create a thread
+ *
+ * @param {any} params
+ * @param {any} user
+ * @return {Promise<any>}
+ */
+module.exports = async (params, user) => new Promise(async (res, rej) => {
+	// Get 'title' parameter
+	const [title, titleErr] = $(params.title).string().range(1, 100).$;
+	if (titleErr) return rej('invalid title param');
+
+	// Create a thread
+	const thread = await Thread.insert({
+		created_at: new Date(),
+		user_id: user._id,
+		title: title
+	});
+
+	// Response
+	res(await serialize(thread));
+});
diff --git a/src/api/models/bbs-thread.ts b/src/api/models/bbs-thread.ts
new file mode 100644
index 0000000000..a92157c6f4
--- /dev/null
+++ b/src/api/models/bbs-thread.ts
@@ -0,0 +1,13 @@
+import * as mongo from 'mongodb';
+import db from '../../db/mongodb';
+
+const collection = db.get('bbs_threads');
+
+export default collection as any; // fuck type definition
+
+export type IBbsThread = {
+	_id: mongo.ObjectID;
+	created_at: Date;
+	title: string;
+	user_id: mongo.ObjectID;
+};
diff --git a/src/api/serializers/bbs-thread.ts b/src/api/serializers/bbs-thread.ts
new file mode 100644
index 0000000000..d9e41a8468
--- /dev/null
+++ b/src/api/serializers/bbs-thread.ts
@@ -0,0 +1,44 @@
+/**
+ * Module dependencies
+ */
+import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
+import { IUser } from '../models/user';
+import { default as Thread, IBbsThread } from '../models/bbs-thread';
+
+/**
+ * Serialize a thread
+ *
+ * @param thread target
+ * @param me? serializee
+ * @return response
+ */
+export default (
+	thread: string | mongo.ObjectID | IBbsThread,
+	me?: string | mongo.ObjectID | IUser
+) => new Promise<any>(async (resolve, reject) => {
+
+	let _thread: any;
+
+	// Populate the thread if 'thread' is ID
+	if (mongo.ObjectID.prototype.isPrototypeOf(thread)) {
+		_thread = await Thread.findOne({
+			_id: thread
+		});
+	} else if (typeof thread === 'string') {
+		_thread = await Thread.findOne({
+			_id: new mongo.ObjectID(thread)
+		});
+	} else {
+		_thread = deepcopy(thread);
+	}
+
+	// Rename _id to id
+	_thread.id = _thread._id;
+	delete _thread._id;
+
+	// Remove needless properties
+	delete _thread.user_id;
+
+	resolve(_thread);
+});
diff --git a/src/web/app/desktop/tags/index.js b/src/web/app/desktop/tags/index.js
index 4e286013a1..fa7161ddfa 100644
--- a/src/web/app/desktop/tags/index.js
+++ b/src/web/app/desktop/tags/index.js
@@ -61,6 +61,7 @@ require('./pages/user.tag');
 require('./pages/post.tag');
 require('./pages/search.tag');
 require('./pages/not-found.tag');
+require('./pages/bbs.tag');
 require('./autocomplete-suggestion.tag');
 require('./progress-dialog.tag');
 require('./user-preview.tag');
diff --git a/src/web/app/desktop/tags/pages/bbs.tag b/src/web/app/desktop/tags/pages/bbs.tag
new file mode 100644
index 0000000000..cb58af1934
--- /dev/null
+++ b/src/web/app/desktop/tags/pages/bbs.tag
@@ -0,0 +1,30 @@
+<mk-bbs-page>
+	<mk-ui ref="ui">
+		<main>
+			<h1>%i18n:desktop.tags.mk-bbs-page.title%</h1>
+			<button onclick={ parent.new }>%i18n:desktop.tags.mk-bbs-page.new%</button>
+		</main>
+	</mk-ui>
+	<style>
+		:scope
+			display block
+
+	</style>
+	<script>
+		this.mixin('api');
+
+		this.on('mount', () => {
+			document.title = '%i18n:desktop.tags.mk-bbs-page.title%';
+		});
+
+		this.new = () => {
+			const title = window.prompt('%i18n:desktop.tags.mk-bbs-page.thread-title%');
+
+			this.api('bbs/threads/create', {
+				title: title
+			}).then(thread => {
+				location.href = '/bbs/' + thread.id;
+			});
+		};
+	</script>
+</mk-bbs-page>
diff --git a/src/web/app/desktop/tags/ui.tag b/src/web/app/desktop/tags/ui.tag
index e0d7393b08..452a72c00a 100644
--- a/src/web/app/desktop/tags/ui.tag
+++ b/src/web/app/desktop/tags/ui.tag
@@ -319,18 +319,26 @@
 </mk-ui-header-notifications>
 
 <mk-ui-header-nav>
-	<ul if={ SIGNIN }>
-		<li class="home { active: page == 'home' }">
-			<a href={ CONFIG.url }>
-				<i class="fa fa-home"></i>
-				<p>%i18n:desktop.tags.mk-ui-header-nav.home%</p>
-			</a>
-		</li>
-		<li class="messaging">
-			<a onclick={ messaging }>
-				<i class="fa fa-comments"></i>
-				<p>%i18n:desktop.tags.mk-ui-header-nav.messaging%</p>
-				<i class="fa fa-circle" if={ hasUnreadMessagingMessages }></i>
+	<ul>
+		<virtual if={ SIGNIN }>
+			<li class="home { active: page == 'home' }">
+				<a href={ CONFIG.url }>
+					<i class="fa fa-home"></i>
+					<p>%i18n:desktop.tags.mk-ui-header-nav.home%</p>
+				</a>
+			</li>
+			<li class="messaging">
+				<a onclick={ messaging }>
+					<i class="fa fa-comments"></i>
+					<p>%i18n:desktop.tags.mk-ui-header-nav.messaging%</p>
+					<i class="fa fa-circle" if={ hasUnreadMessagingMessages }></i>
+				</a>
+			</li>
+		</virtual>
+		<li class="bbs">
+			<a href={ CONFIG.url + '/bbs' }>
+				<i class="fa fa-coffee"></i>
+				<p>%i18n:desktop.tags.mk-ui-header-nav.bbs%</p>
 			</a>
 		</li>
 		<li class="info">

From caa47cb38cfc3950539c78ca2e70f2c50e815d2c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 30 Oct 2017 22:12:10 +0900
Subject: [PATCH 273/364] =?UTF-8?q?=E6=9C=AA=E8=AA=AD=E3=81=AE=E9=80=9A?=
 =?UTF-8?q?=E7=9F=A5=E3=81=8C=E3=81=82=E3=82=8B=E5=A0=B4=E5=90=88=E3=82=A2?=
 =?UTF-8?q?=E3=82=A4=E3=82=B3=E3=83=B3=E3=82=92=E8=A1=A8=E7=A4=BA=E3=81=99?=
 =?UTF-8?q?=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

---
 CHANGELOG.md                                  |   4 +
 locales/en.yml                                |   1 +
 locales/ja.yml                                |   1 +
 src/api/common/read-notification.ts           |  52 +++
 src/api/endpoints.ts                          |  10 +-
 src/api/endpoints/i/notifications.ts          |  14 +-
 .../notifications/get_unread_count.ts         |  23 ++
 .../endpoints/notifications/mark_as_read.ts   |  47 ---
 .../notifications/mark_as_read_all.ts         |  32 ++
 src/api/models/notification.ts                |   5 +
 src/api/stream/home.ts                        |   6 +
 src/web/app/desktop/tags/notifications.tag    |   6 +
 src/web/app/mobile/tags/index.js              |   2 -
 src/web/app/mobile/tags/notifications.tag     |   6 +
 .../app/mobile/tags/page/notifications.tag    |  14 +
 src/web/app/mobile/tags/ui-header.tag         | 156 --------
 src/web/app/mobile/tags/ui-nav.tag            | 170 --------
 src/web/app/mobile/tags/ui.tag                | 368 ++++++++++++++++++
 18 files changed, 525 insertions(+), 392 deletions(-)
 create mode 100644 src/api/common/read-notification.ts
 create mode 100644 src/api/endpoints/notifications/get_unread_count.ts
 delete mode 100644 src/api/endpoints/notifications/mark_as_read.ts
 create mode 100644 src/api/endpoints/notifications/mark_as_read_all.ts
 delete mode 100644 src/web/app/mobile/tags/ui-header.tag
 delete mode 100644 src/web/app/mobile/tags/ui-nav.tag

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ca41d016c1..bf5c1fcb2c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+unreleased
+----------
+* New: 未読の通知がある場合アイコンを表示するように
+
 2747 (2017/10/25)
 -----------------
 * Fix: 非ログイン状態ですべてのページが致命的な問題を発生させる (#89)
diff --git a/locales/en.yml b/locales/en.yml
index 03d5306d3e..020813ddbb 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -389,6 +389,7 @@ mobile:
 
     mk-notifications-page:
       notifications: "Notifications"
+      read-all: "Are you sure you want to mark as read all your notifications?"
 
     mk-post-page:
       title: "Post"
diff --git a/locales/ja.yml b/locales/ja.yml
index b640f0f248..1b3058fe02 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -389,6 +389,7 @@ mobile:
 
     mk-notifications-page:
       notifications: "通知"
+      read-all: "すべての通知を既読にしますか?"
 
     mk-post-page:
       title: "投稿"
diff --git a/src/api/common/read-notification.ts b/src/api/common/read-notification.ts
new file mode 100644
index 0000000000..3009cc5d08
--- /dev/null
+++ b/src/api/common/read-notification.ts
@@ -0,0 +1,52 @@
+import * as mongo from 'mongodb';
+import { default as Notification, INotification } from '../models/notification';
+import publishUserStream from '../event';
+
+/**
+ * Mark as read notification(s)
+ */
+export default (
+	user: string | mongo.ObjectID,
+	message: string | string[] | INotification | INotification[] | mongo.ObjectID | mongo.ObjectID[]
+) => new Promise<any>(async (resolve, reject) => {
+
+	const userId = mongo.ObjectID.prototype.isPrototypeOf(user)
+		? user
+		: new mongo.ObjectID(user);
+
+	const ids: mongo.ObjectID[] = Array.isArray(message)
+		? mongo.ObjectID.prototype.isPrototypeOf(message[0])
+			? (message as mongo.ObjectID[])
+			: typeof message[0] === 'string'
+				? (message as string[]).map(m => new mongo.ObjectID(m))
+				: (message as INotification[]).map(m => m._id)
+		: mongo.ObjectID.prototype.isPrototypeOf(message)
+			? [(message as mongo.ObjectID)]
+			: typeof message === 'string'
+				? [new mongo.ObjectID(message)]
+				: [(message as INotification)._id];
+
+	// Update documents
+	await Notification.update({
+		_id: { $in: ids },
+		is_read: false
+	}, {
+		$set: {
+			is_read: true
+		}
+	}, {
+		multi: true
+	});
+
+	// Calc count of my unread notifications
+	const count = await Notification
+		.count({
+			notifiee_id: userId,
+			is_read: false
+		});
+
+	if (count == 0) {
+		// 全ての(いままで未読だった)通知を(これで)読みましたよというイベントを発行
+		publishUserStream(userId, 'read_all_notifications');
+	}
+});
diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts
index f05762340c..29a97bcb8a 100644
--- a/src/api/endpoints.ts
+++ b/src/api/endpoints.ts
@@ -195,6 +195,11 @@ const endpoints: Endpoint[] = [
 		withCredential: true,
 		kind: 'notification-read'
 	},
+	{
+		name: 'notifications/get_unread_count',
+		withCredential: true,
+		kind: 'notification-read'
+	},
 	{
 		name: 'notifications/delete',
 		withCredential: true,
@@ -205,11 +210,6 @@ const endpoints: Endpoint[] = [
 		withCredential: true,
 		kind: 'notification-write'
 	},
-	{
-		name: 'notifications/mark_as_read',
-		withCredential: true,
-		kind: 'notification-write'
-	},
 	{
 		name: 'notifications/mark_as_read_all',
 		withCredential: true,
diff --git a/src/api/endpoints/i/notifications.ts b/src/api/endpoints/i/notifications.ts
index 5575fb7412..607e0768a4 100644
--- a/src/api/endpoints/i/notifications.ts
+++ b/src/api/endpoints/i/notifications.ts
@@ -5,6 +5,7 @@ import $ from 'cafy';
 import Notification from '../../models/notification';
 import serialize from '../../serializers/notification';
 import getFriends from '../../common/get-friends';
+import read from '../../common/read-notification';
 
 /**
  * Get notifications
@@ -91,17 +92,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Mark as read all
 	if (notifications.length > 0 && markAsRead) {
-		const ids = notifications
-			.filter(x => x.is_read == false)
-			.map(x => x._id);
-
-		// Update documents
-		await Notification.update({
-			_id: { $in: ids }
-		}, {
-			$set: { is_read: true }
-		}, {
-			multi: true
-		});
+		read(user._id, notifications);
 	}
 });
diff --git a/src/api/endpoints/notifications/get_unread_count.ts b/src/api/endpoints/notifications/get_unread_count.ts
new file mode 100644
index 0000000000..9514e78713
--- /dev/null
+++ b/src/api/endpoints/notifications/get_unread_count.ts
@@ -0,0 +1,23 @@
+/**
+ * Module dependencies
+ */
+import Notification from '../../models/notification';
+
+/**
+ * Get count of unread notifications
+ *
+ * @param {any} params
+ * @param {any} user
+ * @return {Promise<any>}
+ */
+module.exports = (params, user) => new Promise(async (res, rej) => {
+	const count = await Notification
+		.count({
+			notifiee_id: user._id,
+			is_read: false
+		});
+
+	res({
+		count: count
+	});
+});
diff --git a/src/api/endpoints/notifications/mark_as_read.ts b/src/api/endpoints/notifications/mark_as_read.ts
deleted file mode 100644
index 5cce33e850..0000000000
--- a/src/api/endpoints/notifications/mark_as_read.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * Module dependencies
- */
-import $ from 'cafy';
-import Notification from '../../models/notification';
-import serialize from '../../serializers/notification';
-import event from '../../event';
-
-/**
- * Mark as read a notification
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
- */
-module.exports = (params, user) => new Promise(async (res, rej) => {
-	const [notificationId, notificationIdErr] = $(params.notification_id).id().$;
-	if (notificationIdErr) return rej('invalid notification_id param');
-
-	// Get notification
-	const notification = await Notification
-		.findOne({
-			_id: notificationId,
-			i: user._id
-		});
-
-	if (notification === null) {
-		return rej('notification-not-found');
-	}
-
-	// Update
-	notification.is_read = true;
-	Notification.update({ _id: notification._id }, {
-		$set: {
-			is_read: true
-		}
-	});
-
-	// Response
-	res();
-
-	// Serialize
-	const notificationObj = await serialize(notification);
-
-	// Publish read_notification event
-	event(user._id, 'read_notification', notificationObj);
-});
diff --git a/src/api/endpoints/notifications/mark_as_read_all.ts b/src/api/endpoints/notifications/mark_as_read_all.ts
new file mode 100644
index 0000000000..3550e344c4
--- /dev/null
+++ b/src/api/endpoints/notifications/mark_as_read_all.ts
@@ -0,0 +1,32 @@
+/**
+ * Module dependencies
+ */
+import Notification from '../../models/notification';
+import event from '../../event';
+
+/**
+ * Mark as read all notifications
+ *
+ * @param {any} params
+ * @param {any} user
+ * @return {Promise<any>}
+ */
+module.exports = (params, user) => new Promise(async (res, rej) => {
+	// Update documents
+	await Notification.update({
+		notifiee_id: user._id,
+		is_read: false
+	}, {
+		$set: {
+			is_read: true
+		}
+	}, {
+		multi: true
+	});
+
+	// Response
+	res();
+
+	// 全ての通知を読みましたよというイベントを発行
+	event(user._id, 'read_all_notifications');
+});
diff --git a/src/api/models/notification.ts b/src/api/models/notification.ts
index 1c1f429a0d..1065e8baaa 100644
--- a/src/api/models/notification.ts
+++ b/src/api/models/notification.ts
@@ -1,3 +1,8 @@
+import * as mongo from 'mongodb';
 import db from '../../db/mongodb';
 
 export default db.get('notifications') as any; // fuck type definition
+
+export interface INotification {
+	_id: mongo.ObjectID;
+}
diff --git a/src/api/stream/home.ts b/src/api/stream/home.ts
index d5fe01c261..7c8f3bfec8 100644
--- a/src/api/stream/home.ts
+++ b/src/api/stream/home.ts
@@ -4,6 +4,7 @@ import * as debug from 'debug';
 
 import User from '../models/user';
 import serializePost from '../serializers/post';
+import readNotification from '../common/read-notification';
 
 const log = debug('misskey');
 
@@ -45,6 +46,11 @@ export default function homeStream(request: websocket.request, connection: webso
 				});
 				break;
 
+			case 'read_notification':
+				if (!msg.id) return;
+				readNotification(user._id, msg.id);
+				break;
+
 			case 'capture':
 				if (!msg.id) return;
 				const postId = msg.id;
diff --git a/src/web/app/desktop/tags/notifications.tag b/src/web/app/desktop/tags/notifications.tag
index 1046358ce9..a4f66105a8 100644
--- a/src/web/app/desktop/tags/notifications.tag
+++ b/src/web/app/desktop/tags/notifications.tag
@@ -252,6 +252,12 @@
 		});
 
 		this.onNotification = notification => {
+			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
+			this.stream.send({
+				type: 'read_notification',
+				id: notification.id
+			});
+
 			this.notifications.unshift(notification);
 			this.update();
 		};
diff --git a/src/web/app/mobile/tags/index.js b/src/web/app/mobile/tags/index.js
index c5aafd20ba..a79f4f7e7e 100644
--- a/src/web/app/mobile/tags/index.js
+++ b/src/web/app/mobile/tags/index.js
@@ -1,6 +1,4 @@
 require('./ui.tag');
-require('./ui-header.tag');
-require('./ui-nav.tag');
 require('./page/entrance.tag');
 require('./page/entrance/signin.tag');
 require('./page/entrance/signup.tag');
diff --git a/src/web/app/mobile/tags/notifications.tag b/src/web/app/mobile/tags/notifications.tag
index 7370aa84d3..2e95990314 100644
--- a/src/web/app/mobile/tags/notifications.tag
+++ b/src/web/app/mobile/tags/notifications.tag
@@ -123,6 +123,12 @@
 		});
 
 		this.onNotification = notification => {
+			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
+			this.stream.send({
+				type: 'read_notification',
+				id: notification.id
+			});
+
 			this.notifications.unshift(notification);
 			this.update();
 		};
diff --git a/src/web/app/mobile/tags/page/notifications.tag b/src/web/app/mobile/tags/page/notifications.tag
index 06a5be039f..743de04393 100644
--- a/src/web/app/mobile/tags/page/notifications.tag
+++ b/src/web/app/mobile/tags/page/notifications.tag
@@ -10,16 +10,30 @@
 		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', '<i class="fa fa-bell-o"></i>%i18n:mobile.tags.mk-notifications-page.notifications%');
 			document.documentElement.style.background = '#313a42';
 
+			ui.trigger('func', () => {
+				this.readAll();
+			}, '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.api('notifications/mark_as_read_all');
+		};
 	</script>
 </mk-notifications-page>
diff --git a/src/web/app/mobile/tags/ui-header.tag b/src/web/app/mobile/tags/ui-header.tag
deleted file mode 100644
index 10b44b2153..0000000000
--- a/src/web/app/mobile/tags/ui-header.tag
+++ /dev/null
@@ -1,156 +0,0 @@
-<mk-ui-header>
-	<mk-special-message/>
-	<div class="main">
-		<div class="backdrop"></div>
-		<div class="content">
-			<button class="nav" onclick={ parent.toggleDrawer }><i class="fa fa-bars"></i></button>
-			<i class="fa fa-circle" if={ hasUnreadMessagingMessages }></i>
-			<h1 ref="title">Misskey</h1>
-			<button if={ func } onclick={ func }><i class="fa fa-{ funcIcon }"></i></button>
-		</div>
-	</div>
-	<style>
-		: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
-
-						> i
-						> .icon
-							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)
-
-						> i
-							transition all 0.2s ease
-
-					> i
-						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>
-		import ui from '../scripts/ui-event';
-
-		this.mixin('api');
-		this.mixin('stream');
-
-		this.func = null;
-		this.funcIcon = null;
-
-		this.on('mount', () => {
-			this.stream.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
-			this.stream.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', () => {
-			this.stream.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
-			this.stream.off('unread_messaging_message', this.onUnreadMessagingMessage);
-
-			ui.off('title', this.setTitle);
-			ui.off('func', this.setFunc);
-		});
-
-		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>
diff --git a/src/web/app/mobile/tags/ui-nav.tag b/src/web/app/mobile/tags/ui-nav.tag
deleted file mode 100644
index 34235ba4f1..0000000000
--- a/src/web/app/mobile/tags/ui-nav.tag
+++ /dev/null
@@ -1,170 +0,0 @@
-<mk-ui-nav>
-	<div class="backdrop" onclick={ 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"/>
-			<p class="name">{ I.name }</p>
-		</a>
-		<div class="links">
-			<ul>
-				<li><a href="/"><i class="fa fa-home"></i>%i18n:mobile.tags.mk-ui-nav.home%<i class="fa fa-angle-right"></i></a></li>
-				<li><a href="/i/notifications"><i class="fa fa-bell-o"></i>%i18n:mobile.tags.mk-ui-nav.notifications%<i class="fa fa-angle-right"></i></a></li>
-				<li><a href="/i/messaging"><i class="fa fa-comments-o"></i>%i18n:mobile.tags.mk-ui-nav.messaging%<i class="i fa fa-circle" if={ hasUnreadMessagingMessages }></i><i class="fa fa-angle-right"></i></a></li>
-			</ul>
-			<ul>
-				<li><a onclick={ search }><i class="fa fa-search"></i>%i18n:mobile.tags.mk-ui-nav.search%<i class="fa fa-angle-right"></i></a></li>
-			</ul>
-			<ul>
-				<li><a href="/i/drive"><i class="fa fa-cloud"></i>%i18n:mobile.tags.mk-ui-nav.drive%<i class="fa fa-angle-right"></i></a></li>
-			</ul>
-			<ul>
-				<li><a href="/i/settings"><i class="fa fa-cog"></i>%i18n:mobile.tags.mk-ui-nav.settings%<i class="fa fa-angle-right"></i></a></li>
-			</ul>
-		</div>
-		<a href={ CONFIG.aboutUrl }><p class="about">%i18n:mobile.tags.mk-ui-nav.about%</p></a>
-	</div>
-	<style>
-		: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
-
-						> i:first-child
-							margin-right 0.5em
-
-						> .i
-							margin-left 6px
-							vertical-align super
-							font-size 10px
-							color $theme-color
-
-						> i: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>
-		this.mixin('i');
-		this.mixin('page');
-		this.mixin('api');
-		this.mixin('stream');
-
-		this.on('mount', () => {
-			this.stream.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
-			this.stream.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', () => {
-			this.stream.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
-			this.stream.off('unread_messaging_message', this.onUnreadMessagingMessage);
-		});
-
-		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:' + query);
-		};
-	</script>
-</mk-ui-nav>
diff --git a/src/web/app/mobile/tags/ui.tag b/src/web/app/mobile/tags/ui.tag
index 9d9cd4d74a..fb8cbcdbd2 100644
--- a/src/web/app/mobile/tags/ui.tag
+++ b/src/web/app/mobile/tags/ui.tag
@@ -30,9 +30,377 @@
 		};
 
 		this.onStreamNotification = notification => {
+			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
+			this.stream.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" onclick={ parent.toggleDrawer }><i class="fa fa-bars"></i></button>
+			<i class="fa fa-circle" if={ hasUnreadNotifications || hasUnreadMessagingMessages }></i>
+			<h1 ref="title">Misskey</h1>
+			<button if={ func } onclick={ func }><i class="fa fa-{ funcIcon }"></i></button>
+		</div>
+	</div>
+	<style>
+		: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
+
+						> i
+						> .icon
+							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)
+
+						> i
+							transition all 0.2s ease
+
+					> i
+						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>
+		import ui from '../scripts/ui-event';
+
+		this.mixin('api');
+		this.mixin('stream');
+
+		this.func = null;
+		this.funcIcon = null;
+
+		this.on('mount', () => {
+			this.stream.on('read_all_notifications', this.onReadAllNotifications);
+			this.stream.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
+			this.stream.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.stream.off('read_all_notifications', this.onReadAllNotifications);
+			this.stream.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
+			this.stream.off('unread_messaging_message', this.onUnreadMessagingMessage);
+
+			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" onclick={ 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"/>
+			<p class="name">{ I.name }</p>
+		</a>
+		<div class="links">
+			<ul>
+				<li><a href="/"><i class="fa fa-home"></i>%i18n:mobile.tags.mk-ui-nav.home%<i class="fa fa-angle-right"></i></a></li>
+				<li><a href="/i/notifications"><i class="fa fa-bell-o"></i>%i18n:mobile.tags.mk-ui-nav.notifications%<i class="i fa fa-circle" if={ hasUnreadNotifications }></i><i class="fa fa-angle-right"></i></a></li>
+				<li><a href="/i/messaging"><i class="fa fa-comments-o"></i>%i18n:mobile.tags.mk-ui-nav.messaging%<i class="i fa fa-circle" if={ hasUnreadMessagingMessages }></i><i class="fa fa-angle-right"></i></a></li>
+			</ul>
+			<ul>
+				<li><a onclick={ search }><i class="fa fa-search"></i>%i18n:mobile.tags.mk-ui-nav.search%<i class="fa fa-angle-right"></i></a></li>
+			</ul>
+			<ul>
+				<li><a href="/i/drive"><i class="fa fa-cloud"></i>%i18n:mobile.tags.mk-ui-nav.drive%<i class="fa fa-angle-right"></i></a></li>
+			</ul>
+			<ul>
+				<li><a href="/i/settings"><i class="fa fa-cog"></i>%i18n:mobile.tags.mk-ui-nav.settings%<i class="fa fa-angle-right"></i></a></li>
+			</ul>
+		</div>
+		<a href={ CONFIG.aboutUrl }><p class="about">%i18n:mobile.tags.mk-ui-nav.about%</p></a>
+	</div>
+	<style>
+		: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
+
+						> i:first-child
+							margin-right 0.5em
+
+						> .i
+							margin-left 6px
+							vertical-align super
+							font-size 10px
+							color $theme-color
+
+						> i: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>
+		this.mixin('i');
+		this.mixin('page');
+		this.mixin('api');
+		this.mixin('stream');
+
+		this.on('mount', () => {
+			this.stream.on('read_all_notifications', this.onReadAllNotifications);
+			this.stream.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
+			this.stream.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.stream.off('read_all_notifications', this.onReadAllNotifications);
+			this.stream.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
+			this.stream.off('unread_messaging_message', this.onUnreadMessagingMessage);
+		});
+
+		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:' + query);
+		};
+	</script>
+</mk-ui-nav>

From 460c6d448bc98a4006bda810fdb30a59f5955d65 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 30 Oct 2017 22:12:52 +0900
Subject: [PATCH 274/364] v2752

---
 CHANGELOG.md | 4 ++--
 package.json | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index bf5c1fcb2c..2f75462e5f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,8 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
-unreleased
-----------
+2752 (2017/10/30)
+-----------------
 * New: 未読の通知がある場合アイコンを表示するように
 
 2747 (2017/10/25)
diff --git a/package.json b/package.json
index 43a0159619..7a81bed7a6 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2747",
+  "version": "0.0.2752",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",

From dc9fddf839df7959a83819eb7064f402db05f200 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 31 Oct 2017 21:42:11 +0900
Subject: [PATCH 275/364] RENAME: bbs -> channel

---
 locales/en.yml                                |  7 ++-
 locales/ja.yml                                |  7 ++-
 src/api/endpoints/bbs/threads/create.ts       | 12 ++---
 src/api/models/{bbs-thread.ts => channel.ts}  |  4 +-
 src/api/serializers/bbs-thread.ts             | 44 -------------------
 src/api/serializers/channel.ts                | 44 +++++++++++++++++++
 src/web/app/desktop/tags/index.js             |  2 +-
 .../tags/pages/{bbs.tag => channels.tag}      | 12 ++---
 8 files changed, 65 insertions(+), 67 deletions(-)
 rename src/api/models/{bbs-thread.ts => channel.ts} (75%)
 delete mode 100644 src/api/serializers/bbs-thread.ts
 create mode 100644 src/api/serializers/channel.ts
 rename src/web/app/desktop/tags/pages/{bbs.tag => channels.tag} (64%)

diff --git a/locales/en.yml b/locales/en.yml
index f0204b52cb..da532fc78a 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -352,10 +352,9 @@ desktop:
     mk-repost-form-window:
       title: "Are you sure you want to repost this post?"
 
-    mk-bbs-page:
-      title: "Misskey BBS"
-      new: "Create new thread"
-      thread-title: "Thread title"
+    mk-channels-page:
+      new: "Create new channel"
+      channel-title: "Channel title"
 
 mobile:
   tags:
diff --git a/locales/ja.yml b/locales/ja.yml
index 65d92782f2..1ae94652b5 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -352,10 +352,9 @@ desktop:
     mk-repost-form-window:
       title: "この投稿をRepostしますか?"
 
-    mk-bbs-page:
-      title: "Misskey掲示板"
-      new: "スレッドを作成"
-      thread-title: "スレッドのタイトル"
+    mk-channels-page:
+      new: "チャンネルを作成"
+      channel-title: "チャンネルのタイトル"
 
 mobile:
   tags:
diff --git a/src/api/endpoints/bbs/threads/create.ts b/src/api/endpoints/bbs/threads/create.ts
index 71d61d8711..d9b4d34a0c 100644
--- a/src/api/endpoints/bbs/threads/create.ts
+++ b/src/api/endpoints/bbs/threads/create.ts
@@ -2,11 +2,11 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Thread from '../../../models/bbs-thread';
-import serialize from '../../../serializers/bbs-thread';
+import Channel from '../../../models/channel';
+import serialize from '../../../serializers/channel';
 
 /**
- * Create a thread
+ * Create a channel
  *
  * @param {any} params
  * @param {any} user
@@ -17,13 +17,13 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	const [title, titleErr] = $(params.title).string().range(1, 100).$;
 	if (titleErr) return rej('invalid title param');
 
-	// Create a thread
-	const thread = await Thread.insert({
+	// Create a channel
+	const channel = await Channel.insert({
 		created_at: new Date(),
 		user_id: user._id,
 		title: title
 	});
 
 	// Response
-	res(await serialize(thread));
+	res(await serialize(channel));
 });
diff --git a/src/api/models/bbs-thread.ts b/src/api/models/channel.ts
similarity index 75%
rename from src/api/models/bbs-thread.ts
rename to src/api/models/channel.ts
index a92157c6f4..79edb71367 100644
--- a/src/api/models/bbs-thread.ts
+++ b/src/api/models/channel.ts
@@ -1,11 +1,11 @@
 import * as mongo from 'mongodb';
 import db from '../../db/mongodb';
 
-const collection = db.get('bbs_threads');
+const collection = db.get('channels');
 
 export default collection as any; // fuck type definition
 
-export type IBbsThread = {
+export type IChannel = {
 	_id: mongo.ObjectID;
 	created_at: Date;
 	title: string;
diff --git a/src/api/serializers/bbs-thread.ts b/src/api/serializers/bbs-thread.ts
deleted file mode 100644
index d9e41a8468..0000000000
--- a/src/api/serializers/bbs-thread.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import deepcopy = require('deepcopy');
-import { IUser } from '../models/user';
-import { default as Thread, IBbsThread } from '../models/bbs-thread';
-
-/**
- * Serialize a thread
- *
- * @param thread target
- * @param me? serializee
- * @return response
- */
-export default (
-	thread: string | mongo.ObjectID | IBbsThread,
-	me?: string | mongo.ObjectID | IUser
-) => new Promise<any>(async (resolve, reject) => {
-
-	let _thread: any;
-
-	// Populate the thread if 'thread' is ID
-	if (mongo.ObjectID.prototype.isPrototypeOf(thread)) {
-		_thread = await Thread.findOne({
-			_id: thread
-		});
-	} else if (typeof thread === 'string') {
-		_thread = await Thread.findOne({
-			_id: new mongo.ObjectID(thread)
-		});
-	} else {
-		_thread = deepcopy(thread);
-	}
-
-	// Rename _id to id
-	_thread.id = _thread._id;
-	delete _thread._id;
-
-	// Remove needless properties
-	delete _thread.user_id;
-
-	resolve(_thread);
-});
diff --git a/src/api/serializers/channel.ts b/src/api/serializers/channel.ts
new file mode 100644
index 0000000000..d4e16d6be3
--- /dev/null
+++ b/src/api/serializers/channel.ts
@@ -0,0 +1,44 @@
+/**
+ * Module dependencies
+ */
+import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
+import { IUser } from '../models/user';
+import { default as Channel, IChannel } from '../models/channel';
+
+/**
+ * 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;
+
+	resolve(_channel);
+});
diff --git a/src/web/app/desktop/tags/index.js b/src/web/app/desktop/tags/index.js
index fa7161ddfa..6d49006526 100644
--- a/src/web/app/desktop/tags/index.js
+++ b/src/web/app/desktop/tags/index.js
@@ -61,7 +61,7 @@ require('./pages/user.tag');
 require('./pages/post.tag');
 require('./pages/search.tag');
 require('./pages/not-found.tag');
-require('./pages/bbs.tag');
+require('./pages/channels.tag');
 require('./autocomplete-suggestion.tag');
 require('./progress-dialog.tag');
 require('./user-preview.tag');
diff --git a/src/web/app/desktop/tags/pages/bbs.tag b/src/web/app/desktop/tags/pages/channels.tag
similarity index 64%
rename from src/web/app/desktop/tags/pages/bbs.tag
rename to src/web/app/desktop/tags/pages/channels.tag
index cb58af1934..9e47e52d25 100644
--- a/src/web/app/desktop/tags/pages/bbs.tag
+++ b/src/web/app/desktop/tags/pages/channels.tag
@@ -1,4 +1,4 @@
-<mk-bbs-page>
+<mk-channels-page>
 	<mk-ui ref="ui">
 		<main>
 			<h1>%i18n:desktop.tags.mk-bbs-page.title%</h1>
@@ -18,13 +18,13 @@
 		});
 
 		this.new = () => {
-			const title = window.prompt('%i18n:desktop.tags.mk-bbs-page.thread-title%');
+			const title = window.prompt('%i18n:desktop.tags.mk-bbs-page.channel-title%');
 
-			this.api('bbs/threads/create', {
+			this.api('bbs/channels/create', {
 				title: title
-			}).then(thread => {
-				location.href = '/bbs/' + thread.id;
+			}).then(channel => {
+				location.href = '/bbs/' + channel.id;
 			});
 		};
 	</script>
-</mk-bbs-page>
+</mk-channels-page>

From b4340b1d91a6fc1679c3cb891ea800e1b491109c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 31 Oct 2017 22:09:09 +0900
Subject: [PATCH 276/364] wip

---
 src/api/endpoints/posts/create.ts | 47 +++++++++++++++++++++++++++----
 src/api/models/post.ts            |  1 +
 2 files changed, 42 insertions(+), 6 deletions(-)

diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index 805dba7f83..42a55f850e 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -4,9 +4,9 @@
 import $ from 'cafy';
 import deepEqual = require('deep-equal');
 import parse from '../../common/text';
-import Post from '../../models/post';
-import { isValidText } from '../../models/post';
+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 DriveFile from '../../models/drive-file';
 import Watching from '../../models/post-watching';
@@ -62,7 +62,8 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 	const [repostId, repostIdErr] = $(params.repost_id).optional.id().$;
 	if (repostIdErr) return rej('invalid repost_id');
 
-	let repost = null;
+	let repost: IPost = null;
+	let isQuote = false;
 	if (repostId !== undefined) {
 		// Fetch repost to post
 		repost = await Post.findOne({
@@ -84,18 +85,20 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 			}
 		});
 
+		isQuote = text != null || files != null;
+
 		// 直近と同じRepost対象かつ引用じゃなかったらエラー
 		if (latestPost &&
 			latestPost.repost_id &&
 			latestPost.repost_id.equals(repost._id) &&
-			text === undefined && files === null) {
+			!isQuote) {
 			return rej('cannot repost same post that already reposted in your latest post');
 		}
 
 		// 直近がRepost対象かつ引用じゃなかったらエラー
 		if (latestPost &&
 			latestPost._id.equals(repost._id) &&
-			text === undefined && files === null) {
+			!isQuote) {
 			return rej('cannot repost your latest post');
 		}
 	}
@@ -104,7 +107,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 	const [inReplyToPostId, inReplyToPostIdErr] = $(params.reply_to_id).optional.id().$;
 	if (inReplyToPostIdErr) return rej('invalid in_reply_to_post_id');
 
-	let inReplyToPost = null;
+	let inReplyToPost: IPost = null;
 	if (inReplyToPostId !== undefined) {
 		// Fetch reply
 		inReplyToPost = await Post.findOne({
@@ -121,6 +124,37 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		}
 	}
 
+	// Get 'channel_id' parameter
+	const [channelId, channelIdErr] = $(params.channel_id).optional.id().$;
+	if (channelIdErr) return rej('invalid channel_id');
+
+	let channel: IChannel = null;
+	if (channelId !== undefined) {
+		// Fetch channel
+		channel = await Channel.findOne({
+			_id: channelId
+		});
+
+		if (channel === null) {
+			return rej('channel not found');
+		}
+
+		// 返信対象の投稿がこのチャンネルじゃなかったらダメ
+		if (inReplyToPost && !channelId.equals(inReplyToPost.channel_id)) {
+			return rej('チャンネル内部からチャンネル外部の投稿に返信することはできません');
+		}
+
+		// Repost対象の投稿がこのチャンネルじゃなかったらダメ
+		if (repost && !channelId.equals(repost.channel_id)) {
+			return rej('チャンネル内部からチャンネル外部の投稿をRepostすることはできません');
+		}
+
+		// 引用ではないRepostはダメ
+		if (repost && !isQuote) {
+			return rej('チャンネル内部では引用ではないRepostをすることはできません');
+		}
+	}
+
 	// Get 'poll' parameter
 	const [poll, pollErr] = $(params.poll).optional.strict.object()
 		.have('choices', $().array('string')
@@ -164,6 +198,7 @@ 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,
 		media_ids: files ? files.map(file => file._id) : undefined,
 		reply_to_id: inReplyToPost ? inReplyToPost._id : undefined,
 		repost_id: repost ? repost._id : undefined,
diff --git a/src/api/models/post.ts b/src/api/models/post.ts
index 8b9f7f5ef6..fe07dcb0b1 100644
--- a/src/api/models/post.ts
+++ b/src/api/models/post.ts
@@ -10,6 +10,7 @@ export function isValidText(text: string): boolean {
 
 export type IPost = {
 	_id: mongo.ObjectID;
+	channel_id: mongo.ObjectID;
 	created_at: Date;
 	media_ids: mongo.ObjectID[];
 	reply_to_id: mongo.ObjectID;

From 30a4e839a687bed7ed839e3c17f6781bb4b76499 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 31 Oct 2017 22:14:12 +0900
Subject: [PATCH 277/364] Fix indent

---
 src/api/endpoints/posts/create.ts | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index 42a55f850e..e0a02fa4a0 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -186,11 +186,11 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 			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: text,
-				reply: inReplyToPost ? inReplyToPost._id.toString() : null,
-				repost: repost ? repost._id.toString() : null,
-				media_ids: (files || []).map(file => file._id.toString())
-			})) {
+			text: text,
+			reply: inReplyToPost ? inReplyToPost._id.toString() : null,
+			repost: repost ? repost._id.toString() : null,
+			media_ids: (files || []).map(file => file._id.toString())
+		})) {
 			return rej('duplicate');
 		}
 	}

From 5efb52b9f563ae7d6b5383d054a6c21fee676b68 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 31 Oct 2017 22:35:31 +0900
Subject: [PATCH 278/364] wip

---
 locales/en.yml                                       |  2 +-
 locales/ja.yml                                       |  2 +-
 src/api/endpoints.ts                                 | 12 ++++++++++--
 .../endpoints/{bbs/threads => channels}/create.ts    |  4 ++--
 src/web/app/desktop/router.js                        |  5 +++++
 src/web/app/desktop/tags/pages/channels.tag          |  8 +++-----
 src/web/app/desktop/tags/ui.tag                      |  8 ++++----
 7 files changed, 26 insertions(+), 15 deletions(-)
 rename src/api/endpoints/{bbs/threads => channels}/create.ts (84%)

diff --git a/locales/en.yml b/locales/en.yml
index da532fc78a..5c7a1165ba 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -241,7 +241,7 @@ desktop:
     mk-ui-header-nav:
       home: "Home"
       messaging: "Messages"
-      bbs: "BBS"
+      channels: "Channels"
       info: "News"
 
     mk-ui-header-search:
diff --git a/locales/ja.yml b/locales/ja.yml
index 1ae94652b5..dd76a2b900 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -241,7 +241,7 @@ desktop:
     mk-ui-header-nav:
       home: "ホーム"
       messaging: "メッセージ"
-      bbs: "掲示板"
+      channels: "チャンネル"
       info: "お知らせ"
 
     mk-ui-header-search:
diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts
index 29a97bcb8a..26177b8775 100644
--- a/src/api/endpoints.ts
+++ b/src/api/endpoints.ts
@@ -474,8 +474,16 @@ const endpoints: Endpoint[] = [
 		name: 'messaging/messages/create',
 		withCredential: true,
 		kind: 'messaging-write'
-	}
-
+	},
+	{
+		name: 'channels/create',
+		withCredential: true,
+		limit: {
+			duration: ms('1hour'),
+			max: 3,
+			minInterval: ms('10seconds')
+		}
+	},
 ];
 
 export default endpoints;
diff --git a/src/api/endpoints/bbs/threads/create.ts b/src/api/endpoints/channels/create.ts
similarity index 84%
rename from src/api/endpoints/bbs/threads/create.ts
rename to src/api/endpoints/channels/create.ts
index d9b4d34a0c..74b089dfc3 100644
--- a/src/api/endpoints/bbs/threads/create.ts
+++ b/src/api/endpoints/channels/create.ts
@@ -2,8 +2,8 @@
  * Module dependencies
  */
 import $ from 'cafy';
-import Channel from '../../../models/channel';
-import serialize from '../../../serializers/channel';
+import Channel from '../../models/channel';
+import serialize from '../../serializers/channel';
 
 /**
  * Create a channel
diff --git a/src/web/app/desktop/router.js b/src/web/app/desktop/router.js
index afa8a2dce3..51738f3afa 100644
--- a/src/web/app/desktop/router.js
+++ b/src/web/app/desktop/router.js
@@ -9,6 +9,7 @@ let page = null;
 export default me => {
 	route('/',              index);
 	route('/i>mentions',    mentions);
+	route('/channel',       channels);
 	route('/post::post',    post);
 	route('/search::query', search);
 	route('/:user',         user.bind(null, 'home'));
@@ -54,6 +55,10 @@ export default me => {
 		mount(el);
 	}
 
+	function channels() {
+		mount(document.createElement('mk-channels-page'));
+	}
+
 	function notFound() {
 		mount(document.createElement('mk-not-found'));
 	}
diff --git a/src/web/app/desktop/tags/pages/channels.tag b/src/web/app/desktop/tags/pages/channels.tag
index 9e47e52d25..03fae3c8d1 100644
--- a/src/web/app/desktop/tags/pages/channels.tag
+++ b/src/web/app/desktop/tags/pages/channels.tag
@@ -1,8 +1,7 @@
 <mk-channels-page>
 	<mk-ui ref="ui">
 		<main>
-			<h1>%i18n:desktop.tags.mk-bbs-page.title%</h1>
-			<button onclick={ parent.new }>%i18n:desktop.tags.mk-bbs-page.new%</button>
+			<button onclick={ parent.new }>%i18n:desktop.tags.mk-channels-page.new%</button>
 		</main>
 	</mk-ui>
 	<style>
@@ -14,16 +13,15 @@
 		this.mixin('api');
 
 		this.on('mount', () => {
-			document.title = '%i18n:desktop.tags.mk-bbs-page.title%';
 		});
 
 		this.new = () => {
-			const title = window.prompt('%i18n:desktop.tags.mk-bbs-page.channel-title%');
+			const title = window.prompt('%i18n:desktop.tags.mk-channels-page.channel-title%');
 
 			this.api('bbs/channels/create', {
 				title: title
 			}).then(channel => {
-				location.href = '/bbs/' + channel.id;
+				location.href = '/channel/' + channel.id;
 			});
 		};
 	</script>
diff --git a/src/web/app/desktop/tags/ui.tag b/src/web/app/desktop/tags/ui.tag
index 452a72c00a..7527358dce 100644
--- a/src/web/app/desktop/tags/ui.tag
+++ b/src/web/app/desktop/tags/ui.tag
@@ -335,10 +335,10 @@
 				</a>
 			</li>
 		</virtual>
-		<li class="bbs">
-			<a href={ CONFIG.url + '/bbs' }>
-				<i class="fa fa-coffee"></i>
-				<p>%i18n:desktop.tags.mk-ui-header-nav.bbs%</p>
+		<li class="channels">
+			<a href={ CONFIG.url + '/channel' }>
+				<i class="fa fa-television"></i>
+				<p>%i18n:desktop.tags.mk-ui-header-nav.channels%</p>
 			</a>
 		</li>
 		<li class="info">

From f87ec61e96a8c1f070abefc6a3b5f7e68e24705d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 31 Oct 2017 23:11:22 +0900
Subject: [PATCH 279/364] wip

---
 src/api/endpoints.ts                        |  3 ++
 src/api/endpoints/channels/show.ts          | 31 +++++++++++++++
 src/web/app/desktop/router.js               | 26 ++++++++-----
 src/web/app/desktop/tags/index.js           |  1 +
 src/web/app/desktop/tags/pages/channel.tag  | 43 +++++++++++++++++++++
 src/web/app/desktop/tags/pages/channels.tag |  2 +-
 src/web/app/desktop/tags/pages/user.tag     |  2 +-
 7 files changed, 97 insertions(+), 11 deletions(-)
 create mode 100644 src/api/endpoints/channels/show.ts
 create mode 100644 src/web/app/desktop/tags/pages/channel.tag

diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts
index 26177b8775..45b83fc9e5 100644
--- a/src/api/endpoints.ts
+++ b/src/api/endpoints.ts
@@ -484,6 +484,9 @@ const endpoints: Endpoint[] = [
 			minInterval: ms('10seconds')
 		}
 	},
+	{
+		name: 'channels/show'
+	},
 ];
 
 export default endpoints;
diff --git a/src/api/endpoints/channels/show.ts b/src/api/endpoints/channels/show.ts
new file mode 100644
index 0000000000..8861e54594
--- /dev/null
+++ b/src/api/endpoints/channels/show.ts
@@ -0,0 +1,31 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import { default as Channel, IChannel } from '../../models/channel';
+import serialize from '../../serializers/channel';
+
+/**
+ * Show a channel
+ *
+ * @param {any} params
+ * @param {any} user
+ * @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');
+
+	// Fetch channel
+	const channel: IChannel = await Channel.findOne({
+		_id: channelId
+	});
+
+	if (channel === null) {
+		return rej('channel not found');
+	}
+
+	// Serialize
+	res(await serialize(channel, user));
+});
diff --git a/src/web/app/desktop/router.js b/src/web/app/desktop/router.js
index 51738f3afa..d9300cc69a 100644
--- a/src/web/app/desktop/router.js
+++ b/src/web/app/desktop/router.js
@@ -7,15 +7,16 @@ const route = require('page');
 let page = null;
 
 export default me => {
-	route('/',              index);
-	route('/i>mentions',    mentions);
-	route('/channel',       channels);
-	route('/post::post',    post);
-	route('/search::query', search);
-	route('/:user',         user.bind(null, 'home'));
-	route('/:user/graphs',  user.bind(null, 'graphs'));
-	route('/:user/:post',   post);
-	route('*',              notFound);
+	route('/',                 index);
+	route('/i>mentions',       mentions);
+	route('/channel',          channels);
+	route('/channel/:channel', channel);
+	route('/post::post',       post);
+	route('/search::query',    search);
+	route('/:user',            user.bind(null, 'home'));
+	route('/:user/graphs',     user.bind(null, 'graphs'));
+	route('/:user/:post',      post);
+	route('*',                 notFound);
 
 	function index() {
 		me ? home() : entrance();
@@ -55,6 +56,12 @@ export default me => {
 		mount(el);
 	}
 
+	function channel(ctx) {
+		const el = document.createElement('mk-channel-page');
+		el.setAttribute('id', ctx.params.channel);
+		mount(el);
+	}
+
 	function channels() {
 		mount(document.createElement('mk-channels-page'));
 	}
@@ -72,6 +79,7 @@ export default me => {
 };
 
 function mount(content) {
+	document.documentElement.style.background = '#313a42';
 	document.documentElement.removeAttribute('data-page');
 	if (page) page.unmount();
 	const body = document.getElementById('app');
diff --git a/src/web/app/desktop/tags/index.js b/src/web/app/desktop/tags/index.js
index 6d49006526..7fdeb6884d 100644
--- a/src/web/app/desktop/tags/index.js
+++ b/src/web/app/desktop/tags/index.js
@@ -61,6 +61,7 @@ require('./pages/user.tag');
 require('./pages/post.tag');
 require('./pages/search.tag');
 require('./pages/not-found.tag');
+require('./pages/channel.tag');
 require('./pages/channels.tag');
 require('./autocomplete-suggestion.tag');
 require('./progress-dialog.tag');
diff --git a/src/web/app/desktop/tags/pages/channel.tag b/src/web/app/desktop/tags/pages/channel.tag
new file mode 100644
index 0000000000..4fa172f99d
--- /dev/null
+++ b/src/web/app/desktop/tags/pages/channel.tag
@@ -0,0 +1,43 @@
+<mk-channel-page>
+	<mk-ui ref="ui">
+		<main if={ !parent.fetching }>
+			<h1>{ parent.channel.title }</h1>
+		</main>
+	</mk-ui>
+	<style>
+		:scope
+			display block
+
+			main
+				> h1
+					color #f00
+	</style>
+	<script>
+		import Progress from '../../../common/scripts/loading';
+
+		this.mixin('api');
+
+		this.id = this.opts.id;
+		this.fetching = true;
+		this.channel = null;
+
+		this.on('mount', () => {
+			document.documentElement.style.background = '#efefef';
+
+			Progress.start();
+
+			this.api('channels/show', {
+				channel_id: this.id
+			}).then(channel => {
+				Progress.done();
+
+				this.update({
+					fetching: false,
+					channel: channel
+				});
+
+				document.title = channel.title + ' | Misskey'
+			});
+		});
+	</script>
+</mk-channel-page>
diff --git a/src/web/app/desktop/tags/pages/channels.tag b/src/web/app/desktop/tags/pages/channels.tag
index 03fae3c8d1..220f1ca50e 100644
--- a/src/web/app/desktop/tags/pages/channels.tag
+++ b/src/web/app/desktop/tags/pages/channels.tag
@@ -18,7 +18,7 @@
 		this.new = () => {
 			const title = window.prompt('%i18n:desktop.tags.mk-channels-page.channel-title%');
 
-			this.api('bbs/channels/create', {
+			this.api('channels/create', {
 				title: title
 			}).then(channel => {
 				location.href = '/channel/' + channel.id;
diff --git a/src/web/app/desktop/tags/pages/user.tag b/src/web/app/desktop/tags/pages/user.tag
index 864fe22735..811ca5c0fd 100644
--- a/src/web/app/desktop/tags/pages/user.tag
+++ b/src/web/app/desktop/tags/pages/user.tag
@@ -16,7 +16,7 @@
 
 			this.refs.ui.refs.user.on('user-fetched', user => {
 				Progress.set(0.5);
-				document.title = user.name + ' | Misskey'
+				document.title = user.name + ' | Misskey';
 			});
 
 			this.refs.ui.refs.user.on('loaded', () => {

From 346c2959e058fa445ebb82e71eb37ef023ba6bd4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 00:10:30 +0900
Subject: [PATCH 280/364] wip

---
 src/api/endpoints.ts                          |  3 +
 src/api/endpoints/channels/posts.ts           | 79 +++++++++++++++++
 src/web/app/common/scripts/channel-stream.js  | 14 +++
 src/web/app/desktop/tags/pages/channel.tag    | 87 +++++++++++++++++++
 .../app/desktop/tags/pages/drive-chooser.tag  | 44 ++++++++++
 5 files changed, 227 insertions(+)
 create mode 100644 src/api/endpoints/channels/posts.ts
 create mode 100644 src/web/app/common/scripts/channel-stream.js
 create mode 100644 src/web/app/desktop/tags/pages/drive-chooser.tag

diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts
index 45b83fc9e5..88c01d4e7f 100644
--- a/src/api/endpoints.ts
+++ b/src/api/endpoints.ts
@@ -487,6 +487,9 @@ const endpoints: Endpoint[] = [
 	{
 		name: 'channels/show'
 	},
+	{
+		name: 'channels/posts'
+	},
 ];
 
 export default endpoints;
diff --git a/src/api/endpoints/channels/posts.ts b/src/api/endpoints/channels/posts.ts
new file mode 100644
index 0000000000..fa91fb93ee
--- /dev/null
+++ b/src/api/endpoints/channels/posts.ts
@@ -0,0 +1,79 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import { default as Channel, IChannel } from '../../models/channel';
+import { default as Post, IPost } from '../../models/post';
+import serialize from '../../serializers/post';
+
+/**
+ * Show a posts of a channel
+ *
+ * @param {any} params
+ * @param {any} user
+ * @return {Promise<any>}
+ */
+module.exports = (params, user) => new Promise(async (res, rej) => {
+	// Get 'limit' parameter
+	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 'max_id' parameter
+	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
+	if (maxIdErr) return rej('invalid max_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');
+	}
+
+	// Get 'channel_id' parameter
+	const [channelId, channelIdErr] = $(params.channel_id).id().$;
+	if (channelIdErr) return rej('invalid channel_id param');
+
+	// Fetch channel
+	const channel: IChannel = await Channel.findOne({
+		_id: channelId
+	});
+
+	if (channel === null) {
+		return rej('channel not found');
+	}
+
+	//#region Construct query
+	const sort = {
+		_id: -1
+	};
+
+	const query = {
+		channel_id: channel._id
+	} as any;
+
+	if (sinceId) {
+		sort._id = 1;
+		query._id = {
+			$gt: sinceId
+		};
+	} else if (maxId) {
+		query._id = {
+			$lt: maxId
+		};
+	}
+	//#endregion Construct query
+
+	// Issue query
+	const posts = await Post
+		.find(query, {
+			limit: limit,
+			sort: sort
+		});
+
+	// Serialize
+	res(await Promise.all(posts.map(async (post) =>
+		await serialize(post, user)
+	)));
+});
diff --git a/src/web/app/common/scripts/channel-stream.js b/src/web/app/common/scripts/channel-stream.js
new file mode 100644
index 0000000000..38e7d91132
--- /dev/null
+++ b/src/web/app/common/scripts/channel-stream.js
@@ -0,0 +1,14 @@
+'use strict';
+
+import Stream from './stream';
+
+/**
+ * Channel stream connection
+ */
+class Connection extends Stream {
+	constructor() {
+		super('channel');
+	}
+}
+
+export default Connection;
diff --git a/src/web/app/desktop/tags/pages/channel.tag b/src/web/app/desktop/tags/pages/channel.tag
index 4fa172f99d..8a3034f40c 100644
--- a/src/web/app/desktop/tags/pages/channel.tag
+++ b/src/web/app/desktop/tags/pages/channel.tag
@@ -2,6 +2,8 @@
 	<mk-ui ref="ui">
 		<main if={ !parent.fetching }>
 			<h1>{ parent.channel.title }</h1>
+			<mk-channel-post each={ parent.posts } post={ this }/>
+			<mk-channel-form channel={ parent.channel }/>
 		</main>
 	</mk-ui>
 	<style>
@@ -14,12 +16,15 @@
 	</style>
 	<script>
 		import Progress from '../../../common/scripts/loading';
+		import ChannelStream from '../../../common/scripts/channel-stream';
 
 		this.mixin('api');
 
 		this.id = this.opts.id;
 		this.fetching = true;
 		this.channel = null;
+		this.posts = null;
+		this.connection = new ChannelStream();
 
 		this.on('mount', () => {
 			document.documentElement.style.background = '#efefef';
@@ -38,6 +43,88 @@
 
 				document.title = channel.title + ' | Misskey'
 			});
+
+			this.api('channels/posts', {
+				channel_id: this.id
+			}).then(posts => {
+				this.update({
+					posts: posts
+				});
+			});
 		});
 	</script>
 </mk-channel-page>
+
+<mk-channel-post>
+	<header>
+		<b>{ post.user.name }</b>
+	</header>
+	<div>
+		{ post.text }
+	</div>
+	<style>
+		:scope
+			display block
+			margin 0
+			padding 0
+
+			> header
+				> b
+					color #008000
+
+	</style>
+	<script>
+		this.post = this.opts.post;
+	</script>
+</mk-channel-post>
+
+<mk-channel-form>
+	<p if={ reply }>{ reply.user.name }への返信: (or <a onclick={ clearReply }>キャンセル</a>)</p>
+	<textarea ref="text" disabled={ wait }></textarea>
+	<button class={ wait: wait } ref="submit" disabled={ wait || (refs.text.value.length == 0) } onclick={ post }>
+		{ wait ? 'やってます' : 'やる' }<mk-ellipsis if={ wait }/>
+	</button>
+
+	<style>
+		:scope
+			display block
+
+	</style>
+	<script>
+		this.mixin('api');
+
+		this.channel = this.opts.channel;
+
+		this.clearReply = () => {
+			this.update({
+				reply: null
+			});
+		};
+
+		this.clear = () => {
+			this.clearReply();
+			this.refs.text.value = '';
+		};
+
+		this.post = e => {
+			this.update({
+				wait: true
+			});
+
+			this.api('posts/create', {
+				text: this.refs.text.value,
+				reply_to_id: this.reply ? this.reply.id : undefined,
+				channel_id: this.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/tags/pages/drive-chooser.tag b/src/web/app/desktop/tags/pages/drive-chooser.tag
new file mode 100644
index 0000000000..49741ad40c
--- /dev/null
+++ b/src/web/app/desktop/tags/pages/drive-chooser.tag
@@ -0,0 +1,44 @@
+<mk-drive-chooser>
+	<mk-drive-browser ref="browser" multiple={ parent.multiple }/>
+	<div>
+		<button class="upload" title="PCからドライブにファイルをアップロード" onclick={ upload }><i class="fa fa-upload"></i></button>
+		<button class="cancel" onclick={ close }>キャンセル</button>
+		<button class="ok" onclick={ parent.ok }>決定</button>
+	</div>
+
+	<style>
+		:scope
+			display block
+			height 100%
+
+	</style>
+	<script>
+		this.multiple = this.opts.multiple != null ? this.opts.multiple : false;
+
+		this.on('mount', () => {
+			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-drive-chooser>

From 71c3e11708dad327924bdcb95193d44c2b11a907 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 01:38:19 +0900
Subject: [PATCH 281/364] wip

---
 src/api/endpoints/channels/create.ts       |  3 +-
 src/api/endpoints/posts/create.ts          | 17 +++++++++++
 src/api/models/channel.ts                  |  1 +
 src/api/serializers/post.ts                |  8 ++++-
 src/web/app/desktop/tags/pages/channel.tag | 35 ++++++++++++++++++----
 src/web/app/desktop/tags/timeline.tag      |  4 +++
 src/web/app/mobile/tags/timeline.tag       |  4 +++
 7 files changed, 65 insertions(+), 7 deletions(-)

diff --git a/src/api/endpoints/channels/create.ts b/src/api/endpoints/channels/create.ts
index 74b089dfc3..e0c0e0192a 100644
--- a/src/api/endpoints/channels/create.ts
+++ b/src/api/endpoints/channels/create.ts
@@ -21,7 +21,8 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 	const channel = await Channel.insert({
 		created_at: new Date(),
 		user_id: user._id,
-		title: title
+		title: title,
+		index: 0
 	});
 
 	// Response
diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index e0a02fa4a0..183cabf135 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -153,6 +153,16 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		if (repost && !isQuote) {
 			return rej('チャンネル内部では引用ではないRepostをすることはできません');
 		}
+	} else {
+		// 返信対象の投稿がチャンネルへの投稿だったらダメ
+		if (inReplyToPost && inReplyToPost.channel_id != null) {
+			return rej('チャンネル外部からチャンネル内部の投稿に返信することはできません');
+		}
+
+		// Repost対象の投稿がチャンネルへの投稿だったらダメ
+		if (repost && repost.channel_id != null) {
+			return rej('チャンネル外部からチャンネル内部の投稿をRepostすることはできません');
+		}
 	}
 
 	// Get 'poll' parameter
@@ -199,6 +209,7 @@ 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,
+		index: channel ? channel.index + 1 : undefined,
 		media_ids: files ? files.map(file => file._id) : undefined,
 		reply_to_id: inReplyToPost ? inReplyToPost._id : undefined,
 		repost_id: repost ? repost._id : undefined,
@@ -217,6 +228,12 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 	// -----------------------------------------------------------
 	// Post processes
 
+	Channel.update({ _id: channel._id }, {
+		$inc: {
+			index: 1
+		}
+	});
+
 	User.update({ _id: user._id }, {
 		$set: {
 			latest_post: post
diff --git a/src/api/models/channel.ts b/src/api/models/channel.ts
index 79edb71367..c80e84dbc8 100644
--- a/src/api/models/channel.ts
+++ b/src/api/models/channel.ts
@@ -10,4 +10,5 @@ export type IChannel = {
 	created_at: Date;
 	title: string;
 	user_id: mongo.ObjectID;
+	index: number;
 };
diff --git a/src/api/serializers/post.ts b/src/api/serializers/post.ts
index df917a8595..7d40df2d6a 100644
--- a/src/api/serializers/post.ts
+++ b/src/api/serializers/post.ts
@@ -8,6 +8,7 @@ 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';
@@ -76,8 +77,13 @@ const self = (
 		_post.app = await serializeApp(_post.app_id);
 	}
 
+	// Populate channel
+	if (_post.channel_id) {
+		_post.channel = await serializeChannel(_post.channel_id);
+	}
+
+	// Populate media
 	if (_post.media_ids) {
-		// Populate media
 		_post.media = await Promise.all(_post.media_ids.map(async fileId =>
 			await serializeDriveFile(fileId)
 		));
diff --git a/src/web/app/desktop/tags/pages/channel.tag b/src/web/app/desktop/tags/pages/channel.tag
index 8a3034f40c..ebd26f07b8 100644
--- a/src/web/app/desktop/tags/pages/channel.tag
+++ b/src/web/app/desktop/tags/pages/channel.tag
@@ -2,8 +2,9 @@
 	<mk-ui ref="ui">
 		<main if={ !parent.fetching }>
 			<h1>{ parent.channel.title }</h1>
-			<mk-channel-post each={ parent.posts } post={ this }/>
-			<mk-channel-form channel={ parent.channel }/>
+			<mk-channel-post if={ parent.posts } each={ parent.posts.reverse() } post={ this } form={ parent.refs.form }/>
+			<hr>
+			<mk-channel-form channel={ parent.channel } ref="form"/>
 		</main>
 	</mk-ui>
 	<style>
@@ -11,6 +12,8 @@
 			display block
 
 			main
+				padding 8px
+
 				> h1
 					color #f00
 	</style>
@@ -57,9 +60,13 @@
 
 <mk-channel-post>
 	<header>
-		<b>{ post.user.name }</b>
+		<a class="index" onclick={ reply }>{ post.index }:</a>
+		<a class="name" href={ '/' + post.user.username }><b>{ post.user.name }</b></a>
+		<mk-time time={ post.created_at } mode="detail"/>
+		<span>ID:<i>{ post.user.username }</i></span>
 	</header>
 	<div>
+		<a if={ post.reply_to }>&gt;&gt;{ post.reply_to.index }</a>
 		{ post.text }
 	</div>
 	<style>
@@ -69,17 +76,35 @@
 			padding 0
 
 			> header
-				> b
+				> .index
+					margin-right 0.25em
+					color #000
+
+				> .name
+					margin-right 0.5em
 					color #008000
 
+				> mk-time
+					margin-right 0.5em
+
+			> div
+				padding 0 0 1em 2em
+
 	</style>
 	<script>
 		this.post = this.opts.post;
+		this.form = this.opts.form;
+
+		this.reply = () => {
+			this.form.update({
+				reply: this.post
+			});
+		};
 	</script>
 </mk-channel-post>
 
 <mk-channel-form>
-	<p if={ reply }>{ reply.user.name }への返信: (or <a onclick={ clearReply }>キャンセル</a>)</p>
+	<p if={ reply }><b>&gt;&gt;{ reply.index }</b> ({ reply.user.name }): <a onclick={ clearReply }>[x]</a></p>
 	<textarea ref="text" disabled={ wait }></textarea>
 	<button class={ wait: wait } ref="submit" disabled={ wait || (refs.text.value.length == 0) } onclick={ post }>
 		{ wait ? 'やってます' : 'やる' }<mk-ellipsis if={ wait }/>
diff --git a/src/web/app/desktop/tags/timeline.tag b/src/web/app/desktop/tags/timeline.tag
index 2d6b439e38..17b2c66dc8 100644
--- a/src/web/app/desktop/tags/timeline.tag
+++ b/src/web/app/desktop/tags/timeline.tag
@@ -112,6 +112,7 @@
 			</header>
 			<div class="body">
 				<div class="text" ref="text">
+					<p class="channel" if={ p.channel != null }><a href={ '/channel/' + p.channel.id }>{ p.channel.title }</a>:</p>
 					<a class="reply" if={ p.reply_to }>
 						<i class="fa fa-reply"></i>
 					</a>
@@ -333,6 +334,9 @@
 									font-weight 400
 									font-style normal
 
+							> .channel
+								margin 0
+
 							> .reply
 								margin-right 8px
 								color #717171
diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag
index c7f5bfd681..b26a5cb108 100644
--- a/src/web/app/mobile/tags/timeline.tag
+++ b/src/web/app/mobile/tags/timeline.tag
@@ -164,6 +164,7 @@
 			</header>
 			<div class="body">
 				<div class="text" ref="text">
+					<p class="channel" if={ p.channel != null }><a href={ '/channel/' + p.channel.id }>{ p.channel.title }</a>:</p>
 					<a class="reply" if={ p.reply_to }>
 						<i class="fa fa-reply"></i>
 					</a>
@@ -373,6 +374,9 @@
 							mk-url-preview
 								margin-top 8px
 
+							> .channel
+								margin 0
+
 							> .reply
 								margin-right 8px
 								color #717171

From 1ecc35ca6fa2e8a5b4a3df1b93893b31e192a9f4 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 31 Oct 2017 17:03:16 +0000
Subject: [PATCH 282/364] fix(package): update typescript to version 2.6.1

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

diff --git a/package.json b/package.json
index 7a81bed7a6..4c6cfb5f3a 100644
--- a/package.json
+++ b/package.json
@@ -149,7 +149,7 @@
     "tcp-port-used": "0.1.2",
     "textarea-caret": "3.0.2",
     "ts-node": "3.3.0",
-    "typescript": "2.5.3",
+    "typescript": "2.6.1",
     "uuid": "3.1.0",
     "vhost": "3.0.2",
     "websocket": "1.0.25",

From e770cd6f55a5e424e731ebb89b5a091afa129904 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 02:16:05 +0900
Subject: [PATCH 283/364] wip

---
 src/web/app/desktop/router.js                 |   5 +
 src/web/app/desktop/tags/index.js             |   1 +
 src/web/app/desktop/tags/pages/channel.tag    |  33 +++-
 .../app/desktop/tags/pages/drive-chooser.tag  |  44 -----
 .../app/desktop/tags/pages/selectdrive.tag    | 159 ++++++++++++++++++
 5 files changed, 196 insertions(+), 46 deletions(-)
 delete mode 100644 src/web/app/desktop/tags/pages/drive-chooser.tag
 create mode 100644 src/web/app/desktop/tags/pages/selectdrive.tag

diff --git a/src/web/app/desktop/router.js b/src/web/app/desktop/router.js
index d9300cc69a..df67bb7b7c 100644
--- a/src/web/app/desktop/router.js
+++ b/src/web/app/desktop/router.js
@@ -8,6 +8,7 @@ let page = null;
 
 export default me => {
 	route('/',                 index);
+	route('/selectdrive',      selectDrive);
 	route('/i>mentions',       mentions);
 	route('/channel',          channels);
 	route('/channel/:channel', channel);
@@ -66,6 +67,10 @@ export default me => {
 		mount(document.createElement('mk-channels-page'));
 	}
 
+	function selectDrive() {
+		mount(document.createElement('mk-selectdrive-page'));
+	}
+
 	function notFound() {
 		mount(document.createElement('mk-not-found'));
 	}
diff --git a/src/web/app/desktop/tags/index.js b/src/web/app/desktop/tags/index.js
index 7fdeb6884d..0b92d8c236 100644
--- a/src/web/app/desktop/tags/index.js
+++ b/src/web/app/desktop/tags/index.js
@@ -63,6 +63,7 @@ require('./pages/search.tag');
 require('./pages/not-found.tag');
 require('./pages/channel.tag');
 require('./pages/channels.tag');
+require('./pages/selectdrive.tag');
 require('./autocomplete-suggestion.tag');
 require('./progress-dialog.tag');
 require('./user-preview.tag');
diff --git a/src/web/app/desktop/tags/pages/channel.tag b/src/web/app/desktop/tags/pages/channel.tag
index ebd26f07b8..a14c0648c4 100644
--- a/src/web/app/desktop/tags/pages/channel.tag
+++ b/src/web/app/desktop/tags/pages/channel.tag
@@ -2,7 +2,9 @@
 	<mk-ui ref="ui">
 		<main if={ !parent.fetching }>
 			<h1>{ parent.channel.title }</h1>
-			<mk-channel-post if={ parent.posts } each={ parent.posts.reverse() } post={ this } form={ parent.refs.form }/>
+			<virtual if={ parent.posts }>
+				<mk-channel-post each={ parent.posts.reverse() } post={ this } form={ parent.refs.form }/>
+			</virtual>
 			<hr>
 			<mk-channel-form channel={ parent.channel } ref="form"/>
 		</main>
@@ -68,6 +70,11 @@
 	<div>
 		<a if={ post.reply_to }>&gt;&gt;{ post.reply_to.index }</a>
 		{ post.text }
+		<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>
+		</div>
 	</div>
 	<style>
 		:scope
@@ -109,13 +116,19 @@
 	<button class={ wait: wait } ref="submit" disabled={ wait || (refs.text.value.length == 0) } onclick={ post }>
 		{ wait ? 'やってます' : 'やる' }<mk-ellipsis if={ wait }/>
 	</button>
-
+	<br>
+	<button onclick={ drive }>ドライブ</button>
+	<ol if={ files }>
+		<li each={ files }>{ name }</li>
+	</ol>
 	<style>
 		:scope
 			display block
 
 	</style>
 	<script>
+		import CONFIG from '../../../common/scripts/config';
+
 		this.mixin('api');
 
 		this.channel = this.opts.channel;
@@ -128,6 +141,9 @@
 
 		this.clear = () => {
 			this.clearReply();
+			this.update({
+				files: null
+			});
 			this.refs.text.value = '';
 		};
 
@@ -136,8 +152,13 @@
 				wait: true
 			});
 
+			const files = this.files && this.files.length > 0
+				? this.files.map(f => f.id)
+				: undefined;
+
 			this.api('posts/create', {
 				text: this.refs.text.value,
+				media_ids: files,
 				reply_to_id: this.reply ? this.reply.id : undefined,
 				channel_id: this.channel.id
 			}).then(data => {
@@ -151,5 +172,13 @@
 			});
 		};
 
+		this.drive = () => {
+			window['cb'] = files => {
+				this.update({
+					files: files
+				});
+			};
+			window.open(CONFIG.url + '/selectdrive?multiple=true', '_blank');
+		};
 	</script>
 </mk-channel-form>
diff --git a/src/web/app/desktop/tags/pages/drive-chooser.tag b/src/web/app/desktop/tags/pages/drive-chooser.tag
deleted file mode 100644
index 49741ad40c..0000000000
--- a/src/web/app/desktop/tags/pages/drive-chooser.tag
+++ /dev/null
@@ -1,44 +0,0 @@
-<mk-drive-chooser>
-	<mk-drive-browser ref="browser" multiple={ parent.multiple }/>
-	<div>
-		<button class="upload" title="PCからドライブにファイルをアップロード" onclick={ upload }><i class="fa fa-upload"></i></button>
-		<button class="cancel" onclick={ close }>キャンセル</button>
-		<button class="ok" onclick={ parent.ok }>決定</button>
-	</div>
-
-	<style>
-		:scope
-			display block
-			height 100%
-
-	</style>
-	<script>
-		this.multiple = this.opts.multiple != null ? this.opts.multiple : false;
-
-		this.on('mount', () => {
-			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-drive-chooser>
diff --git a/src/web/app/desktop/tags/pages/selectdrive.tag b/src/web/app/desktop/tags/pages/selectdrive.tag
new file mode 100644
index 0000000000..b196357d85
--- /dev/null
+++ b/src/web/app/desktop/tags/pages/selectdrive.tag
@@ -0,0 +1,159 @@
+<mk-selectdrive-page>
+	<mk-drive-browser ref="browser" multiple={ multiple }/>
+	<div>
+		<button class="upload" title="PCからドライブにファイルをアップロード" onclick={ upload }><i class="fa fa-upload"></i></button>
+		<button class="cancel" onclick={ close }>キャンセル</button>
+		<button class="ok" onclick={ ok }>決定</button>
+	</div>
+
+	<style>
+		:scope
+			display block
+			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>
+		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>

From f37fb38640a31c4b8865a5562628197ff21f3cce Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 03:17:14 +0900
Subject: [PATCH 284/364] wip

---
 docs/setup.en.md                              |  1 +
 docs/setup.ja.md                              |  1 +
 locales/en.yml                                | 13 +++--
 locales/ja.yml                                | 13 +++--
 src/api/endpoints/posts/create.ts             |  7 ++-
 src/api/event.ts                              |  6 +++
 src/api/stream/channel.ts                     | 12 +++++
 src/api/streaming.ts                          | 22 +++++---
 src/config.ts                                 |  2 +
 src/web/app/ch/router.js                      | 32 ++++++++++++
 src/web/app/ch/script.js                      | 18 +++++++
 src/web/app/ch/style.styl                     |  4 ++
 .../tags/pages => ch/tags}/channel.tag        | 52 +++++++++++++------
 src/web/app/ch/tags/index.js                  |  2 +
 src/web/app/ch/tags/index.tag                 | 24 +++++++++
 src/web/app/common/scripts/channel-stream.js  |  6 ++-
 src/web/app/common/scripts/config.js          |  2 +
 src/web/app/desktop/router.js                 | 12 -----
 src/web/app/desktop/tags/index.js             |  2 -
 src/web/app/desktop/tags/pages/channels.tag   | 28 ----------
 src/web/app/desktop/tags/timeline.tag         |  2 +-
 src/web/app/desktop/tags/ui.tag               |  6 +--
 src/web/app/mobile/tags/timeline.tag          |  2 +-
 src/web/app/mobile/tags/ui.tag                |  5 +-
 webpack/webpack.config.ts                     |  1 +
 25 files changed, 189 insertions(+), 86 deletions(-)
 create mode 100644 src/api/stream/channel.ts
 create mode 100644 src/web/app/ch/router.js
 create mode 100644 src/web/app/ch/script.js
 create mode 100644 src/web/app/ch/style.styl
 rename src/web/app/{desktop/tags/pages => ch/tags}/channel.tag (76%)
 create mode 100644 src/web/app/ch/tags/index.js
 create mode 100644 src/web/app/ch/tags/index.tag
 delete mode 100644 src/web/app/desktop/tags/pages/channels.tag

diff --git a/docs/setup.en.md b/docs/setup.en.md
index 3e48935346..dbc0599b5a 100644
--- a/docs/setup.en.md
+++ b/docs/setup.en.md
@@ -25,6 +25,7 @@ Note that Misskey uses following subdomains:
 * **api**.*{primary domain}*
 * **auth**.*{primary domain}*
 * **about**.*{primary domain}*
+* **ch**.*{primary domain}*
 * **stats**.*{primary domain}*
 * **status**.*{primary domain}*
 * **dev**.*{primary domain}*
diff --git a/docs/setup.ja.md b/docs/setup.ja.md
index 4f48a08088..602fd9b6a1 100644
--- a/docs/setup.ja.md
+++ b/docs/setup.ja.md
@@ -26,6 +26,7 @@ Misskeyは以下のサブドメインを使います:
 * **api**.*{primary domain}*
 * **auth**.*{primary domain}*
 * **about**.*{primary domain}*
+* **ch**.*{primary domain}*
 * **stats**.*{primary domain}*
 * **status**.*{primary domain}*
 * **dev**.*{primary domain}*
diff --git a/locales/en.yml b/locales/en.yml
index 5c7a1165ba..643649b46c 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -164,6 +164,12 @@ common:
     mk-uploader:
       waiting: "Waiting"
 
+ch:
+  tags:
+    mk-index:
+      new: "Create new channel"
+      channel-title: "Channel title"
+
 desktop:
   tags:
     mk-api-info:
@@ -241,7 +247,7 @@ desktop:
     mk-ui-header-nav:
       home: "Home"
       messaging: "Messages"
-      channels: "Channels"
+      ch: "Channels"
       info: "News"
 
     mk-ui-header-search:
@@ -352,10 +358,6 @@ desktop:
     mk-repost-form-window:
       title: "Are you sure you want to repost this post?"
 
-    mk-channels-page:
-      new: "Create new channel"
-      channel-title: "Channel title"
-
 mobile:
   tags:
     mk-drive-file-viewer:
@@ -496,6 +498,7 @@ mobile:
       home: "Home"
       notifications: "Notifications"
       messaging: "Messages"
+      ch: "Channels"
       drive: "Drive"
       settings: "Settings"
       about: "About Misskey"
diff --git a/locales/ja.yml b/locales/ja.yml
index dd76a2b900..9fd7d94f0b 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -164,6 +164,12 @@ common:
     mk-uploader:
       waiting: "待機中"
 
+ch:
+  tags:
+    mk-index:
+      new: "チャンネルを作成"
+      channel-title: "チャンネルのタイトル"
+
 desktop:
   tags:
     mk-api-info:
@@ -241,7 +247,7 @@ desktop:
     mk-ui-header-nav:
       home: "ホーム"
       messaging: "メッセージ"
-      channels: "チャンネル"
+      ch: "チャンネル"
       info: "お知らせ"
 
     mk-ui-header-search:
@@ -352,10 +358,6 @@ desktop:
     mk-repost-form-window:
       title: "この投稿をRepostしますか?"
 
-    mk-channels-page:
-      new: "チャンネルを作成"
-      channel-title: "チャンネルのタイトル"
-
 mobile:
   tags:
     mk-drive-file-viewer:
@@ -496,6 +498,7 @@ mobile:
       home: "ホーム"
       notifications: "通知"
       messaging: "メッセージ"
+      ch: "チャンネル"
       search: "検索"
       drive: "ドライブ"
       settings: "設定"
diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index 183cabf135..34265dcbc3 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -13,7 +13,7 @@ import Watching from '../../models/post-watching';
 import serialize from '../../serializers/post';
 import notify from '../../common/notify';
 import watch from '../../common/watch-post';
-import event from '../../event';
+import { default as event, publishChannelStream } from '../../event';
 import config from '../../../conf';
 
 /**
@@ -258,6 +258,11 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 	// Publish event to myself's stream
 	event(user._id, 'post', postObj);
 
+	// Publish event to channel
+	if (channel) {
+		publishChannelStream(channel._id, 'post', postObj);
+	}
+
 	// Fetch all followers
 	const followers = await Following
 		.find({
diff --git a/src/api/event.ts b/src/api/event.ts
index 9613a9f7cc..909b0d2556 100644
--- a/src/api/event.ts
+++ b/src/api/event.ts
@@ -25,6 +25,10 @@ class MisskeyEvent {
 		this.publish(`messaging-stream:${userId}-${otherpartyId}`, 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);
+	}
+
 	private publish(channel: string, type: string, value?: any): void {
 		const message = value == null ?
 			{ type: type } :
@@ -41,3 +45,5 @@ export default ev.publishUserStream.bind(ev);
 export const publishPostStream = ev.publishPostStream.bind(ev);
 
 export const publishMessagingStream = ev.publishMessagingStream.bind(ev);
+
+export const publishChannelStream = ev.publishChannelStream.bind(ev);
diff --git a/src/api/stream/channel.ts b/src/api/stream/channel.ts
new file mode 100644
index 0000000000..d67d77cbf4
--- /dev/null
+++ b/src/api/stream/channel.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 channel = request.resourceURL.query.channel;
+
+	// Subscribe channel stream
+	subscriber.subscribe(`misskey:channel-stream:${channel}`);
+	subscriber.on('message', (_, data) => {
+		connection.send(data);
+	});
+}
diff --git a/src/api/streaming.ts b/src/api/streaming.ts
index db600013b9..0e512fb210 100644
--- a/src/api/streaming.ts
+++ b/src/api/streaming.ts
@@ -9,6 +9,7 @@ import isNativeToken from './common/is-native-token';
 import homeStream from './stream/home';
 import messagingStream from './stream/messaging';
 import serverStream from './stream/server';
+import channelStream from './stream/channel';
 
 module.exports = (server: http.Server) => {
 	/**
@@ -26,14 +27,6 @@ module.exports = (server: http.Server) => {
 			return;
 		}
 
-		const user = await authenticate(request.resourceURL.query.i);
-
-		if (user == null) {
-			connection.send('authentication-failed');
-			connection.close();
-			return;
-		}
-
 		// Connect to Redis
 		const subscriber = redis.createClient(
 			config.redis.port, config.redis.host);
@@ -43,6 +36,19 @@ module.exports = (server: http.Server) => {
 			subscriber.quit();
 		});
 
+		if (request.resourceURL.pathname === '/channel') {
+			channelStream(request, connection, subscriber);
+			return;
+		}
+
+		const user = await authenticate(request.resourceURL.query.i);
+
+		if (user == null) {
+			connection.send('authentication-failed');
+			connection.close();
+			return;
+		}
+
 		const channel =
 			request.resourceURL.pathname === '/' ? homeStream :
 			request.resourceURL.pathname === '/messaging' ? messagingStream :
diff --git a/src/config.ts b/src/config.ts
index 46a93f5fef..18017e9740 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -88,6 +88,7 @@ type Mixin = {
 	api_url: string;
 	auth_url: string;
 	about_url: string;
+	ch_url: stirng;
 	stats_url: string;
 	status_url: string;
 	dev_url: string;
@@ -122,6 +123,7 @@ export default function load() {
 	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}`;
+	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.stats_url = `${mixin.scheme}://stats.${mixin.host}`;
diff --git a/src/web/app/ch/router.js b/src/web/app/ch/router.js
new file mode 100644
index 0000000000..424158f403
--- /dev/null
+++ b/src/web/app/ch/router.js
@@ -0,0 +1,32 @@
+import * as riot from 'riot';
+const route = require('page');
+let page = null;
+
+export default me => {
+	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();
+};
+
+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.js b/src/web/app/ch/script.js
new file mode 100644
index 0000000000..760d405c52
--- /dev/null
+++ b/src/web/app/ch/script.js
@@ -0,0 +1,18 @@
+/**
+ * Channels
+ */
+
+// Style
+import './style.styl';
+
+require('./tags');
+import init from '../init';
+import route from './router';
+
+/**
+ * init
+ */
+init(me => {
+	// Start routing
+	route(me);
+});
diff --git a/src/web/app/ch/style.styl b/src/web/app/ch/style.styl
new file mode 100644
index 0000000000..2fc3ac3fca
--- /dev/null
+++ b/src/web/app/ch/style.styl
@@ -0,0 +1,4 @@
+@import "../base"
+
+html
+	background #efefef
diff --git a/src/web/app/desktop/tags/pages/channel.tag b/src/web/app/ch/tags/channel.tag
similarity index 76%
rename from src/web/app/desktop/tags/pages/channel.tag
rename to src/web/app/ch/tags/channel.tag
index a14c0648c4..b16844b8bc 100644
--- a/src/web/app/desktop/tags/pages/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -1,14 +1,19 @@
-<mk-channel-page>
-	<mk-ui ref="ui">
-		<main if={ !parent.fetching }>
-			<h1>{ parent.channel.title }</h1>
-			<virtual if={ parent.posts }>
-				<mk-channel-post each={ parent.posts.reverse() } post={ this } form={ parent.refs.form }/>
-			</virtual>
-			<hr>
-			<mk-channel-form channel={ parent.channel } ref="form"/>
-		</main>
-	</mk-ui>
+<mk-channel>
+	<main if={ !fetching }>
+		<h1>{ channel.title }</h1>
+		<virtual if={ posts }>
+			<mk-channel-post each={ posts.slice().reverse() } post={ this } form={ parent.refs.form }/>
+		</virtual>
+		<hr>
+		<mk-channel-form if={ SIGNIN } channel={ channel } ref="form"/>
+		<div if={ !SIGNIN }>
+			<p>参加するには<a href={ CONFIG.url }>ログインまたは新規登録</a>してください</p>
+		</div>
+		<hr>
+		<footer>
+			<small>Misskey ver { version } (葵 aoi)</small>
+		</footer>
+	</main>
 	<style>
 		:scope
 			display block
@@ -20,16 +25,18 @@
 					color #f00
 	</style>
 	<script>
-		import Progress from '../../../common/scripts/loading';
-		import ChannelStream from '../../../common/scripts/channel-stream';
+		import Progress from '../../common/scripts/loading';
+		import ChannelStream from '../../common/scripts/channel-stream';
 
+		this.mixin('i');
 		this.mixin('api');
 
 		this.id = this.opts.id;
 		this.fetching = true;
 		this.channel = null;
 		this.posts = null;
-		this.connection = new ChannelStream();
+		this.connection = new ChannelStream(this.id);
+		this.version = VERSION;
 
 		this.on('mount', () => {
 			document.documentElement.style.background = '#efefef';
@@ -56,9 +63,22 @@
 					posts: posts
 				});
 			});
+
+			this.connection.on('post', this.onPost);
 		});
+
+		this.on('unmount', () => {
+			this.connection.off('post', this.onPost);
+			this.connection.close();
+		});
+
+		this.onPost = post => {
+			this.posts.unshift(post);
+			this.update();
+		};
+
 	</script>
-</mk-channel-page>
+</mk-channel>
 
 <mk-channel-post>
 	<header>
@@ -127,7 +147,7 @@
 
 	</style>
 	<script>
-		import CONFIG from '../../../common/scripts/config';
+		import CONFIG from '../../common/scripts/config';
 
 		this.mixin('api');
 
diff --git a/src/web/app/ch/tags/index.js b/src/web/app/ch/tags/index.js
new file mode 100644
index 0000000000..1e99ccd43e
--- /dev/null
+++ b/src/web/app/ch/tags/index.js
@@ -0,0 +1,2 @@
+require('./index.tag');
+require('./channel.tag');
diff --git a/src/web/app/ch/tags/index.tag b/src/web/app/ch/tags/index.tag
new file mode 100644
index 0000000000..1c0a037c2d
--- /dev/null
+++ b/src/web/app/ch/tags/index.tag
@@ -0,0 +1,24 @@
+<mk-index>
+	<button onclick={ new }>%i18n:ch.tags.mk-index.new%</button>
+	<style>
+		:scope
+			display block
+
+	</style>
+	<script>
+		this.mixin('api');
+
+		this.on('mount', () => {
+		});
+
+		this.new = () => {
+			const title = window.prompt('%i18n:ch.tags.mk-index.channel-title%');
+
+			this.api('channels/create', {
+				title: title
+			}).then(channel => {
+				location.href = '/' + channel.id;
+			});
+		};
+	</script>
+</mk-index>
diff --git a/src/web/app/common/scripts/channel-stream.js b/src/web/app/common/scripts/channel-stream.js
index 38e7d91132..17944dbe45 100644
--- a/src/web/app/common/scripts/channel-stream.js
+++ b/src/web/app/common/scripts/channel-stream.js
@@ -6,8 +6,10 @@ import Stream from './stream';
  * Channel stream connection
  */
 class Connection extends Stream {
-	constructor() {
-		super('channel');
+	constructor(channelId) {
+		super('channel', {
+			channel: channelId
+		});
 	}
 }
 
diff --git a/src/web/app/common/scripts/config.js b/src/web/app/common/scripts/config.js
index 75a7abba29..c5015622f0 100644
--- a/src/web/app/common/scripts/config.js
+++ b/src/web/app/common/scripts/config.js
@@ -6,6 +6,7 @@ const host = isRoot ? Url.host : Url.host.substring(Url.host.indexOf('.') + 1, U
 const scheme = Url.protocol;
 const url = `${scheme}//${host}`;
 const apiUrl = `${scheme}//api.${host}`;
+const chUrl = `${scheme}//ch.${host}`;
 const devUrl = `${scheme}//dev.${host}`;
 const aboutUrl = `${scheme}//about.${host}`;
 const statsUrl = `${scheme}//stats.${host}`;
@@ -16,6 +17,7 @@ export default {
 	scheme,
 	url,
 	apiUrl,
+	chUrl,
 	devUrl,
 	aboutUrl,
 	statsUrl,
diff --git a/src/web/app/desktop/router.js b/src/web/app/desktop/router.js
index df67bb7b7c..977e3fa9a6 100644
--- a/src/web/app/desktop/router.js
+++ b/src/web/app/desktop/router.js
@@ -10,8 +10,6 @@ export default me => {
 	route('/',                 index);
 	route('/selectdrive',      selectDrive);
 	route('/i>mentions',       mentions);
-	route('/channel',          channels);
-	route('/channel/:channel', channel);
 	route('/post::post',       post);
 	route('/search::query',    search);
 	route('/:user',            user.bind(null, 'home'));
@@ -57,16 +55,6 @@ export default me => {
 		mount(el);
 	}
 
-	function channel(ctx) {
-		const el = document.createElement('mk-channel-page');
-		el.setAttribute('id', ctx.params.channel);
-		mount(el);
-	}
-
-	function channels() {
-		mount(document.createElement('mk-channels-page'));
-	}
-
 	function selectDrive() {
 		mount(document.createElement('mk-selectdrive-page'));
 	}
diff --git a/src/web/app/desktop/tags/index.js b/src/web/app/desktop/tags/index.js
index 0b92d8c236..37fdfe37e4 100644
--- a/src/web/app/desktop/tags/index.js
+++ b/src/web/app/desktop/tags/index.js
@@ -61,8 +61,6 @@ require('./pages/user.tag');
 require('./pages/post.tag');
 require('./pages/search.tag');
 require('./pages/not-found.tag');
-require('./pages/channel.tag');
-require('./pages/channels.tag');
 require('./pages/selectdrive.tag');
 require('./autocomplete-suggestion.tag');
 require('./progress-dialog.tag');
diff --git a/src/web/app/desktop/tags/pages/channels.tag b/src/web/app/desktop/tags/pages/channels.tag
deleted file mode 100644
index 220f1ca50e..0000000000
--- a/src/web/app/desktop/tags/pages/channels.tag
+++ /dev/null
@@ -1,28 +0,0 @@
-<mk-channels-page>
-	<mk-ui ref="ui">
-		<main>
-			<button onclick={ parent.new }>%i18n:desktop.tags.mk-channels-page.new%</button>
-		</main>
-	</mk-ui>
-	<style>
-		:scope
-			display block
-
-	</style>
-	<script>
-		this.mixin('api');
-
-		this.on('mount', () => {
-		});
-
-		this.new = () => {
-			const title = window.prompt('%i18n:desktop.tags.mk-channels-page.channel-title%');
-
-			this.api('channels/create', {
-				title: title
-			}).then(channel => {
-				location.href = '/channel/' + channel.id;
-			});
-		};
-	</script>
-</mk-channels-page>
diff --git a/src/web/app/desktop/tags/timeline.tag b/src/web/app/desktop/tags/timeline.tag
index 17b2c66dc8..64b64f902f 100644
--- a/src/web/app/desktop/tags/timeline.tag
+++ b/src/web/app/desktop/tags/timeline.tag
@@ -112,7 +112,7 @@
 			</header>
 			<div class="body">
 				<div class="text" ref="text">
-					<p class="channel" if={ p.channel != null }><a href={ '/channel/' + p.channel.id }>{ p.channel.title }</a>:</p>
+					<p class="channel" if={ p.channel != null }><a href={ CONFIG.chUrl + '/' + p.channel.id } target="_blank">{ p.channel.title }</a>:</p>
 					<a class="reply" if={ p.reply_to }>
 						<i class="fa fa-reply"></i>
 					</a>
diff --git a/src/web/app/desktop/tags/ui.tag b/src/web/app/desktop/tags/ui.tag
index 7527358dce..3123c34f4f 100644
--- a/src/web/app/desktop/tags/ui.tag
+++ b/src/web/app/desktop/tags/ui.tag
@@ -335,10 +335,10 @@
 				</a>
 			</li>
 		</virtual>
-		<li class="channels">
-			<a href={ CONFIG.url + '/channel' }>
+		<li class="ch">
+			<a href={ CONFIG.chUrl } target="_blank">
 				<i class="fa fa-television"></i>
-				<p>%i18n:desktop.tags.mk-ui-header-nav.channels%</p>
+				<p>%i18n:desktop.tags.mk-ui-header-nav.ch%</p>
 			</a>
 		</li>
 		<li class="info">
diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag
index b26a5cb108..ad18521df6 100644
--- a/src/web/app/mobile/tags/timeline.tag
+++ b/src/web/app/mobile/tags/timeline.tag
@@ -164,7 +164,7 @@
 			</header>
 			<div class="body">
 				<div class="text" ref="text">
-					<p class="channel" if={ p.channel != null }><a href={ '/channel/' + p.channel.id }>{ p.channel.title }</a>:</p>
+					<p class="channel" if={ p.channel != null }><a href={ CONFIG.chUrl + '/' + p.channel.id } target="_blank">{ p.channel.title }</a>:</p>
 					<a class="reply" if={ p.reply_to }>
 						<i class="fa fa-reply"></i>
 					</a>
diff --git a/src/web/app/mobile/tags/ui.tag b/src/web/app/mobile/tags/ui.tag
index fb8cbcdbd2..b2d96f6b8b 100644
--- a/src/web/app/mobile/tags/ui.tag
+++ b/src/web/app/mobile/tags/ui.tag
@@ -231,10 +231,11 @@
 				<li><a href="/i/messaging"><i class="fa fa-comments-o"></i>%i18n:mobile.tags.mk-ui-nav.messaging%<i class="i fa fa-circle" if={ hasUnreadMessagingMessages }></i><i class="fa fa-angle-right"></i></a></li>
 			</ul>
 			<ul>
-				<li><a onclick={ search }><i class="fa fa-search"></i>%i18n:mobile.tags.mk-ui-nav.search%<i class="fa fa-angle-right"></i></a></li>
+				<li><a href={ CONFIG.chUrl } target="_blank"><i class="fa fa-television"></i>%i18n:mobile.tags.mk-ui-nav.ch%<i class="fa fa-angle-right"></i></a></li>
+				<li><a href="/i/drive"><i class="fa fa-cloud"></i>%i18n:mobile.tags.mk-ui-nav.drive%<i class="fa fa-angle-right"></i></a></li>
 			</ul>
 			<ul>
-				<li><a href="/i/drive"><i class="fa fa-cloud"></i>%i18n:mobile.tags.mk-ui-nav.drive%<i class="fa fa-angle-right"></i></a></li>
+				<li><a onclick={ search }><i class="fa fa-search"></i>%i18n:mobile.tags.mk-ui-nav.search%<i class="fa fa-angle-right"></i></a></li>
 			</ul>
 			<ul>
 				<li><a href="/i/settings"><i class="fa fa-cog"></i>%i18n:mobile.tags.mk-ui-nav.settings%<i class="fa fa-angle-right"></i></a></li>
diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts
index 5199285d55..066df18157 100644
--- a/webpack/webpack.config.ts
+++ b/webpack/webpack.config.ts
@@ -16,6 +16,7 @@ module.exports = langs.map(([lang, locale]) => {
 	const entry = {
 		desktop: './src/web/app/desktop/script.js',
 		mobile: './src/web/app/mobile/script.js',
+		ch: './src/web/app/ch/script.js',
 		stats: './src/web/app/stats/script.js',
 		status: './src/web/app/status/script.js',
 		dev: './src/web/app/dev/script.js',

From 3c4719a0b119c78108edeff2ecf7965f1c517237 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 03:31:36 +0900
Subject: [PATCH 285/364] wip

---
 src/web/app/ch/tags/channel.tag              |  2 +-
 src/web/app/mobile/router.js                 |  5 ++
 src/web/app/mobile/tags/drive.tag            |  6 +-
 src/web/app/mobile/tags/index.js             |  1 +
 src/web/app/mobile/tags/page/selectdrive.tag | 83 ++++++++++++++++++++
 5 files changed, 95 insertions(+), 2 deletions(-)
 create mode 100644 src/web/app/mobile/tags/page/selectdrive.tag

diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index b16844b8bc..d43113a554 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -11,7 +11,7 @@
 		</div>
 		<hr>
 		<footer>
-			<small>Misskey ver { version } (葵 aoi)</small>
+			<small><a href={ CONFIG.url }>Misskey</a> ver { version } (葵 aoi)</small>
 		</footer>
 	</main>
 	<style>
diff --git a/src/web/app/mobile/router.js b/src/web/app/mobile/router.js
index d59b2ec3a1..01eb3c8145 100644
--- a/src/web/app/mobile/router.js
+++ b/src/web/app/mobile/router.js
@@ -8,6 +8,7 @@ let page = null;
 
 export default me => {
 	route('/',                           index);
+	route('/selectdrive',                selectDrive);
 	route('/i/notifications',            notifications);
 	route('/i/messaging',                messaging);
 	route('/i/messaging/:username',      messaging);
@@ -122,6 +123,10 @@ export default me => {
 		mount(el);
 	}
 
+	function selectDrive() {
+		mount(document.createElement('mk-selectdrive-page'));
+	}
+
 	function notFound() {
 		mount(document.createElement('mk-not-found'));
 	}
diff --git a/src/web/app/mobile/tags/drive.tag b/src/web/app/mobile/tags/drive.tag
index 9f3e647735..c17b7ce579 100644
--- a/src/web/app/mobile/tags/drive.tag
+++ b/src/web/app/mobile/tags/drive.tag
@@ -483,7 +483,7 @@
 			if (fn == null || fn == '') return;
 			switch (fn) {
 				case '1':
-					this.refs.file.click();
+					this.selectLocalFile();
 					break;
 				case '2':
 					this.urlUpload();
@@ -503,6 +503,10 @@
 			}
 		};
 
+		this.selectLocalFile = () => {
+			this.refs.file.click();
+		};
+
 		this.createFolder = () => {
 			const name = window.prompt('フォルダー名');
 			if (name == null || name == '') return;
diff --git a/src/web/app/mobile/tags/index.js b/src/web/app/mobile/tags/index.js
index a79f4f7e7e..19952c20cd 100644
--- a/src/web/app/mobile/tags/index.js
+++ b/src/web/app/mobile/tags/index.js
@@ -19,6 +19,7 @@ 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');
diff --git a/src/web/app/mobile/tags/page/selectdrive.tag b/src/web/app/mobile/tags/page/selectdrive.tag
new file mode 100644
index 0000000000..d9e7d95c41
--- /dev/null
+++ b/src/web/app/mobile/tags/page/selectdrive.tag
@@ -0,0 +1,83 @@
+<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 }><i class="fa fa-upload"></i></button>
+		<button if={ multiple } class="ok" onclick={ ok }><i class="fa fa-check"></i></button>
+	</header>
+	<mk-drive ref="browser" select-file={ true } multiple={ multiple }/>
+
+	<style>
+		:scope
+			display block
+			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
+
+				> .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
+				height calc(100% - 42px)
+				overflow scroll
+				-webkit-overflow-scrolling touch
+
+	</style>
+	<script>
+		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>

From 20707d6fd9ce2dea1342ad38156c32fcec82217a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 03:41:34 +0900
Subject: [PATCH 286/364] wip

---
 locales/en.yml                  |  3 +++
 locales/ja.yml                  |  3 +++
 src/web/app/ch/tags/channel.tag | 24 +++++++++++++++++++++---
 3 files changed, 27 insertions(+), 3 deletions(-)

diff --git a/locales/en.yml b/locales/en.yml
index 643649b46c..afb6d2f2fb 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -360,6 +360,9 @@ desktop:
 
 mobile:
   tags:
+    mk-selectdrive-page:
+      select-file: "Select file(s)"
+
     mk-drive-file-viewer:
       download: "Download"
       rename: "Rename"
diff --git a/locales/ja.yml b/locales/ja.yml
index 9fd7d94f0b..03975556b5 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -360,6 +360,9 @@ desktop:
 
 mobile:
   tags:
+    mk-selectdrive-page:
+      select-file: "ファイルを選択"
+
     mk-drive-file-viewer:
       download: "ダウンロード"
       rename: "名前を変更"
diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index d43113a554..e8537e3f0a 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -1,9 +1,13 @@
 <mk-channel>
 	<main if={ !fetching }>
 		<h1>{ channel.title }</h1>
-		<virtual if={ posts }>
-			<mk-channel-post each={ posts.slice().reverse() } post={ this } form={ parent.refs.form }/>
-		</virtual>
+		<p if={ postsFetching }>読み込み中<mk-ellipsis/></p>
+		<div if={ !postsFetching }>
+			<p if={ posts == null }></p>>
+			<virtual if={ posts != null }>
+				<mk-channel-post each={ posts.slice().reverse() } post={ this } form={ parent.refs.form }/>
+			</virtual>
+		</div>
 		<hr>
 		<mk-channel-form if={ SIGNIN } channel={ channel } ref="form"/>
 		<div if={ !SIGNIN }>
@@ -33,6 +37,7 @@
 
 		this.id = this.opts.id;
 		this.fetching = true;
+		this.postsFetching = true;
 		this.channel = null;
 		this.posts = null;
 		this.connection = new ChannelStream(this.id);
@@ -60,6 +65,7 @@
 				channel_id: this.id
 			}).then(posts => {
 				this.update({
+					postsFetching: false,
 					posts: posts
 				});
 			});
@@ -84,6 +90,7 @@
 	<header>
 		<a class="index" onclick={ reply }>{ post.index }:</a>
 		<a class="name" href={ '/' + 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>
 	</header>
@@ -114,6 +121,17 @@
 				> mk-time
 					margin-right 0.5em
 
+					&:first-of-type
+						display none
+
+				@media (max-width 600px)
+					> mk-time
+						&:first-of-type
+							display initial
+
+						&:last-of-type
+							display none
+
 			> div
 				padding 0 0 1em 2em
 

From 0cffc1cac0140a420c64e039487c32237c581d5e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 03:42:50 +0900
Subject: [PATCH 287/364] wip

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

diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index e8537e3f0a..43a1f851f8 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -3,7 +3,7 @@
 		<h1>{ channel.title }</h1>
 		<p if={ postsFetching }>読み込み中<mk-ellipsis/></p>
 		<div if={ !postsFetching }>
-			<p if={ posts == null }></p>>
+			<p if={ posts == null }>まだ投稿がありません</p>
 			<virtual if={ posts != null }>
 				<mk-channel-post each={ posts.slice().reverse() } post={ this } form={ parent.refs.form }/>
 			</virtual>

From 42be937fcb6f02037ff4024a2fb1cf463c50ce0c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 04:11:56 +0900
Subject: [PATCH 288/364] wip

---
 src/api/endpoints.ts            |  3 ++
 src/api/endpoints/channels.ts   | 59 +++++++++++++++++++++++++++++++++
 src/web/app/ch/tags/channel.tag |  7 ++--
 src/web/app/ch/tags/index.tag   | 13 ++++++--
 4 files changed, 77 insertions(+), 5 deletions(-)
 create mode 100644 src/api/endpoints/channels.ts

diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts
index 88c01d4e7f..c4dacad857 100644
--- a/src/api/endpoints.ts
+++ b/src/api/endpoints.ts
@@ -490,6 +490,9 @@ const endpoints: Endpoint[] = [
 	{
 		name: 'channels/posts'
 	},
+	{
+		name: 'channels'
+	},
 ];
 
 export default endpoints;
diff --git a/src/api/endpoints/channels.ts b/src/api/endpoints/channels.ts
new file mode 100644
index 0000000000..e10c943896
--- /dev/null
+++ b/src/api/endpoints/channels.ts
@@ -0,0 +1,59 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import Channel from '../models/channel';
+import serialize from '../serializers/channel';
+
+/**
+ * Get all channels
+ *
+ * @param {any} params
+ * @param {any} me
+ * @return {Promise<any>}
+ */
+module.exports = (params, me) => new Promise(async (res, rej) => {
+	// 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 'max_id' parameter
+	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
+	if (maxIdErr) return rej('invalid max_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');
+	}
+
+	// Construct query
+	const sort = {
+		_id: -1
+	};
+	const query = {} as any;
+	if (sinceId) {
+		sort._id = 1;
+		query._id = {
+			$gt: sinceId
+		};
+	} else if (maxId) {
+		query._id = {
+			$lt: maxId
+		};
+	}
+
+	// Issue query
+	const channels = await Channel
+		.find(query, {
+			limit: limit,
+			sort: sort
+		});
+
+	// Serialize
+	res(await Promise.all(channels.map(async channel =>
+		await serialize(channel, me))));
+});
diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index 43a1f851f8..12a6b5a3b9 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -1,4 +1,6 @@
 <mk-channel>
+	<header><a href={ CONFIG.chUrl }>Misskey Channels</a></header>
+	<hr>
 	<main if={ !fetching }>
 		<h1>{ channel.title }</h1>
 		<p if={ postsFetching }>読み込み中<mk-ellipsis/></p>
@@ -21,10 +23,9 @@
 	<style>
 		:scope
 			display block
+			padding 8px
 
-			main
-				padding 8px
-
+			> main
 				> h1
 					color #f00
 	</style>
diff --git a/src/web/app/ch/tags/index.tag b/src/web/app/ch/tags/index.tag
index 1c0a037c2d..a64ddb6ccd 100644
--- a/src/web/app/ch/tags/index.tag
+++ b/src/web/app/ch/tags/index.tag
@@ -1,5 +1,9 @@
 <mk-index>
-	<button onclick={ new }>%i18n:ch.tags.mk-index.new%</button>
+	<button onclick={ n }>%i18n:ch.tags.mk-index.new%</button>
+	<hr>
+	<ul if={ channels }>
+		<li each={ channels }><a href={ '/' + this.id }>{ this.title }</a></li>
+	</ul>
 	<style>
 		:scope
 			display block
@@ -9,9 +13,14 @@
 		this.mixin('api');
 
 		this.on('mount', () => {
+			this.api('channels').then(channels => {
+				this.update({
+					channels: channels
+				});
+			});
 		});
 
-		this.new = () => {
+		this.n = () => {
 			const title = window.prompt('%i18n:ch.tags.mk-index.channel-title%');
 
 			this.api('channels/create', {

From 6f242f229a48eeb97e8d43f9c75b35c172f6e4b1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 04:16:16 +0900
Subject: [PATCH 289/364] :v:

---
 src/common/get-post-summary.ts | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/common/get-post-summary.ts b/src/common/get-post-summary.ts
index f628a32b41..ac15077b28 100644
--- a/src/common/get-post-summary.ts
+++ b/src/common/get-post-summary.ts
@@ -3,7 +3,13 @@
  * @param {*} post 投稿
  */
 const summarize = (post: any): string => {
-	let summary = post.text ? post.text : '';
+	let summary = '';
+
+	// チャンネル
+	summary += post.channel ? `${post.channel.title}:` : '';
+
+	// 本文
+	summary += post.text ? post.text : '';
 
 	// メディアが添付されているとき
 	if (post.media) {

From 92cd2265b17898201a1e45c89e82c321b78e5018 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 04:18:02 +0900
Subject: [PATCH 290/364] v2769

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2f75462e5f..4bf0f6abc1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+2769 (2017/11/01)
+-----------------
+* New: チャンネルシステム
+
 2752 (2017/10/30)
 -----------------
 * New: 未読の通知がある場合アイコンを表示するように
diff --git a/package.json b/package.json
index 7a81bed7a6..57b3439d65 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2752",
+  "version": "0.0.2769",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",

From c5d5f7cef39eec27db34ac9ff2cb6db25bbab206 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 04:28:53 +0900
Subject: [PATCH 291/364] Fix bug

---
 src/api/endpoints/posts/create.ts | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index 34265dcbc3..2e9f1d90fb 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -228,11 +228,13 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 	// -----------------------------------------------------------
 	// Post processes
 
-	Channel.update({ _id: channel._id }, {
-		$inc: {
-			index: 1
-		}
-	});
+	if (channel) {
+		Channel.update({ _id: channel._id }, {
+			$inc: {
+				index: 1
+			}
+		});
+	}
 
 	User.update({ _id: user._id }, {
 		$set: {

From b60ed8f20cec2cbaa0d233f0c882b9f3f49a74f6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 04:32:18 +0900
Subject: [PATCH 292/364] Refactor

---
 src/api/endpoints/posts/create.ts | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index 2e9f1d90fb..43b503b981 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -228,13 +228,6 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 	// -----------------------------------------------------------
 	// Post processes
 
-	if (channel) {
-		Channel.update({ _id: channel._id }, {
-			$inc: {
-				index: 1
-			}
-		});
-	}
 
 	User.update({ _id: user._id }, {
 		$set: {
@@ -260,8 +253,15 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 	// Publish event to myself's stream
 	event(user._id, 'post', postObj);
 
-	// Publish event to channel
 	if (channel) {
+		// Increment channel index(posts count)
+		Channel.update({ _id: channel._id }, {
+			$inc: {
+				index: 1
+			}
+		});
+
+		// Publish event to channel
 		publishChannelStream(channel._id, 'post', postObj);
 	}
 

From 7600ce2591add1f73797e0937758ecfa37f5406f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 05:00:53 +0900
Subject: [PATCH 293/364] Fix bug

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

diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index 12a6b5a3b9..6ffa6bccf8 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -196,7 +196,7 @@
 				: undefined;
 
 			this.api('posts/create', {
-				text: this.refs.text.value,
+				text: this.refs.text.value == '' ? undefined : this.refs.text.value,
 				media_ids: files,
 				reply_to_id: this.reply ? this.reply.id : undefined,
 				channel_id: this.channel.id

From 47cabbad92e2c2d6cbeb5de6afc5ec689e8ff79d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 05:02:06 +0900
Subject: [PATCH 294/364] Oops

---
 src/api/endpoints/posts/create.ts | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index 43b503b981..2bb1a7af17 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -228,7 +228,6 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 	// -----------------------------------------------------------
 	// Post processes
 
-
 	User.update({ _id: user._id }, {
 		$set: {
 			latest_post: post

From 48695af5f535d7f53a70f02077e2ae2a3cd313e4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 05:02:49 +0900
Subject: [PATCH 295/364] v2775

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4bf0f6abc1..a566fbec8c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+2775 (2017/11/01)
+-----------------
+* Fix: バグ修正
+
 2769 (2017/11/01)
 -----------------
 * New: チャンネルシステム
diff --git a/package.json b/package.json
index 57b3439d65..9700adeba5 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2769",
+  "version": "0.0.2775",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",

From f1f552c4f1d5ba5c1e99a3e548563bf5877ac88b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 05:16:05 +0900
Subject: [PATCH 296/364] :v:

---
 src/web/app/ch/tags/channel.tag | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index 6ffa6bccf8..8657652fb0 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -100,7 +100,9 @@
 		{ post.text }
 		<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 }/>
+				<a href={ file.url } target="_blank">
+					<img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/>
+				</a>
 			</virtual>
 		</div>
 	</div>
@@ -136,6 +138,14 @@
 			> div
 				padding 0 0 1em 2em
 
+				> .media
+					> a
+						display block
+
+						> img
+							max-width 100%
+							vertical-align bottom
+
 	</style>
 	<script>
 		this.post = this.opts.post;

From d6a8e6b7c247a0a2750774e5070d07573660622e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 05:17:48 +0900
Subject: [PATCH 297/364] v2777

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index a566fbec8c..bda30f1d95 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+2777 (2017/11/01)
+-----------------
+* 細かいブラッシュアップ
+
 2775 (2017/11/01)
 -----------------
 * Fix: バグ修正
diff --git a/package.json b/package.json
index 9700adeba5..cd1c37a3a3 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2775",
+  "version": "0.0.2777",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",

From 60a7640eb1547dc61997ba5db1eb2c28bdec33a0 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, 1 Nov 2017 10:22:40 +0900
Subject: [PATCH 298/364] RENAME: reply_to -> reply

---
 src/api/endpoints/aggregation/posts.ts               |  4 ++--
 src/api/endpoints/aggregation/posts/reply.ts         |  2 +-
 src/api/endpoints/aggregation/users/activity.ts      |  4 ++--
 src/api/endpoints/aggregation/users/post.ts          |  4 ++--
 src/api/endpoints/posts.ts                           |  2 +-
 src/api/endpoints/posts/context.ts                   |  8 ++++----
 src/api/endpoints/posts/create.ts                    | 10 +++++-----
 src/api/endpoints/posts/replies.ts                   |  2 +-
 src/api/endpoints/posts/trend.ts                     |  2 +-
 .../endpoints/users/get_frequently_replied_users.ts  |  6 +++---
 src/api/endpoints/users/posts.ts                     |  2 +-
 src/api/models/post.ts                               |  2 +-
 src/api/serializers/post.ts                          |  4 ++--
 src/common/get-post-summary.ts                       |  6 +++---
 src/docs/api/entities/post.pug                       | 10 +++++-----
 src/web/app/ch/tags/channel.tag                      |  4 ++--
 src/web/app/desktop/tags/post-detail.tag             |  8 ++++----
 src/web/app/desktop/tags/post-form.tag               |  2 +-
 src/web/app/desktop/tags/sub-post-content.tag        |  2 +-
 src/web/app/desktop/tags/timeline.tag                |  6 +++---
 src/web/app/mobile/tags/post-detail.tag              |  8 ++++----
 src/web/app/mobile/tags/post-form.tag                |  2 +-
 src/web/app/mobile/tags/sub-post-content.tag         |  2 +-
 src/web/app/mobile/tags/timeline.tag                 |  6 +++---
 test/api.js                                          | 12 ++++++------
 25 files changed, 60 insertions(+), 60 deletions(-)

diff --git a/src/api/endpoints/aggregation/posts.ts b/src/api/endpoints/aggregation/posts.ts
index 48ee225129..9d8bccbdb2 100644
--- a/src/api/endpoints/aggregation/posts.ts
+++ b/src/api/endpoints/aggregation/posts.ts
@@ -19,7 +19,7 @@ module.exports = params => new Promise(async (res, rej) => {
 		.aggregate([
 			{ $project: {
 				repost_id: '$repost_id',
-				reply_to_id: '$reply_to_id',
+				reply_id: '$reply_id',
 				created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
 			}},
 			{ $project: {
@@ -34,7 +34,7 @@ module.exports = params => new Promise(async (res, rej) => {
 						then: 'repost',
 						else: {
 							$cond: {
-								if: { $ne: ['$reply_to_id', null] },
+								if: { $ne: ['$reply_id', null] },
 								then: 'reply',
 								else: 'post'
 							}
diff --git a/src/api/endpoints/aggregation/posts/reply.ts b/src/api/endpoints/aggregation/posts/reply.ts
index 02a60c8969..b114c34e1e 100644
--- a/src/api/endpoints/aggregation/posts/reply.ts
+++ b/src/api/endpoints/aggregation/posts/reply.ts
@@ -26,7 +26,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
 
 	const datas = await Post
 		.aggregate([
-			{ $match: { reply_to: post._id } },
+			{ $match: { reply: post._id } },
 			{ $project: {
 				created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
 			}},
diff --git a/src/api/endpoints/aggregation/users/activity.ts b/src/api/endpoints/aggregation/users/activity.ts
index 5a3e78c441..102a71d7cb 100644
--- a/src/api/endpoints/aggregation/users/activity.ts
+++ b/src/api/endpoints/aggregation/users/activity.ts
@@ -40,7 +40,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
 			{ $match: { user_id: user._id } },
 			{ $project: {
 				repost_id: '$repost_id',
-				reply_to_id: '$reply_to_id',
+				reply_id: '$reply_id',
 				created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
 			}},
 			{ $project: {
@@ -55,7 +55,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
 						then: 'repost',
 						else: {
 							$cond: {
-								if: { $ne: ['$reply_to_id', null] },
+								if: { $ne: ['$reply_id', null] },
 								then: 'reply',
 								else: 'post'
 							}
diff --git a/src/api/endpoints/aggregation/users/post.ts b/src/api/endpoints/aggregation/users/post.ts
index c964815a0c..c6a75eee39 100644
--- a/src/api/endpoints/aggregation/users/post.ts
+++ b/src/api/endpoints/aggregation/users/post.ts
@@ -34,7 +34,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
 			{ $match: { user_id: user._id } },
 			{ $project: {
 				repost_id: '$repost_id',
-				reply_to_id: '$reply_to_id',
+				reply_id: '$reply_id',
 				created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
 			}},
 			{ $project: {
@@ -49,7 +49,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
 						then: 'repost',
 						else: {
 							$cond: {
-								if: { $ne: ['$reply_to_id', null] },
+								if: { $ne: ['$reply_id', null] },
 								then: 'reply',
 								else: 'post'
 							}
diff --git a/src/api/endpoints/posts.ts b/src/api/endpoints/posts.ts
index 23b9bd0b66..f6efcc108d 100644
--- a/src/api/endpoints/posts.ts
+++ b/src/api/endpoints/posts.ts
@@ -62,7 +62,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
 	}
 
 	if (reply != undefined) {
-		query.reply_to_id = reply ? { $exists: true, $ne: null } : null;
+		query.reply_id = reply ? { $exists: true, $ne: null } : null;
 	}
 
 	if (repost != undefined) {
diff --git a/src/api/endpoints/posts/context.ts b/src/api/endpoints/posts/context.ts
index cd5f15f481..bad59a6bee 100644
--- a/src/api/endpoints/posts/context.ts
+++ b/src/api/endpoints/posts/context.ts
@@ -49,13 +49,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 			return;
 		}
 
-		if (p.reply_to_id) {
-			await get(p.reply_to_id);
+		if (p.reply_id) {
+			await get(p.reply_id);
 		}
 	}
 
-	if (post.reply_to_id) {
-		await get(post.reply_to_id);
+	if (post.reply_id) {
+		await get(post.reply_id);
 	}
 
 	// Serialize
diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index 2bb1a7af17..3b9e0d8997 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -103,9 +103,9 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		}
 	}
 
-	// Get 'in_reply_to_post_id' parameter
-	const [inReplyToPostId, inReplyToPostIdErr] = $(params.reply_to_id).optional.id().$;
-	if (inReplyToPostIdErr) return rej('invalid in_reply_to_post_id');
+	// Get 'in_reply_post_id' parameter
+	const [inReplyToPostId, inReplyToPostIdErr] = $(params.reply_id).optional.id().$;
+	if (inReplyToPostIdErr) return rej('invalid in_reply_post_id');
 
 	let inReplyToPost: IPost = null;
 	if (inReplyToPostId !== undefined) {
@@ -192,7 +192,7 @@ 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_to_id ? user.latest_post.reply_to_id.toString() : null,
+			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())
 		}, {
@@ -211,7 +211,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		channel_id: channel ? channel._id : undefined,
 		index: channel ? channel.index + 1 : undefined,
 		media_ids: files ? files.map(file => file._id) : undefined,
-		reply_to_id: inReplyToPost ? inReplyToPost._id : undefined,
+		reply_id: inReplyToPost ? inReplyToPost._id : undefined,
 		repost_id: repost ? repost._id : undefined,
 		poll: poll,
 		text: text,
diff --git a/src/api/endpoints/posts/replies.ts b/src/api/endpoints/posts/replies.ts
index 89f4d99841..3fd6a46769 100644
--- a/src/api/endpoints/posts/replies.ts
+++ b/src/api/endpoints/posts/replies.ts
@@ -40,7 +40,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Issue query
 	const replies = await Post
-		.find({ reply_to_id: post._id }, {
+		.find({ reply_id: post._id }, {
 			limit: limit,
 			skip: offset,
 			sort: {
diff --git a/src/api/endpoints/posts/trend.ts b/src/api/endpoints/posts/trend.ts
index 3277206d26..64a195dff1 100644
--- a/src/api/endpoints/posts/trend.ts
+++ b/src/api/endpoints/posts/trend.ts
@@ -48,7 +48,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	} as any;
 
 	if (reply != undefined) {
-		query.reply_to_id = reply ? { $exists: true, $ne: null } : null;
+		query.reply_id = reply ? { $exists: true, $ne: null } : null;
 	}
 
 	if (repost != undefined) {
diff --git a/src/api/endpoints/users/get_frequently_replied_users.ts b/src/api/endpoints/users/get_frequently_replied_users.ts
index 2e0e2e40a7..bb0f3b4cea 100644
--- a/src/api/endpoints/users/get_frequently_replied_users.ts
+++ b/src/api/endpoints/users/get_frequently_replied_users.ts
@@ -27,7 +27,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	// Fetch recent posts
 	const recentPosts = await Post.find({
 		user_id: user._id,
-		reply_to_id: {
+		reply_id: {
 			$exists: true,
 			$ne: null
 		}
@@ -38,7 +38,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 		limit: 1000,
 		fields: {
 			_id: false,
-			reply_to_id: true
+			reply_id: true
 		}
 	});
 
@@ -49,7 +49,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 
 	const replyTargetPosts = await Post.find({
 		_id: {
-			$in: recentPosts.map(p => p.reply_to_id)
+			$in: recentPosts.map(p => p.reply_id)
 		},
 		user_id: {
 			$ne: user._id
diff --git a/src/api/endpoints/users/posts.ts b/src/api/endpoints/users/posts.ts
index e37b660773..d8204b8b80 100644
--- a/src/api/endpoints/users/posts.ts
+++ b/src/api/endpoints/users/posts.ts
@@ -85,7 +85,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
 	}
 
 	if (!includeReplies) {
-		query.reply_to_id = null;
+		query.reply_id = null;
 	}
 
 	if (withMedia) {
diff --git a/src/api/models/post.ts b/src/api/models/post.ts
index fe07dcb0b1..7584ce182d 100644
--- a/src/api/models/post.ts
+++ b/src/api/models/post.ts
@@ -13,7 +13,7 @@ export type IPost = {
 	channel_id: mongo.ObjectID;
 	created_at: Date;
 	media_ids: mongo.ObjectID[];
-	reply_to_id: mongo.ObjectID;
+	reply_id: mongo.ObjectID;
 	repost_id: mongo.ObjectID;
 	poll: {}; // todo
 	text: string;
diff --git a/src/api/serializers/post.ts b/src/api/serializers/post.ts
index 7d40df2d6a..7c3690ef79 100644
--- a/src/api/serializers/post.ts
+++ b/src/api/serializers/post.ts
@@ -123,9 +123,9 @@ const self = (
 		});
 		_post.next = next ? next._id : null;
 
-		if (_post.reply_to_id) {
+		if (_post.reply_id) {
 			// Populate reply to post
-			_post.reply_to = await self(_post.reply_to_id, meId, {
+			_post.reply = await self(_post.reply_id, meId, {
 				detail: false
 			});
 		}
diff --git a/src/common/get-post-summary.ts b/src/common/get-post-summary.ts
index ac15077b28..6e8f65708e 100644
--- a/src/common/get-post-summary.ts
+++ b/src/common/get-post-summary.ts
@@ -22,9 +22,9 @@ const summarize = (post: any): string => {
 	}
 
 	// 返信のとき
-	if (post.reply_to_id) {
-		if (post.reply_to) {
-			summary += ` RE: ${summarize(post.reply_to)}`;
+	if (post.reply_id) {
+		if (post.reply) {
+			summary += ` RE: ${summarize(post.reply)}`;
 		} else {
 			summary += ' RE: ...';
 		}
diff --git a/src/docs/api/entities/post.pug b/src/docs/api/entities/post.pug
index e505d3fcb6..954f172717 100644
--- a/src/docs/api/entities/post.pug
+++ b/src/docs/api/entities/post.pug
@@ -52,11 +52,11 @@ block content
 					td Number
 					td 返信数
 				tr.optional
-					td reply_to
+					td reply
 					td: a(href='./post', target='_blank') Post
 					td 返信先の投稿
 				tr.nullable
-					td reply_to_id
+					td reply_id
 					td ID
 					td 返信先の投稿のID
 				tr.optional
@@ -90,7 +90,7 @@ block content
 			{
 				"created_at": "2016-12-10T00:28:50.114Z",
 				"media_ids": null,
-				"reply_to_id": "584a16b15860fc52320137e3",
+				"reply_id": "584a16b15860fc52320137e3",
 				"repost_id": null,
 				"text": "小日向美穂だぞ!",
 				"user_id": "5848bf7764e572683f4402f8",
@@ -117,10 +117,10 @@ block content
 					"is_following": true,
 					"is_followed": true
 				},
-				"reply_to": {
+				"reply": {
 					"created_at": "2016-12-09T02:28:01.563Z",
 					"media_ids": null,
-					"reply_to_id": "5849d35e547e4249be329884",
+					"reply_id": "5849d35e547e4249be329884",
 					"repost_id": null,
 					"text": "アイコン小日向美穂?",
 					"user_id": "57d01a501fdf2d07be417afe",
diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index 8657652fb0..f1eea6b9bc 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -96,7 +96,7 @@
 		<span>ID:<i>{ post.user.username }</i></span>
 	</header>
 	<div>
-		<a if={ post.reply_to }>&gt;&gt;{ post.reply_to.index }</a>
+		<a if={ post.reply }>&gt;&gt;{ post.reply.index }</a>
 		{ post.text }
 		<div class="media" if={ post.media }>
 			<virtual each={ file in post.media }>
@@ -208,7 +208,7 @@
 			this.api('posts/create', {
 				text: this.refs.text.value == '' ? undefined : this.refs.text.value,
 				media_ids: files,
-				reply_to_id: this.reply ? this.reply.id : undefined,
+				reply_id: this.reply ? this.reply.id : undefined,
 				channel_id: this.channel.id
 			}).then(data => {
 				this.clear();
diff --git a/src/web/app/desktop/tags/post-detail.tag b/src/web/app/desktop/tags/post-detail.tag
index 58343482d0..ce7f81e32c 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_to && p.reply_to.reply_to_id && context == null } title="会話をもっと読み込む" onclick={ loadContext } disabled={ contextFetching }>
+		<button class="read-more" if={ p.reply && p.reply.reply_id && context == null } title="会話をもっと読み込む" onclick={ loadContext } disabled={ contextFetching }>
 			<i class="fa fa-ellipsis-v" if={ !contextFetching }></i>
 			<i class="fa fa-spinner fa-pulse" if={ contextFetching }></i>
 		</button>
@@ -9,8 +9,8 @@
 				<mk-post-detail-sub post={ post }/>
 			</virtual>
 		</div>
-		<div class="reply-to" if={ p.reply_to }>
-			<mk-post-detail-sub post={ p.reply_to }/>
+		<div class="reply-to" if={ p.reply }>
+			<mk-post-detail-sub post={ p.reply }/>
 		</div>
 		<div class="repost" if={ isRepost }>
 			<p>
@@ -329,7 +329,7 @@
 
 			// Fetch context
 			this.api('posts/context', {
-				post_id: this.p.reply_to_id
+				post_id: this.p.reply_id
 			}).then(context => {
 				this.update({
 					contextFetching: false,
diff --git a/src/web/app/desktop/tags/post-form.tag b/src/web/app/desktop/tags/post-form.tag
index 6a363d67cd..5041078bee 100644
--- a/src/web/app/desktop/tags/post-form.tag
+++ b/src/web/app/desktop/tags/post-form.tag
@@ -475,7 +475,7 @@
 			this.api('posts/create', {
 				text: this.refs.text.value == '' ? undefined : this.refs.text.value,
 				media_ids: files,
-				reply_to_id: this.inReplyToPost ? this.inReplyToPost.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 => {
diff --git a/src/web/app/desktop/tags/sub-post-content.tag b/src/web/app/desktop/tags/sub-post-content.tag
index 02cb5251b2..c75ae2911c 100644
--- a/src/web/app/desktop/tags/sub-post-content.tag
+++ b/src/web/app/desktop/tags/sub-post-content.tag
@@ -1,6 +1,6 @@
 <mk-sub-post-content>
 	<div class="body">
-		<a class="reply" if={ post.reply_to_id }>
+		<a class="reply" if={ post.reply_id }>
 			<i class="fa fa-reply"></i>
 		</a>
 		<span ref="text"></span>
diff --git a/src/web/app/desktop/tags/timeline.tag b/src/web/app/desktop/tags/timeline.tag
index 64b64f902f..44f3d5d8ec 100644
--- a/src/web/app/desktop/tags/timeline.tag
+++ b/src/web/app/desktop/tags/timeline.tag
@@ -82,8 +82,8 @@
 </mk-timeline>
 
 <mk-timeline-post tabindex="-1" title={ title } onkeydown={ onKeyDown } dblclick={ onDblClick }>
-	<div class="reply-to" if={ p.reply_to }>
-		<mk-timeline-post-sub post={ p.reply_to }/>
+	<div class="reply-to" if={ p.reply }>
+		<mk-timeline-post-sub post={ p.reply }/>
 	</div>
 	<div class="repost" if={ isRepost }>
 		<p>
@@ -113,7 +113,7 @@
 			<div class="body">
 				<div class="text" ref="text">
 					<p class="channel" if={ p.channel != null }><a href={ CONFIG.chUrl + '/' + p.channel.id } target="_blank">{ p.channel.title }</a>:</p>
-					<a class="reply" if={ p.reply_to }>
+					<a class="reply" if={ p.reply }>
 						<i class="fa fa-reply"></i>
 					</a>
 					<p class="dummy"></p>
diff --git a/src/web/app/mobile/tags/post-detail.tag b/src/web/app/mobile/tags/post-detail.tag
index ed275749ec..8a32101036 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_to && p.reply_to.reply_to_id && context == null } onclick={ loadContext } disabled={ loadingContext }>
+	<button class="read-more" if={ p.reply && p.reply.reply_id && context == null } onclick={ loadContext } disabled={ loadingContext }>
 		<i class="fa fa-ellipsis-v" if={ !contextFetching }></i>
 		<i class="fa fa-spinner fa-pulse" if={ contextFetching }></i>
 	</button>
@@ -8,8 +8,8 @@
 			<mk-post-detail-sub post={ post }/>
 		</virtual>
 	</div>
-	<div class="reply-to" if={ p.reply_to }>
-		<mk-post-detail-sub post={ p.reply_to }/>
+	<div class="reply-to" if={ p.reply }>
+		<mk-post-detail-sub post={ p.reply }/>
 	</div>
 	<div class="repost" if={ isRepost }>
 		<p>
@@ -348,7 +348,7 @@
 
 			// Fetch context
 			this.api('posts/context', {
-				post_id: this.p.reply_to_id
+				post_id: this.p.reply_id
 			}).then(context => {
 				this.update({
 					contextFetching: false,
diff --git a/src/web/app/mobile/tags/post-form.tag b/src/web/app/mobile/tags/post-form.tag
index cf267de94a..d7d382c9e2 100644
--- a/src/web/app/mobile/tags/post-form.tag
+++ b/src/web/app/mobile/tags/post-form.tag
@@ -267,7 +267,7 @@
 			this.api('posts/create', {
 				text: this.refs.text.value == '' ? undefined : this.refs.text.value,
 				media_ids: files,
-				reply_to_id: opts.reply ? opts.reply.id : undefined,
+				reply_id: opts.reply ? opts.reply.id : undefined,
 				poll: this.poll ? this.refs.poll.get() : undefined
 			}).then(data => {
 				this.trigger('post');
diff --git a/src/web/app/mobile/tags/sub-post-content.tag b/src/web/app/mobile/tags/sub-post-content.tag
index 97e0ecec03..e32e245185 100644
--- a/src/web/app/mobile/tags/sub-post-content.tag
+++ b/src/web/app/mobile/tags/sub-post-content.tag
@@ -1,5 +1,5 @@
 <mk-sub-post-content>
-	<div class="body"><a class="reply" if={ post.reply_to_id }><i class="fa fa-reply"></i></a><span ref="text"></span><a class="quote" if={ post.repost_id } href={ '/post:' + post.repost_id }>RP: ...</a></div>
+	<div class="body"><a class="reply" if={ post.reply_id }><i class="fa fa-reply"></i></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 }/>
diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag
index ad18521df6..f9ec2cca60 100644
--- a/src/web/app/mobile/tags/timeline.tag
+++ b/src/web/app/mobile/tags/timeline.tag
@@ -137,8 +137,8 @@
 </mk-timeline>
 
 <mk-timeline-post class={ repost: isRepost }>
-	<div class="reply-to" if={ p.reply_to }>
-		<mk-timeline-post-sub post={ p.reply_to }/>
+	<div class="reply-to" if={ p.reply }>
+		<mk-timeline-post-sub post={ p.reply }/>
 	</div>
 	<div class="repost" if={ isRepost }>
 		<p>
@@ -165,7 +165,7 @@
 			<div class="body">
 				<div class="text" ref="text">
 					<p class="channel" if={ p.channel != null }><a href={ CONFIG.chUrl + '/' + p.channel.id } target="_blank">{ p.channel.title }</a>:</p>
-					<a class="reply" if={ p.reply_to }>
+					<a class="reply" if={ p.reply }>
 						<i class="fa fa-reply"></i>
 					</a>
 					<p class="dummy"></p>
diff --git a/test/api.js b/test/api.js
index 1e731b5549..b43eb7ff62 100644
--- a/test/api.js
+++ b/test/api.js
@@ -277,15 +277,15 @@ describe('API', () => {
 			const me = await insertSakurako();
 			const post = {
 				text: 'さく',
-				reply_to_id: himaPost._id.toString()
+				reply_id: 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('text').eql(post.text);
-			res.body.should.have.property('reply_to_id').eql(post.reply_to_id);
-			res.body.should.have.property('reply_to');
-			res.body.reply_to.should.have.property('text').eql(himaPost.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);
 		}));
 
 		it('repostできる', async(async () => {
@@ -350,7 +350,7 @@ describe('API', () => {
 			const me = await insertSakurako();
 			const post = {
 				text: 'さく',
-				reply_to_id: '000000000000000000000000'
+				reply_id: '000000000000000000000000'
 			};
 			const res = await request('/posts/create', post, me);
 			res.should.have.status(400);
@@ -369,7 +369,7 @@ describe('API', () => {
 			const me = await insertSakurako();
 			const post = {
 				text: 'さく',
-				reply_to_id: 'kyoppie'
+				reply_id: 'kyoppie'
 			};
 			const res = await request('/posts/create', post, me);
 			res.should.have.status(400);

From 23282c2414693b948cbdabe9261b52c00b31ca68 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, 1 Nov 2017 10:45:01 +0900
Subject: [PATCH 299/364] Refactor

---
 src/api/endpoints/posts/create.ts | 40 +++++++++++++++----------------
 1 file changed, 20 insertions(+), 20 deletions(-)

diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index 3b9e0d8997..e1138c3edc 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -103,23 +103,23 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		}
 	}
 
-	// Get 'in_reply_post_id' parameter
-	const [inReplyToPostId, inReplyToPostIdErr] = $(params.reply_id).optional.id().$;
-	if (inReplyToPostIdErr) return rej('invalid in_reply_post_id');
+	// Get 'reply_id' parameter
+	const [replyId, replyIdErr] = $(params.reply_id).optional.id().$;
+	if (replyIdErr) return rej('invalid reply_id');
 
-	let inReplyToPost: IPost = null;
-	if (inReplyToPostId !== undefined) {
+	let reply: IPost = null;
+	if (replyId !== undefined) {
 		// Fetch reply
-		inReplyToPost = await Post.findOne({
-			_id: inReplyToPostId
+		reply = await Post.findOne({
+			_id: replyId
 		});
 
-		if (inReplyToPost === null) {
+		if (reply === null) {
 			return rej('in reply to post is not found');
 		}
 
 		// 返信対象が引用でないRepostだったらエラー
-		if (inReplyToPost.repost_id && !inReplyToPost.text && !inReplyToPost.media_ids) {
+		if (reply.repost_id && !reply.text && !reply.media_ids) {
 			return rej('cannot reply to repost');
 		}
 	}
@@ -140,7 +140,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		}
 
 		// 返信対象の投稿がこのチャンネルじゃなかったらダメ
-		if (inReplyToPost && !channelId.equals(inReplyToPost.channel_id)) {
+		if (reply && !channelId.equals(reply.channel_id)) {
 			return rej('チャンネル内部からチャンネル外部の投稿に返信することはできません');
 		}
 
@@ -155,7 +155,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		}
 	} else {
 		// 返信対象の投稿がチャンネルへの投稿だったらダメ
-		if (inReplyToPost && inReplyToPost.channel_id != null) {
+		if (reply && reply.channel_id != null) {
 			return rej('チャンネル外部からチャンネル内部の投稿に返信することはできません');
 		}
 
@@ -197,7 +197,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 			media_ids: (user.latest_post.media_ids || []).map(id => id.toString())
 		}, {
 			text: text,
-			reply: inReplyToPost ? inReplyToPost._id.toString() : null,
+			reply: reply ? reply._id.toString() : null,
 			repost: repost ? repost._id.toString() : null,
 			media_ids: (files || []).map(file => file._id.toString())
 		})) {
@@ -211,7 +211,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		channel_id: channel ? channel._id : undefined,
 		index: channel ? channel.index + 1 : undefined,
 		media_ids: files ? files.map(file => file._id) : undefined,
-		reply_id: inReplyToPost ? inReplyToPost._id : undefined,
+		reply_id: reply ? reply._id : undefined,
 		repost_id: repost ? repost._id : undefined,
 		poll: poll,
 		text: text,
@@ -287,23 +287,23 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 	});
 
 	// If has in reply to post
-	if (inReplyToPost) {
+	if (reply) {
 		// Increment replies count
-		Post.update({ _id: inReplyToPost._id }, {
+		Post.update({ _id: reply._id }, {
 			$inc: {
 				replies_count: 1
 			}
 		});
 
 		// 自分自身へのリプライでない限りは通知を作成
-		notify(inReplyToPost.user_id, user._id, 'reply', {
+		notify(reply.user_id, user._id, 'reply', {
 			post_id: post._id
 		});
 
 		// Fetch watchers
 		Watching
 			.find({
-				post_id: inReplyToPost._id,
+				post_id: reply._id,
 				user_id: { $ne: user._id },
 				// 削除されたドキュメントは除く
 				deleted_at: { $exists: false }
@@ -323,10 +323,10 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		// この投稿をWatchする
 		// TODO: ユーザーが「返信したときに自動でWatchする」設定を
 		//       オフにしていた場合はしない
-		watch(user._id, inReplyToPost);
+		watch(user._id, reply);
 
 		// Add mention
-		addMention(inReplyToPost.user_id, 'reply');
+		addMention(reply.user_id, 'reply');
 	}
 
 	// If it is repost
@@ -427,7 +427,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 			if (mentionee == null) return;
 
 			// 既に言及されたユーザーに対する返信や引用repostの場合も無視
-			if (inReplyToPost && inReplyToPost.user_id.equals(mentionee._id)) return;
+			if (reply && reply.user_id.equals(mentionee._id)) return;
 			if (repost && repost.user_id.equals(mentionee._id)) return;
 
 			// Add mention

From 04051008fac45e8225d6a7c203d0893aae2d0008 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, 1 Nov 2017 10:54:59 +0900
Subject: [PATCH 300/364] Better English

---
 locales/en.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/locales/en.yml b/locales/en.yml
index afb6d2f2fb..cf75bee92b 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -399,7 +399,7 @@ mobile:
 
     mk-notifications-page:
       notifications: "Notifications"
-      read-all: "Are you sure you want to mark as read all your notifications?"
+      read-all: "Are you sure you want to mark all unread notifications as read?"
 
     mk-post-page:
       title: "Post"

From f60eae5c9138840f56880bf0b000bf629c1c0437 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 12:28:35 +0900
Subject: [PATCH 301/364] :v:

---
 src/web/app/ch/tags/channel.tag | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index f1eea6b9bc..a9060c8895 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -27,6 +27,7 @@
 
 			> main
 				> h1
+					font-size 1.5em
 					color #f00
 	</style>
 	<script>
@@ -90,7 +91,7 @@
 <mk-channel-post>
 	<header>
 		<a class="index" onclick={ reply }>{ post.index }:</a>
-		<a class="name" href={ '/' + post.user.username }><b>{ post.user.name }</b></a>
+		<a class="name" href={ CONFIG.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>
@@ -113,6 +114,12 @@
 			padding 0
 
 			> header
+				position -webkit-sticky
+				position sticky
+				z-index 1
+				top 0
+				background rgba(239, 239, 239, 0.7)
+
 				> .index
 					margin-right 0.25em
 					color #000

From bbd3b6da079c43e62a76e3b1e24ffd422e42c883 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 12:34:12 +0900
Subject: [PATCH 302/364] wip

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

diff --git a/src/api/endpoints/posts/timeline.ts b/src/api/endpoints/posts/timeline.ts
index 314e992344..5fe8200010 100644
--- a/src/api/endpoints/posts/timeline.ts
+++ b/src/api/endpoints/posts/timeline.ts
@@ -42,6 +42,12 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
 	const query = {
 		user_id: {
 			$in: followingIds
+		},
+		// TODO
+		channel_id: {
+			$or: [{
+				$exists: false
+			}, null]
 		}
 	} as any;
 	if (sinceId) {

From 2a919adf1269cc4498c453b7401d4b01bd79f727 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 12:34:57 +0900
Subject: [PATCH 303/364] v2783

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index bda30f1d95..bd8ecb57e3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+2783 (2017/11/01)
+-----------------
+* なんか
+
 2777 (2017/11/01)
 -----------------
 * 細かいブラッシュアップ
diff --git a/package.json b/package.json
index cd1c37a3a3..eaafeb9fd5 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2777",
+  "version": "0.0.2783",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",

From 1c73b08e95064e7fd2c1c4b1d584ad9c09d34331 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 12:36:22 +0900
Subject: [PATCH 304/364] :v:

---
 tools/migration/reply_to-to-reply.js | 5 +++++
 1 file changed, 5 insertions(+)
 create mode 100644 tools/migration/reply_to-to-reply.js

diff --git a/tools/migration/reply_to-to-reply.js b/tools/migration/reply_to-to-reply.js
new file mode 100644
index 0000000000..ceb272ebc9
--- /dev/null
+++ b/tools/migration/reply_to-to-reply.js
@@ -0,0 +1,5 @@
+db.posts.update({}, {
+	$rename: {
+		reply_to_id: 'reply_id'
+	}
+}, false, true);

From 5f968f3f259db5110c395e9f29a59463137b30c5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 12:44:53 +0900
Subject: [PATCH 305/364] Fix bug

---
 src/api/endpoints/posts/timeline.ts | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/src/api/endpoints/posts/timeline.ts b/src/api/endpoints/posts/timeline.ts
index 5fe8200010..fe096442b4 100644
--- a/src/api/endpoints/posts/timeline.ts
+++ b/src/api/endpoints/posts/timeline.ts
@@ -44,11 +44,13 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
 			$in: followingIds
 		},
 		// TODO
-		channel_id: {
-			$or: [{
+		$or: [{
+			channel_id: {
 				$exists: false
-			}, null]
-		}
+			}
+		}, {
+			channel_id: null
+		}]
 	} as any;
 	if (sinceId) {
 		sort._id = 1;

From 8a0c0383d1f4ed5ae495c5ff68208d02b805f8af Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 12:46:58 +0900
Subject: [PATCH 306/364] :art:

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

diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index a9060c8895..df0e18264f 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -118,7 +118,7 @@
 				position sticky
 				z-index 1
 				top 0
-				background rgba(239, 239, 239, 0.7)
+				background rgba(239, 239, 239, 0.9)
 
 				> .index
 					margin-right 0.25em

From 079dd2f68cf1a01de0cfe82156c5d7b63eb81b71 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 12:49:54 +0900
Subject: [PATCH 307/364] :art:

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

diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index df0e18264f..c0ce9efcc9 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -147,7 +147,7 @@
 
 				> .media
 					> a
-						display block
+						display inline-block
 
 						> img
 							max-width 100%

From 6ab0e81e1c65e7395963ad5f21b1cd639db36063 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 13:11:40 +0900
Subject: [PATCH 308/364] :v:

---
 src/web/app/mobile/tags/drive.tag            | 10 +++++-----
 src/web/app/mobile/tags/page/drive.tag       |  2 +-
 src/web/app/mobile/tags/page/selectdrive.tag | 14 +++++++++-----
 3 files changed, 15 insertions(+), 11 deletions(-)

diff --git a/src/web/app/mobile/tags/drive.tag b/src/web/app/mobile/tags/drive.tag
index c17b7ce579..6929c50ab1 100644
--- a/src/web/app/mobile/tags/drive.tag
+++ b/src/web/app/mobile/tags/drive.tag
@@ -1,5 +1,5 @@
 <mk-drive>
-	<nav>
+	<nav ref="nav">
 		<p onclick={ goRoot }><i class="fa fa-cloud"></i>%i18n:mobile.tags.mk-drive.drive%</p>
 		<virtual each={ folder in hierarchyFolders }>
 			<span><i class="fa fa-angle-right"></i></span>
@@ -56,10 +56,6 @@
 			display block
 			background #fff
 
-			&[data-is-naked]
-				> nav
-					top 48px
-
 			> nav
 				display block
 				position sticky
@@ -205,6 +201,10 @@
 			} else {
 				this.fetch();
 			}
+
+			if (this.opts.isNaked) {
+				this.refs.nav.style.top = `${this.opts.top}px`;
+			}
 		});
 
 		this.on('unmount', () => {
diff --git a/src/web/app/mobile/tags/page/drive.tag b/src/web/app/mobile/tags/page/drive.tag
index 1169e3b9eb..218960c702 100644
--- a/src/web/app/mobile/tags/page/drive.tag
+++ b/src/web/app/mobile/tags/page/drive.tag
@@ -1,6 +1,6 @@
 <mk-drive-page>
 	<mk-ui ref="ui">
-		<mk-drive ref="browser" folder={ parent.opts.folder } file={ parent.opts.file } data-is-naked="true"/>
+		<mk-drive ref="browser" folder={ parent.opts.folder } file={ parent.opts.file } is-naked={ true } top={ 48 }/>
 	</mk-ui>
 	<style>
 		:scope
diff --git a/src/web/app/mobile/tags/page/selectdrive.tag b/src/web/app/mobile/tags/page/selectdrive.tag
index d9e7d95c41..79ea3548f8 100644
--- a/src/web/app/mobile/tags/page/selectdrive.tag
+++ b/src/web/app/mobile/tags/page/selectdrive.tag
@@ -4,7 +4,7 @@
 		<button class="upload" onclick={ upload }><i class="fa fa-upload"></i></button>
 		<button if={ multiple } class="ok" onclick={ ok }><i class="fa fa-check"></i></button>
 	</header>
-	<mk-drive ref="browser" select-file={ true } multiple={ multiple }/>
+	<mk-drive ref="browser" select-file={ true } multiple={ multiple } is-naked={ true } top={ 42 }/>
 
 	<style>
 		:scope
@@ -14,7 +14,13 @@
 			background #fff
 
 			> header
-				border-bottom solid 1px #eee
+				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
@@ -43,9 +49,7 @@
 					width 42px
 
 			> mk-drive
-				height calc(100% - 42px)
-				overflow scroll
-				-webkit-overflow-scrolling touch
+				top 42px
 
 	</style>
 	<script>

From c3174e976820cfe9fd022a563497685543e7a418 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 13:17:47 +0900
Subject: [PATCH 309/364] Fix bug

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

diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index c0ce9efcc9..4421a4b0ed 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -7,7 +7,7 @@
 		<div if={ !postsFetching }>
 			<p if={ posts == null }>まだ投稿がありません</p>
 			<virtual if={ posts != null }>
-				<mk-channel-post each={ posts.slice().reverse() } post={ this } form={ parent.refs.form }/>
+				<mk-channel-post each={ post in posts.slice().reverse() } post={ post } form={ parent.refs.form }/>
 			</virtual>
 		</div>
 		<hr>

From c5b6dabd07e33ae7972300caf260b690d27db8cd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 13:20:55 +0900
Subject: [PATCH 310/364] wip

---
 src/api/endpoints/posts/create.ts | 29 ++++++++++++++++-------------
 1 file changed, 16 insertions(+), 13 deletions(-)

diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index e1138c3edc..360b5df0d9 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -264,20 +264,23 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		publishChannelStream(channel._id, 'post', postObj);
 	}
 
-	// Fetch all followers
-	const followers = await Following
-		.find({
-			followee_id: user._id,
-			// 削除されたドキュメントは除く
-			deleted_at: { $exists: false }
-		}, {
-			follower_id: true,
-			_id: false
-		});
+	// TODO
+	if (!channel) {
+		// Fetch all followers
+		const followers = await Following
+			.find({
+				followee_id: user._id,
+				// 削除されたドキュメントは除く
+				deleted_at: { $exists: false }
+			}, {
+				follower_id: true,
+				_id: false
+			});
 
-	// Publish event to followers stream
-	followers.forEach(following =>
-		event(following.follower_id, 'post', postObj));
+		// Publish event to followers stream
+		followers.forEach(following =>
+			event(following.follower_id, 'post', postObj));
+	}
 
 	// Increment my posts count
 	User.update({ _id: user._id }, {

From 7bb6c66b3409ad9608823112fe7970a8286716ea Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 13:25:09 +0900
Subject: [PATCH 311/364] :v:

---
 src/web/app/ch/tags/channel.tag | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index 4421a4b0ed..602b80bc11 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -44,6 +44,7 @@
 		this.posts = null;
 		this.connection = new ChannelStream(this.id);
 		this.version = VERSION;
+		this.unreadCount = 0;
 
 		this.on('mount', () => {
 			document.documentElement.style.background = '#efefef';
@@ -73,18 +74,31 @@
 			});
 
 			this.connection.on('post', this.onPost);
+			document.addEventListener('visibilitychange', this.onVisibilitychange, false);
 		});
 
 		this.on('unmount', () => {
 			this.connection.off('post', this.onPost);
 			this.connection.close();
+			document.removeEventListener('visibilitychange', this.onVisibilitychange);
 		});
 
 		this.onPost = post => {
 			this.posts.unshift(post);
 			this.update();
+
+			if (document.hidden && this.SIGNIN && post.user_id !== this.I.id) {
+				this.unreadCount++;
+				document.title = `(${this.unreadCount}) ${this.channel.title} | Misskey`;
+			}
 		};
 
+		this.onVisibilitychange = () => {
+			if (!document.hidden) {
+				this.unreadCount = 0;
+				document.title = this.channel.title + ' | Misskey'
+			}
+		};
 	</script>
 </mk-channel>
 

From 51006a6815fe02aa915c59ca5d42ab3234884442 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 13:25:43 +0900
Subject: [PATCH 312/364] v2793

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index bd8ecb57e3..6a86e24c61 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+2793 (2017/11/01)
+-----------------
+* なんか
+
 2783 (2017/11/01)
 -----------------
 * なんか
diff --git a/package.json b/package.json
index eaafeb9fd5..87db0c8e1e 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2783",
+  "version": "0.0.2793",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",

From 3cbb3ff81fc12feeedc779dc5ff00733c67f9133 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 13:39:05 +0900
Subject: [PATCH 313/364] wip

---
 src/api/endpoints/posts/create.ts | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index 360b5df0d9..b3fbdf6fa2 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -249,8 +249,11 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		}
 	}
 
-	// Publish event to myself's stream
-	event(user._id, 'post', postObj);
+	// TODO
+	if (!channel) {
+		// Publish event to myself's stream
+		event(user._id, 'post', postObj);
+	}
 
 	if (channel) {
 		// Increment channel index(posts count)

From fcdf2c4f89c4eb7bb666337d7e162e1c5e727e61 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 1 Nov 2017 06:55:03 +0000
Subject: [PATCH 314/364] chore(package): update awesome-typescript-loader to
 version 3.3.0

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

diff --git a/package.json b/package.json
index 87db0c8e1e..dd5a57015a 100644
--- a/package.json
+++ b/package.json
@@ -64,7 +64,7 @@
     "@types/webpack": "3.0.13",
     "@types/webpack-stream": "3.2.7",
     "@types/websocket": "0.0.34",
-    "awesome-typescript-loader": "3.2.3",
+    "awesome-typescript-loader": "3.3.0",
     "chai": "4.1.2",
     "chai-http": "3.0.0",
     "css-loader": "0.28.7",

From 86901b68b84bb68167c6a3d8cd043e63ba66bed2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 16:42:15 +0900
Subject: [PATCH 315/364] =?UTF-8?q?=E3=81=84=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

---
 src/web/app/ch/tags/channel.tag | 30 ++++++++++++++++++++++++++++--
 1 file changed, 28 insertions(+), 2 deletions(-)

diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index 602b80bc11..fdc9ab4cef 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -182,12 +182,13 @@
 
 <mk-channel-form>
 	<p if={ reply }><b>&gt;&gt;{ reply.index }</b> ({ reply.user.name }): <a onclick={ clearReply }>[x]</a></p>
-	<textarea ref="text" disabled={ wait }></textarea>
+	<textarea ref="text" disabled={ wait } oninput={ update } onkeydown={ onkeydown } onpaste={ onpaste }></textarea>
 	<button class={ wait: wait } ref="submit" disabled={ wait || (refs.text.value.length == 0) } onclick={ post }>
 		{ wait ? 'やってます' : 'やる' }<mk-ellipsis if={ wait }/>
 	</button>
 	<br>
 	<button onclick={ drive }>ドライブ</button>
+	<mk-uploader ref="uploader"/>
 	<ol if={ files }>
 		<li each={ files }>{ name }</li>
 	</ol>
@@ -202,6 +203,19 @@
 		this.mixin('api');
 
 		this.channel = this.opts.channel;
+		this.files = null;
+
+		this.on('mount', () => {
+			this.refs.uploader.on('uploaded', file => {
+				this.update({
+					files: [file]
+				});
+			});
+		});
+
+		this.upload = file => {
+			this.refs.uploader.upload(file);
+		};
 
 		this.clearReply = () => {
 			this.update({
@@ -217,7 +231,7 @@
 			this.refs.text.value = '';
 		};
 
-		this.post = e => {
+		this.post = () => {
 			this.update({
 				wait: true
 			});
@@ -250,5 +264,17 @@
 			};
 			window.open(CONFIG.url + '/selectdrive?multiple=true', '_blank');
 		};
+
+		this.onkeydown = e => {
+			if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post();
+		};
+
+		this.onpaste = e => {
+			e.clipboardData.items.forEach(item => {
+				if (item.kind == 'file') {
+					this.upload(item.getAsFile());
+				}
+			});
+		};
 	</script>
 </mk-channel-form>

From 8234862bf759efab6d5214c4250913a80458d890 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 16:42:51 +0900
Subject: [PATCH 316/364] v2795

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6a86e24c61..9b2f3d7c0b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+2795 (2017/11/01)
+-----------------
+* いい感じに
+
 2793 (2017/11/01)
 -----------------
 * なんか
diff --git a/package.json b/package.json
index 87db0c8e1e..09e5a62399 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2793",
+  "version": "0.0.2795",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",

From b65e038686913812a1e6ddf7e1288337c6fe5fe7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 16:48:00 +0900
Subject: [PATCH 317/364] Better progress bar

---
 src/web/app/ch/tags/channel.tag | 18 +++++++++++++++++-
 1 file changed, 17 insertions(+), 1 deletion(-)

diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index fdc9ab4cef..c6921e1a5c 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -51,10 +51,18 @@
 
 			Progress.start();
 
+			const fetched = false;
+
+			// チャンネル概要読み込み
 			this.api('channels/show', {
 				channel_id: this.id
 			}).then(channel => {
-				Progress.done();
+				if (fetched) {
+					Progress.done();
+				} else {
+					Progress.set(0.5);
+					fetched = true;
+				}
 
 				this.update({
 					fetching: false,
@@ -64,9 +72,17 @@
 				document.title = channel.title + ' | Misskey'
 			});
 
+			// 投稿読み込み
 			this.api('channels/posts', {
 				channel_id: this.id
 			}).then(posts => {
+				if (fetched) {
+					Progress.done();
+				} else {
+					Progress.set(0.5);
+					fetched = true;
+				}
+
 				this.update({
 					postsFetching: false,
 					posts: posts

From e221d410e056ea348c994efb9d0a7f3b9addd2eb Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 17:09:24 +0900
Subject: [PATCH 318/364] Fix bug

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

diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index c6921e1a5c..67b012cb5c 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -51,7 +51,7 @@
 
 			Progress.start();
 
-			const fetched = false;
+			let fetched = false;
 
 			// チャンネル概要読み込み
 			this.api('channels/show', {

From 5e2053ca869cbfe18a2e552f228e8138b6a95f61 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 17:20:54 +0900
Subject: [PATCH 319/364] :v:

---
 src/web/app/ch/tags/channel.tag | 54 +++++++++++++++++++++++++++++----
 1 file changed, 48 insertions(+), 6 deletions(-)

diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index 67b012cb5c..ad254c98e5 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -3,12 +3,20 @@
 	<hr>
 	<main if={ !fetching }>
 		<h1>{ channel.title }</h1>
-		<p if={ postsFetching }>読み込み中<mk-ellipsis/></p>
-		<div if={ !postsFetching }>
-			<p if={ posts == null }>まだ投稿がありません</p>
-			<virtual if={ posts != null }>
-				<mk-channel-post each={ post in posts.slice().reverse() } post={ post } form={ parent.refs.form }/>
-			</virtual>
+
+		<div class="share">
+			<mk-twitter-button/>
+			<mk-line-button/>
+		</div>
+
+		<div class="body">
+			<p if={ postsFetching }>読み込み中<mk-ellipsis/></p>
+			<div if={ !postsFetching }>
+				<p if={ posts == null }>まだ投稿がありません</p>
+				<virtual 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"/>
@@ -29,6 +37,14 @@
 				> h1
 					font-size 1.5em
 					color #f00
+
+				> .share
+					> *
+						margin-right 4px
+
+				> .body
+					margin 8px 0 0 0
+
 	</style>
 	<script>
 		import Progress from '../../common/scripts/loading';
@@ -294,3 +310,29 @@
 		};
 	</script>
 </mk-channel-form>
+
+<mk-twitter-button>
+	<a href="https://twitter.com/share?ref_src=twsrc%5Etfw" class="twitter-share-button" data-show-count="false">Tweet</a>
+	<script>
+		this.on('mount', () => {
+			const head = document.getElementsByTagName('head')[0];
+			const script = document.createElement('script');
+			script.setAttribute('src', 'https://platform.twitter.com/widgets.js');
+			script.setAttribute('async', 'async');
+			head.appendChild(script);
+		});
+	</script>
+</mk-twitter-button>
+
+<mk-line-button>
+	<div class="line-it-button" data-lang="ja" data-type="share-a" data-url={ CONFIG.chUrl } style="display: none;"></div>
+	<script>
+		this.on('mount', () => {
+			const head = document.getElementsByTagName('head')[0];
+			const script = document.createElement('script');
+			script.setAttribute('src', 'https://d.line-scdn.net/r/web/social-plugin/js/thirdparty/loader.min.js');
+			script.setAttribute('async', 'async');
+			head.appendChild(script);
+		});
+	</script>
+</mk-line-button>

From 2b3937d7318f06344c9524fca7c71d81da25d603 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 17:21:26 +0900
Subject: [PATCH 320/364] v2799

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9b2f3d7c0b..03282eb3cc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+2799 (2017/11/01)
+-----------------
+* いい感じに
+
 2795 (2017/11/01)
 -----------------
 * いい感じに
diff --git a/package.json b/package.json
index 09e5a62399..a45d3b36ca 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2795",
+  "version": "0.0.2799",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",

From d6b03c43eb818a5e13a8ad1ec69697e4600c5c2c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 19:33:08 +0900
Subject: [PATCH 321/364] Implement Channel Watching

---
 src/api/endpoints.ts                  |  8 ++++
 src/api/endpoints/channels/create.ts  | 11 ++++-
 src/api/endpoints/channels/unwatch.ts | 60 +++++++++++++++++++++++++++
 src/api/endpoints/channels/watch.ts   | 58 ++++++++++++++++++++++++++
 src/api/endpoints/posts/create.ts     | 43 ++++++++++++-------
 src/api/endpoints/posts/timeline.ts   | 39 ++++++++++++-----
 src/api/models/channel-watching.ts    |  3 ++
 src/api/serializers/channel.ts        | 22 ++++++++++
 src/web/app/ch/tags/channel.tag       | 27 ++++++++++++
 9 files changed, 244 insertions(+), 27 deletions(-)
 create mode 100644 src/api/endpoints/channels/unwatch.ts
 create mode 100644 src/api/endpoints/channels/watch.ts
 create mode 100644 src/api/models/channel-watching.ts

diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts
index c4dacad857..afefce39e5 100644
--- a/src/api/endpoints.ts
+++ b/src/api/endpoints.ts
@@ -490,6 +490,14 @@ const endpoints: Endpoint[] = [
 	{
 		name: 'channels/posts'
 	},
+	{
+		name: 'channels/watch',
+		withCredential: true
+	},
+	{
+		name: 'channels/unwatch',
+		withCredential: true
+	},
 	{
 		name: 'channels'
 	},
diff --git a/src/api/endpoints/channels/create.ts b/src/api/endpoints/channels/create.ts
index e0c0e0192a..a8d7c29dc1 100644
--- a/src/api/endpoints/channels/create.ts
+++ b/src/api/endpoints/channels/create.ts
@@ -3,6 +3,7 @@
  */
 import $ from 'cafy';
 import Channel from '../../models/channel';
+import Watching from '../../models/channel-watching';
 import serialize from '../../serializers/channel';
 
 /**
@@ -22,9 +23,17 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 		created_at: new Date(),
 		user_id: user._id,
 		title: title,
-		index: 0
+		index: 0,
+		watching_count: 1
 	});
 
 	// Response
 	res(await serialize(channel));
+
+	// Create Watching
+	await Watching.insert({
+		created_at: new Date(),
+		user_id: user._id,
+		channel_id: channel._id
+	});
 });
diff --git a/src/api/endpoints/channels/unwatch.ts b/src/api/endpoints/channels/unwatch.ts
new file mode 100644
index 0000000000..19d3be118a
--- /dev/null
+++ b/src/api/endpoints/channels/unwatch.ts
@@ -0,0 +1,60 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import Channel from '../../models/channel';
+import Watching from '../../models/channel-watching';
+
+/**
+ * Unwatch a channel
+ *
+ * @param {any} params
+ * @param {any} user
+ * @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');
+
+	//#region Fetch channel
+	const channel = await Channel.findOne({
+		_id: channelId
+	});
+
+	if (channel === null) {
+		return rej('channel not found');
+	}
+	//#endregion
+
+	//#region Check whether not watching
+	const exist = await Watching.findOne({
+		user_id: user._id,
+		channel_id: channel._id,
+		deleted_at: { $exists: false }
+	});
+
+	if (exist === null) {
+		return rej('already not watching');
+	}
+	//#endregion
+
+	// Delete watching
+	await Watching.update({
+		_id: exist._id
+	}, {
+		$set: {
+			deleted_at: new Date()
+		}
+	});
+
+	// Send response
+	res();
+
+	// Decrement watching count
+	Channel.update(channel._id, {
+		$inc: {
+			watching_count: -1
+		}
+	});
+});
diff --git a/src/api/endpoints/channels/watch.ts b/src/api/endpoints/channels/watch.ts
new file mode 100644
index 0000000000..030e0dd411
--- /dev/null
+++ b/src/api/endpoints/channels/watch.ts
@@ -0,0 +1,58 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import Channel from '../../models/channel';
+import Watching from '../../models/channel-watching';
+
+/**
+ * Watch a channel
+ *
+ * @param {any} params
+ * @param {any} user
+ * @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');
+
+	//#region Fetch channel
+	const channel = await Channel.findOne({
+		_id: channelId
+	});
+
+	if (channel === null) {
+		return rej('channel not found');
+	}
+	//#endregion
+
+	//#region Check whether already watching
+	const exist = await Watching.findOne({
+		user_id: user._id,
+		channel_id: channel._id,
+		deleted_at: { $exists: false }
+	});
+
+	if (exist !== null) {
+		return rej('already watching');
+	}
+	//#endregion
+
+	// Create Watching
+	await Watching.insert({
+		created_at: new Date(),
+		user_id: user._id,
+		channel_id: channel._id
+	});
+
+	// Send response
+	res();
+
+	// Increment watching count
+	Channel.update(channel._id, {
+		$inc: {
+			watching_count: 1
+		}
+	});
+});
diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index b3fbdf6fa2..2326f7baf1 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -10,6 +10,7 @@ import { default as Channel, IChannel } from '../../models/channel';
 import Following from '../../models/following';
 import DriveFile from '../../models/drive-file';
 import Watching from '../../models/post-watching';
+import ChannelWatching from '../../models/channel-watching';
 import serialize from '../../serializers/post';
 import notify from '../../common/notify';
 import watch from '../../common/watch-post';
@@ -249,26 +250,11 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 		}
 	}
 
-	// TODO
+	// タイムラインへの投稿
 	if (!channel) {
 		// Publish event to myself's stream
 		event(user._id, '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);
-	}
-
-	// TODO
-	if (!channel) {
 		// Fetch all followers
 		const followers = await Following
 			.find({
@@ -285,6 +271,31 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 			event(following.follower_id, '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({
+			channel_id: channel._id,
+			// 削除されたドキュメントは除く
+			deleted_at: { $exists: false }
+		});
+
+		// チャンネルの視聴者(のタイムライン)に配信
+		watches.forEach(w => {
+			event(w.user_id, 'post', postObj);
+		});
+	}
+
 	// Increment my posts count
 	User.update({ _id: user._id }, {
 		$inc: {
diff --git a/src/api/endpoints/posts/timeline.ts b/src/api/endpoints/posts/timeline.ts
index fe096442b4..aa5aff5ba5 100644
--- a/src/api/endpoints/posts/timeline.ts
+++ b/src/api/endpoints/posts/timeline.ts
@@ -3,6 +3,7 @@
  */
 import $ from 'cafy';
 import Post from '../../models/post';
+import ChannelWatching from '../../models/channel-watching';
 import getFriends from '../../common/get-friends';
 import serialize from '../../serializers/post';
 
@@ -32,26 +33,43 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
 		return rej('cannot set since_id and max_id');
 	}
 
-	// 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);
 
-	// Construct query
+	// Watchしているチャンネルを取得
+	const watches = await ChannelWatching.find({
+		user_id: user._id,
+		// 削除されたドキュメントは除く
+		deleted_at: { $exists: false }
+	});
+
+	//#region Construct query
 	const sort = {
 		_id: -1
 	};
+
 	const query = {
-		user_id: {
-			$in: followingIds
-		},
-		// TODO
 		$or: [{
-			channel_id: {
-				$exists: false
-			}
+			// フォローしている人のタイムラインへの投稿
+			user_id: {
+				$in: followingIds
+			},
+			// 「タイムラインへの」投稿に限定するためにチャンネルが指定されていないもののみに限る
+			$or: [{
+				channel_id: {
+					$exists: false
+				}
+			}, {
+				channel_id: null
+			}]
 		}, {
-			channel_id: null
+			// Watchしているチャンネルへの投稿
+			channel_id: {
+				$in: watches.map(w => w.channel_id)
+			}
 		}]
 	} as any;
+
 	if (sinceId) {
 		sort._id = 1;
 		query._id = {
@@ -62,6 +80,7 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
 			$lt: maxId
 		};
 	}
+	//#endregion
 
 	// Issue query
 	const timeline = await Post
diff --git a/src/api/models/channel-watching.ts b/src/api/models/channel-watching.ts
new file mode 100644
index 0000000000..6184ae408d
--- /dev/null
+++ b/src/api/models/channel-watching.ts
@@ -0,0 +1,3 @@
+import db from '../../db/mongodb';
+
+export default db.get('channel_watching') as any; // fuck type definition
diff --git a/src/api/serializers/channel.ts b/src/api/serializers/channel.ts
index d4e16d6be3..3cba39aa16 100644
--- a/src/api/serializers/channel.ts
+++ b/src/api/serializers/channel.ts
@@ -5,6 +5,7 @@ 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
@@ -40,5 +41,26 @@ export default (
 	// 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/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index ad254c98e5..57cedf10d4 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -4,6 +4,11 @@
 	<main if={ !fetching }>
 		<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>
+		</div>
+
 		<div class="share">
 			<mk-twitter-button/>
 			<mk-line-button/>
@@ -131,6 +136,28 @@
 				document.title = this.channel.title + ' | Misskey'
 			}
 		};
+
+		this.watch = () => {
+			this.api('channels/watch', {
+				channel_id: this.id
+			}).then(() => {
+				this.channel.is_watching = true;
+				this.update();
+			}, e => {
+				alert('error');
+			});
+		};
+
+		this.unwatch = () => {
+			this.api('channels/unwatch', {
+				channel_id: this.id
+			}).then(() => {
+				this.channel.is_watching = false;
+				this.update();
+			}, e => {
+				alert('error');
+			});
+		};
 	</script>
 </mk-channel>
 

From a7b7f2a40f86b0a20e8f8987991bca3b196ba0ce Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 1 Nov 2017 19:33:14 +0900
Subject: [PATCH 322/364] v2801

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 03282eb3cc..554e12093a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+2801 (2017/11/01)
+-----------------
+* チャンネルのWatch実装
+
 2799 (2017/11/01)
 -----------------
 * いい感じに
diff --git a/package.json b/package.json
index a45d3b36ca..b6bf0cec79 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2799",
+  "version": "0.0.2801",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",

From ec4f3595b9f8a845b27c07ced517d00377e4942c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 2 Nov 2017 00:02:03 +0900
Subject: [PATCH 323/364] Improve usability

---
 src/web/app/ch/tags/channel.tag | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index 57cedf10d4..4c1e66963f 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -321,7 +321,10 @@
 					files: files
 				});
 			};
-			window.open(CONFIG.url + '/selectdrive?multiple=true', '_blank');
+
+			window.open(CONFIG.url + '/selectdrive?multiple=true',
+				'drive_window',
+				'height=500,width=800');
 		};
 
 		this.onkeydown = e => {

From 667ddfea9a9621b26f9b648c28d601d309807fbd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 2 Nov 2017 00:04:39 +0900
Subject: [PATCH 324/364] Fix bug

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

diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index 4c1e66963f..85560e7b79 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -17,7 +17,7 @@
 		<div class="body">
 			<p if={ postsFetching }>読み込み中<mk-ellipsis/></p>
 			<div if={ !postsFetching }>
-				<p if={ posts == null }>まだ投稿がありません</p>
+				<p if={ posts == null || posts.length == 0 }>まだ投稿がありません</p>
 				<virtual if={ posts != null }>
 					<mk-channel-post each={ post in posts.slice().reverse() } post={ post } form={ parent.refs.form }/>
 				</virtual>

From 9f6cc8bafb326e3b9bb6bc6ee657277ac8effb36 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 2 Nov 2017 00:20:33 +0900
Subject: [PATCH 325/364] :art:

---
 src/web/app/ch/style.styl       |  1 +
 src/web/app/ch/tags/channel.tag |  3 +--
 src/web/app/ch/tags/header.tag  | 20 ++++++++++++++++++++
 src/web/app/ch/tags/index.js    |  1 +
 src/web/app/ch/tags/index.tag   |  2 ++
 5 files changed, 25 insertions(+), 2 deletions(-)
 create mode 100644 src/web/app/ch/tags/header.tag

diff --git a/src/web/app/ch/style.styl b/src/web/app/ch/style.styl
index 2fc3ac3fca..8ad6fbce0b 100644
--- a/src/web/app/ch/style.styl
+++ b/src/web/app/ch/style.styl
@@ -1,4 +1,5 @@
 @import "../base"
 
 html
+	padding 8px
 	background #efefef
diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index 85560e7b79..35463bc0b8 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -1,5 +1,5 @@
 <mk-channel>
-	<header><a href={ CONFIG.chUrl }>Misskey Channels</a></header>
+	<mk-header/>
 	<hr>
 	<main if={ !fetching }>
 		<h1>{ channel.title }</h1>
@@ -36,7 +36,6 @@
 	<style>
 		:scope
 			display block
-			padding 8px
 
 			> main
 				> h1
diff --git a/src/web/app/ch/tags/header.tag b/src/web/app/ch/tags/header.tag
new file mode 100644
index 0000000000..5cdcbd09cc
--- /dev/null
+++ b/src/web/app/ch/tags/header.tag
@@ -0,0 +1,20 @@
+<mk-header>
+	<div>
+		<a href={ CONFIG.chUrl }>Index</a> | <a href={ CONFIG.url }>Misskey</a>
+	</div>
+	<div>
+		<a if={ !SIGNIN } href={ CONFIG.url }>ログイン(新規登録)</a>
+		<a if={ SIGNIN } href={ CONFIG.url + '/' + I.username }>{ I.username }</a>
+	</div>
+	<style>
+		:scope
+			display flex
+
+			> div:last-child
+				margin-left auto
+
+	</style>
+	<script>
+		this.mixin('i');
+	</script>
+</mk-header>
diff --git a/src/web/app/ch/tags/index.js b/src/web/app/ch/tags/index.js
index 1e99ccd43e..12ffdaeb84 100644
--- a/src/web/app/ch/tags/index.js
+++ b/src/web/app/ch/tags/index.js
@@ -1,2 +1,3 @@
 require('./index.tag');
 require('./channel.tag');
+require('./header.tag');
diff --git a/src/web/app/ch/tags/index.tag b/src/web/app/ch/tags/index.tag
index a64ddb6ccd..50ccc0d91c 100644
--- a/src/web/app/ch/tags/index.tag
+++ b/src/web/app/ch/tags/index.tag
@@ -1,4 +1,6 @@
 <mk-index>
+	<mk-header/>
+	<hr>
 	<button onclick={ n }>%i18n:ch.tags.mk-index.new%</button>
 	<hr>
 	<ul if={ channels }>

From 1d828c9784ffeac79146a3926173f1285ee4c7d6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 2 Nov 2017 00:21:22 +0900
Subject: [PATCH 326/364] v2805

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 554e12093a..20323bd96c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+2805 (2017/11/02)
+-----------------
+* いい感じに
+
 2801 (2017/11/01)
 -----------------
 * チャンネルのWatch実装
diff --git a/package.json b/package.json
index b6bf0cec79..9f04d3125c 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2801",
+  "version": "0.0.2805",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",

From e7fbf873ef5d5c588dc6269763e44029a5be77f1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 2 Nov 2017 12:39:19 +0900
Subject: [PATCH 327/364] :v:

---
 locales/en.yml                      |  7 +++++
 locales/ja.yml                      |  7 +++++
 src/web/app/{base.styl => app.styl} |  8 +++--
 src/web/app/auth/style.styl         |  3 +-
 src/web/app/ch/style.styl           |  7 ++++-
 src/web/app/ch/tags/channel.tag     | 48 +++++++++++++++++++++++++----
 src/web/app/desktop/style.styl      |  3 +-
 src/web/app/dev/style.styl          |  3 +-
 src/web/app/mobile/style.styl       |  3 +-
 src/web/app/reset.styl              | 13 --------
 src/web/app/stats/style.styl        |  3 +-
 src/web/app/status/style.styl       |  3 +-
 12 files changed, 80 insertions(+), 28 deletions(-)
 rename src/web/app/{base.styl => app.styl} (94%)

diff --git a/locales/en.yml b/locales/en.yml
index cf75bee92b..52e8dfdb4b 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -170,6 +170,13 @@ ch:
       new: "Create new channel"
       channel-title: "Channel title"
 
+    mk-channel-form:
+      textarea: "Write here"
+      upload: "Upload"
+      drive: "Drive"
+      post: "Do"
+      posting: "Doing"
+
 desktop:
   tags:
     mk-api-info:
diff --git a/locales/ja.yml b/locales/ja.yml
index 03975556b5..3dae21d4a2 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -170,6 +170,13 @@ ch:
       new: "チャンネルを作成"
       channel-title: "チャンネルのタイトル"
 
+    mk-channel-form:
+      textarea: "書いて"
+      upload: "アップロード"
+      drive: "ドライブ"
+      post: "やる"
+      posting: "やってます"
+
 desktop:
   tags:
     mk-api-info:
diff --git a/src/web/app/base.styl b/src/web/app/app.styl
similarity index 94%
rename from src/web/app/base.styl
rename to src/web/app/app.styl
index 81c039f0a3..94faba73d4 100644
--- a/src/web/app/base.styl
+++ b/src/web/app/app.styl
@@ -5,8 +5,6 @@ json('../../const.json')
 $theme-color = themeColor
 $theme-color-foreground = themeColorForeground
 
-@import './reset'
-
 /*
 	::selection
 		background $theme-color
@@ -14,6 +12,9 @@ $theme-color-foreground = themeColorForeground
 */
 
 *
+	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)
 
@@ -29,6 +30,9 @@ html
 		&, *
 			cursor progress !important
 
+body
+	overflow-wrap break-word
+
 #error
 	padding 32px
 	color #fff
diff --git a/src/web/app/auth/style.styl b/src/web/app/auth/style.styl
index 046a5ff6ee..bd25e1b572 100644
--- a/src/web/app/auth/style.styl
+++ b/src/web/app/auth/style.styl
@@ -1,4 +1,5 @@
-@import "../base"
+@import "../app"
+@import "../reset"
 
 html
 	background #eee
diff --git a/src/web/app/ch/style.styl b/src/web/app/ch/style.styl
index 8ad6fbce0b..21ca648cbe 100644
--- a/src/web/app/ch/style.styl
+++ b/src/web/app/ch/style.styl
@@ -1,5 +1,10 @@
-@import "../base"
+@import "../app"
 
 html
 	padding 8px
 	background #efefef
+
+#wait
+	top auto
+	bottom 15px
+	left 15px
diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index 35463bc0b8..4ae62e7b39 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -49,6 +49,9 @@
 				> .body
 					margin 8px 0 0 0
 
+				> mk-channel-form
+					max-width 500px
+
 	</style>
 	<script>
 		import Progress from '../../common/scripts/loading';
@@ -240,20 +243,45 @@
 
 <mk-channel-form>
 	<p if={ reply }><b>&gt;&gt;{ reply.index }</b> ({ reply.user.name }): <a onclick={ clearReply }>[x]</a></p>
-	<textarea ref="text" disabled={ wait } oninput={ update } onkeydown={ onkeydown } onpaste={ onpaste }></textarea>
-	<button class={ wait: wait } ref="submit" disabled={ wait || (refs.text.value.length == 0) } onclick={ post }>
-		{ wait ? 'やってます' : 'やる' }<mk-ellipsis if={ wait }/>
-	</button>
-	<br>
-	<button onclick={ drive }>ドライブ</button>
+	<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 }><i class="fa fa-upload"></i>%i18n:ch.tags.mk-channel-form.upload%</button>
+		<button onclick={ drive }><i class="fa fa-cloud"></i>%i18n:ch.tags.mk-channel-form.drive%</button>
+		<button class={ wait: wait } ref="submit" disabled={ wait || (refs.text.value.length == 0) } onclick={ post }>
+			<i class="fa fa-paper-plane" if={ !wait }></i>{ wait ? '%i18n:ch.tags.mk-channel-form.posting%' : '%i18n:ch.tags.mk-channel-form.post%' }<mk-ellipsis if={ wait }/>
+		</button>
+	</div>
 	<mk-uploader ref="uploader"/>
 	<ol if={ files }>
 		<li each={ files }>{ name }</li>
 	</ol>
+	<input ref="file" type="file" accept="image/*" multiple="multiple" onchange={ changeFile }/>
 	<style>
 		:scope
 			display block
 
+			> textarea
+				width 100%
+				max-width 100%
+				min-width 100%
+				min-height 5em
+
+			> .actions
+				display flex
+
+				> button
+					> i
+						margin-right 0.25em
+
+					&:last-child
+						margin-left auto
+
+					&.wait
+						cursor wait
+
+			> input[type='file']
+				display none
+
 	</style>
 	<script>
 		import CONFIG from '../../common/scripts/config';
@@ -314,6 +342,14 @@
 			});
 		};
 
+		this.changeFile = () => {
+			this.refs.file.files.forEach(this.upload);
+		};
+
+		this.selectFile = () => {
+			this.refs.file.click();
+		};
+
 		this.drive = () => {
 			window['cb'] = files => {
 				this.update({
diff --git a/src/web/app/desktop/style.styl b/src/web/app/desktop/style.styl
index 88adb68b2b..4597dffdb3 100644
--- a/src/web/app/desktop/style.styl
+++ b/src/web/app/desktop/style.styl
@@ -1,4 +1,5 @@
-@import "../base"
+@import "../app"
+@import "../reset"
 @import "../../../../node_modules/cropperjs/dist/cropper.css"
 
 *::input-placeholder
diff --git a/src/web/app/dev/style.styl b/src/web/app/dev/style.styl
index 4fd537709d..cdbcb0e261 100644
--- a/src/web/app/dev/style.styl
+++ b/src/web/app/dev/style.styl
@@ -1,4 +1,5 @@
-@import "../base"
+@import "../app"
+@import "../reset"
 
 html
 	background-color #fff
diff --git a/src/web/app/mobile/style.styl b/src/web/app/mobile/style.styl
index bd6965e402..63e4f2349f 100644
--- a/src/web/app/mobile/style.styl
+++ b/src/web/app/mobile/style.styl
@@ -1,4 +1,5 @@
-@import "../base"
+@import "../app"
+@import "../reset"
 
 #wait
 	top auto
diff --git a/src/web/app/reset.styl b/src/web/app/reset.styl
index 85bbd11473..3d4b06dbdf 100644
--- a/src/web/app/reset.styl
+++ b/src/web/app/reset.styl
@@ -1,16 +1,3 @@
-*
-	position relative
-	box-sizing border-box
-	background-clip padding-box !important
-
-html
-body
-	margin 0
-	padding 0
-
-body
-	overflow-wrap break-word
-
 input:not([type])
 input[type='text']
 input[type='password']
diff --git a/src/web/app/stats/style.styl b/src/web/app/stats/style.styl
index b48d7aeb9e..5ae230ea56 100644
--- a/src/web/app/stats/style.styl
+++ b/src/web/app/stats/style.styl
@@ -1,4 +1,5 @@
-@import "../base"
+@import "../app"
+@import "../reset"
 
 html
 	color #456267
diff --git a/src/web/app/status/style.styl b/src/web/app/status/style.styl
index b48d7aeb9e..5ae230ea56 100644
--- a/src/web/app/status/style.styl
+++ b/src/web/app/status/style.styl
@@ -1,4 +1,5 @@
-@import "../base"
+@import "../app"
+@import "../reset"
 
 html
 	color #456267

From 335b7dfc7ba2f91cb2aac0e8c65c83745c39d13b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 2 Nov 2017 12:40:15 +0900
Subject: [PATCH 328/364] v2807

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 20323bd96c..f8018e4e25 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+2807 (2017/11/02)
+-----------------
+* いい感じに
+
 2805 (2017/11/02)
 -----------------
 * いい感じに
diff --git a/package.json b/package.json
index 9f04d3125c..051eb1cb83 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2805",
+  "version": "0.0.2807",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",

From b77ffdbeb14e5b758a436f0defa3d93c17da67af Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 2 Nov 2017 12:56:07 +0900
Subject: [PATCH 329/364] Refactor

---
 src/api/endpoints/posts/create.ts | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index 2326f7baf1..f982b9ee93 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -226,8 +226,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 	// Reponse
 	res(postObj);
 
-	// -----------------------------------------------------------
-	// Post processes
+	//#region Post processes
 
 	User.update({ _id: user._id }, {
 		$set: {
@@ -481,4 +480,6 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 			}
 		});
 	}
+
+	//#endregion
 });

From 9f9f2a99450a60dc4298ac284cefc1d65009dade Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 2 Nov 2017 14:43:56 +0900
Subject: [PATCH 330/364] Fix

---
 src/web/app/desktop/tags/pages/selectdrive.tag | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/web/app/desktop/tags/pages/selectdrive.tag b/src/web/app/desktop/tags/pages/selectdrive.tag
index b196357d85..63fc588fac 100644
--- a/src/web/app/desktop/tags/pages/selectdrive.tag
+++ b/src/web/app/desktop/tags/pages/selectdrive.tag
@@ -9,6 +9,7 @@
 	<style>
 		:scope
 			display block
+			position fixed
 			height 100%
 			background #fff
 

From d11aea263fab43d05a0bed96e3aa1f6096046032 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, 3 Nov 2017 13:24:50 +0900
Subject: [PATCH 331/364] Update ja.yml

---
 locales/ja.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/locales/ja.yml b/locales/ja.yml
index 3dae21d4a2..dcd012bb89 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -84,7 +84,7 @@ common:
         no-internet: "インターネットに接続されていません"
         no-internet-desc: "ネットワークには接続されていますが、インターネットには接続されていないようです。お使いのPCのインターネット接続が正常か確認してください。"
         no-server: "Misskeyのサーバーに接続できません"
-        no-server-desc: "お使いのPCのネットワーク接続は正常ですが、Misskeyのサーバーには接続できませんでした。サーバーがダウンまたはメンテナンスしている可能性があるので、しばらくしてから再度御アクセスください。"
+        no-server-desc: "お使いのPCのインターネット接続は正常ですが、Misskeyのサーバーには接続できませんでした。サーバーがダウンまたはメンテナンスしている可能性があるので、しばらくしてから再度御アクセスください。"
         success: "Misskeyのサーバーに接続できました"
         success-desc: "正常に接続できるようです。ページを再度読み込みしてください。"
 

From f7bc60d5bc5e94ba2704da2aeac290f1d541fac3 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, 3 Nov 2017 17:37:05 +0900
Subject: [PATCH 332/364] Update safe.js

---
 src/web/app/safe.js | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/web/app/safe.js b/src/web/app/safe.js
index c5fbb83a92..77293be81d 100644
--- a/src/web/app/safe.js
+++ b/src/web/app/safe.js
@@ -7,5 +7,8 @@
 if (!('fetch' in window)) {
 	alert(
 		'お使いのブラウザが古いためMisskeyを動作させることができません。' +
-		'バージョンを最新のものに更新するか、別のブラウザをお試しください。');
+		'バージョンを最新のものに更新するか、別のブラウザをお試しください。' +
+		'\n\n' +
+		'Your browser seems outdated.' +
+		'To run Misskey, please update your browser to latest version or try other browsers.');
 }

From 0ff0107cb89b169aba1dba23b722d6c526cc45cb Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 3 Nov 2017 17:44:52 +0900
Subject: [PATCH 333/364] typo

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

diff --git a/src/config.ts b/src/config.ts
index 18017e9740..d37d227a41 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -88,7 +88,7 @@ type Mixin = {
 	api_url: string;
 	auth_url: string;
 	about_url: string;
-	ch_url: stirng;
+	ch_url: string;
 	stats_url: string;
 	status_url: string;
 	dev_url: string;

From 97f0b29d4a716bf1bba7d12c250062da9b2e7c03 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 3 Nov 2017 17:46:42 +0900
Subject: [PATCH 334/364] [Client] set lang

---
 src/web/app/init.js       | 5 +++++
 webpack/plugins/const.ts  | 3 ++-
 webpack/plugins/index.ts  | 4 ++--
 webpack/webpack.config.ts | 2 +-
 4 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/src/web/app/init.js b/src/web/app/init.js
index cb661c2595..5a6899ed4f 100644
--- a/src/web/app/init.js
+++ b/src/web/app/init.js
@@ -21,6 +21,11 @@ require('./common/tags');
 
 console.info(`Misskey v${VERSION} (葵 aoi)`);
 
+{ // Set lang attr
+	const html = document.documentElement;
+	html.setAttribute('lang', LANG);
+}
+
 { // Set description meta tag
 	const head = document.getElementsByTagName('head')[0];
 	const meta = document.createElement('meta');
diff --git a/webpack/plugins/const.ts b/webpack/plugins/const.ts
index ccfcb45260..f64160b01a 100644
--- a/webpack/plugins/const.ts
+++ b/webpack/plugins/const.ts
@@ -7,7 +7,8 @@ import * as webpack from 'webpack';
 import version from '../../src/version';
 const constants = require('../../src/const.json');
 
-export default () => new webpack.DefinePlugin({
+export default lang => new webpack.DefinePlugin({
 	VERSION: JSON.stringify(version),
+	LANG: JSON.stringify(lang),
 	THEME_COLOR: JSON.stringify(constants.themeColor)
 });
diff --git a/webpack/plugins/index.ts b/webpack/plugins/index.ts
index d5191f1555..345af7df9e 100644
--- a/webpack/plugins/index.ts
+++ b/webpack/plugins/index.ts
@@ -8,9 +8,9 @@ import banner from './banner';
 const env = process.env.NODE_ENV;
 const isProduction = env === 'production';
 
-export default version => {
+export default (version, lang) => {
 	const plugins = [
-		constant(),
+		constant(lang),
 		new StringReplacePlugin(),
 		hoist()
 	];
diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts
index 066df18157..97782a4102 100644
--- a/webpack/webpack.config.ts
+++ b/webpack/webpack.config.ts
@@ -32,7 +32,7 @@ module.exports = langs.map(([lang, locale]) => {
 		name,
 		entry,
 		module: module_(lang, locale),
-		plugins: plugins(version),
+		plugins: plugins(version, lang),
 		output
 	};
 });

From 78487934c7e55b36b07f30b73127577ddec31f32 Mon Sep 17 00:00:00 2001
From: otofune <otofune@gmail.com>
Date: Sun, 5 Nov 2017 21:11:16 +0900
Subject: [PATCH 336/364] selializers - posts: unneed async-await

Promise.all resolves all Promise, and selializeDriveFile returns Promise.
---
 src/api/serializers/post.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/api/serializers/post.ts b/src/api/serializers/post.ts
index 7c3690ef79..b2c54e9df8 100644
--- a/src/api/serializers/post.ts
+++ b/src/api/serializers/post.ts
@@ -84,8 +84,8 @@ const self = (
 
 	// Populate media
 	if (_post.media_ids) {
-		_post.media = await Promise.all(_post.media_ids.map(async fileId =>
-			await serializeDriveFile(fileId)
+		_post.media = await Promise.all(_post.media_ids.map(fileId =>
+			serializeDriveFile(fileId)
 		));
 	}
 

From 11190f56ad85a12faaa3653f8743dd75948ff11e Mon Sep 17 00:00:00 2001
From: otofune <otofune@gmail.com>
Date: Sun, 5 Nov 2017 22:13:28 +0900
Subject: [PATCH 337/364] serializers/post - run promises in parallel

now w/ opts.detail, returns my_reaction field as 'null' w/ no reaction
(before: field appears w/ some reaction)
---
 package.json                |   1 +
 src/api/serializers/post.ts | 122 ++++++++++++++++++++----------------
 2 files changed, 70 insertions(+), 53 deletions(-)

diff --git a/package.json b/package.json
index 051eb1cb83..1e6e8d8136 100644
--- a/package.json
+++ b/package.json
@@ -95,6 +95,7 @@
     "webpack": "3.8.1"
   },
   "dependencies": {
+    "@prezzemolo/rap": "^0.1.0",
     "accesses": "2.5.0",
     "animejs": "2.2.0",
     "autwh": "0.0.1",
diff --git a/src/api/serializers/post.ts b/src/api/serializers/post.ts
index b2c54e9df8..352932acff 100644
--- a/src/api/serializers/post.ts
+++ b/src/api/serializers/post.ts
@@ -12,6 +12,7 @@ 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
@@ -70,21 +71,21 @@ const self = (
 	}
 
 	// Populate user
-	_post.user = await serializeUser(_post.user_id, meId);
+	_post.user = serializeUser(_post.user_id, meId);
 
 	// Populate app
 	if (_post.app_id) {
-		_post.app = await serializeApp(_post.app_id);
+		_post.app = serializeApp(_post.app_id);
 	}
 
 	// Populate channel
 	if (_post.channel_id) {
-		_post.channel = await serializeChannel(_post.channel_id);
+		_post.channel = serializeChannel(_post.channel_id);
 	}
 
 	// Populate media
 	if (_post.media_ids) {
-		_post.media = await Promise.all(_post.media_ids.map(fileId =>
+		_post.media = Promise.all(_post.media_ids.map(fileId =>
 			serializeDriveFile(fileId)
 		));
 	}
@@ -92,82 +93,97 @@ const self = (
 	// When requested a detailed post data
 	if (opts.detail) {
 		// Get previous post info
-		const prev = await Post.findOne({
-			user_id: _post.user_id,
-			_id: {
-				$lt: id
-			}
-		}, {
-			fields: {
-				_id: true
-			},
-			sort: {
-				_id: -1
-			}
-		});
-		_post.prev = prev ? prev._id : null;
+		_post.prev = (async () => {
+			const prev = 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
-		const next = await Post.findOne({
-			user_id: _post.user_id,
-			_id: {
-				$gt: id
-			}
-		}, {
-			fields: {
-				_id: true
-			},
-			sort: {
-				_id: 1
-			}
-		});
-		_post.next = next ? next._id : null;
+		_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 = await self(_post.reply_id, meId, {
+			_post.reply = self(_post.reply_id, meId, {
 				detail: false
 			});
 		}
 
 		if (_post.repost_id) {
 			// Populate repost
-			_post.repost = await self(_post.repost_id, meId, {
+			_post.repost = self(_post.repost_id, meId, {
 				detail: _post.text == null
 			});
 		}
 
 		// Poll
 		if (meId && _post.poll) {
-			const vote = await Vote
-				.findOne({
-					user_id: meId,
-					post_id: id
-				});
+			_post.poll = (async (poll) => {
+				const vote = await Vote
+					.findOne({
+						user_id: meId,
+						post_id: id
+					});
 
-			if (vote != null) {
-				const myChoice = _post.poll.choices
-					.filter(c => c.id == vote.choice)[0];
+				if (vote != null) {
+					const myChoice = poll.choices
+						.filter(c => c.id == vote.choice)[0];
 
-				myChoice.is_voted = true;
-			}
+					myChoice.is_voted = true;
+				}
+
+				return poll
+			})(_post.poll)
 		}
 
 		// Fetch my reaction
 		if (meId) {
-			const reaction = await Reaction
-				.findOne({
-					user_id: meId,
-					post_id: id,
-					deleted_at: { $exists: false }
-				});
+			_post.my_reaction = (async () => {
+				const reaction = await Reaction
+					.findOne({
+						user_id: meId,
+						post_id: id,
+						deleted_at: { $exists: false }
+					});
 
-			if (reaction) {
-				_post.my_reaction = reaction.reaction;
-			}
+				if (reaction) {
+					return reaction.reaction;
+				}
+
+				return null
+			})();
 		}
 	}
 
+	// resolve promises in _post object
+	_post = await rap(_post)
+
 	resolve(_post);
 });
 

From 5aa5e5cc7074003cec3417636ea1972b6d88150d Mon Sep 17 00:00:00 2001
From: otofune <otofune@gmail.com>
Date: Sun, 5 Nov 2017 22:22:49 +0900
Subject: [PATCH 338/364] serializers - user: run promises in parallel as
 possible

---
 src/api/serializers/post.ts |  2 +-
 src/api/serializers/user.ts | 40 +++++++++++++++++++++----------------
 2 files changed, 24 insertions(+), 18 deletions(-)

diff --git a/src/api/serializers/post.ts b/src/api/serializers/post.ts
index 352932acff..99e9bb667c 100644
--- a/src/api/serializers/post.ts
+++ b/src/api/serializers/post.ts
@@ -12,7 +12,7 @@ import serializeChannel from './channel';
 import serializeUser from './user';
 import serializeDriveFile from './drive-file';
 import parse from '../common/text';
-import rap from '@prezzemolo/rap'
+import rap from '@prezzemolo/rap';
 
 /**
  * Serialize a post
diff --git a/src/api/serializers/user.ts b/src/api/serializers/user.ts
index 3deff2d003..3527921ded 100644
--- a/src/api/serializers/user.ts
+++ b/src/api/serializers/user.ts
@@ -8,6 +8,7 @@ import serializePost from './post';
 import Following from '../models/following';
 import getFriends from '../common/get-friends';
 import config from '../../conf';
+import rap from '@prezzemolo/rap';
 
 /**
  * Serialize a user
@@ -104,26 +105,30 @@ export default (
 
 	if (meId && !meId.equals(_user.id)) {
 		// If the user is following
-		const follow = await Following.findOne({
-			follower_id: meId,
-			followee_id: _user.id,
-			deleted_at: { $exists: false }
-		});
-		_user.is_following = follow !== null;
+		_user.is_following = (async () => {
+			const follow = await Following.findOne({
+				follower_id: meId,
+				followee_id: _user.id,
+				deleted_at: { $exists: false }
+			});
+			return follow !== null;
+		})()
 
 		// If the user is followed
-		const follow2 = await Following.findOne({
-			follower_id: _user.id,
-			followee_id: meId,
-			deleted_at: { $exists: false }
-		});
-		_user.is_followed = follow2 !== null;
+		_user.is_followed = (async () => {
+			const follow2 = await Following.findOne({
+				follower_id: _user.id,
+				followee_id: meId,
+				deleted_at: { $exists: false }
+			});
+			return follow2 !== null;
+		})()
 	}
 
 	if (opts.detail) {
 		if (_user.pinned_post_id) {
 			// Populate pinned post
-			_user.pinned_post = await serializePost(_user.pinned_post_id, meId, {
+			_user.pinned_post = serializePost(_user.pinned_post_id, meId, {
 				detail: true
 			});
 		}
@@ -132,23 +137,24 @@ export default (
 			const myFollowingIds = await getFriends(meId);
 
 			// Get following you know count
-			const followingYouKnowCount = await Following.count({
+			_user.following_you_know_count = Following.count({
 				followee_id: { $in: myFollowingIds },
 				follower_id: _user.id,
 				deleted_at: { $exists: false }
 			});
-			_user.following_you_know_count = followingYouKnowCount;
 
 			// Get followers you know count
-			const followersYouKnowCount = await Following.count({
+			_user.followers_you_know_count = Following.count({
 				followee_id: _user.id,
 				follower_id: { $in: myFollowingIds },
 				deleted_at: { $exists: false }
 			});
-			_user.followers_you_know_count = followersYouKnowCount;
 		}
 	}
 
+	// resolve promises in _user object
+	_user = await rap(_user)
+
 	resolve(_user);
 });
 /*

From 7cd6b1c666605c7a256e4a8dd8db5edeb02da6db Mon Sep 17 00:00:00 2001
From: otofune <otofune@gmail.com>
Date: Sun, 5 Nov 2017 22:26:16 +0900
Subject: [PATCH 339/364] follow lint

---
 src/api/serializers/post.ts | 12 ++++++------
 src/api/serializers/user.ts |  6 +++---
 2 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/src/api/serializers/post.ts b/src/api/serializers/post.ts
index 99e9bb667c..e1ab784359 100644
--- a/src/api/serializers/post.ts
+++ b/src/api/serializers/post.ts
@@ -108,7 +108,7 @@ const self = (
 				}
 			});
 			return prev ? prev._id : null;
-		})()
+		})();
 
 		// Get next post info
 		_post.next = (async () => {
@@ -126,7 +126,7 @@ const self = (
 				}
 			});
 			return next ? next._id : null;
-		})()
+		})();
 
 		if (_post.reply_id) {
 			// Populate reply to post
@@ -158,8 +158,8 @@ const self = (
 					myChoice.is_voted = true;
 				}
 
-				return poll
-			})(_post.poll)
+				return poll;
+			})(_post.poll);
 		}
 
 		// Fetch my reaction
@@ -176,13 +176,13 @@ const self = (
 					return reaction.reaction;
 				}
 
-				return null
+				return null;
 			})();
 		}
 	}
 
 	// resolve promises in _post object
-	_post = await rap(_post)
+	_post = await rap(_post);
 
 	resolve(_post);
 });
diff --git a/src/api/serializers/user.ts b/src/api/serializers/user.ts
index 3527921ded..d00f073897 100644
--- a/src/api/serializers/user.ts
+++ b/src/api/serializers/user.ts
@@ -112,7 +112,7 @@ export default (
 				deleted_at: { $exists: false }
 			});
 			return follow !== null;
-		})()
+		})();
 
 		// If the user is followed
 		_user.is_followed = (async () => {
@@ -122,7 +122,7 @@ export default (
 				deleted_at: { $exists: false }
 			});
 			return follow2 !== null;
-		})()
+		})();
 	}
 
 	if (opts.detail) {
@@ -153,7 +153,7 @@ export default (
 	}
 
 	// resolve promises in _user object
-	_user = await rap(_user)
+	_user = await rap(_user);
 
 	resolve(_user);
 });

From 09baf205ead75eab3eaf0f3de82215665c2a3e73 Mon Sep 17 00:00:00 2001
From: otofune <otofune@gmail.com>
Date: Sun, 5 Nov 2017 22:29:58 +0900
Subject: [PATCH 340/364] remove ^ from @prezzemolo/rap dependency

---
 package.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index 1e6e8d8136..c3a093420c 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,7 @@
     "clean": "gulp clean",
     "cleanall": "gulp cleanall",
     "lint": "gulp lint",
-    "test": "gulp test"
+		"test": "gulp test"
   },
   "devDependencies": {
     "@types/bcryptjs": "2.4.0",
@@ -95,7 +95,7 @@
     "webpack": "3.8.1"
   },
   "dependencies": {
-    "@prezzemolo/rap": "^0.1.0",
+    "@prezzemolo/rap": "0.1.0",
     "accesses": "2.5.0",
     "animejs": "2.2.0",
     "autwh": "0.0.1",

From 327d2705b4a3dad6ef8a8dfa8165c25a3a40d109 Mon Sep 17 00:00:00 2001
From: otofune <otofune@gmail.com>
Date: Sun, 5 Nov 2017 22:37:00 +0900
Subject: [PATCH 341/364] update @prezzemolo/rap to 0.1.1

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

diff --git a/package.json b/package.json
index c3a093420c..27e292cc1a 100644
--- a/package.json
+++ b/package.json
@@ -95,7 +95,7 @@
     "webpack": "3.8.1"
   },
   "dependencies": {
-    "@prezzemolo/rap": "0.1.0",
+    "@prezzemolo/rap": "0.1.1",
     "accesses": "2.5.0",
     "animejs": "2.2.0",
     "autwh": "0.0.1",

From ac2a0f46cd9ee877adda57bb939a1b31f7109911 Mon Sep 17 00:00:00 2001
From: otofune <otofune@gmail.com>
Date: Sun, 5 Nov 2017 22:47:04 +0900
Subject: [PATCH 342/364] update @prezzemolo/rap to 0.1.2

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

diff --git a/package.json b/package.json
index 27e292cc1a..6ea91a7f53 100644
--- a/package.json
+++ b/package.json
@@ -95,7 +95,7 @@
     "webpack": "3.8.1"
   },
   "dependencies": {
-    "@prezzemolo/rap": "0.1.1",
+    "@prezzemolo/rap": "0.1.2",
     "accesses": "2.5.0",
     "animejs": "2.2.0",
     "autwh": "0.0.1",

From 55fc8de44d18deb6cd89c887895b6b6d30bcd229 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Sun, 5 Nov 2017 20:40:07 +0000
Subject: [PATCH 343/364] fix(package): update riot to version 3.7.4

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

diff --git a/package.json b/package.json
index 051eb1cb83..7a7cd1c09c 100644
--- a/package.json
+++ b/package.json
@@ -140,7 +140,7 @@
     "redis": "2.8.0",
     "request": "2.83.0",
     "rimraf": "2.6.2",
-    "riot": "3.7.3",
+    "riot": "3.7.4",
     "rndstr": "1.0.0",
     "s-age": "1.1.0",
     "serve-favicon": "2.4.5",

From 7e81e0db6ac1289ae9504f7e3da5db6e56f41a51 Mon Sep 17 00:00:00 2001
From: otofune <otofune@gmail.com>
Date: Mon, 6 Nov 2017 14:37:00 +0900
Subject: [PATCH 344/364] support GridFS

---
 src/api/common/add-file-to-drive.ts | 37 ++++++++++++++++++-----------
 src/api/models/drive-file.ts        | 15 ++++++++++--
 src/db/mongodb.ts                   | 35 +++++++++++++++++++++++----
 3 files changed, 67 insertions(+), 20 deletions(-)

diff --git a/src/api/common/add-file-to-drive.ts b/src/api/common/add-file-to-drive.ts
index 714eeb520d..f48f0cbcf5 100644
--- a/src/api/common/add-file-to-drive.ts
+++ b/src/api/common/add-file-to-drive.ts
@@ -4,14 +4,27 @@ import * as gm from 'gm';
 import * as debug from 'debug';
 import fileType = require('file-type');
 import prominence = require('prominence');
-import DriveFile from '../models/drive-file';
+import DriveFile, { getGridFSBucket } from '../models/drive-file';
 import DriveFolder from '../models/drive-folder';
 import serialize from '../serializers/drive-file';
 import event from '../event';
 import config from '../../conf';
+import { Duplex } from 'stream';
 
 const log = debug('misskey:register-drive-file');
 
+const addToGridFS = (name, binary, metadata): Promise<any> => new Promise(async (resolve, reject) => {
+	const dataStream = new Duplex()
+	dataStream.push(binary)
+	dataStream.push(null)
+
+	const bucket = await getGridFSBucket()
+	const writeStream = bucket.openUploadStream(name, { metadata })
+	writeStream.once('finish', (doc) => { resolve(doc) })
+	writeStream.on('error', reject)
+	dataStream.pipe(writeStream)
+})
+
 /**
  * Add file to drive
  *
@@ -58,7 +71,7 @@ export default (
 
 	// Generate hash
 	const hash = crypto
-		.createHash('sha256')
+		.createHash('md5')
 		.update(data)
 		.digest('hex') as string;
 
@@ -67,8 +80,10 @@ export default (
 	if (!force) {
 		// Check if there is a file with the same hash
 		const much = await DriveFile.findOne({
-			user_id: user._id,
-			hash: hash
+			md5: hash,
+			metadata: {
+				user_id: user._id
+			}
 		});
 
 		if (much !== null) {
@@ -82,13 +97,13 @@ export default (
 	// Calculate drive usage
 	const usage = ((await DriveFile
 		.aggregate([
-			{ $match: { user_id: user._id } },
+			{ $match: { metadata: { user_id: user._id } } },
 			{ $project: {
-				datasize: true
+				length: true
 			}},
 			{ $group: {
 				_id: null,
-				usage: { $sum: '$datasize' }
+				usage: { $sum: '$length' }
 			}}
 		]))[0] || {
 			usage: 0
@@ -131,21 +146,15 @@ export default (
 	}
 
 	// Create DriveFile document
-	const file = await DriveFile.insert({
-		created_at: new Date(),
+	const file = await addToGridFS(`${user._id}/${name}`, data, {
 		user_id: user._id,
 		folder_id: folder !== null ? folder._id : null,
-		data: data,
-		datasize: size,
 		type: mime,
 		name: name,
 		comment: comment,
-		hash: hash,
 		properties: properties
 	});
 
-	delete file.data;
-
 	log(`drive file has been created ${file._id}`);
 
 	resolve(file);
diff --git a/src/api/models/drive-file.ts b/src/api/models/drive-file.ts
index 8d158cf563..79a87f6572 100644
--- a/src/api/models/drive-file.ts
+++ b/src/api/models/drive-file.ts
@@ -1,11 +1,22 @@
-import db from '../../db/mongodb';
+import * as mongodb from 'mongodb';
+import monkDb, { nativeDbConn } from '../../db/mongodb';
 
-const collection = db.get('drive_files');
+const collection = monkDb.get('drive_files.files');
 
 (collection as any).createIndex('hash'); // fuck type definition
 
 export default collection as any; // fuck type definition
 
+const getGridFSBucket = async (): Promise<mongodb.GridFSBucket> => {
+	const db = await nativeDbConn()
+	const bucket = new mongodb.GridFSBucket(db, {
+		bucketName: 'drive_files'
+	})
+	return bucket
+}
+
+export { getGridFSBucket }
+
 export function validateFileName(name: string): boolean {
 	return (
 		(name.trim().length > 0) &&
diff --git a/src/db/mongodb.ts b/src/db/mongodb.ts
index 6ee7f4534f..75f1a1d3c6 100644
--- a/src/db/mongodb.ts
+++ b/src/db/mongodb.ts
@@ -1,11 +1,38 @@
-import * as mongo from 'monk';
-
 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
+ */
+import * as mongo from 'monk';
 
 const db = mongo(uri);
 
 export default db;
+
+/**
+ * MongoDB native module (officialy)
+ */
+import * as mongodb from 'mongodb'
+
+let mdb: mongodb.Db;
+
+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) => {
+			if (e) return reject(e)
+			resolve(db)
+		})
+	}))()
+
+	mdb = db
+
+	return db
+}
+
+export { nativeDbConn }

From 18b1ef29adc6166c2b1a327b378c3e159a18b80c Mon Sep 17 00:00:00 2001
From: otofune <otofune@gmail.com>
Date: Mon, 6 Nov 2017 15:18:45 +0900
Subject: [PATCH 345/364] migration to GridFS's DriveFile

---
 src/api/common/add-file-to-drive.ts           |  1 +
 src/api/endpoints/drive.ts                    |  6 ++--
 src/api/endpoints/drive/files.ts              |  9 +++--
 src/api/endpoints/drive/files/find.ts         | 10 +++---
 src/api/endpoints/drive/files/show.ts         |  6 ++--
 src/api/endpoints/drive/files/update.ts       | 31 +++++++++--------
 .../endpoints/messaging/messages/create.ts    |  6 ++--
 src/api/endpoints/posts/create.ts             |  6 ++--
 src/api/endpoints/posts/timeline.ts           | 24 +++++++-------
 src/api/serializers/drive-file.ts             | 33 ++++++++-----------
 src/api/serializers/drive-folder.ts           |  4 ++-
 11 files changed, 66 insertions(+), 70 deletions(-)

diff --git a/src/api/common/add-file-to-drive.ts b/src/api/common/add-file-to-drive.ts
index f48f0cbcf5..376c470e93 100644
--- a/src/api/common/add-file-to-drive.ts
+++ b/src/api/common/add-file-to-drive.ts
@@ -154,6 +154,7 @@ export default (
 		comment: comment,
 		properties: properties
 	});
+	console.dir(file)
 
 	log(`drive file has been created ${file._id}`);
 
diff --git a/src/api/endpoints/drive.ts b/src/api/endpoints/drive.ts
index 41ad6301d7..b9c4e3e506 100644
--- a/src/api/endpoints/drive.ts
+++ b/src/api/endpoints/drive.ts
@@ -14,16 +14,16 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Calculate drive usage
 	const usage = ((await DriveFile
 		.aggregate([
-			{ $match: { user_id: user._id } },
+			{ $match: { metadata: { user_id: user._id } } },
 			{
 				$project: {
-					datasize: true
+					length: true
 				}
 			},
 			{
 				$group: {
 					_id: null,
-					usage: { $sum: '$datasize' }
+					usage: { $sum: '$length' }
 				}
 			}
 		]))[0] || {
diff --git a/src/api/endpoints/drive/files.ts b/src/api/endpoints/drive/files.ts
index a68ae34817..eb0bfe6ba5 100644
--- a/src/api/endpoints/drive/files.ts
+++ b/src/api/endpoints/drive/files.ts
@@ -40,8 +40,10 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
 		_id: -1
 	};
 	const query = {
-		user_id: user._id,
-		folder_id: folderId
+		metadata: {
+			user_id: user._id,
+			folder_id: folderId
+		}
 	} as any;
 	if (sinceId) {
 		sort._id = 1;
@@ -57,9 +59,6 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
 	// Issue query
 	const files = await DriveFile
 		.find(query, {
-			fields: {
-				data: false
-			},
 			limit: limit,
 			sort: sort
 		});
diff --git a/src/api/endpoints/drive/files/find.ts b/src/api/endpoints/drive/files/find.ts
index cd0b33f2ca..255faf94ec 100644
--- a/src/api/endpoints/drive/files/find.ts
+++ b/src/api/endpoints/drive/files/find.ts
@@ -24,12 +24,10 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Issue query
 	const files = await DriveFile
 		.find({
-			name: name,
-			user_id: user._id,
-			folder_id: folderId
-		}, {
-			fields: {
-				data: false
+			metadata: {
+				name: name,
+				user_id: user._id,
+				folder_id: folderId
 			}
 		});
 
diff --git a/src/api/endpoints/drive/files/show.ts b/src/api/endpoints/drive/files/show.ts
index 8dbc297e4f..9135a04c57 100644
--- a/src/api/endpoints/drive/files/show.ts
+++ b/src/api/endpoints/drive/files/show.ts
@@ -21,10 +21,8 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const file = await DriveFile
 		.findOne({
 			_id: fileId,
-			user_id: user._id
-		}, {
-			fields: {
-				data: false
+			metadata: {
+				user_id: user._id
 			}
 		});
 
diff --git a/src/api/endpoints/drive/files/update.ts b/src/api/endpoints/drive/files/update.ts
index 1cfbdd8f0b..c4d2673688 100644
--- a/src/api/endpoints/drive/files/update.ts
+++ b/src/api/endpoints/drive/files/update.ts
@@ -20,25 +20,29 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [fileId, fileIdErr] = $(params.file_id).id().$;
 	if (fileIdErr) return rej('invalid file_id param');
 
+	console.dir(user)
+
 	// Fetch file
 	const file = await DriveFile
 		.findOne({
 			_id: fileId,
-			user_id: user._id
-		}, {
-			fields: {
-				data: false
+			metadata: {
+				user_id: user._id
 			}
 		});
 
+	console.dir(file)
+
 	if (file === null) {
 		return rej('file-not-found');
 	}
 
+	const updateQuery: any = {}
+
 	// Get 'name' parameter
 	const [name, nameErr] = $(params.name).optional.string().pipe(validateFileName).$;
 	if (nameErr) return rej('invalid name param');
-	if (name) file.name = name;
+	if (name) updateQuery.name = name;
 
 	// Get 'folder_id' parameter
 	const [folderId, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
@@ -46,7 +50,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	if (folderId !== undefined) {
 		if (folderId === null) {
-			file.folder_id = null;
+			updateQuery.folder_id = null;
 		} else {
 			// Fetch folder
 			const folder = await DriveFolder
@@ -59,19 +63,20 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 				return rej('folder-not-found');
 			}
 
-			file.folder_id = folder._id;
+			updateQuery.folder_id = folder._id;
 		}
 	}
 
-	DriveFile.update(file._id, {
-		$set: {
-			name: file.name,
-			folder_id: file.folder_id
-		}
+	const updated = await DriveFile.update(file._id, {
+		$set: { metadata: updateQuery }
 	});
 
+	console.dir(updated)
+
 	// Serialize
-	const fileObj = await serialize(file);
+	const fileObj = await serialize(updated);
+
+	console.dir(fileObj)
 
 	// Response
 	res(fileObj);
diff --git a/src/api/endpoints/messaging/messages/create.ts b/src/api/endpoints/messaging/messages/create.ts
index 8af55d850c..1d186268fb 100644
--- a/src/api/endpoints/messaging/messages/create.ts
+++ b/src/api/endpoints/messaging/messages/create.ts
@@ -54,9 +54,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	if (fileId !== undefined) {
 		file = await DriveFile.findOne({
 			_id: fileId,
-			user_id: user._id
-		}, {
-			data: false
+			metadata: {
+				user_id: user._id
+			}
 		});
 
 		if (file === null) {
diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index f982b9ee93..1507639776 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -44,9 +44,9 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
 			// SELECT _id
 			const entity = await DriveFile.findOne({
 				_id: mediaId,
-				user_id: user._id
-			}, {
-				_id: true
+				metadata: {
+					user_id: user._id
+				}
 			});
 
 			if (entity === null) {
diff --git a/src/api/endpoints/posts/timeline.ts b/src/api/endpoints/posts/timeline.ts
index aa5aff5ba5..496de62b69 100644
--- a/src/api/endpoints/posts/timeline.ts
+++ b/src/api/endpoints/posts/timeline.ts
@@ -2,6 +2,7 @@
  * Module dependencies
  */
 import $ from 'cafy';
+import rap from '@prezzemolo/rap';
 import Post from '../../models/post';
 import ChannelWatching from '../../models/channel-watching';
 import getFriends from '../../common/get-friends';
@@ -33,14 +34,15 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
 		return rej('cannot set since_id and max_id');
 	}
 
-	// ID list of the user itself and other users who the user follows
-	const followingIds = await getFriends(user._id);
-
-	// Watchしているチャンネルを取得
-	const watches = await ChannelWatching.find({
-		user_id: user._id,
-		// 削除されたドキュメントは除く
-		deleted_at: { $exists: false }
+	const { followingIds, watchChannelIds } = await rap({
+		// ID list of the user itself and other users who the user follows
+		followingIds: getFriends(user._id),
+		// Watchしているチャンネルを取得
+		watchChannelIds: ChannelWatching.find({
+			user_id: user._id,
+			// 削除されたドキュメントは除く
+			deleted_at: { $exists: false }
+		}).then(watches => watches.map(w => w.channel_id))
 	});
 
 	//#region Construct query
@@ -65,7 +67,7 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
 		}, {
 			// Watchしているチャンネルへの投稿
 			channel_id: {
-				$in: watches.map(w => w.channel_id)
+				$in: watchChannelIds
 			}
 		}]
 	} as any;
@@ -90,7 +92,5 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
 		});
 
 	// Serialize
-	res(await Promise.all(timeline.map(async post =>
-		await serialize(post, user)
-	)));
+	res(Promise.all(timeline.map(post => serialize(post, user))));
 });
diff --git a/src/api/serializers/drive-file.ts b/src/api/serializers/drive-file.ts
index b4e2ab064a..4c750f4c6b 100644
--- a/src/api/serializers/drive-file.ts
+++ b/src/api/serializers/drive-file.ts
@@ -31,44 +31,37 @@ export default (
 	if (mongo.ObjectID.prototype.isPrototypeOf(file)) {
 		_file = await DriveFile.findOne({
 			_id: file
-		}, {
-				fields: {
-					data: false
-				}
-			});
+		});
 	} else if (typeof file === 'string') {
 		_file = await DriveFile.findOne({
 			_id: new mongo.ObjectID(file)
-		}, {
-				fields: {
-					data: false
-				}
-			});
+		});
 	} else {
 		_file = deepcopy(file);
 	}
 
-	// Rename _id to id
-	_file.id = _file._id;
-	delete _file._id;
+	// rendered target
+	let _target: any = {};
 
-	delete _file.data;
+	_target.id = _file._id;
 
-	_file.url = `${config.drive_url}/${_file.id}/${encodeURIComponent(_file.name)}`;
+	_target = Object.assign(_target, _file.metadata);
 
-	if (opts.detail && _file.folder_id) {
+	_target.url = `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`;
+
+	if (opts.detail && _target.folder_id) {
 		// Populate folder
-		_file.folder = await serializeDriveFolder(_file.folder_id, {
+		_target.folder = await serializeDriveFolder(_target.folder_id, {
 			detail: true
 		});
 	}
 
-	if (opts.detail && _file.tags) {
+	if (opts.detail && _target.tags) {
 		// Populate tags
-		_file.tags = await _file.tags.map(async (tag: any) =>
+		_target.tags = await _target.tags.map(async (tag: any) =>
 			await serializeDriveTag(tag)
 		);
 	}
 
-	resolve(_file);
+	resolve(_target);
 });
diff --git a/src/api/serializers/drive-folder.ts b/src/api/serializers/drive-folder.ts
index a428464108..3b5f61aeed 100644
--- a/src/api/serializers/drive-folder.ts
+++ b/src/api/serializers/drive-folder.ts
@@ -44,7 +44,9 @@ const self = (
 		});
 
 		const childFilesCount = await DriveFile.count({
-			folder_id: _folder.id
+			metadata: {
+				folder_id: _folder.id
+			}
 		});
 
 		_folder.folders_count = childFoldersCount;

From d0dab265f40a37cd715b7d4b64a364c78a7a35b9 Mon Sep 17 00:00:00 2001
From: otofune <otofune@gmail.com>
Date: Mon, 6 Nov 2017 15:27:16 +0900
Subject: [PATCH 346/364] serializers - drive-file: add created_at field by
 uploadedDate

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

diff --git a/src/api/serializers/drive-file.ts b/src/api/serializers/drive-file.ts
index 4c750f4c6b..f98cdaa599 100644
--- a/src/api/serializers/drive-file.ts
+++ b/src/api/serializers/drive-file.ts
@@ -44,6 +44,7 @@ export default (
 	let _target: any = {};
 
 	_target.id = _file._id;
+	_target.created_at = _file.uploadDate
 
 	_target = Object.assign(_target, _file.metadata);
 

From a5160a1bbaa3dd75d7ef45b305a90020317e95a8 Mon Sep 17 00:00:00 2001
From: otofune <otofune@gmail.com>
Date: Mon, 6 Nov 2017 15:35:20 +0900
Subject: [PATCH 347/364] fileserver - support DriveFile w/ GridFS

---
 src/file/server.ts | 23 +++++++++++++++++------
 1 file changed, 17 insertions(+), 6 deletions(-)

diff --git a/src/file/server.ts b/src/file/server.ts
index ee67cf7860..bd29e13c5c 100644
--- a/src/file/server.ts
+++ b/src/file/server.ts
@@ -9,7 +9,7 @@ import * as cors from 'cors';
 import * as mongodb from 'mongodb';
 import * as gm from 'gm';
 
-import File from '../api/models/drive-file';
+import DriveFile, { getGridFSBucket } from '../api/models/drive-file';
 
 /**
  * Init app
@@ -97,17 +97,28 @@ app.get('/:id', async (req, res) => {
 		return;
 	}
 
-	const file = await File.findOne({ _id: new mongodb.ObjectID(req.params.id) });
+	const fileId = new mongodb.ObjectID(req.params.id)
+	const file = await DriveFile.findOne({ _id: fileId });
 
 	if (file == null) {
 		res.status(404).sendFile(`${__dirname} / assets / dummy.png`);
 		return;
-	} else if (file.data == null) {
-		res.sendStatus(400);
-		return;
 	}
 
-	send(file.data.buffer, file.type, req, res);
+	const bucket = await getGridFSBucket()
+
+	const buffer = await ((id): Promise<Buffer> => new Promise((resolve, reject) => {
+		const chunks = []
+		const readableStream = bucket.openDownloadStream(id)
+	  readableStream.on('data', chunk => {
+			chunks.push(chunk);
+		})
+		readableStream.on('end', () => {
+			resolve(Buffer.concat(chunks))
+		})
+	}))(fileId)
+
+	send(buffer, file.metadata.type, req, res);
 });
 
 app.get('/:id/:name', async (req, res) => {

From 2ce3179d5000501391b020dd98385aab9fed8094 Mon Sep 17 00:00:00 2001
From: otofune <otofune@gmail.com>
Date: Mon, 6 Nov 2017 15:37:04 +0900
Subject: [PATCH 348/364] fileserver - fix dummy path

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

diff --git a/src/file/server.ts b/src/file/server.ts
index bd29e13c5c..068e88546b 100644
--- a/src/file/server.ts
+++ b/src/file/server.ts
@@ -101,7 +101,7 @@ app.get('/:id', async (req, res) => {
 	const file = await DriveFile.findOne({ _id: fileId });
 
 	if (file == null) {
-		res.status(404).sendFile(`${__dirname} / assets / dummy.png`);
+		res.status(404).sendFile(`${__dirname}/assets/dummy.png`);
 		return;
 	}
 

From 28a39bccf96549a35ef77c10dce5f90f9f8cc654 Mon Sep 17 00:00:00 2001
From: otofune <otofune@gmail.com>
Date: Mon, 6 Nov 2017 15:39:16 +0900
Subject: [PATCH 349/364] file-server - support new DriveFile w/ GridFS on
 '/:id/:name'

---
 src/file/server.ts | 21 ++++++++++++++++-----
 1 file changed, 16 insertions(+), 5 deletions(-)

diff --git a/src/file/server.ts b/src/file/server.ts
index 068e88546b..f38599b89c 100644
--- a/src/file/server.ts
+++ b/src/file/server.ts
@@ -128,17 +128,28 @@ app.get('/:id/:name', async (req, res) => {
 		return;
 	}
 
-	const file = await File.findOne({ _id: new mongodb.ObjectID(req.params.id) });
+	const fileId = new mongodb.ObjectID(req.params.id)
+	const file = await DriveFile.findOne({ _id: fileId });
 
 	if (file == null) {
 		res.status(404).sendFile(`${__dirname}/assets/dummy.png`);
 		return;
-	} else if (file.data == null) {
-		res.sendStatus(400);
-		return;
 	}
 
-	send(file.data.buffer, file.type, req, res);
+	const bucket = await getGridFSBucket()
+
+	const buffer = await ((id): Promise<Buffer> => new Promise((resolve, reject) => {
+		const chunks = []
+		const readableStream = bucket.openDownloadStream(id)
+	  readableStream.on('data', chunk => {
+			chunks.push(chunk);
+		})
+		readableStream.on('end', () => {
+			resolve(Buffer.concat(chunks))
+		})
+	}))(fileId)
+
+	send(buffer, file.metadata.type, req, res);
 });
 
 module.exports = app;

From 0ee6d6592113c5b2df071f4451cb1c1697b59d61 Mon Sep 17 00:00:00 2001
From: otofune <otofune@gmail.com>
Date: Mon, 6 Nov 2017 15:45:21 +0900
Subject: [PATCH 350/364] fix timeline

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

diff --git a/src/api/endpoints/posts/timeline.ts b/src/api/endpoints/posts/timeline.ts
index 496de62b69..19578e59b1 100644
--- a/src/api/endpoints/posts/timeline.ts
+++ b/src/api/endpoints/posts/timeline.ts
@@ -92,5 +92,5 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
 		});
 
 	// Serialize
-	res(Promise.all(timeline.map(post => serialize(post, user))));
+	res(await Promise.all(timeline.map(post => serialize(post, user))));
 });

From 7553c6dd38c6f8574894a009238d946d50c53477 Mon Sep 17 00:00:00 2001
From: otofune <otofune@gmail.com>
Date: Mon, 6 Nov 2017 15:52:09 +0900
Subject: [PATCH 351/364] serializers - posts: no need Promise wrapping

---
 src/api/serializers/post.ts | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/api/serializers/post.ts b/src/api/serializers/post.ts
index e1ab784359..d1dcb66002 100644
--- a/src/api/serializers/post.ts
+++ b/src/api/serializers/post.ts
@@ -22,13 +22,13 @@ import rap from '@prezzemolo/rap';
  * @param options? serialize options
  * @return response
  */
-const self = (
+const self = async (
 	post: string | mongo.ObjectID | IPost,
 	me?: string | mongo.ObjectID | IUser,
 	options?: {
 		detail: boolean
 	}
-) => new Promise<any>(async (resolve, reject) => {
+) => {
 	const opts = options || {
 		detail: true,
 	};
@@ -184,7 +184,7 @@ const self = (
 	// resolve promises in _post object
 	_post = await rap(_post);
 
-	resolve(_post);
-});
+	return _post;
+};
 
 export default self;

From 7b1fc2c5d62e229542e9411a29e078236a9d96db Mon Sep 17 00:00:00 2001
From: otofune <otofune@gmail.com>
Date: Mon, 6 Nov 2017 15:55:47 +0900
Subject: [PATCH 352/364] api - endpoint:timeline: unneed promise wrapping

---
 src/api/endpoints/posts/timeline.ts | 15 ++++++++-------
 1 file changed, 8 insertions(+), 7 deletions(-)

diff --git a/src/api/endpoints/posts/timeline.ts b/src/api/endpoints/posts/timeline.ts
index 19578e59b1..978825a109 100644
--- a/src/api/endpoints/posts/timeline.ts
+++ b/src/api/endpoints/posts/timeline.ts
@@ -16,22 +16,22 @@ import serialize from '../../serializers/post';
  * @param {any} app
  * @return {Promise<any>}
  */
-module.exports = (params, user, app) => new Promise(async (res, rej) => {
+module.exports = async (params, user, app) => {
 	// Get 'limit' parameter
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
-	if (limitErr) return rej('invalid limit param');
+	if (limitErr) throw 'invalid limit param';
 
 	// Get 'since_id' parameter
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) return rej('invalid since_id param');
+	if (sinceIdErr) throw 'invalid since_id param';
 
 	// Get 'max_id' parameter
 	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) return rej('invalid max_id param');
+	if (maxIdErr) throw 'invalid max_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');
+		throw 'cannot set since_id and max_id';
 	}
 
 	const { followingIds, watchChannelIds } = await rap({
@@ -92,5 +92,6 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
 		});
 
 	// Serialize
-	res(await Promise.all(timeline.map(post => serialize(post, user))));
-});
+	const _timeline = await Promise.all(timeline.map(post => serialize(post, user)))
+	return _timeline
+};

From b50813649afed671b75189551342b179d8cd60f7 Mon Sep 17 00:00:00 2001
From: otofune <otofune@gmail.com>
Date: Mon, 6 Nov 2017 15:58:39 +0900
Subject: [PATCH 353/364] serializers - posts: fix awaiting

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

diff --git a/src/api/serializers/post.ts b/src/api/serializers/post.ts
index d1dcb66002..5788b226f4 100644
--- a/src/api/serializers/post.ts
+++ b/src/api/serializers/post.ts
@@ -94,7 +94,7 @@ const self = async (
 	if (opts.detail) {
 		// Get previous post info
 		_post.prev = (async () => {
-			const prev = Post.findOne({
+			const prev = await Post.findOne({
 				user_id: _post.user_id,
 				_id: {
 					$lt: id

From 5279d062df205514f1f3cf95e3aab4fee425a3e4 Mon Sep 17 00:00:00 2001
From: otofune <otofune@gmail.com>
Date: Mon, 6 Nov 2017 16:09:51 +0900
Subject: [PATCH 354/364] fix

---
 src/api/endpoints/drive/files.ts        | 18 +++++++++---------
 src/api/endpoints/drive/files/show.ts   | 14 ++++++++------
 src/api/endpoints/drive/folders/find.ts |  3 +--
 src/api/serializers/drive-file.ts       |  2 ++
 4 files changed, 20 insertions(+), 17 deletions(-)

diff --git a/src/api/endpoints/drive/files.ts b/src/api/endpoints/drive/files.ts
index eb0bfe6ba5..41687c4993 100644
--- a/src/api/endpoints/drive/files.ts
+++ b/src/api/endpoints/drive/files.ts
@@ -13,27 +13,27 @@ import serialize from '../../serializers/drive-file';
  * @param {any} app
  * @return {Promise<any>}
  */
-module.exports = (params, user, app) => new Promise(async (res, rej) => {
+module.exports = async (params, user, app) => {
 	// Get 'limit' parameter
 	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
-	if (limitErr) return rej('invalid limit param');
+	if (limitErr) throw 'invalid limit param';
 
 	// Get 'since_id' parameter
 	const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
-	if (sinceIdErr) return rej('invalid since_id param');
+	if (sinceIdErr) throw 'invalid since_id param';
 
 	// Get 'max_id' parameter
 	const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
-	if (maxIdErr) return rej('invalid max_id param');
+	if (maxIdErr) throw 'invalid max_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');
+		throw 'cannot set since_id and max_id';
 	}
 
 	// Get 'folder_id' parameter
 	const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
-	if (folderIdErr) return rej('invalid folder_id param');
+	if (folderIdErr) throw 'invalid folder_id param';
 
 	// Construct query
 	const sort = {
@@ -64,6 +64,6 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
 		});
 
 	// Serialize
-	res(await Promise.all(files.map(async file =>
-		await serialize(file))));
-});
+	const _files = await Promise.all(files.map(file => serialize(file)));
+	return _files
+};
diff --git a/src/api/endpoints/drive/files/show.ts b/src/api/endpoints/drive/files/show.ts
index 9135a04c57..8830346008 100644
--- a/src/api/endpoints/drive/files/show.ts
+++ b/src/api/endpoints/drive/files/show.ts
@@ -12,10 +12,10 @@ import serialize from '../../../serializers/drive-file';
  * @param {any} user
  * @return {Promise<any>}
  */
-module.exports = (params, user) => new Promise(async (res, rej) => {
+module.exports = async (params, user) => {
 	// Get 'file_id' parameter
 	const [fileId, fileIdErr] = $(params.file_id).id().$;
-	if (fileIdErr) return rej('invalid file_id param');
+	if (fileIdErr) throw 'invalid file_id param';
 
 	// Fetch file
 	const file = await DriveFile
@@ -27,11 +27,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		});
 
 	if (file === null) {
-		return rej('file-not-found');
+		throw 'file-not-found';
 	}
 
 	// Serialize
-	res(await serialize(file, {
+	const _file = await serialize(file, {
 		detail: true
-	}));
-});
+	});
+
+	return _file
+};
diff --git a/src/api/endpoints/drive/folders/find.ts b/src/api/endpoints/drive/folders/find.ts
index cdf055839a..a5eb8e015d 100644
--- a/src/api/endpoints/drive/folders/find.ts
+++ b/src/api/endpoints/drive/folders/find.ts
@@ -30,6 +30,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		});
 
 	// Serialize
-	res(await Promise.all(folders.map(async folder =>
-		await serialize(folder))));
+	res(await Promise.all(folders.map(folder => serialize(folder))));
 });
diff --git a/src/api/serializers/drive-file.ts b/src/api/serializers/drive-file.ts
index f98cdaa599..9858c3b3c7 100644
--- a/src/api/serializers/drive-file.ts
+++ b/src/api/serializers/drive-file.ts
@@ -25,6 +25,8 @@ export default (
 		detail: false
 	}, options);
 
+	if (!file) return reject('invalid file arg.')
+
 	let _file: any;
 
 	// Populate the file if 'file' is ID

From b266ed3e4f98ab16d95e52cff517d6519b78742a Mon Sep 17 00:00:00 2001
From: otofune <otofune@gmail.com>
Date: Mon, 6 Nov 2017 16:11:24 +0900
Subject: [PATCH 355/364] fix

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

diff --git a/src/api/serializers/drive-file.ts b/src/api/serializers/drive-file.ts
index 9858c3b3c7..e749f80387 100644
--- a/src/api/serializers/drive-file.ts
+++ b/src/api/serializers/drive-file.ts
@@ -25,8 +25,6 @@ export default (
 		detail: false
 	}, options);
 
-	if (!file) return reject('invalid file arg.')
-
 	let _file: any;
 
 	// Populate the file if 'file' is ID
@@ -42,6 +40,8 @@ export default (
 		_file = deepcopy(file);
 	}
 
+	if (!_file) return reject('invalid file arg.')
+
 	// rendered target
 	let _target: any = {};
 

From 64be0d6deddef4b8caced377dc22f94425cc4358 Mon Sep 17 00:00:00 2001
From: otofune <otofune@gmail.com>
Date: Mon, 6 Nov 2017 16:22:18 +0900
Subject: [PATCH 356/364] =?UTF-8?q?MongoDB=E3=81=AE=E9=9A=8E=E5=B1=A4?=
 =?UTF-8?q?=E6=A7=8B=E9=80=A0=E6=A4=9C=E7=B4=A2=E3=81=AB=E9=96=A2=E3=81=99?=
 =?UTF-8?q?=E3=82=8B=E6=80=9D=E3=81=84=E9=81=95=E3=81=84=E3=81=AE=E4=BF=AE?=
 =?UTF-8?q?=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/endpoints/drive.ts                    |  2 +-
 src/api/endpoints/drive/files.ts              |  6 ++----
 src/api/endpoints/drive/files/find.ts         |  8 +++-----
 src/api/endpoints/drive/files/show.ts         |  4 +---
 src/api/endpoints/drive/files/update.ts       | 19 +++++--------------
 .../endpoints/messaging/messages/create.ts    |  4 +---
 src/api/endpoints/posts/create.ts             |  4 +---
 src/api/serializers/drive-folder.ts           |  4 +---
 8 files changed, 15 insertions(+), 36 deletions(-)

diff --git a/src/api/endpoints/drive.ts b/src/api/endpoints/drive.ts
index b9c4e3e506..d92473633a 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.user_id': user._id } },
 			{
 				$project: {
 					length: true
diff --git a/src/api/endpoints/drive/files.ts b/src/api/endpoints/drive/files.ts
index 41687c4993..035916b309 100644
--- a/src/api/endpoints/drive/files.ts
+++ b/src/api/endpoints/drive/files.ts
@@ -40,10 +40,8 @@ module.exports = async (params, user, app) => {
 		_id: -1
 	};
 	const query = {
-		metadata: {
-			user_id: user._id,
-			folder_id: folderId
-		}
+		'metadata.user_id': user._id,
+		'metadata.folder_id': folderId
 	} as any;
 	if (sinceId) {
 		sort._id = 1;
diff --git a/src/api/endpoints/drive/files/find.ts b/src/api/endpoints/drive/files/find.ts
index 255faf94ec..1c818131d7 100644
--- a/src/api/endpoints/drive/files/find.ts
+++ b/src/api/endpoints/drive/files/find.ts
@@ -24,11 +24,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Issue query
 	const files = await DriveFile
 		.find({
-			metadata: {
-				name: name,
-				user_id: user._id,
-				folder_id: folderId
-			}
+			'metadata.name': name,
+			'metadata.user_id': user._id,
+			'metadata.folder_id': folderId
 		});
 
 	// Serialize
diff --git a/src/api/endpoints/drive/files/show.ts b/src/api/endpoints/drive/files/show.ts
index 8830346008..0a19b19939 100644
--- a/src/api/endpoints/drive/files/show.ts
+++ b/src/api/endpoints/drive/files/show.ts
@@ -21,9 +21,7 @@ module.exports = async (params, user) => {
 	const file = await DriveFile
 		.findOne({
 			_id: fileId,
-			metadata: {
-				user_id: user._id
-			}
+			'metadata.user_id': user._id
 		});
 
 	if (file === null) {
diff --git a/src/api/endpoints/drive/files/update.ts b/src/api/endpoints/drive/files/update.ts
index c4d2673688..7a6d2562fb 100644
--- a/src/api/endpoints/drive/files/update.ts
+++ b/src/api/endpoints/drive/files/update.ts
@@ -20,19 +20,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [fileId, fileIdErr] = $(params.file_id).id().$;
 	if (fileIdErr) return rej('invalid file_id param');
 
-	console.dir(user)
 
 	// Fetch file
 	const file = await DriveFile
 		.findOne({
 			_id: fileId,
-			metadata: {
-				user_id: user._id
-			}
+			'metadata.user_id': user._id
 		});
 
-	console.dir(file)
-
 	if (file === null) {
 		return rej('file-not-found');
 	}
@@ -42,7 +37,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Get 'name' parameter
 	const [name, nameErr] = $(params.name).optional.string().pipe(validateFileName).$;
 	if (nameErr) return rej('invalid name param');
-	if (name) updateQuery.name = name;
+	if (name) updateQuery['metadata.name'] = name;
 
 	// Get 'folder_id' parameter
 	const [folderId, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
@@ -50,7 +45,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	if (folderId !== undefined) {
 		if (folderId === null) {
-			updateQuery.folder_id = null;
+			updateQuery['metadata.folder_id'] = null;
 		} else {
 			// Fetch folder
 			const folder = await DriveFolder
@@ -63,21 +58,17 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 				return rej('folder-not-found');
 			}
 
-			updateQuery.folder_id = folder._id;
+			updateQuery['metadata.folder_id'] = folder._id;
 		}
 	}
 
 	const updated = await DriveFile.update(file._id, {
-		$set: { metadata: updateQuery }
+		$set: { updateQuery }
 	});
 
-	console.dir(updated)
-
 	// Serialize
 	const fileObj = await serialize(updated);
 
-	console.dir(fileObj)
-
 	// Response
 	res(fileObj);
 
diff --git a/src/api/endpoints/messaging/messages/create.ts b/src/api/endpoints/messaging/messages/create.ts
index 1d186268fb..149852c093 100644
--- a/src/api/endpoints/messaging/messages/create.ts
+++ b/src/api/endpoints/messaging/messages/create.ts
@@ -54,9 +54,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	if (fileId !== undefined) {
 		file = await DriveFile.findOne({
 			_id: fileId,
-			metadata: {
-				user_id: user._id
-			}
+			'metadata.user_id': user._id
 		});
 
 		if (file === null) {
diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index 1507639776..4f4b7e2e83 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -44,9 +44,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.user_id': user._id
 			});
 
 			if (entity === null) {
diff --git a/src/api/serializers/drive-folder.ts b/src/api/serializers/drive-folder.ts
index 3b5f61aeed..6ebf454a28 100644
--- a/src/api/serializers/drive-folder.ts
+++ b/src/api/serializers/drive-folder.ts
@@ -44,9 +44,7 @@ const self = (
 		});
 
 		const childFilesCount = await DriveFile.count({
-			metadata: {
-				folder_id: _folder.id
-			}
+			'metadata.folder_id': _folder.id
 		});
 
 		_folder.folders_count = childFoldersCount;

From 4c5a4d259738ba617bf29d2158d180cc5fa8401c Mon Sep 17 00:00:00 2001
From: otofune <otofune@gmail.com>
Date: Mon, 6 Nov 2017 16:26:17 +0900
Subject: [PATCH 357/364] core - fix metadata searching

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

diff --git a/src/api/common/add-file-to-drive.ts b/src/api/common/add-file-to-drive.ts
index 376c470e93..1f882389ac 100644
--- a/src/api/common/add-file-to-drive.ts
+++ b/src/api/common/add-file-to-drive.ts
@@ -81,9 +81,7 @@ export default (
 		// Check if there is a file with the same hash
 		const much = await DriveFile.findOne({
 			md5: hash,
-			metadata: {
-				user_id: user._id
-			}
+			'metadata.user_id': user._id
 		});
 
 		if (much !== null) {
@@ -97,7 +95,7 @@ export default (
 	// Calculate drive usage
 	const usage = ((await DriveFile
 		.aggregate([
-			{ $match: { metadata: { user_id: user._id } } },
+			{ $match: { 'metadata.user_id': user._id } },
 			{ $project: {
 				length: true
 			}},

From 04648db1c235b0de14d3e0a2dc83f9346d0408f8 Mon Sep 17 00:00:00 2001
From: otofune <otofune@gmail.com>
Date: Mon, 6 Nov 2017 16:29:13 +0900
Subject: [PATCH 358/364] remove console

---
 src/api/common/add-file-to-drive.ts | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/api/common/add-file-to-drive.ts b/src/api/common/add-file-to-drive.ts
index 1f882389ac..dff2d52356 100644
--- a/src/api/common/add-file-to-drive.ts
+++ b/src/api/common/add-file-to-drive.ts
@@ -152,7 +152,6 @@ export default (
 		comment: comment,
 		properties: properties
 	});
-	console.dir(file)
 
 	log(`drive file has been created ${file._id}`);
 

From d5cc4cc9c28eb6a981ce37859def97cd7c57abc6 Mon Sep 17 00:00:00 2001
From: otofune <otofune@gmail.com>
Date: Mon, 6 Nov 2017 16:32:01 +0900
Subject: [PATCH 359/364] fix lint (automattic)

---
 src/api/common/add-file-to-drive.ts     | 18 ++++++-------
 src/api/endpoints/drive/files.ts        |  2 +-
 src/api/endpoints/drive/files/show.ts   |  2 +-
 src/api/endpoints/drive/files/update.ts |  3 +--
 src/api/endpoints/posts/timeline.ts     |  4 +--
 src/api/models/drive-file.ts            | 10 +++----
 src/api/serializers/drive-file.ts       |  4 +--
 src/db/mongodb.ts                       | 18 ++++++-------
 src/file/server.ts                      | 36 ++++++++++++-------------
 9 files changed, 48 insertions(+), 49 deletions(-)

diff --git a/src/api/common/add-file-to-drive.ts b/src/api/common/add-file-to-drive.ts
index dff2d52356..f9c22ccacd 100644
--- a/src/api/common/add-file-to-drive.ts
+++ b/src/api/common/add-file-to-drive.ts
@@ -14,16 +14,16 @@ import { Duplex } from 'stream';
 const log = debug('misskey:register-drive-file');
 
 const addToGridFS = (name, binary, metadata): Promise<any> => new Promise(async (resolve, reject) => {
-	const dataStream = new Duplex()
-	dataStream.push(binary)
-	dataStream.push(null)
+	const dataStream = new Duplex();
+	dataStream.push(binary);
+	dataStream.push(null);
 
-	const bucket = await getGridFSBucket()
-	const writeStream = bucket.openUploadStream(name, { metadata })
-	writeStream.once('finish', (doc) => { resolve(doc) })
-	writeStream.on('error', reject)
-	dataStream.pipe(writeStream)
-})
+	const bucket = await getGridFSBucket();
+	const writeStream = bucket.openUploadStream(name, { metadata });
+	writeStream.once('finish', (doc) => { resolve(doc); });
+	writeStream.on('error', reject);
+	dataStream.pipe(writeStream);
+});
 
 /**
  * Add file to drive
diff --git a/src/api/endpoints/drive/files.ts b/src/api/endpoints/drive/files.ts
index 035916b309..53b48a8bec 100644
--- a/src/api/endpoints/drive/files.ts
+++ b/src/api/endpoints/drive/files.ts
@@ -63,5 +63,5 @@ module.exports = async (params, user, app) => {
 
 	// Serialize
 	const _files = await Promise.all(files.map(file => serialize(file)));
-	return _files
+	return _files;
 };
diff --git a/src/api/endpoints/drive/files/show.ts b/src/api/endpoints/drive/files/show.ts
index 0a19b19939..3c7cf774f9 100644
--- a/src/api/endpoints/drive/files/show.ts
+++ b/src/api/endpoints/drive/files/show.ts
@@ -33,5 +33,5 @@ module.exports = async (params, user) => {
 		detail: true
 	});
 
-	return _file
+	return _file;
 };
diff --git a/src/api/endpoints/drive/files/update.ts b/src/api/endpoints/drive/files/update.ts
index 7a6d2562fb..4e56b30ace 100644
--- a/src/api/endpoints/drive/files/update.ts
+++ b/src/api/endpoints/drive/files/update.ts
@@ -20,7 +20,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	const [fileId, fileIdErr] = $(params.file_id).id().$;
 	if (fileIdErr) return rej('invalid file_id param');
 
-
 	// Fetch file
 	const file = await DriveFile
 		.findOne({
@@ -32,7 +31,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		return rej('file-not-found');
 	}
 
-	const updateQuery: any = {}
+	const updateQuery: any = {};
 
 	// Get 'name' parameter
 	const [name, nameErr] = $(params.name).optional.string().pipe(validateFileName).$;
diff --git a/src/api/endpoints/posts/timeline.ts b/src/api/endpoints/posts/timeline.ts
index 978825a109..203413e23a 100644
--- a/src/api/endpoints/posts/timeline.ts
+++ b/src/api/endpoints/posts/timeline.ts
@@ -92,6 +92,6 @@ module.exports = async (params, user, app) => {
 		});
 
 	// Serialize
-	const _timeline = await Promise.all(timeline.map(post => serialize(post, user)))
-	return _timeline
+	const _timeline = await Promise.all(timeline.map(post => serialize(post, user)));
+	return _timeline;
 };
diff --git a/src/api/models/drive-file.ts b/src/api/models/drive-file.ts
index 79a87f6572..8968d065cd 100644
--- a/src/api/models/drive-file.ts
+++ b/src/api/models/drive-file.ts
@@ -8,14 +8,14 @@ const collection = monkDb.get('drive_files.files');
 export default collection as any; // fuck type definition
 
 const getGridFSBucket = async (): Promise<mongodb.GridFSBucket> => {
-	const db = await nativeDbConn()
+	const db = await nativeDbConn();
 	const bucket = new mongodb.GridFSBucket(db, {
 		bucketName: 'drive_files'
-	})
-	return bucket
-}
+	});
+	return bucket;
+};
 
-export { getGridFSBucket }
+export { getGridFSBucket };
 
 export function validateFileName(name: string): boolean {
 	return (
diff --git a/src/api/serializers/drive-file.ts b/src/api/serializers/drive-file.ts
index e749f80387..2af7db5726 100644
--- a/src/api/serializers/drive-file.ts
+++ b/src/api/serializers/drive-file.ts
@@ -40,13 +40,13 @@ export default (
 		_file = deepcopy(file);
 	}
 
-	if (!_file) return reject('invalid file arg.')
+	if (!_file) return reject('invalid file arg.');
 
 	// rendered target
 	let _target: any = {};
 
 	_target.id = _file._id;
-	_target.created_at = _file.uploadDate
+	_target.created_at = _file.uploadDate;
 
 	_target = Object.assign(_target, _file.metadata);
 
diff --git a/src/db/mongodb.ts b/src/db/mongodb.ts
index 75f1a1d3c6..c978e6460f 100644
--- a/src/db/mongodb.ts
+++ b/src/db/mongodb.ts
@@ -16,7 +16,7 @@ export default db;
 /**
  * MongoDB native module (officialy)
  */
-import * as mongodb from 'mongodb'
+import * as mongodb from 'mongodb';
 
 let mdb: mongodb.Db;
 
@@ -25,14 +25,14 @@ const nativeDbConn = async (): Promise<mongodb.Db> => {
 
 	const db = await ((): Promise<mongodb.Db> => new Promise((resolve, reject) => {
 		mongodb.MongoClient.connect(uri, (e, db) => {
-			if (e) return reject(e)
-			resolve(db)
-		})
-	}))()
+			if (e) return reject(e);
+			resolve(db);
+		});
+	}))();
 
-	mdb = db
+	mdb = db;
 
-	return db
-}
+	return db;
+};
 
-export { nativeDbConn }
+export { nativeDbConn };
diff --git a/src/file/server.ts b/src/file/server.ts
index f38599b89c..375f29487d 100644
--- a/src/file/server.ts
+++ b/src/file/server.ts
@@ -97,7 +97,7 @@ app.get('/:id', async (req, res) => {
 		return;
 	}
 
-	const fileId = new mongodb.ObjectID(req.params.id)
+	const fileId = new mongodb.ObjectID(req.params.id);
 	const file = await DriveFile.findOne({ _id: fileId });
 
 	if (file == null) {
@@ -105,18 +105,18 @@ app.get('/:id', async (req, res) => {
 		return;
 	}
 
-	const bucket = await getGridFSBucket()
+	const bucket = await getGridFSBucket();
 
 	const buffer = await ((id): Promise<Buffer> => new Promise((resolve, reject) => {
-		const chunks = []
-		const readableStream = bucket.openDownloadStream(id)
-	  readableStream.on('data', chunk => {
+		const chunks = [];
+		const readableStream = bucket.openDownloadStream(id);
+	 readableStream.on('data', chunk => {
 			chunks.push(chunk);
-		})
+		});
 		readableStream.on('end', () => {
-			resolve(Buffer.concat(chunks))
-		})
-	}))(fileId)
+			resolve(Buffer.concat(chunks));
+		});
+	}))(fileId);
 
 	send(buffer, file.metadata.type, req, res);
 });
@@ -128,7 +128,7 @@ app.get('/:id/:name', async (req, res) => {
 		return;
 	}
 
-	const fileId = new mongodb.ObjectID(req.params.id)
+	const fileId = new mongodb.ObjectID(req.params.id);
 	const file = await DriveFile.findOne({ _id: fileId });
 
 	if (file == null) {
@@ -136,18 +136,18 @@ app.get('/:id/:name', async (req, res) => {
 		return;
 	}
 
-	const bucket = await getGridFSBucket()
+	const bucket = await getGridFSBucket();
 
 	const buffer = await ((id): Promise<Buffer> => new Promise((resolve, reject) => {
-		const chunks = []
-		const readableStream = bucket.openDownloadStream(id)
-	  readableStream.on('data', chunk => {
+		const chunks = [];
+		const readableStream = bucket.openDownloadStream(id);
+	 readableStream.on('data', chunk => {
 			chunks.push(chunk);
-		})
+		});
 		readableStream.on('end', () => {
-			resolve(Buffer.concat(chunks))
-		})
-	}))(fileId)
+			resolve(Buffer.concat(chunks));
+		});
+	}))(fileId);
 
 	send(buffer, file.metadata.type, req, res);
 });

From 3be69a8cb7bacca181fa400f234fd77c1d1d5bde Mon Sep 17 00:00:00 2001
From: otofune <otofune@gmail.com>
Date: Mon, 6 Nov 2017 16:49:07 +0900
Subject: [PATCH 360/364] /drive/files/update - return collectly value

---
 src/api/endpoints/drive/files/update.ts | 17 +++++++++--------
 1 file changed, 9 insertions(+), 8 deletions(-)

diff --git a/src/api/endpoints/drive/files/update.ts b/src/api/endpoints/drive/files/update.ts
index 4e56b30ace..d7b858c2ba 100644
--- a/src/api/endpoints/drive/files/update.ts
+++ b/src/api/endpoints/drive/files/update.ts
@@ -31,12 +31,10 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		return rej('file-not-found');
 	}
 
-	const updateQuery: any = {};
-
 	// Get 'name' parameter
 	const [name, nameErr] = $(params.name).optional.string().pipe(validateFileName).$;
 	if (nameErr) return rej('invalid name param');
-	if (name) updateQuery['metadata.name'] = name;
+	if (name) file.metadata.name = name;
 
 	// Get 'folder_id' parameter
 	const [folderId, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
@@ -44,7 +42,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	if (folderId !== undefined) {
 		if (folderId === null) {
-			updateQuery['metadata.folder_id'] = null;
+			file.metadata.folder_id = null;
 		} else {
 			// Fetch folder
 			const folder = await DriveFolder
@@ -57,16 +55,19 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 				return rej('folder-not-found');
 			}
 
-			updateQuery['metadata.folder_id'] = folder._id;
+			file.metadata.folder_id = folder._id;
 		}
 	}
 
-	const updated = await DriveFile.update(file._id, {
-		$set: { updateQuery }
+	await DriveFile.update(file._id, {
+		$set: {
+			'metadata.name': file.metadata.name,
+			'metadata.folder_id': file.metadata.folder_id
+		}
 	});
 
 	// Serialize
-	const fileObj = await serialize(updated);
+	const fileObj = await serialize(file);
 
 	// Response
 	res(fileObj);

From 73bb81de8f17fe603dfde57ae70aa61669161bfc Mon Sep 17 00:00:00 2001
From: otofune <otofune@gmail.com>
Date: Mon, 6 Nov 2017 16:59:09 +0900
Subject: [PATCH 361/364] update test for GridFS

---
 test/api.js | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/test/api.js b/test/api.js
index b43eb7ff62..c0da9d6c5b 100644
--- a/test/api.js
+++ b/test/api.js
@@ -1152,9 +1152,12 @@ async function insertHimawari(opts) {
 }
 
 async function insertDriveFile(opts) {
-	return await db.get('drive_files').insert(Object.assign({
-		name: 'strawberry-pasta.png'
-	}, opts));
+	return await db.get('drive_files.files').insert({
+		length: opts.datasize,
+		metadata: Object.assign({
+			name: 'strawberry-pasta.png'
+		}, opts)
+	});
 }
 
 async function insertDriveFolder(opts) {

From 26602dcd209198dead66081f54b1800627e0bff8 Mon Sep 17 00:00:00 2001
From: otofune <otofune@gmail.com>
Date: Mon, 6 Nov 2017 17:57:03 +0900
Subject: [PATCH 362/364] migration - add GridFS migration

---
 tools/migration/use-gridfs.js | 49 +++++++++++++++++++++++++++++++++++
 1 file changed, 49 insertions(+)
 create mode 100644 tools/migration/use-gridfs.js

diff --git a/tools/migration/use-gridfs.js b/tools/migration/use-gridfs.js
new file mode 100644
index 0000000000..d41514416c
--- /dev/null
+++ b/tools/migration/use-gridfs.js
@@ -0,0 +1,49 @@
+// 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 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.buffer
+	const created_at = doc.created_at
+
+	delete doc._id
+	delete doc.created_at
+	delete doc.datasize
+	delete doc.hash
+	delete doc.data
+
+	const bucket = await getGridFSBucket()
+	const added = await writeToGridFS(bucket, buffer, id, `${id}/${doc.name}`, { metadata: doc })
+
+	const result = await DriveFile.update(id, {
+		$set: {
+			uploadDate: created_at
+		}
+	})
+
+	return added && result.ok === 1
+}
+
+const main = async () => {
+	const docs = await db.get('drive_files').find()
+	const all = await Promise.all(docs.map(migrateToGridFS))
+	return all
+}
+
+main().then(console.dir).catch(console.error)

From c1fc3b9f6ec176999932958a7856d160317b7762 Mon Sep 17 00:00:00 2001
From: otofune <otofune@gmail.com>
Date: Mon, 6 Nov 2017 18:30:49 +0900
Subject: [PATCH 363/364] add safety guard to serializers & fix importing
 uncorrect serializer

---
 src/api/endpoints/drive/folders/update.ts | 2 +-
 src/api/serializers/post.ts               | 2 ++
 src/api/serializers/user.ts               | 2 ++
 3 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/api/endpoints/drive/folders/update.ts b/src/api/endpoints/drive/folders/update.ts
index eec2757878..4f2e3d2a7a 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-file';
+import serialize from '../../../serializers/drive-folder';
 import event from '../../../event';
 
 /**
diff --git a/src/api/serializers/post.ts b/src/api/serializers/post.ts
index 5788b226f4..5a63384f0e 100644
--- a/src/api/serializers/post.ts
+++ b/src/api/serializers/post.ts
@@ -57,6 +57,8 @@ const self = async (
 		_post = deepcopy(post);
 	}
 
+	if (!_post) throw 'invalid post arg.';	
+
 	const id = _post._id;
 
 	// Rename _id to id
diff --git a/src/api/serializers/user.ts b/src/api/serializers/user.ts
index d00f073897..0d24d6cc04 100644
--- a/src/api/serializers/user.ts
+++ b/src/api/serializers/user.ts
@@ -56,6 +56,8 @@ export default (
 		_user = deepcopy(user);
 	}
 
+	if (!_user) return reject('invalid user arg.');
+
 	// Me
 	const meId: mongo.ObjectID = me
 		? mongo.ObjectID.prototype.isPrototypeOf(me)

From d7e1ffb0055f0786a707015350a14351b8a0fbf0 Mon Sep 17 00:00:00 2001
From: otofune <otofune@gmail.com>
Date: Mon, 6 Nov 2017 18:38:59 +0900
Subject: [PATCH 364/364] remove whitespace

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

diff --git a/src/api/serializers/post.ts b/src/api/serializers/post.ts
index 5a63384f0e..03fd120772 100644
--- a/src/api/serializers/post.ts
+++ b/src/api/serializers/post.ts
@@ -57,7 +57,7 @@ const self = async (
 		_post = deepcopy(post);
 	}
 
-	if (!_post) throw 'invalid post arg.';	
+	if (!_post) throw 'invalid post arg.';
 
 	const id = _post._id;