From ba23c63a3824328cb87de03e98da6db6a7114673 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Tue, 15 Aug 2017 21:51:32 +0000
Subject: [PATCH 001/122] chore(package): update @types/js-yaml to version
 3.9.1

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

diff --git a/package.json b/package.json
index 875ffc3c66..1632eed245 100644
--- a/package.json
+++ b/package.json
@@ -46,7 +46,7 @@
     "@types/inquirer": "0.0.34",
     "@types/is-root": "1.0.0",
     "@types/is-url": "1.2.28",
-    "@types/js-yaml": "3.9.0",
+    "@types/js-yaml": "3.9.1",
     "@types/mocha": "2.2.41",
     "@types/mongodb": "2.2.9",
     "@types/monk": "1.0.5",

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 002/122] 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 e2b359399fb38d423490e1c418ce5f6c2e43ea84 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 21 Aug 2017 23:28:05 +0000
Subject: [PATCH 003/122] chore(package): update @types/rimraf to version 2.0.2

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

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

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 004/122] 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 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 005/122] 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 29edeb2dc08fdb7072e288177abe0aa26c4c2109 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Fri, 6 Oct 2017 03:32:55 +0000
Subject: [PATCH 006/122] chore(package): update mocha to version 4.0.1

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

diff --git a/package.json b/package.json
index b56f97c797..4dc11b02f2 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.3",
+    "mocha": "4.0.1",
     "riot-tag-loader": "1.0.0",
     "string-replace-webpack-plugin": "0.1.3",
     "style-loader": "0.19.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 007/122] 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 008/122] 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 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 009/122] 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 73a209359fad3cfdb6b96064bc228686a4f2d4fa Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Fri, 20 Oct 2017 19:19:57 +0000
Subject: [PATCH 010/122] chore(package): update tslint to version 5.8.0

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

diff --git a/package.json b/package.json
index dd1622a37b..09a8b7c889 100644
--- a/package.json
+++ b/package.json
@@ -88,7 +88,7 @@
     "stylus": "0.54.5",
     "stylus-loader": "3.0.1",
     "swagger-jsdoc": "1.9.7",
-    "tslint": "5.7.0",
+    "tslint": "5.8.0",
     "uglify-es": "3.0.27",
     "uglify-es-webpack-plugin": "0.10.0",
     "uglify-js": "git+https://github.com/mishoo/UglifyJS2.git#harmony",

From a39168b081eb4b977930e73c050801df97ea0845 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Mon, 23 Oct 2017 20:25:17 +0000
Subject: [PATCH 011/122] chore(package): update gulp-typescript to version
 3.2.3

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

diff --git a/package.json b/package.json
index 4ddb3cb451..aa98122de8 100644
--- a/package.json
+++ b/package.json
@@ -78,7 +78,7 @@
     "gulp-rename": "1.2.2",
     "gulp-replace": "0.6.1",
     "gulp-tslint": "8.1.2",
-    "gulp-typescript": "3.2.2",
+    "gulp-typescript": "3.2.3",
     "gulp-uglify": "3.0.0",
     "gulp-util": "3.0.8",
     "mocha": "3.5.3",

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 012/122] 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 013/122] 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 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 014/122] 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 513ed7d90122069ee850cc6d830e5476d7f0b042 Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Wed, 25 Oct 2017 01:05:05 +0000
Subject: [PATCH 015/122] chore(package): update @types/gulp-uglify to version
 3.0.3

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

diff --git a/package.json b/package.json
index 4ddb3cb451..96d3973071 100644
--- a/package.json
+++ b/package.json
@@ -41,7 +41,7 @@
     "@types/gulp-replace": "0.0.30",
     "@types/gulp-tslint": "3.6.31",
     "@types/gulp-typescript": "2.13.0",
-    "@types/gulp-uglify": "0.0.30",
+    "@types/gulp-uglify": "3.0.3",
     "@types/gulp-util": "3.0.31",
     "@types/inquirer": "0.0.34",
     "@types/is-root": "1.0.0",

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 016/122] 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 017/122] 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 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 018/122] 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 019/122] 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 020/122] 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 021/122] 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 753df86030e6c503850333a1c0d7c6bd5334603c Mon Sep 17 00:00:00 2001
From: "greenkeeper[bot]" <greenkeeper[bot]@users.noreply.github.com>
Date: Thu, 26 Oct 2017 15:08:12 +0000
Subject: [PATCH 022/122] chore(package): update @types/gulp-util to version
 3.0.33

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

diff --git a/package.json b/package.json
index 43a0159619..1da36f5be9 100644
--- a/package.json
+++ b/package.json
@@ -42,7 +42,7 @@
     "@types/gulp-tslint": "3.6.31",
     "@types/gulp-typescript": "2.13.0",
     "@types/gulp-uglify": "0.0.30",
-    "@types/gulp-util": "3.0.31",
+    "@types/gulp-util": "3.0.33",
     "@types/inquirer": "0.0.34",
     "@types/is-root": "1.0.0",
     "@types/is-url": "1.2.28",

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 023/122] 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 024/122] 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 025/122] 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 026/122] 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 027/122] 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 028/122] 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 029/122] 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 030/122] 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 031/122] =?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 032/122] 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 033/122] 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 034/122] 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 035/122] 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 036/122] 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 037/122] 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 038/122] 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 039/122] 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 040/122] 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 041/122] 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 042/122] 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 043/122] 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 044/122] 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 045/122] 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 046/122] 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 047/122] :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 048/122] 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 049/122] 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 050/122] 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 051/122] 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 052/122] 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 053/122] 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 054/122] :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 055/122] 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 056/122] 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 057/122] 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 058/122] 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 059/122] :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 060/122] 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 061/122] 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 062/122] :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 063/122] 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 064/122] :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 065/122] :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 066/122] :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 067/122] 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 068/122] 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 069/122] :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 070/122] 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 071/122] 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 072/122] 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 073/122] =?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 074/122] 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 075/122] 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 076/122] 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 077/122] :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 078/122] 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 079/122] 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 080/122] 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 081/122] 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 082/122] 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 083/122] :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 084/122] 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 085/122] :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 086/122] 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 087/122] 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 088/122] 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 089/122] 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 090/122] 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 091/122] 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 092/122] [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 0e8a4f36a24826909a2c0e864c4c131b56ea0e8a Mon Sep 17 00:00:00 2001
From: otofune <otofune@gmail.com>
Date: Sun, 5 Nov 2017 20:55:51 +0900
Subject: [PATCH 093/122] start to improve serializers


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 094/122] 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 095/122] 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 096/122] 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 097/122] 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 098/122] 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 099/122] 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 100/122] 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 101/122] 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 102/122] 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 103/122] 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 104/122] 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 105/122] 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 106/122] 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 107/122] 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 108/122] 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 109/122] 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 110/122] 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 111/122] 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 112/122] 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 113/122] 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 114/122] =?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 115/122] 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 116/122] 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 117/122] 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 118/122] /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 119/122] 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 120/122] 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 121/122] 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 122/122] 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;