From 5c38084af523b6f21fa915b8d442a9d3f6a24f8f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 17 May 2018 15:41:07 +0900
Subject: [PATCH 01/35] :v:

---
 gulpfile.ts                                          | 2 ++
 locales/index.ts                                     | 1 +
 src/client/app/boot.js                               | 2 +-
 src/client/app/desktop/views/components/settings.vue | 5 +++--
 4 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/gulpfile.ts b/gulpfile.ts
index a9ccbbdb5e..fa1155878c 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -20,6 +20,7 @@ import * as replace from 'gulp-replace';
 import * as htmlmin from 'gulp-htmlmin';
 const uglifyes = require('uglify-es');
 
+import locales from './locales';
 import { fa } from './src/build/fa';
 const client = require('./built/client/meta.json');
 import config from './src/config';
@@ -122,6 +123,7 @@ gulp.task('build:client:script', () =>
 		.pipe(replace('VERSION', JSON.stringify(client.version)))
 		.pipe(replace('API', JSON.stringify(config.api_url)))
 		.pipe(replace('ENV', JSON.stringify(env)))
+		.pipe(replace('LANGS', JSON.stringify(Object.keys(locales))))
 		.pipe(isProduction ? uglify({
 			toplevel: true
 		} as any) : gutil.noop())
diff --git a/locales/index.ts b/locales/index.ts
index 89d18190f6..319d178e0a 100644
--- a/locales/index.ts
+++ b/locales/index.ts
@@ -11,6 +11,7 @@ const loadLang = lang => yaml.safeLoad(
 const native = loadLang('ja');
 
 const langs = {
+	'de': loadLang('de'),
 	'en': loadLang('en'),
 	'fr': loadLang('fr'),
 	'ja': native,
diff --git a/src/client/app/boot.js b/src/client/app/boot.js
index 35d02cf9c5..9338bc501e 100644
--- a/src/client/app/boot.js
+++ b/src/client/app/boot.js
@@ -32,7 +32,7 @@
 	// Detect the user language
 	// Note: The default language is Japanese
 	let lang = navigator.language.split('-')[0];
-	if (!/^(en|ja)$/.test(lang)) lang = 'ja';
+	if (!LANGS.includes(lang)) lang = 'en';
 	if (localStorage.getItem('lang')) lang = localStorage.getItem('lang');
 
 	// Detect the user agent
diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue
index 9439ded2fc..9e13aba13a 100644
--- a/src/client/app/desktop/views/components/settings.vue
+++ b/src/client/app/desktop/views/components/settings.vue
@@ -80,10 +80,11 @@
 					<el-option label="自動" value=""/>
 				</el-option-group>
 				<el-option-group label="言語を指定">
-					<el-option label="ja-JP" value="ja"/>
-					<el-option label="en-US" value="en"/>
+					<el-option label="ja" value="ja"/>
+					<el-option label="en" value="en"/>
 					<el-option label="fr" value="fr"/>
 					<el-option label="pl" value="pl"/>
+					<el-option label="de" value="de"/>
 				</el-option-group>
 			</el-select>
 			<div class="none ui info">

From a1692ebc7cfa7b3c6943d552ae059261fa5d18d3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 17 May 2018 16:24:01 +0900
Subject: [PATCH 02/35] =?UTF-8?q?=E3=83=A2=E3=83=90=E3=82=A4=E3=83=AB?=
 =?UTF-8?q?=E7=89=88=E3=81=AE=E3=82=A6=E3=82=A3=E3=82=B8=E3=82=A7=E3=83=83?=
 =?UTF-8?q?=E3=83=88=E5=BE=A9=E6=B4=BB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../app/common/scripts/streaming/home.ts      | 11 ++++
 src/client/app/mobile/script.ts               |  2 +
 .../app/mobile/views/components/ui.nav.vue    |  1 +
 .../pages/{dashboard.vue => widgets.vue}      | 57 +++++++------------
 src/client/app/store.ts                       | 36 ++++++++++++
 5 files changed, 71 insertions(+), 36 deletions(-)
 rename src/client/app/mobile/views/pages/{dashboard.vue => widgets.vue} (77%)

diff --git a/src/client/app/common/scripts/streaming/home.ts b/src/client/app/common/scripts/streaming/home.ts
index 32685f3c2c..09d830bece 100644
--- a/src/client/app/common/scripts/streaming/home.ts
+++ b/src/client/app/common/scripts/streaming/home.ts
@@ -48,6 +48,17 @@ export class HomeStream extends Stream {
 			}
 		});
 
+		this.on('mobile_home_updated', x => {
+			if (x.home) {
+				os.store.commit('settings/setMobileHome', x.home);
+			} else {
+				os.store.commit('settings/setMobileHomeWidget', {
+					id: x.id,
+					data: x.data
+				});
+			}
+		});
+
 		// トークンが再生成されたとき
 		// このままではMisskeyが利用できないので強制的にサインアウトさせる
 		this.on('my_token_regenerated', () => {
diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts
index 2e9805e0d0..1405139be6 100644
--- a/src/client/app/mobile/script.ts
+++ b/src/client/app/mobile/script.ts
@@ -23,6 +23,7 @@ import MkUser from './views/pages/user.vue';
 import MkSelectDrive from './views/pages/selectdrive.vue';
 import MkDrive from './views/pages/drive.vue';
 import MkNotifications from './views/pages/notifications.vue';
+import MkWidgets from './views/pages/widgets.vue';
 import MkMessaging from './views/pages/messaging.vue';
 import MkMessagingRoom from './views/pages/messaging-room.vue';
 import MkNote from './views/pages/note.vue';
@@ -56,6 +57,7 @@ init((launch) => {
 			{ path: '/i/settings', component: MkSettings },
 			{ path: '/i/settings/profile', component: MkProfileSetting },
 			{ path: '/i/notifications', name: 'notifications', component: MkNotifications },
+			{ path: '/i/widgets', name: 'widgets', component: MkWidgets },
 			{ path: '/i/messaging', name: 'messaging', component: MkMessaging },
 			{ path: '/i/messaging/:user', component: MkMessagingRoom },
 			{ path: '/i/drive', name: 'drive', component: MkDrive },
diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue
index 5c65d52237..ec42dbc99d 100644
--- a/src/client/app/mobile/views/components/ui.nav.vue
+++ b/src/client/app/mobile/views/components/ui.nav.vue
@@ -21,6 +21,7 @@
 					<li><router-link to="/othello" :data-active="$route.name == 'othello'">%fa:gamepad%ゲーム<template v-if="hasGameInvitations">%fa:circle%</template>%fa:angle-right%</router-link></li>
 				</ul>
 				<ul>
+					<li><router-link to="/i/widgets" :data-active="$route.name == 'widgets'">%fa:quidditch%%i18n:@widgets%%fa:angle-right%</router-link></li>
 					<li><router-link to="/i/drive" :data-active="$route.name == 'drive'">%fa:cloud%%i18n:@drive%%fa:angle-right%</router-link></li>
 				</ul>
 				<ul>
diff --git a/src/client/app/mobile/views/pages/dashboard.vue b/src/client/app/mobile/views/pages/widgets.vue
similarity index 77%
rename from src/client/app/mobile/views/pages/dashboard.vue
rename to src/client/app/mobile/views/pages/widgets.vue
index a5ca6cb4a2..338a5288bb 100644
--- a/src/client/app/mobile/views/pages/dashboard.vue
+++ b/src/client/app/mobile/views/pages/widgets.vue
@@ -40,7 +40,7 @@
 			</x-draggable>
 		</template>
 		<template v-else>
-			<component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" :is-mobile="true" @chosen="warp"/>
+			<component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" :is-mobile="true"/>
 		</template>
 	</main>
 </mk-ui>
@@ -55,17 +55,24 @@ export default Vue.extend({
 	components: {
 		XDraggable
 	},
+
 	data() {
 		return {
 			showNav: false,
-			widgets: [],
 			customizing: false,
 			widgetAdderSelected: null
 		};
 	},
+
+	computed: {
+		widgets(): any[] {
+			return this.$store.state.settings.data.mobileHome;
+		}
+	},
+
 	created() {
-		if ((this as any).clientSettings.mobileHome == null) {
-			Vue.set((this as any).clientSettings, 'mobileHome', [{
+		if (this.widgets.length == 0) {
+			this.widgets = [{
 				name: 'calendar',
 				id: 'a', data: {}
 			}, {
@@ -86,18 +93,9 @@ export default Vue.extend({
 			}, {
 				name: 'version',
 				id: 'g', data: {}
-			}]);
-			this.widgets = (this as any).clientSettings.mobileHome;
+			}];
 			this.saveHome();
-		} else {
-			this.widgets = (this as any).clientSettings.mobileHome;
 		}
-
-		this.$watch('clientSettings', i => {
-			this.widgets = (this as any).clientSettings.mobileHome;
-		}, {
-			deep: true
-		});
 	},
 
 	mounted() {
@@ -105,46 +103,33 @@ export default Vue.extend({
 	},
 
 	methods: {
-		onHomeUpdated(data) {
-			if (data.home) {
-				(this as any).clientSettings.mobileHome = data.home;
-				this.widgets = data.home;
-			} else {
-				const w = (this as any).clientSettings.mobileHome.find(w => w.id == data.id);
-				if (w != null) {
-					w.data = data.data;
-					this.$refs[w.id][0].preventSave = true;
-					this.$refs[w.id][0].props = w.data;
-					this.widgets = (this as any).clientSettings.mobileHome;
-				}
-			}
-		},
 		hint() {
 			alert('ウィジェットを追加/削除したり並べ替えたりできます。ウィジェットを移動するには「三」をドラッグします。ウィジェットを削除するには「x」をタップします。いくつかのウィジェットはタップすることで表示を変更できます。');
 		},
+
 		widgetFunc(id) {
 			const w = this.$refs[id][0];
 			if (w.func) w.func();
 		},
+
 		onWidgetSort() {
 			this.saveHome();
 		},
+
 		addWidget() {
-			const widget = {
+			this.$store.dispatch('settings/addMobileHomeWidget', {
 				name: this.widgetAdderSelected,
 				id: uuid(),
 				data: {}
-			};
+			});
+		},
 
-			this.widgets.unshift(widget);
-			this.saveHome();
-		},
 		removeWidget(widget) {
-			this.widgets = this.widgets.filter(w => w.id != widget.id);
-			this.saveHome();
+			this.$store.dispatch('settings/removeMobileHomeWidget', widget);
 		},
+
 		saveHome() {
-			(this as any).clientSettings.mobileHome = this.widgets;
+			this.$store.commit('settings/setMobileHome', this.widgets);
 			(this as any).api('i/update_mobile_home', {
 				home: this.widgets
 			});
diff --git a/src/client/app/store.ts b/src/client/app/store.ts
index 0bdfdef6a0..e9cd952bde 100644
--- a/src/client/app/store.ts
+++ b/src/client/app/store.ts
@@ -3,6 +3,7 @@ import MiOS from './mios';
 
 const defaultSettings = {
 	home: [],
+	mobileHome: [],
 	fetchOnScroll: true,
 	showMaps: true,
 	showPostFormOnTopOfTl: false,
@@ -58,6 +59,25 @@ export default (os: MiOS) => new Vuex.Store({
 
 				addHomeWidget(state, widget) {
 					state.data.home.unshift(widget);
+				},
+
+				setMobileHome(state, data) {
+					state.data.mobileHome = data;
+				},
+
+				setMobileHomeWidget(state, x) {
+					const w = state.data.mobileHome.find(w => w.id == x.id);
+					if (w) {
+						w.data = x.data;
+					}
+				},
+
+				addMobileHomeWidget(state, widget) {
+					state.data.mobileHome.unshift(widget);
+				},
+
+				removeMobileHomeWidget(state, widget) {
+					state.data.mobileHome = state.data.mobileHome.filter(w => w.id != widget.id);
 				}
 			},
 
@@ -85,6 +105,22 @@ export default (os: MiOS) => new Vuex.Store({
 					os.api('i/update_home', {
 						home: ctx.state.data.home
 					});
+				},
+
+				addMobileHomeWidget(ctx, widget) {
+					ctx.commit('addMobileHomeWidget', widget);
+
+					os.api('i/update_mobile_home', {
+						home: ctx.state.data.mobileHome
+					});
+				},
+
+				removeMobileHomeWidget(ctx, widget) {
+					ctx.commit('removeMobileHomeWidget', widget);
+
+					os.api('i/update_mobile_home', {
+						home: ctx.state.data.mobileHome.filter(w => w.id != widget.id)
+					});
 				}
 			}
 		}

From cf0351225ff393cd61718cf27ff7e54682bf36ea Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 17 May 2018 17:01:07 +0900
Subject: [PATCH 03/35] Update dependencies :rocket:

---
 package.json | 52 ++++++++++++++++++++++++++--------------------------
 1 file changed, 26 insertions(+), 26 deletions(-)

diff --git a/package.json b/package.json
index 544dda6036..6edc7c5be9 100644
--- a/package.json
+++ b/package.json
@@ -35,9 +35,9 @@
 		"@types/chai-http": "3.0.4",
 		"@types/debug": "0.0.30",
 		"@types/deep-equal": "1.0.1",
-		"@types/elasticsearch": "5.0.22",
+		"@types/elasticsearch": "5.0.23",
 		"@types/eventemitter3": "2.0.2",
-		"@types/gm": "1.17.33",
+		"@types/gm": "1.18.0",
 		"@types/gulp": "3.8.36",
 		"@types/gulp-htmlmin": "1.3.32",
 		"@types/gulp-mocha": "0.0.32",
@@ -64,10 +64,10 @@
 		"@types/license-checker": "15.0.0",
 		"@types/mkdirp": "0.5.2",
 		"@types/mocha": "5.2.0",
-		"@types/mongodb": "3.0.15",
+		"@types/mongodb": "3.0.18",
 		"@types/monk": "6.0.0",
 		"@types/ms": "0.7.30",
-		"@types/node": "9.6.6",
+		"@types/node": "10.1.0",
 		"@types/nopt": "3.0.29",
 		"@types/parse5": "^3.0.0",
 		"@types/pug": "2.0.4",
@@ -82,12 +82,12 @@
 		"@types/speakeasy": "2.0.2",
 		"@types/tmp": "0.0.33",
 		"@types/uuid": "3.4.3",
-		"@types/webpack": "4.1.4",
+		"@types/webpack": "4.1.7",
 		"@types/webpack-stream": "3.2.10",
-		"@types/websocket": "0.0.38",
-		"@types/ws": "4.0.2",
+		"@types/websocket": "0.0.39",
+		"@types/ws": "5.1.1",
 		"animejs": "2.2.0",
-		"autosize": "4.0.1",
+		"autosize": "4.0.2",
 		"autwh": "0.1.0",
 		"bcryptjs": "2.4.3",
 		"bootstrap-vue": "2.0.0-rc.6",
@@ -101,9 +101,9 @@
 		"deep-equal": "1.0.1",
 		"deepcopy": "0.6.3",
 		"diskusage": "0.2.4",
-		"dompurify": "1.0.3",
+		"dompurify": "1.0.4",
 		"elasticsearch": "14.2.2",
-		"element-ui": "2.3.6",
+		"element-ui": "2.3.8",
 		"emojilib": "2.2.12",
 		"escape-regexp": "0.0.1",
 		"eslint": "4.19.1",
@@ -111,7 +111,7 @@
 		"eventemitter3": "3.1.0",
 		"exif-js": "2.3.0",
 		"file-loader": "1.1.11",
-		"file-type": "7.6.0",
+		"file-type": "8.0.0",
 		"fuckadblock": "3.2.1",
 		"gm": "1.23.1",
 		"gulp": "3.9.1",
@@ -120,15 +120,15 @@
 		"gulp-imagemin": "4.1.0",
 		"gulp-mocha": "5.0.0",
 		"gulp-pug": "4.0.1",
-		"gulp-rename": "1.2.2",
-		"gulp-replace": "0.6.1",
+		"gulp-rename": "1.2.3",
+		"gulp-replace": "1.0.0",
 		"gulp-sourcemaps": "2.6.4",
 		"gulp-stylus": "2.7.0",
 		"gulp-tslint": "8.1.3",
 		"gulp-typescript": "4.0.2",
 		"gulp-uglify": "3.0.0",
 		"gulp-util": "3.0.8",
-		"hard-source-webpack-plugin": "0.6.4",
+		"hard-source-webpack-plugin": "0.6.7",
 		"highlight.js": "9.12.0",
 		"html-minifier": "3.5.15",
 		"http-signature": "1.2.0",
@@ -136,7 +136,7 @@
 		"is-root": "2.0.0",
 		"is-url": "1.2.4",
 		"js-yaml": "3.11.0",
-		"jsdom": "11.9.0",
+		"jsdom": "11.10.0",
 		"koa": "2.5.1",
 		"koa-bodyparser": "4.2.0",
 		"koa-compress": "3.0.0",
@@ -150,14 +150,14 @@
 		"koa-slow": "2.1.0",
 		"koa-views": "^6.1.4",
 		"kue": "0.11.6",
-		"license-checker": "18.0.0",
+		"license-checker": "19.0.0",
 		"loader-utils": "1.1.0",
 		"mecab-async": "0.1.2",
 		"mkdirp": "0.5.1",
 		"mocha": "5.1.1",
 		"moji": "0.5.1",
-		"mongodb": "3.0.7",
-		"monk": "6.0.5",
+		"mongodb": "3.0.8",
+		"monk": "6.0.6",
 		"ms": "2.1.1",
 		"nan": "2.10.0",
 		"node-sass": "4.9.0",
@@ -178,7 +178,7 @@
 		"recaptcha-promise": "0.1.3",
 		"reconnecting-websocket": "3.2.2",
 		"redis": "2.8.0",
-		"request": "2.85.0",
+		"request": "2.86.0",
 		"request-promise-native": "1.0.5",
 		"rimraf": "2.6.2",
 		"rndstr": "1.0.0",
@@ -196,9 +196,9 @@
 		"tcp-port-used": "0.1.2",
 		"textarea-caret": "3.1.0",
 		"tmp": "0.0.33",
-		"ts-loader": "4.2.0",
-		"ts-node": "6.0.1",
-		"tslint": "5.9.1",
+		"ts-loader": "4.3.0",
+		"ts-node": "6.0.3",
+		"tslint": "5.10.0",
 		"typescript": "2.8.3",
 		"typescript-eslint-parser": "15.0.0",
 		"uglify-es": "3.3.9",
@@ -209,15 +209,15 @@
 		"vue-cropperjs": "2.2.0",
 		"vue-js-modal": "1.3.13",
 		"vue-json-tree-view": "2.1.4",
-		"vue-loader": "15.0.3",
+		"vue-loader": "15.0.11",
 		"vue-router": "3.0.1",
 		"vue-template-compiler": "2.5.16",
 		"vuedraggable": "2.16.0",
 		"vuex": "3.0.1",
-		"web-push": "3.3.0",
+		"web-push": "3.3.1",
 		"webfinger.js": "2.6.6",
-		"webpack": "4.6.0",
-		"webpack-cli": "2.0.15",
+		"webpack": "4.8.3",
+		"webpack-cli": "2.1.3",
 		"webpack-replace-loader": "1.3.0",
 		"websocket": "1.0.26",
 		"ws": "5.1.1",

From d253df4574a899b24ec34cbcf73096ab3faac7e1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 17 May 2018 18:18:45 +0900
Subject: [PATCH 04/35] :v:

---
 package.json               | 2 +-
 src/build/fa.ts            | 5 +----
 webpack.config.ts          | 4 ++--
 webpack/loaders/replace.js | 8 ++++----
 4 files changed, 8 insertions(+), 11 deletions(-)

diff --git a/package.json b/package.json
index 6edc7c5be9..0d9646acce 100644
--- a/package.json
+++ b/package.json
@@ -209,7 +209,7 @@
 		"vue-cropperjs": "2.2.0",
 		"vue-js-modal": "1.3.13",
 		"vue-json-tree-view": "2.1.4",
-		"vue-loader": "15.0.11",
+		"vue-loader": "15.0.5",
 		"vue-router": "3.0.1",
 		"vue-template-compiler": "2.5.16",
 		"vuedraggable": "2.16.0",
diff --git a/src/build/fa.ts b/src/build/fa.ts
index f6f2427d0a..111c19ae66 100644
--- a/src/build/fa.ts
+++ b/src/build/fa.ts
@@ -7,10 +7,7 @@ import * as regular from '@fortawesome/fontawesome-free-regular';
 import * as solid from '@fortawesome/fontawesome-free-solid';
 import * as brands from '@fortawesome/fontawesome-free-brands';
 
-// Add icons
-fontawesome.library.add(regular);
-fontawesome.library.add(solid);
-fontawesome.library.add(brands);
+fontawesome.library.add(regular, solid, brands);
 
 export const pattern = /%fa:(.+?)%/g;
 
diff --git a/webpack.config.ts b/webpack.config.ts
index b2f67c914f..d56ed23972 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -110,14 +110,14 @@ const plugins = [
 		//#region i18n
 		langs.forEach(lang => {
 			Object.keys(entry).forEach(file => {
-				let src = fs.readFileSync(`${__dirname}/built/client/assets/${file}.${version}.-.${isProduction ? 'min' : 'raw'}.js`, 'utf8');
+				let src = fs.readFileSync(`${__dirname}/built/client/assets/${file}.${version}.-.${isProduction ? 'min' : 'raw'}.js`, 'utf-8');
 
 				const i18nReplacer = new I18nReplacer(lang);
 
 				src = src.replace(i18nReplacer.pattern, i18nReplacer.replacement);
 				src = src.replace('%lang%', lang);
 
-				fs.writeFileSync(`${__dirname}/built/client/assets/${file}.${version}.${lang}.${isProduction ? 'min' : 'raw'}.js`, src, 'utf8');
+				fs.writeFileSync(`${__dirname}/built/client/assets/${file}.${version}.${lang}.${isProduction ? 'min' : 'raw'}.js`, src, 'utf-8');
 			});
 		});
 		//#endregion
diff --git a/webpack/loaders/replace.js b/webpack/loaders/replace.js
index 0326dcdab3..d8a81c245a 100644
--- a/webpack/loaders/replace.js
+++ b/webpack/loaders/replace.js
@@ -1,12 +1,12 @@
-const loaderUtils = require('loader-utils');
+import { getOptions } from 'loader-utils';
 
 function trim(text, g) {
 	return text.substring(1, text.length - (g ? 2 : 0));
 }
 
-module.exports = function(src) {
+export default function(src) {
 	this.cacheable();
-	const options = loaderUtils.getOptions(this);
+	const options = getOptions(this);
 	const search = options.search;
 	const g = search[search.length - 1] == 'g';
 	const file = this.resourcePath.replace(/\\/g, '/');
@@ -19,4 +19,4 @@ module.exports = function(src) {
 	src = src.replace(new RegExp(trim(search, g), g ? 'g' : ''), replace);
 	this.callback(null, src);
 	return src;
-};
+}

From b2368b04db322a890029561816fd3dcac4aef1d8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 17 May 2018 18:33:56 +0900
Subject: [PATCH 05/35] 2.7.0

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

diff --git a/package.json b/package.json
index 0d9646acce..724db93a7e 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "2.6.2",
-	"clientVersion": "1.0.5260",
+	"version": "2.7.0",
+	"clientVersion": "1.0.5345",
 	"codename": "nighthike",
 	"main": "./built/index.js",
 	"private": true,

From 90a4fe471d06a4a0b598d947caff2ddd69d1ed5b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 17 May 2018 19:38:20 +0900
Subject: [PATCH 06/35] :v:

---
 package.json               |  2 +-
 webpack.config.ts          | 35 ++++++++++++++---------------------
 webpack/loaders/replace.js | 28 ++++++++++++++++++----------
 3 files changed, 33 insertions(+), 32 deletions(-)

diff --git a/package.json b/package.json
index 724db93a7e..356ac1f517 100644
--- a/package.json
+++ b/package.json
@@ -209,7 +209,7 @@
 		"vue-cropperjs": "2.2.0",
 		"vue-js-modal": "1.3.13",
 		"vue-json-tree-view": "2.1.4",
-		"vue-loader": "15.0.5",
+		"vue-loader": "15.0.11",
 		"vue-router": "3.0.1",
 		"vue-template-compiler": "2.5.16",
 		"vuedraggable": "2.16.0",
diff --git a/webpack.config.ts b/webpack.config.ts
index d56ed23972..3aeecbd8a7 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -146,27 +146,20 @@ module.exports = {
 			}, {
 				loader: 'replace',
 				query: {
-					search: /%base64:(.+?)%/g.toString(),
-					replace: 'base64replacement'
-				}
-			}, {
-				loader: 'replace',
-				query: {
-					search: i18nPattern.toString(),
-					replace: 'i18nReplacement',
-					i18n: true
-				}
-			}, {
-				loader: 'replace',
-				query: {
-					search: faPattern.toString(),
-					replace: 'faReplacement'
-				}
-			}, {
-				loader: 'replace',
-				query: {
-					search: /^<template>([\s\S]+?)\r?\n<\/template>/.toString(),
-					replace: 'collapseSpacesReplacement'
+					qs: [{
+						search: /%base64:(.+?)%/g.toString(),
+						replace: 'base64replacement'
+					}, {
+						search: i18nPattern.toString(),
+						replace: 'i18nReplacement',
+						i18n: true
+					}, {
+						search: faPattern.toString(),
+						replace: 'faReplacement'
+					}, {
+						search: /^<template>([\s\S]+?)\r?\n<\/template>/.toString(),
+						replace: 'collapseSpacesReplacement'
+					}]
 				}
 			}]
 		}, {
diff --git a/webpack/loaders/replace.js b/webpack/loaders/replace.js
index d8a81c245a..fd6bb3617b 100644
--- a/webpack/loaders/replace.js
+++ b/webpack/loaders/replace.js
@@ -5,18 +5,26 @@ function trim(text, g) {
 }
 
 export default function(src) {
+	const fn = options => {
+		const search = options.search;
+		const g = search[search.length - 1] == 'g';
+		const file = this.resourcePath.replace(/\\/g, '/');
+		const replace = options.i18n ? global[options.replace].bind(null, {
+			src: file,
+			lang: options.lang
+		}) : global[options.replace];
+		if (typeof search != 'string' || search.length == 0) console.error('invalid search');
+		if (typeof replace != 'function') console.error('invalid replacer:', replace, this.request);
+		src = src.replace(new RegExp(trim(search, g), g ? 'g' : ''), replace);
+	};
+
 	this.cacheable();
 	const options = getOptions(this);
-	const search = options.search;
-	const g = search[search.length - 1] == 'g';
-	const file = this.resourcePath.replace(/\\/g, '/');
-	const replace = options.i18n ? global[options.replace].bind(null, {
-		src: file,
-		lang: options.lang
-	}) : global[options.replace];
-	if (typeof search != 'string' || search.length == 0) console.error('invalid search');
-	if (typeof replace != 'function') console.error('invalid replacer:', replace, this.request);
-	src = src.replace(new RegExp(trim(search, g), g ? 'g' : ''), replace);
+	if (options.qs) {
+		options.qs.forEach(q => fn(q));
+	} else {
+		fn(options);
+	}
 	this.callback(null, src);
 	return src;
 }

From 1e61ea15919c9613a51360e26b78c839afed47aa Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 17 May 2018 19:38:50 +0900
Subject: [PATCH 07/35] 2.7.1

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

diff --git a/package.json b/package.json
index 356ac1f517..01f5e45244 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "2.7.0",
+	"version": "2.7.1",
 	"clientVersion": "1.0.5345",
 	"codename": "nighthike",
 	"main": "./built/index.js",

From 518f6e96771cda0bd048cf798c0640cde79f6258 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 17 May 2018 23:10:53 +0900
Subject: [PATCH 08/35] :art:

---
 .../views/components/widget-container.vue     | 19 +++++++++++--------
 src/client/app/mobile/views/pages/widgets.vue | 11 +++++++++--
 2 files changed, 20 insertions(+), 10 deletions(-)

diff --git a/src/client/app/mobile/views/components/widget-container.vue b/src/client/app/mobile/views/components/widget-container.vue
index 1bdc875763..8a97848b73 100644
--- a/src/client/app/mobile/views/components/widget-container.vue
+++ b/src/client/app/mobile/views/components/widget-container.vue
@@ -25,15 +25,12 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-widget-container
-	background #eee
+root(isDark)
+	background isDark ? #21242f : #eee
 	border-radius 8px
-	box-shadow 0 0 0 1px rgba(#000, 0.2)
+	box-shadow 0 4px 16px rgba(#000, 0.1)
 	overflow hidden
 
-	&.hideHeader
-		background #fff
-
 	&.naked
 		background transparent !important
 		box-shadow none !important
@@ -44,8 +41,8 @@ export default Vue.extend({
 			padding 8px 10px
 			font-size 15px
 			font-weight normal
-			color #465258
-			background #fff
+			color isDark ? #b8c5cc : #465258
+			background isDark ? #282c37 : #fff
 			border-radius 8px 8px 0 0
 
 			> [data-fa]
@@ -65,4 +62,10 @@ export default Vue.extend({
 			font-size 15px
 			color #465258
 
+.mk-widget-container[data-darkmode]
+	root(true)
+
+.mk-widget-container:not([data-darkmode])
+	root(false)
+
 </style>
diff --git a/src/client/app/mobile/views/pages/widgets.vue b/src/client/app/mobile/views/pages/widgets.vue
index 338a5288bb..b4022fd58f 100644
--- a/src/client/app/mobile/views/pages/widgets.vue
+++ b/src/client/app/mobile/views/pages/widgets.vue
@@ -141,17 +141,24 @@ export default Vue.extend({
 <style lang="stylus" scoped>
 main
 	margin 0 auto
+	padding 8px
 	max-width 500px
 
 	@media (min-width 500px)
-		padding 8px
+		padding 16px 8px
+
+	@media (min-width 600px)
+		padding 32px 8px
 
 	> header
 		padding 8px
 		background #fff
 
 	.widget
-		margin 8px
+		margin-bottom 8px
+
+		@media (min-width 600px)
+			margin-bottom 16px
 
 	.customize-container
 		margin 8px

From e1672e539b3d62929ee2db1d79307ed207d81c5d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 17 May 2018 23:18:24 +0900
Subject: [PATCH 09/35] :art:

---
 .../common/views/components/url-preview.vue   | 19 ++++++++++++-------
 1 file changed, 12 insertions(+), 7 deletions(-)

diff --git a/src/client/app/common/views/components/url-preview.vue b/src/client/app/common/views/components/url-preview.vue
index 3bae6e5078..028b911e24 100644
--- a/src/client/app/common/views/components/url-preview.vue
+++ b/src/client/app/common/views/components/url-preview.vue
@@ -126,16 +126,21 @@ root(isDark)
 					line-height 16px
 					vertical-align top
 
-		@media (max-width 500px)
-			font-size 8px
-			border none
-
+		@media (max-width 700px)
 			> .thumbnail
-				width 70px
+				position relative
+				width 100%
+				height 100px
 
 				& + article
-					left 70px
-					width calc(100% - 70px)
+					left 0
+					width 100%
+
+		@media (max-width 500px)
+			font-size 8px
+
+			> .thumbnail
+				height 70px
 
 			> article
 				padding 8px

From 89a58dc5964f4df8c54c9c216d8fff3a63d86462 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 17 May 2018 23:38:35 +0900
Subject: [PATCH 10/35] =?UTF-8?q?=E6=B7=BB=E4=BB=98=E3=81=95=E3=82=8C?=
 =?UTF-8?q?=E3=81=9F=E3=83=A1=E3=83=87=E3=82=A3=E3=82=A2=E3=81=AEURL?=
 =?UTF-8?q?=E3=81=AF=E7=9C=81=E7=95=A5=E3=81=97=E3=81=A6=E8=A1=A8=E7=A4=BA?=
 =?UTF-8?q?=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/client/app/common/scripts/can-hide-text.ts | 16 ++++++++++++++++
 .../desktop/views/components/notes.note.vue    | 18 ++++++++++++++++--
 .../app/mobile/views/components/note.vue       | 15 ++++++++++++++-
 src/models/drive-file.ts                       |  1 +
 4 files changed, 47 insertions(+), 3 deletions(-)
 create mode 100644 src/client/app/common/scripts/can-hide-text.ts

diff --git a/src/client/app/common/scripts/can-hide-text.ts b/src/client/app/common/scripts/can-hide-text.ts
new file mode 100644
index 0000000000..4a4be8d9d0
--- /dev/null
+++ b/src/client/app/common/scripts/can-hide-text.ts
@@ -0,0 +1,16 @@
+export default function(note) {
+	if (note.text == null) return true;
+
+	let txt = note.text;
+
+	if (note.media) {
+		note.media.forEach(file => {
+			txt = txt.replace(file.url, '');
+			if (file.src) txt = txt.replace(file.src, '');
+		});
+
+		if (txt == '') return true;
+	}
+
+	return false;
+}
diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue
index 057c3c0956..3ecef33d9a 100644
--- a/src/client/app/desktop/views/components/notes.note.vue
+++ b/src/client/app/desktop/views/components/notes.note.vue
@@ -44,7 +44,7 @@
 					<div class="text">
 						<span v-if="p.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
 						<a class="reply" v-if="p.reply">%fa:reply%</a>
-						<mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/>
+						<mk-note-html v-if="p.text && !canHideText(p)" :text="p.text" :i="os.i" :class="$style.text"/>
 						<a class="rp" v-if="p.renote">RP:</a>
 					</div>
 					<div class="media" v-if="p.media.length > 0">
@@ -94,6 +94,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
+import canHideText from '../../../common/scripts/can-hide-text';
 import parse from '../../../../../text/parse';
 
 import MkPostFormWindow from './post-form-window.vue';
@@ -130,16 +131,17 @@ export default Vue.extend({
 	},
 
 	computed: {
-
 		isRenote(): boolean {
 			return (this.note.renote &&
 				this.note.text == null &&
 				this.note.mediaIds.length == 0 &&
 				this.note.poll == null);
 		},
+
 		p(): any {
 			return this.isRenote ? this.note.renote : this.note;
 		},
+
 		reactionsCount(): number {
 			return this.p.reactionCounts
 				? Object.keys(this.p.reactionCounts)
@@ -147,9 +149,11 @@ export default Vue.extend({
 					.reduce((a, b) => a + b)
 				: 0;
 		},
+
 		title(): string {
 			return dateStringify(this.p.createdAt);
 		},
+
 		urls(): string[] {
 			if (this.p.text) {
 				const ast = parse(this.p.text);
@@ -205,6 +209,8 @@ export default Vue.extend({
 	},
 
 	methods: {
+		canHideText,
+
 		capture(withHandler = false) {
 			if ((this as any).os.isSignedIn) {
 				this.connection.send({
@@ -214,6 +220,7 @@ export default Vue.extend({
 				if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
 			}
 		},
+
 		decapture(withHandler = false) {
 			if ((this as any).os.isSignedIn) {
 				this.connection.send({
@@ -223,9 +230,11 @@ export default Vue.extend({
 				if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated);
 			}
 		},
+
 		onStreamConnected() {
 			this.capture();
 		},
+
 		onStreamNoteUpdated(data) {
 			const note = data.note;
 			if (note.id == this.note.id) {
@@ -234,28 +243,33 @@ export default Vue.extend({
 				this.note.renote = note;
 			}
 		},
+
 		reply() {
 			(this as any).os.new(MkPostFormWindow, {
 				reply: this.p
 			});
 		},
+
 		renote() {
 			(this as any).os.new(MkRenoteFormWindow, {
 				note: this.p
 			});
 		},
+
 		react() {
 			(this as any).os.new(MkReactionPicker, {
 				source: this.$refs.reactButton,
 				note: this.p
 			});
 		},
+
 		menu() {
 			(this as any).os.new(MkNoteMenu, {
 				source: this.$refs.menuButton,
 				note: this.p
 			});
 		},
+
 		onKeydown(e) {
 			let shouldBeCancel = true;
 
diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue
index d66f5a1016..f5428b80cd 100644
--- a/src/client/app/mobile/views/components/note.vue
+++ b/src/client/app/mobile/views/components/note.vue
@@ -41,7 +41,7 @@
 					<div class="text">
 						<span v-if="p.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
 						<a class="reply" v-if="p.reply">%fa:reply%</a>
-						<mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/>
+						<mk-note-html v-if="p.text && !canHideText(p)" :text="p.text" :i="os.i" :class="$style.text"/>
 						<a class="rp" v-if="p.renote != null">RP:</a>
 					</div>
 					<div class="media" v-if="p.media.length > 0">
@@ -85,6 +85,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import parse from '../../../../../text/parse';
+import canHideText from '../../../common/scripts/can-hide-text';
 
 import MkNoteMenu from '../../../common/views/components/note-menu.vue';
 import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
@@ -112,9 +113,11 @@ export default Vue.extend({
 				this.note.mediaIds.length == 0 &&
 				this.note.poll == null);
 		},
+
 		p(): any {
 			return this.isRenote ? this.note.renote : this.note;
 		},
+
 		reactionsCount(): number {
 			return this.p.reactionCounts
 				? Object.keys(this.p.reactionCounts)
@@ -122,6 +125,7 @@ export default Vue.extend({
 					.reduce((a, b) => a + b)
 				: 0;
 		},
+
 		urls(): string[] {
 			if (this.p.text) {
 				const ast = parse(this.p.text);
@@ -177,6 +181,8 @@ export default Vue.extend({
 	},
 
 	methods: {
+		canHideText,
+
 		capture(withHandler = false) {
 			if ((this as any).os.isSignedIn) {
 				this.connection.send({
@@ -186,6 +192,7 @@ export default Vue.extend({
 				if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
 			}
 		},
+
 		decapture(withHandler = false) {
 			if ((this as any).os.isSignedIn) {
 				this.connection.send({
@@ -195,9 +202,11 @@ export default Vue.extend({
 				if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated);
 			}
 		},
+
 		onStreamConnected() {
 			this.capture();
 		},
+
 		onStreamNoteUpdated(data) {
 			const note = data.note;
 			if (note.id == this.note.id) {
@@ -206,16 +215,19 @@ export default Vue.extend({
 				this.note.renote = note;
 			}
 		},
+
 		reply() {
 			(this as any).apis.post({
 				reply: this.p
 			});
 		},
+
 		renote() {
 			(this as any).apis.post({
 				renote: this.p
 			});
 		},
+
 		react() {
 			(this as any).os.new(MkReactionPicker, {
 				source: this.$refs.reactButton,
@@ -223,6 +235,7 @@ export default Vue.extend({
 				compact: true
 			});
 		},
+
 		menu() {
 			(this as any).os.new(MkNoteMenu, {
 				source: this.$refs.menuButton,
diff --git a/src/models/drive-file.ts b/src/models/drive-file.ts
index f8cad36f9a..8a18567dc6 100644
--- a/src/models/drive-file.ts
+++ b/src/models/drive-file.ts
@@ -154,6 +154,7 @@ export const pack = (
 
 	_target = Object.assign(_target, _file.metadata);
 
+	_target.src = _file.metadata.url;
 	_target.url = `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`;
 
 	if (_target.properties == null) _target.properties = {};

From 4ad51672c15814841421d4b5076a0f3040f10c6d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 17 May 2018 23:53:55 +0900
Subject: [PATCH 11/35] Fix bug

---
 .../app/mobile/views/components/notes.vue     | 19 ++++++++++---------
 .../app/mobile/views/components/ui.header.vue |  5 +++++
 src/client/app/store.ts                       |  5 +++++
 3 files changed, 20 insertions(+), 9 deletions(-)

diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue
index 53e232e521..e77698dea9 100644
--- a/src/client/app/mobile/views/components/notes.vue
+++ b/src/client/app/mobile/views/components/notes.vue
@@ -1,7 +1,5 @@
 <template>
 <div class="mk-notes">
-	<div class="newer-indicator" :style="{ top: $store.state.uiHeaderHeight + 'px' }" v-show="queue.length > 0"></div>
-
 	<slot name="head"></slot>
 
 	<slot name="empty" v-if="notes.length == 0 && !fetching && requestInitPromise == null"></slot>
@@ -71,6 +69,16 @@ export default Vue.extend({
 		}
 	},
 
+	watch: {
+		queue(x) {
+			if (x.length > 0) {
+				this.$store.commit('indicate', true);
+			} else {
+				this.$store.commit('indicate', false);
+			}
+		}
+	},
+
 	mounted() {
 		document.addEventListener('visibilitychange', this.onVisibilitychange, false);
 		window.addEventListener('scroll', this.onScroll);
@@ -238,13 +246,6 @@ root(isDark)
 			[data-fa]
 				margin-right 8px
 
-	> .newer-indicator
-		position -webkit-sticky
-		position sticky
-		z-index 100
-		height 3px
-		background $theme-color
-
 	> .init
 		padding 64px 0
 		text-align center
diff --git a/src/client/app/mobile/views/components/ui.header.vue b/src/client/app/mobile/views/components/ui.header.vue
index 509463333d..a49462b159 100644
--- a/src/client/app/mobile/views/components/ui.header.vue
+++ b/src/client/app/mobile/views/components/ui.header.vue
@@ -13,6 +13,7 @@
 			<slot name="func"></slot>
 		</div>
 	</div>
+	<div class="indicator" v-show="$store.state.indicate"></div>
 </div>
 </template>
 
@@ -156,6 +157,10 @@ root(isDark)
 	&, *
 		user-select none
 
+	> .indicator
+		height 3px
+		background $theme-color
+
 	> .main
 		color rgba(#fff, 0.9)
 
diff --git a/src/client/app/store.ts b/src/client/app/store.ts
index e9cd952bde..1f1189054d 100644
--- a/src/client/app/store.ts
+++ b/src/client/app/store.ts
@@ -24,10 +24,15 @@ export default (os: MiOS) => new Vuex.Store({
 	}],
 
 	state: {
+		indicate: false,
 		uiHeaderHeight: 0
 	},
 
 	mutations: {
+		indicate(state, x) {
+			state.indicate = x;
+		},
+
 		setUiHeaderHeight(state, height) {
 			state.uiHeaderHeight = height;
 		}

From 605700b98ebd03590086ca38a4c3cac2e2586a1c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 17 May 2018 23:54:47 +0900
Subject: [PATCH 12/35] 2.8.0

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

diff --git a/package.json b/package.json
index 01f5e45244..d4ca353c8f 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "2.7.1",
-	"clientVersion": "1.0.5345",
+	"version": "2.8.0",
+	"clientVersion": "1.0.5352",
 	"codename": "nighthike",
 	"main": "./built/index.js",
 	"private": true,

From 1529a2eded380bfdecad116351127dcf45c4619b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marcin=20Miko=C5=82ajczak?= <me@m4sk.in>
Date: Thu, 17 May 2018 20:07:11 +0200
Subject: [PATCH 13/35] moar i18n
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
---
 locales/ja.yml                                | 42 ++++++++++++++++
 .../app/desktop/views/components/home.vue     | 48 +++++++++----------
 .../desktop/views/components/settings.api.vue |  2 +-
 .../app/desktop/views/components/settings.vue |  6 +--
 .../views/components/user-lists-window.vue    |  2 +-
 .../desktop/views/components/user-preview.vue |  6 +--
 .../desktop/views/components/users-list.vue   | 10 ++--
 7 files changed, 79 insertions(+), 37 deletions(-)

diff --git a/locales/ja.yml b/locales/ja.yml
index d8e15fb3c0..74d36ddb6e 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -253,6 +253,32 @@ desktop/views/components/drive.vue:
     upload: "ファイルをアップロード"
     url-upload: "URLからアップロード"
 
+desktop/views/components/home.vue:
+  done: "完了"
+  add-widget: "ウィジェットを追加:"
+  profile: "プロフィール"
+  calendar: "カレンダー"
+  timemachine: "カレンダー(タイムマシン)"
+  activity: "アクティビティ"
+  rss: "RSSリーダー"
+  trends: "トレンド"
+  photostream: "フォトストリーム"
+  slideshow: "スライドショー"
+  version: "バージョン"
+  broadcast: "ブロードキャスト"
+  notifications: "通知"
+  users: "おすすめユーザー"
+  polls: "投票"
+  post-form: "投稿フォーム"
+  messaging: "メッセージ"
+  channel: "チャンネル"
+  access-log: "アクセスログ"
+  server: "サーバー情報"
+  donation: "寄付のお願い"
+  nav: "ナビゲーション"
+  tips: "ヒント"
+  add: "追加"
+
 desktop/views/components/messaging-window.vue:
   title: "メッセージ"
 
@@ -312,6 +338,7 @@ desktop/views/components/settings.vue:
   mute: "ミュート"
   drive: "ドライブ"
   security: "セキュリティ"
+  signin: "サインイン履歴"
   password: "パスワード"
   2fa: "二段階認証"
   other: "その他"
@@ -341,6 +368,7 @@ desktop/views/components/settings.api.vue:
   caution: "アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。"
   regeneration-of-token: "万が一このトークンが漏れたりその可能性がある場合はトークンを再生成できます。"
   regenerate-token: "トークンを再生成"
+  token: "Token:"
   enter-password: "パスワードを入力してください"
 
 desktop/views/components/settings.app.vue:
@@ -396,6 +424,20 @@ desktop/views/components/ui.header.post.vue:
 desktop/views/components/ui.header.search.vue:
   placeholder: "検索"
 
+desktop/views/components/user-lists-window.vue:
+  create-list: "リストを作成"
+
+desktop/views/components/user-preview.vue:
+  notes: "投稿"
+  following: "フォロー"
+  followers: "フォロワー"
+
+desktop/views/components/users-list.vue:
+  all: "すべて"
+  iknow: "知り合い"
+  load-more: "もっと"
+  fetching: "読み込んでいます"
+
 desktop/views/pages/note.vue:
   prev: "前の投稿"
   next: "次の投稿"
diff --git a/src/client/app/desktop/views/components/home.vue b/src/client/app/desktop/views/components/home.vue
index cae6233cd8..a3d7927cfc 100644
--- a/src/client/app/desktop/views/components/home.vue
+++ b/src/client/app/desktop/views/components/home.vue
@@ -1,34 +1,34 @@
 <template>
 <div class="mk-home" :data-customize="customize">
 	<div class="customize" v-if="customize">
-		<router-link to="/">%fa:check%完了</router-link>
+		<router-link to="/">%fa:check%%i18n:@done%</router-link>
 		<div>
 			<div class="adder">
-				<p>ウィジェットを追加:</p>
+				<p>%i18n:@add-widget%</p>
 				<select v-model="widgetAdderSelected">
-					<option value="profile">プロフィール</option>
-					<option value="calendar">カレンダー</option>
-					<option value="timemachine">カレンダー(タイムマシン)</option>
-					<option value="activity">アクティビティ</option>
-					<option value="rss">RSSリーダー</option>
-					<option value="trends">トレンド</option>
-					<option value="photo-stream">フォトストリーム</option>
-					<option value="slideshow">スライドショー</option>
-					<option value="version">バージョン</option>
-					<option value="broadcast">ブロードキャスト</option>
-					<option value="notifications">通知</option>
-					<option value="users">おすすめユーザー</option>
-					<option value="polls">投票</option>
-					<option value="post-form">投稿フォーム</option>
-					<option value="messaging">メッセージ</option>
-					<option value="channel">チャンネル</option>
-					<option value="access-log">アクセスログ</option>
-					<option value="server">サーバー情報</option>
-					<option value="donation">寄付のお願い</option>
-					<option value="nav">ナビゲーション</option>
-					<option value="tips">ヒント</option>
+					<option value="profile">%i18n:@profile%</option>
+					<option value="calendar">%i18n:@calendar%</option>
+					<option value="timemachine">%i18n:@timemachine%</option>
+					<option value="activity">%i18n:@activity%</option>
+					<option value="rss">%i18n:@rss%</option>
+					<option value="trends">%i18n:@trends%</option>
+					<option value="photo-stream">%i18n:@photo-stream%</option>
+					<option value="slideshow">%i18n:@slideshow%</option>
+					<option value="version">%i18n:@version%</option>
+					<option value="broadcast">%i18n:@broadcast%</option>
+					<option value="notifications">%i18n:@notifications%</option>
+					<option value="users">%i18n:@users%</option>
+					<option value="polls">%i18n:@polls%</option>
+					<option value="post-form">%i18n:@post-form%</option>
+					<option value="messaging">%i18n:@messaging%</option>
+					<option value="channel">%i18n:@channel%</option>
+					<option value="access-log">%i18n:@access-log%</option>
+					<option value="server">%i18n:@server%</option>
+					<option value="donation">%i18n:@donation%</option>
+					<option value="nav">%i18n:@nav%</option>
+					<option value="tips">%i18n:@tips%</option>
 				</select>
-				<button @click="addWidget">追加</button>
+				<button @click="addWidget">%i18n:@add%</button>
 			</div>
 			<div class="trash">
 				<x-draggable v-model="trash" :options="{ group: 'x' }" @add="onTrash"></x-draggable>
diff --git a/src/client/app/desktop/views/components/settings.api.vue b/src/client/app/desktop/views/components/settings.api.vue
index 377f2e689b..b22ee6cdab 100644
--- a/src/client/app/desktop/views/components/settings.api.vue
+++ b/src/client/app/desktop/views/components/settings.api.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="root api">
-	<p>Token: <code>{{ os.i.token }}</code></p>
+	<p>%i18n:@token% <code>{{ os.i.token }}</code></p>
 	<p>%i18n:@intro%</p>
 	<div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:@caution%</p></div>
 	<p>%i18n:@regeneration-of-token%</p>
diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue
index 9e13aba13a..4e5e281fd0 100644
--- a/src/client/app/desktop/views/components/settings.vue
+++ b/src/client/app/desktop/views/components/settings.vue
@@ -101,7 +101,7 @@
 		</section>
 
 		<section class="notification" v-show="page == 'notification'">
-			<h1>通知</h1>
+			<h1>%i18n:@notification%</h1>
 			<mk-switch v-model="os.i.settings.autoWatch" @change="onChangeAutoWatch" text="投稿の自動ウォッチ">
 				<span>リアクションしたり返信したりした投稿に関する通知を自動的に受け取るようにします。</span>
 			</mk-switch>
@@ -118,7 +118,7 @@
 		</section>
 
 		<section class="apps" v-show="page == 'apps'">
-			<h1>アプリケーション</h1>
+			<h1>%i18n:@apps%</h1>
 			<x-apps/>
 		</section>
 
@@ -138,7 +138,7 @@
 		</section>
 
 		<section class="signin" v-show="page == 'security'">
-			<h1>サインイン履歴</h1>
+			<h1>%i18n:@signin%</h1>
 			<x-signins/>
 		</section>
 
diff --git a/src/client/app/desktop/views/components/user-lists-window.vue b/src/client/app/desktop/views/components/user-lists-window.vue
index d082610132..585c0a864f 100644
--- a/src/client/app/desktop/views/components/user-lists-window.vue
+++ b/src/client/app/desktop/views/components/user-lists-window.vue
@@ -3,7 +3,7 @@
 	<span slot="header">%fa:list% リスト</span>
 
 	<div data-id="6e4caea3-d8f9-4ab7-96de-ab67fe8d5c82" :data-darkmode="_darkmode_">
-		<button class="ui" @click="add">リストを作成</button>
+		<button class="ui" @click="add">%i18n:@create-list%</button>
 		<a v-for="list in lists" :key="list.id" @click="choice(list)">{{ list.title }}</a>
 	</div>
 </mk-window>
diff --git a/src/client/app/desktop/views/components/user-preview.vue b/src/client/app/desktop/views/components/user-preview.vue
index cc5e021390..f40e60dff9 100644
--- a/src/client/app/desktop/views/components/user-preview.vue
+++ b/src/client/app/desktop/views/components/user-preview.vue
@@ -10,13 +10,13 @@
 		<div class="description">{{ u.description }}</div>
 		<div class="status">
 			<div>
-				<p>投稿</p><a>{{ u.notesCount }}</a>
+				<p>%i18n:@notes%</p><a>{{ u.notesCount }}</a>
 			</div>
 			<div>
-				<p>フォロー</p><a>{{ u.followingCount }}</a>
+				<p>%i18n:@following%</p><a>{{ u.followingCount }}</a>
 			</div>
 			<div>
-				<p>フォロワー</p><a>{{ u.followersCount }}</a>
+				<p>%i18n:@followers%</p><a>{{ u.followersCount }}</a>
 			</div>
 		</div>
 		<mk-follow-button v-if="os.isSignedIn && user.id != os.i.id" :user="u"/>
diff --git a/src/client/app/desktop/views/components/users-list.vue b/src/client/app/desktop/views/components/users-list.vue
index 13d0d07bbc..1ed5c33b13 100644
--- a/src/client/app/desktop/views/components/users-list.vue
+++ b/src/client/app/desktop/views/components/users-list.vue
@@ -2,8 +2,8 @@
 <div class="mk-users-list">
 	<nav>
 		<div>
-			<span :data-active="mode == 'all'" @click="mode = 'all'">すべて<span>{{ count }}</span></span>
-			<span v-if="os.isSignedIn && youKnowCount" :data-active="mode == 'iknow'" @click="mode = 'iknow'">知り合い<span>{{ youKnowCount }}</span></span>
+			<span :data-active="mode == 'all'" @click="mode = 'all'">%i18n:@all%<span>{{ count }}</span></span>
+			<span v-if="os.isSignedIn && youKnowCount" :data-active="mode == 'iknow'" @click="mode = 'iknow'">%i18n:@iknow%<span>{{ youKnowCount }}</span></span>
 		</div>
 	</nav>
 	<div class="users" v-if="!fetching && users.length != 0">
@@ -12,13 +12,13 @@
 		</div>
 	</div>
 	<button class="more" v-if="!fetching && next != null" @click="more" :disabled="moreFetching">
-		<span v-if="!moreFetching">もっと</span>
-		<span v-if="moreFetching">読み込み中<mk-ellipsis/></span>
+		<span v-if="!moreFetching">%i18n:@load-more%</span>
+		<span v-if="moreFetching">%i18n:common.loading%<mk-ellipsis/></span>
 	</button>
 	<p class="no" v-if="!fetching && users.length == 0">
 		<slot></slot>
 	</p>
-	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p>
+	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@fetching%<mk-ellipsis/></p>
 </div>
 </template>
 

From c5feafb893ca4b4987d356a250ad537b3053dedd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 18 May 2018 06:14:33 +0900
Subject: [PATCH 14/35] Fix test

---
 test/api.ts | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/test/api.ts b/test/api.ts
index d8c163e920..98c4cea84e 100644
--- a/test/api.ts
+++ b/test/api.ts
@@ -2,7 +2,9 @@
  * API TESTS
  */
 
+import * as fs from 'fs';
 import * as merge from 'object-assign-deep';
+import * as chai from 'chai';
 
 Error.stackTraceLimit = Infinity;
 
@@ -12,11 +14,9 @@ process.env.NODE_ENV = 'test';
 // Display detail of unhandled promise rejection
 process.on('unhandledRejection', console.dir);
 
-const fs = require('fs');
-const _chai = require('chai');
 const chaiHttp = require('chai-http');
 
-_chai.use(chaiHttp);
+chai.use(chaiHttp);
 
 const server = require('../built/server/api').callback();
 const db = require('../built/db/mongodb').default;
@@ -34,7 +34,7 @@ const request = (endpoint, params, me?) => new Promise<any>((ok, ng) => {
 		i: me.token
 	} : {};
 
-	_chai.request(server)
+	chai.request(server)
 		.post(endpoint)
 		.send(Object.assign(auth, params))
 		.end((err, res) => {
@@ -723,7 +723,7 @@ describe('API', () => {
 	describe('drive/files/create', () => {
 		it('ファイルを作成できる', async(async () => {
 			const me = await insertSakurako();
-			const res = await _chai.request(server)
+			const res = await chai.request(server)
 				.post('/drive/files/create')
 				.field('i', me.token)
 				.attach('file', fs.readFileSync(__dirname + '/resources/Lenna.png'), 'Lenna.png');

From a8ade110e69d1512dc9de411017d1cc799165175 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 18 May 2018 06:27:27 +0900
Subject: [PATCH 15/35] Improve renoting in mobile

---
 locales/ja.yml                                |  3 ++
 src/client/app/mobile/api/post.ts             | 51 ++++++-------------
 .../app/mobile/views/components/post-form.vue | 12 +++--
 3 files changed, 28 insertions(+), 38 deletions(-)

diff --git a/locales/ja.yml b/locales/ja.yml
index 74d36ddb6e..40527adcf3 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -552,6 +552,9 @@ mobile/views/components/notifications.vue:
 
 mobile/views/components/post-form.vue:
   submit: "投稿"
+  reply: "返信"
+  renote: "Renote"
+  renote-placeholder: "この投稿を引用... (オプション)"
   reply-placeholder: "この投稿への返信..."
   note-placeholder: "いまどうしてる?"
 
diff --git a/src/client/app/mobile/api/post.ts b/src/client/app/mobile/api/post.ts
index 72919c6505..0634c52642 100644
--- a/src/client/app/mobile/api/post.ts
+++ b/src/client/app/mobile/api/post.ts
@@ -1,43 +1,24 @@
 import PostForm from '../views/components/post-form.vue';
-//import RenoteForm from '../views/components/renote-form.vue';
-import getNoteSummary from '../../../../renderers/get-note-summary';
 
 export default (os) => (opts) => {
 	const o = opts || {};
 
-	if (o.renote) {
-		/*const vm = new RenoteForm({
-			propsData: {
-				renote: o.renote
-			}
-		}).$mount();
-		vm.$once('cancel', recover);
-		vm.$once('note', recover);
-		document.body.appendChild(vm.$el);*/
+	const app = document.getElementById('app');
+	app.style.display = 'none';
 
-		const text = window.prompt(`「${getNoteSummary(o.renote)}」をRenote`);
-		if (text == null) return;
-		os.api('notes/create', {
-			renoteId: o.renote.id,
-			text: text == '' ? undefined : text
-		});
-	} else {
-		const app = document.getElementById('app');
-		app.style.display = 'none';
-
-		function recover() {
-			app.style.display = 'block';
-		}
-
-		const vm = new PostForm({
-			parent: os.app,
-			propsData: {
-				reply: o.reply
-			}
-		}).$mount();
-		vm.$once('cancel', recover);
-		vm.$once('note', recover);
-		document.body.appendChild(vm.$el);
-		(vm as any).focus();
+	function recover() {
+		app.style.display = 'block';
 	}
+
+	const vm = new PostForm({
+		parent: os.app,
+		propsData: {
+			reply: o.reply,
+			renote: o.renote
+		}
+	}).$mount();
+	vm.$once('cancel', recover);
+	vm.$once('note', recover);
+	document.body.appendChild(vm.$el);
+	(vm as any).focus();
 };
diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue
index 6d80b3046b..0bb498e5d7 100644
--- a/src/client/app/mobile/views/components/post-form.vue
+++ b/src/client/app/mobile/views/components/post-form.vue
@@ -5,17 +5,22 @@
 		<div>
 			<span class="text-count" :class="{ over: text.length > 1000 }">{{ 1000 - text.length }}</span>
 			<span class="geo" v-if="geo">%fa:map-marker-alt%</span>
-			<button class="submit" :disabled="posting" @click="post">{{ reply ? '返信' : '%i18n:!@submit%' }}</button>
+			<button class="submit" :disabled="posting" @click="post">
+				<template v-if="reply">%i18n:@reply%</template>
+				<template v-else-if="renote">%i18n:@renote%</template>
+				<template v-else>%i18n:@submit%</template>
+			</button>
 		</div>
 	</header>
 	<div class="form">
 		<mk-note-preview v-if="reply" :note="reply"/>
+		<mk-note-preview v-if="renote" :note="renote"/>
 		<div v-if="visibility == 'specified'" class="visibleUsers">
 			<span v-for="u in visibleUsers">{{ u | userName }}<a @click="removeVisibleUser(u)">[x]</a></span>
 			<a @click="addVisibleUser">+ユーザーを追加</a>
 		</div>
 		<input v-show="useCw" v-model="cw" placeholder="内容への注釈 (オプション)">
-		<textarea v-model="text" ref="text" :disabled="posting" :placeholder="reply ? '%i18n:!@reply-placeholder%' : '%i18n:!@note-placeholder%'"></textarea>
+		<textarea v-model="text" ref="text" :disabled="posting" :placeholder="reply ? '%i18n:!@reply-placeholder%' : renote ? '%i18n:!@renote-placeholder%' : '%i18n:!@note-placeholder%'"></textarea>
 		<div class="attaches" v-show="files.length != 0">
 			<x-draggable class="files" :list="files" :options="{ animation: 150 }">
 				<div class="file" v-for="file in files" :key="file.id">
@@ -51,7 +56,7 @@ export default Vue.extend({
 		MkVisibilityChooser
 	},
 
-	props: ['reply'],
+	props: ['reply', 'renote'],
 
 	data() {
 		return {
@@ -177,6 +182,7 @@ export default Vue.extend({
 				text: this.text == '' ? undefined : this.text,
 				mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
 				replyId: this.reply ? this.reply.id : undefined,
+				renoteId: this.renote ? this.renote.id : undefined,
 				poll: this.poll ? (this.$refs.poll as any).get() : undefined,
 				cw: this.useCw ? this.cw || '' : undefined,
 				geo: this.geo ? {

From e7890eb6b94870231af0e9654c96845bfb0c1f42 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 18 May 2018 06:37:25 +0900
Subject: [PATCH 16/35] Use Promise

---
 test/api.ts | 13 ++++++-------
 1 file changed, 6 insertions(+), 7 deletions(-)

diff --git a/test/api.ts b/test/api.ts
index 98c4cea84e..c62fc9c56a 100644
--- a/test/api.ts
+++ b/test/api.ts
@@ -29,18 +29,17 @@ const async = fn => (done) => {
 	});
 };
 
-const request = (endpoint, params, me?) => new Promise<any>((ok, ng) => {
+const request = async (endpoint, params, me?) => {
 	const auth = me ? {
 		i: me.token
 	} : {};
 
-	chai.request(server)
+	const res = await chai.request(server)
 		.post(endpoint)
-		.send(Object.assign(auth, params))
-		.end((err, res) => {
-			ok(res);
-		});
-});
+		.send(Object.assign(auth, params));
+
+	return res;
+};
 
 describe('API', () => {
 	// Reset database each test

From 8a1aa08200b813b228e79d93a67dc221d69504ac Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 18 May 2018 06:40:15 +0900
Subject: [PATCH 17/35] Update .travis.yml

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

diff --git a/.travis.yml b/.travis.yml
index d2552bb460..cfc3e59882 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,7 +7,7 @@ notifications:
 language: node_js
 
 node_js:
-  - 9.8.0
+  - 10.1.0
 
 env:
   - CXX=g++-4.8 NODE_ENV=production
@@ -22,9 +22,7 @@ addons:
 
 cache:
   directories:
-    # パッケージをキャッシュすると本来は動かないはずなのに動いてしまう
-    # 場合があり危険なのでキャッシュはしない:
-    #- node_modules
+    - node_modules
 
 services:
   - mongodb

From 49911d15d3b3abe7053049ce5d44088e76282591 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 18 May 2018 06:40:38 +0900
Subject: [PATCH 18/35] Update .travis.yml

---
 .travis.yml | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index cfc3e59882..c86b737d21 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -29,10 +29,7 @@ services:
   - redis-server
 
 before_script:
-  # Travisはproduction環境なので(10行目により)、
-  # npm install しただけでは devDependencies はインストールされないので、
-  # --only=dev オプションを付けてそれらもインストールされるようにする:
-  - npm install --only=dev
+  - npm install
 
   # 設定ファイルを配置
   - cp ./.travis/default.yml ./.config

From 612db861412086128991b54104b99e1872da3c2b Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 18 May 2018 07:14:43 +0900
Subject: [PATCH 19/35] Delete api.ts

---
 test/api.ts | 1151 ---------------------------------------------------
 1 file changed, 1151 deletions(-)
 delete mode 100644 test/api.ts

diff --git a/test/api.ts b/test/api.ts
deleted file mode 100644
index c62fc9c56a..0000000000
--- a/test/api.ts
+++ /dev/null
@@ -1,1151 +0,0 @@
-/**
- * API TESTS
- */
-
-import * as fs from 'fs';
-import * as merge from 'object-assign-deep';
-import * as chai from 'chai';
-
-Error.stackTraceLimit = Infinity;
-
-// During the test the env variable is set to test
-process.env.NODE_ENV = 'test';
-
-// Display detail of unhandled promise rejection
-process.on('unhandledRejection', console.dir);
-
-const chaiHttp = require('chai-http');
-
-chai.use(chaiHttp);
-
-const server = require('../built/server/api').callback();
-const db = require('../built/db/mongodb').default;
-
-const async = fn => (done) => {
-	fn().then(() => {
-		done();
-	}, err => {
-		done(err);
-	});
-};
-
-const request = async (endpoint, params, me?) => {
-	const auth = me ? {
-		i: me.token
-	} : {};
-
-	const res = await chai.request(server)
-		.post(endpoint)
-		.send(Object.assign(auth, params));
-
-	return res;
-};
-
-describe('API', () => {
-	// Reset database each test
-	beforeEach(() => Promise.all([
-		db.get('users').drop(),
-		db.get('posts').drop(),
-		db.get('driveFiles.files').drop(),
-		db.get('driveFiles.chunks').drop(),
-		db.get('driveFolders').drop(),
-		db.get('apps').drop(),
-		db.get('accessTokens').drop(),
-		db.get('authSessions').drop()
-	]));
-
-	describe('signup', () => {
-		it('不正なユーザー名でアカウントが作成できない', async(async () => {
-			const res = await request('/signup', {
-				username: 'sakurako.',
-				password: 'HimawariDaisuki06160907'
-			});
-			res.should.have.status(400);
-		}));
-
-		it('空のパスワードでアカウントが作成できない', async(async () => {
-			const res = await request('/signup', {
-				username: 'sakurako',
-				password: ''
-			});
-			res.should.have.status(400);
-		}));
-
-		it('正しくアカウントが作成できる', async(async () => {
-			const me = {
-				username: 'sakurako',
-				password: 'HimawariDaisuki06160907'
-			};
-			const res = await request('/signup', me);
-			res.should.have.status(200);
-			res.body.should.be.a('object');
-			res.body.should.have.property('username').eql(me.username);
-		}));
-
-		it('同じユーザー名のアカウントは作成できない', async(async () => {
-			const user = await insertSakurako();
-			const res = await request('/signup', {
-				username: user.username,
-				password: 'HimawariDaisuki06160907'
-			});
-			res.should.have.status(400);
-		}));
-	});
-
-	describe('signin', () => {
-		it('間違ったパスワードでサインインできない', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/signin', {
-				username: me.username,
-				password: 'kyoppie'
-			});
-			res.should.have.status(400);
-		}));
-
-		it('クエリをインジェクションできない', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/signin', {
-				username: me.username,
-				password: {
-					$gt: ''
-				}
-			});
-			res.should.have.status(400);
-		}));
-
-		it('正しい情報でサインインできる', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/signin', {
-				username: me.username,
-				password: 'HimawariDaisuki06160907'
-			});
-			res.should.have.status(204);
-		}));
-	});
-
-	describe('i/update', () => {
-		it('アカウント設定を更新できる', async(async () => {
-			const me = await insertSakurako({
-				account: {
-					profile: {
-						gender: 'female'
-					}
-				}
-			});
-
-			const myName = '大室櫻子';
-			const myLocation = '七森中';
-			const myBirthday = '2000-09-07';
-
-			const res = await request('/i/update', {
-				name: myName,
-				location: myLocation,
-				birthday: myBirthday
-			}, me);
-			res.should.have.status(200);
-			res.body.should.be.a('object');
-			res.body.should.have.property('name').eql(myName);
-			res.body.should.have.nested.property('profile').a('object');
-			res.body.should.have.nested.property('profile.location').eql(myLocation);
-			res.body.should.have.nested.property('profile.birthday').eql(myBirthday);
-			res.body.should.have.nested.property('profile.gender').eql('female');
-		}));
-
-		it('名前を空白にできない', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/i/update', {
-				name: ' '
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('誕生日の設定を削除できる', async(async () => {
-			const me = await insertSakurako({
-				birthday: '2000-09-07'
-			});
-			const res = await request('/i/update', {
-				birthday: null
-			}, me);
-			res.should.have.status(200);
-			res.body.should.be.a('object');
-			res.body.should.have.nested.property('profile').a('object');
-			res.body.should.have.nested.property('profile.birthday').eql(null);
-		}));
-
-		it('不正な誕生日の形式で怒られる', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/i/update', {
-				birthday: '2000/09/07'
-			}, me);
-			res.should.have.status(400);
-		}));
-	});
-
-	describe('users/show', () => {
-		it('ユーザーが取得できる', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/users/show', {
-				userId: me._id.toString()
-			}, me);
-			res.should.have.status(200);
-			res.body.should.be.a('object');
-			res.body.should.have.property('id').eql(me._id.toString());
-		}));
-
-		it('ユーザーが存在しなかったら怒る', async(async () => {
-			const res = await request('/users/show', {
-				userId: '000000000000000000000000'
-			});
-			res.should.have.status(400);
-		}));
-
-		it('間違ったIDで怒られる', async(async () => {
-			const res = await request('/users/show', {
-				userId: 'kyoppie'
-			});
-			res.should.have.status(400);
-		}));
-	});
-
-	describe('posts/create', () => {
-		it('投稿できる', async(async () => {
-			const me = await insertSakurako();
-			const post = {
-				text: 'ひまわりー'
-			};
-			const res = await request('/posts/create', post, me);
-			res.should.have.status(200);
-			res.body.should.be.a('object');
-			res.body.should.have.property('createdPost');
-			res.body.createdPost.should.have.property('text').eql(post.text);
-		}));
-
-		it('ファイルを添付できる', async(async () => {
-			const me = await insertSakurako();
-			const file = await insertDriveFile({
-				userId: me._id
-			});
-			const res = await request('/posts/create', {
-				mediaIds: [file._id.toString()]
-			}, me);
-			res.should.have.status(200);
-			res.body.should.be.a('object');
-			res.body.should.have.property('createdPost');
-			res.body.createdPost.should.have.property('mediaIds').eql([file._id.toString()]);
-		}));
-
-		it('他人のファイルは添付できない', async(async () => {
-			const me = await insertSakurako();
-			const hima = await insertHimawari();
-			const file = await insertDriveFile({
-				userId: hima._id
-			});
-			const res = await request('/posts/create', {
-				mediaIds: [file._id.toString()]
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('存在しないファイルは添付できない', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/posts/create', {
-				mediaIds: ['000000000000000000000000']
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('不正なファイルIDで怒られる', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/posts/create', {
-				mediaIds: ['kyoppie']
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('返信できる', async(async () => {
-			const hima = await insertHimawari();
-			const himaPost = await db.get('posts').insert({
-				userId: hima._id,
-				text: 'ひま'
-			});
-
-			const me = await insertSakurako();
-			const post = {
-				text: 'さく',
-				replyId: himaPost._id.toString()
-			};
-			const res = await request('/posts/create', post, me);
-			res.should.have.status(200);
-			res.body.should.be.a('object');
-			res.body.should.have.property('createdPost');
-			res.body.createdPost.should.have.property('text').eql(post.text);
-			res.body.createdPost.should.have.property('replyId').eql(post.replyId);
-			res.body.createdPost.should.have.property('reply');
-			res.body.createdPost.reply.should.have.property('text').eql(himaPost.text);
-		}));
-
-		it('repostできる', async(async () => {
-			const hima = await insertHimawari();
-			const himaPost = await db.get('posts').insert({
-				userId: hima._id,
-				text: 'こらっさくらこ!'
-			});
-
-			const me = await insertSakurako();
-			const post = {
-				repostId: himaPost._id.toString()
-			};
-			const res = await request('/posts/create', post, me);
-			res.should.have.status(200);
-			res.body.should.be.a('object');
-			res.body.should.have.property('createdPost');
-			res.body.createdPost.should.have.property('repostId').eql(post.repostId);
-			res.body.createdPost.should.have.property('repost');
-			res.body.createdPost.repost.should.have.property('text').eql(himaPost.text);
-		}));
-
-		it('引用repostできる', async(async () => {
-			const hima = await insertHimawari();
-			const himaPost = await db.get('posts').insert({
-				userId: hima._id,
-				text: 'こらっさくらこ!'
-			});
-
-			const me = await insertSakurako();
-			const post = {
-				text: 'さく',
-				repostId: himaPost._id.toString()
-			};
-			const res = await request('/posts/create', post, me);
-			res.should.have.status(200);
-			res.body.should.be.a('object');
-			res.body.should.have.property('createdPost');
-			res.body.createdPost.should.have.property('text').eql(post.text);
-			res.body.createdPost.should.have.property('repostId').eql(post.repostId);
-			res.body.createdPost.should.have.property('repost');
-			res.body.createdPost.repost.should.have.property('text').eql(himaPost.text);
-		}));
-
-		it('文字数ぎりぎりで怒られない', async(async () => {
-			const me = await insertSakurako();
-			const post = {
-				text: '!'.repeat(1000)
-			};
-			const res = await request('/posts/create', post, me);
-			res.should.have.status(200);
-		}));
-
-		it('文字数オーバーで怒られる', async(async () => {
-			const me = await insertSakurako();
-			const post = {
-				text: '!'.repeat(1001)
-			};
-			const res = await request('/posts/create', post, me);
-			res.should.have.status(400);
-		}));
-
-		it('存在しないリプライ先で怒られる', async(async () => {
-			const me = await insertSakurako();
-			const post = {
-				text: 'さく',
-				replyId: '000000000000000000000000'
-			};
-			const res = await request('/posts/create', post, me);
-			res.should.have.status(400);
-		}));
-
-		it('存在しないrepost対象で怒られる', async(async () => {
-			const me = await insertSakurako();
-			const post = {
-				repostId: '000000000000000000000000'
-			};
-			const res = await request('/posts/create', post, me);
-			res.should.have.status(400);
-		}));
-
-		it('不正なリプライ先IDで怒られる', async(async () => {
-			const me = await insertSakurako();
-			const post = {
-				text: 'さく',
-				replyId: 'kyoppie'
-			};
-			const res = await request('/posts/create', post, me);
-			res.should.have.status(400);
-		}));
-
-		it('不正なrepost対象IDで怒られる', async(async () => {
-			const me = await insertSakurako();
-			const post = {
-				repostId: 'kyoppie'
-			};
-			const res = await request('/posts/create', post, me);
-			res.should.have.status(400);
-		}));
-
-		it('投票を添付できる', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/posts/create', {
-				text: 'インデントするなら?',
-				poll: {
-					choices: ['スペース', 'タブ']
-				}
-			}, me);
-			res.should.have.status(200);
-			res.body.should.be.a('object');
-			res.body.should.have.property('createdPost');
-			res.body.createdPost.should.have.property('poll');
-		}));
-
-		it('投票の選択肢が無くて怒られる', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/posts/create', {
-				poll: {}
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('投票の選択肢が無くて怒られる (空の配列)', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/posts/create', {
-				poll: {
-					choices: []
-				}
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('投票の選択肢が1つで怒られる', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/posts/create', {
-				poll: {
-					choices: ['Strawberry Pasta']
-				}
-			}, me);
-			res.should.have.status(400);
-		}));
-	});
-
-	describe('posts/show', () => {
-		it('投稿が取得できる', async(async () => {
-			const me = await insertSakurako();
-			const myPost = await db.get('posts').insert({
-				userId: me._id,
-				text: 'お腹ペコい'
-			});
-			const res = await request('/posts/show', {
-				postId: myPost._id.toString()
-			}, me);
-			res.should.have.status(200);
-			res.body.should.be.a('object');
-			res.body.should.have.property('id').eql(myPost._id.toString());
-		}));
-
-		it('投稿が存在しなかったら怒る', async(async () => {
-			const res = await request('/posts/show', {
-				postId: '000000000000000000000000'
-			});
-			res.should.have.status(400);
-		}));
-
-		it('間違ったIDで怒られる', async(async () => {
-			const res = await request('/posts/show', {
-				postId: 'kyoppie'
-			});
-			res.should.have.status(400);
-		}));
-	});
-
-	describe('posts/reactions/create', () => {
-		it('リアクションできる', async(async () => {
-			const hima = await insertHimawari();
-			const himaPost = await db.get('posts').insert({
-				userId: hima._id,
-				text: 'ひま'
-			});
-
-			const me = await insertSakurako();
-			const res = await request('/posts/reactions/create', {
-				postId: himaPost._id.toString(),
-				reaction: 'like'
-			}, me);
-			res.should.have.status(204);
-		}));
-
-		it('自分の投稿にはリアクションできない', async(async () => {
-			const me = await insertSakurako();
-			const myPost = await db.get('posts').insert({
-				userId: me._id,
-				text: 'お腹ペコい'
-			});
-
-			const res = await request('/posts/reactions/create', {
-				postId: myPost._id.toString(),
-				reaction: 'like'
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('二重にリアクションできない', async(async () => {
-			const hima = await insertHimawari();
-			const himaPost = await db.get('posts').insert({
-				userId: hima._id,
-				text: 'ひま'
-			});
-
-			const me = await insertSakurako();
-			await db.get('postReactions').insert({
-				userId: me._id,
-				postId: himaPost._id,
-				reaction: 'like'
-			});
-
-			const res = await request('/posts/reactions/create', {
-				postId: himaPost._id.toString(),
-				reaction: 'like'
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('存在しない投稿にはリアクションできない', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/posts/reactions/create', {
-				postId: '000000000000000000000000',
-				reaction: 'like'
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('空のパラメータで怒られる', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/posts/reactions/create', {}, me);
-			res.should.have.status(400);
-		}));
-
-		it('間違ったIDで怒られる', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/posts/reactions/create', {
-				postId: 'kyoppie',
-				reaction: 'like'
-			}, me);
-			res.should.have.status(400);
-		}));
-	});
-
-	describe('posts/reactions/delete', () => {
-		it('リアクションをキャンセルできる', async(async () => {
-			const hima = await insertHimawari();
-			const himaPost = await db.get('posts').insert({
-				userId: hima._id,
-				text: 'ひま'
-			});
-
-			const me = await insertSakurako();
-			await db.get('postReactions').insert({
-				userId: me._id,
-				postId: himaPost._id,
-				reaction: 'like'
-			});
-
-			const res = await request('/posts/reactions/delete', {
-				postId: himaPost._id.toString()
-			}, me);
-			res.should.have.status(204);
-		}));
-
-		it('リアクションしていない投稿はリアクションをキャンセルできない', async(async () => {
-			const hima = await insertHimawari();
-			const himaPost = await db.get('posts').insert({
-				userId: hima._id,
-				text: 'ひま'
-			});
-
-			const me = await insertSakurako();
-			const res = await request('/posts/reactions/delete', {
-				postId: himaPost._id.toString()
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('存在しない投稿はリアクションをキャンセルできない', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/posts/reactions/delete', {
-				postId: '000000000000000000000000'
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('空のパラメータで怒られる', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/posts/reactions/delete', {}, me);
-			res.should.have.status(400);
-		}));
-
-		it('間違ったIDで怒られる', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/posts/reactions/delete', {
-				postId: 'kyoppie'
-			}, me);
-			res.should.have.status(400);
-		}));
-	});
-
-	describe('following/create', () => {
-		it('フォローできる', async(async () => {
-			const hima = await insertHimawari();
-			const me = await insertSakurako();
-			const res = await request('/following/create', {
-				userId: hima._id.toString()
-			}, me);
-			res.should.have.status(204);
-		}));
-
-		it('既にフォローしている場合は怒る', async(async () => {
-			const hima = await insertHimawari();
-			const me = await insertSakurako();
-			await db.get('following').insert({
-				followeeId: hima._id,
-				followerId: me._id
-			});
-			const res = await request('/following/create', {
-				userId: hima._id.toString()
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('存在しないユーザーはフォローできない', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/following/create', {
-				userId: '000000000000000000000000'
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('自分自身はフォローできない', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/following/create', {
-				userId: me._id.toString()
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('空のパラメータで怒られる', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/following/create', {}, me);
-			res.should.have.status(400);
-		}));
-
-		it('間違ったIDで怒られる', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/following/create', {
-				userId: 'kyoppie'
-			}, me);
-			res.should.have.status(400);
-		}));
-	});
-
-	describe('following/delete', () => {
-		it('フォロー解除できる', async(async () => {
-			const hima = await insertHimawari();
-			const me = await insertSakurako();
-			await db.get('following').insert({
-				followeeId: hima._id,
-				followerId: me._id
-			});
-			const res = await request('/following/delete', {
-				userId: hima._id.toString()
-			}, me);
-			res.should.have.status(204);
-		}));
-
-		it('フォローしていない場合は怒る', async(async () => {
-			const hima = await insertHimawari();
-			const me = await insertSakurako();
-			const res = await request('/following/delete', {
-				userId: hima._id.toString()
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('存在しないユーザーはフォロー解除できない', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/following/delete', {
-				userId: '000000000000000000000000'
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('自分自身はフォロー解除できない', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/following/delete', {
-				userId: me._id.toString()
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('空のパラメータで怒られる', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/following/delete', {}, me);
-			res.should.have.status(400);
-		}));
-
-		it('間違ったIDで怒られる', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/following/delete', {
-				userId: 'kyoppie'
-			}, me);
-			res.should.have.status(400);
-		}));
-	});
-
-	describe('drive', () => {
-		it('ドライブ情報を取得できる', async(async () => {
-			const me = await insertSakurako();
-			await insertDriveFile({
-				userId: me._id,
-				datasize: 256
-			});
-			await insertDriveFile({
-				userId: me._id,
-				datasize: 512
-			});
-			await insertDriveFile({
-				userId: me._id,
-				datasize: 1024
-			});
-			const res = await request('/drive', {}, me);
-			res.should.have.status(200);
-			res.body.should.be.a('object');
-			res.body.should.have.property('usage').eql(1792);
-		}));
-	});
-
-	describe('drive/files/create', () => {
-		it('ファイルを作成できる', async(async () => {
-			const me = await insertSakurako();
-			const res = await chai.request(server)
-				.post('/drive/files/create')
-				.field('i', me.token)
-				.attach('file', fs.readFileSync(__dirname + '/resources/Lenna.png'), 'Lenna.png');
-			res.should.have.status(200);
-			res.body.should.be.a('object');
-			res.body.should.have.property('name').eql('Lenna.png');
-		}));
-
-		it('ファイル無しで怒られる', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/drive/files/create', {}, me);
-			res.should.have.status(400);
-		}));
-	});
-
-	describe('drive/files/update', () => {
-		it('名前を更新できる', async(async () => {
-			const me = await insertSakurako();
-			const file = await insertDriveFile({
-				userId: me._id
-			});
-			const newName = 'いちごパスタ.png';
-			const res = await request('/drive/files/update', {
-				fileId: file._id.toString(),
-				name: newName
-			}, me);
-			res.should.have.status(200);
-			res.body.should.be.a('object');
-			res.body.should.have.property('name').eql(newName);
-		}));
-
-		it('他人のファイルは更新できない', async(async () => {
-			const me = await insertSakurako();
-			const hima = await insertHimawari();
-			const file = await insertDriveFile({
-				userId: hima._id
-			});
-			const res = await request('/drive/files/update', {
-				fileId: file._id.toString(),
-				name: 'いちごパスタ.png'
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('親フォルダを更新できる', async(async () => {
-			const me = await insertSakurako();
-			const file = await insertDriveFile({
-				userId: me._id
-			});
-			const folder = await insertDriveFolder({
-				userId: me._id
-			});
-			const res = await request('/drive/files/update', {
-				fileId: file._id.toString(),
-				folderId: folder._id.toString()
-			}, me);
-			res.should.have.status(200);
-			res.body.should.be.a('object');
-			res.body.should.have.property('folderId').eql(folder._id.toString());
-		}));
-
-		it('親フォルダを無しにできる', async(async () => {
-			const me = await insertSakurako();
-			const file = await insertDriveFile({
-				userId: me._id,
-				folderId: '000000000000000000000000'
-			});
-			const res = await request('/drive/files/update', {
-				fileId: file._id.toString(),
-				folderId: null
-			}, me);
-			res.should.have.status(200);
-			res.body.should.be.a('object');
-			res.body.should.have.property('folderId').eql(null);
-		}));
-
-		it('他人のフォルダには入れられない', async(async () => {
-			const me = await insertSakurako();
-			const hima = await insertHimawari();
-			const file = await insertDriveFile({
-				userId: me._id
-			});
-			const folder = await insertDriveFolder({
-				userId: hima._id
-			});
-			const res = await request('/drive/files/update', {
-				fileId: file._id.toString(),
-				folderId: folder._id.toString()
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('存在しないフォルダで怒られる', async(async () => {
-			const me = await insertSakurako();
-			const file = await insertDriveFile({
-				userId: me._id
-			});
-			const res = await request('/drive/files/update', {
-				fileId: file._id.toString(),
-				folderId: '000000000000000000000000'
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('不正なフォルダIDで怒られる', async(async () => {
-			const me = await insertSakurako();
-			const file = await insertDriveFile({
-				userId: me._id
-			});
-			const res = await request('/drive/files/update', {
-				fileId: file._id.toString(),
-				folderId: 'kyoppie'
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('ファイルが存在しなかったら怒る', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/drive/files/update', {
-				fileId: '000000000000000000000000',
-				name: 'いちごパスタ.png'
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('間違ったIDで怒られる', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/drive/files/update', {
-				fileId: 'kyoppie',
-				name: 'いちごパスタ.png'
-			}, me);
-			res.should.have.status(400);
-		}));
-	});
-
-	describe('drive/folders/create', () => {
-		it('フォルダを作成できる', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/drive/folders/create', {
-				name: 'my folder'
-			}, me);
-			res.should.have.status(200);
-			res.body.should.be.a('object');
-			res.body.should.have.property('name').eql('my folder');
-		}));
-	});
-
-	describe('drive/folders/update', () => {
-		it('名前を更新できる', async(async () => {
-			const me = await insertSakurako();
-			const folder = await insertDriveFolder({
-				userId: me._id
-			});
-			const res = await request('/drive/folders/update', {
-				folderId: folder._id.toString(),
-				name: 'new name'
-			}, me);
-			res.should.have.status(200);
-			res.body.should.be.a('object');
-			res.body.should.have.property('name').eql('new name');
-		}));
-
-		it('他人のフォルダを更新できない', async(async () => {
-			const me = await insertSakurako();
-			const hima = await insertHimawari();
-			const folder = await insertDriveFolder({
-				userId: hima._id
-			});
-			const res = await request('/drive/folders/update', {
-				folderId: folder._id.toString(),
-				name: 'new name'
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('親フォルダを更新できる', async(async () => {
-			const me = await insertSakurako();
-			const folder = await insertDriveFolder({
-				userId: me._id
-			});
-			const parentFolder = await insertDriveFolder({
-				userId: me._id
-			});
-			const res = await request('/drive/folders/update', {
-				folderId: folder._id.toString(),
-				parentId: parentFolder._id.toString()
-			}, me);
-			res.should.have.status(200);
-			res.body.should.be.a('object');
-			res.body.should.have.property('parentId').eql(parentFolder._id.toString());
-		}));
-
-		it('親フォルダを無しに更新できる', async(async () => {
-			const me = await insertSakurako();
-			const folder = await insertDriveFolder({
-				userId: me._id,
-				parentId: '000000000000000000000000'
-			});
-			const res = await request('/drive/folders/update', {
-				folderId: folder._id.toString(),
-				parentId: null
-			}, me);
-			res.should.have.status(200);
-			res.body.should.be.a('object');
-			res.body.should.have.property('parentId').eql(null);
-		}));
-
-		it('他人のフォルダを親フォルダに設定できない', async(async () => {
-			const me = await insertSakurako();
-			const hima = await insertHimawari();
-			const folder = await insertDriveFolder({
-				userId: me._id
-			});
-			const parentFolder = await insertDriveFolder({
-				userId: hima._id
-			});
-			const res = await request('/drive/folders/update', {
-				folderId: folder._id.toString(),
-				parentId: parentFolder._id.toString()
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('フォルダが循環するような構造にできない', async(async () => {
-			const me = await insertSakurako();
-			const folder = await insertDriveFolder();
-			const parentFolder = await insertDriveFolder({
-				parentId: folder._id
-			});
-			const res = await request('/drive/folders/update', {
-				folderId: folder._id.toString(),
-				parentId: parentFolder._id.toString()
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('フォルダが循環するような構造にできない(再帰的)', async(async () => {
-			const me = await insertSakurako();
-			const folderA = await insertDriveFolder();
-			const folderB = await insertDriveFolder({
-				parentId: folderA._id
-			});
-			const folderC = await insertDriveFolder({
-				parentId: folderB._id
-			});
-			const res = await request('/drive/folders/update', {
-				folderId: folderA._id.toString(),
-				parentId: folderC._id.toString()
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('存在しない親フォルダを設定できない', async(async () => {
-			const me = await insertSakurako();
-			const folder = await insertDriveFolder();
-			const res = await request('/drive/folders/update', {
-				folderId: folder._id.toString(),
-				parentId: '000000000000000000000000'
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('不正な親フォルダIDで怒られる', async(async () => {
-			const me = await insertSakurako();
-			const folder = await insertDriveFolder();
-			const res = await request('/drive/folders/update', {
-				folderId: folder._id.toString(),
-				parentId: 'kyoppie'
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('存在しないフォルダを更新できない', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/drive/folders/update', {
-				folderId: '000000000000000000000000'
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('不正なフォルダIDで怒られる', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/drive/folders/update', {
-				folderId: 'kyoppie'
-			}, me);
-			res.should.have.status(400);
-		}));
-	});
-
-	describe('messaging/messages/create', () => {
-		it('メッセージを送信できる', async(async () => {
-			const me = await insertSakurako();
-			const hima = await insertHimawari();
-			const res = await request('/messaging/messages/create', {
-				userId: hima._id.toString(),
-				text: 'Hey hey ひまわり'
-			}, me);
-			res.should.have.status(200);
-			res.body.should.be.a('object');
-			res.body.should.have.property('text').eql('Hey hey ひまわり');
-		}));
-
-		it('自分自身にはメッセージを送信できない', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/messaging/messages/create', {
-				userId: me._id.toString(),
-				text: 'Yo'
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('存在しないユーザーにはメッセージを送信できない', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/messaging/messages/create', {
-				userId: '000000000000000000000000',
-				text: 'Yo'
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('不正なユーザーIDで怒られる', async(async () => {
-			const me = await insertSakurako();
-			const res = await request('/messaging/messages/create', {
-				userId: 'kyoppie',
-				text: 'Yo'
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('テキストが無くて怒られる', async(async () => {
-			const me = await insertSakurako();
-			const hima = await insertHimawari();
-			const res = await request('/messaging/messages/create', {
-				userId: hima._id.toString()
-			}, me);
-			res.should.have.status(400);
-		}));
-
-		it('文字数オーバーで怒られる', async(async () => {
-			const me = await insertSakurako();
-			const hima = await insertHimawari();
-			const res = await request('/messaging/messages/create', {
-				userId: hima._id.toString(),
-				text: '!'.repeat(1001)
-			}, me);
-			res.should.have.status(400);
-		}));
-	});
-
-	describe('auth/session/generate', () => {
-		it('認証セッションを作成できる', async(async () => {
-			const app = await insertApp();
-			const res = await request('/auth/session/generate', {
-				appSecret: app.secret
-			});
-			res.should.have.status(200);
-			res.body.should.be.a('object');
-			res.body.should.have.property('token');
-			res.body.should.have.property('url');
-		}));
-
-		it('appSecret 無しで怒られる', async(async () => {
-			const res = await request('/auth/session/generate', {});
-			res.should.have.status(400);
-		}));
-
-		it('誤った appSecret で怒られる', async(async () => {
-			const res = await request('/auth/session/generate', {
-				appSecret: 'kyoppie'
-			});
-			res.should.have.status(400);
-		}));
-	});
-});
-
-function insertSakurako(opts?) {
-	return db.get('users').insert(merge({
-		username: 'sakurako',
-		usernameLower: 'sakurako',
-		account: {
-			keypair: '-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAtdTG9rlFWjNqhgbg2V6X5XF1WpQXZS3KNXykEWl2UAiMyfVV\nBvf3zQP0dDEdNtcqdPJgis03bpiHCzQusc/YLyHYB0m+TJXsxJatb8cqUogOFeE4\ngQ4Dc5kAT6gLh/d4yz03EIg9bizX07EiGWnZqWxb+21ypqsPxST64sAtG9f5O/G4\nXe2m3cSbfAAvEUP1Ig1LUNyJB4jhM60w1cQic/qO8++sk/+GoX9g71X+i4NArGv+\n1c11acDIIPGAAQpFeYVeGaKakNDNp8RtJJp8R8FLwJXZ4/gATBnScCiHUSrGfRly\nYyR0w/BNlQ6/NijAdB9pR5csPvyIPkx1gauZewIDAQABAoIBAQCwWf/mhuY2h6uG\n9eDZsZ7Mj2/sO7k9Dl4R5iMSKCDxmnlB3slqitExa+aJUqEs8R5icjkkJcjfYNuJ\nCEFJf3YCsGZfGyyQBtCuEh2ATcBEb2SJ3/f3YuoCEaB1oVwdsOzc4TAovpol4yQo\nUqHp1/mdElVb01jhQQN4h1c02IJnfzvfU1C8szBni+Etfd+MxqGfv006DY3KOEb3\nlCrCS3GmooJW2Fjj7q1kCcaEQbMB1/aQHLXd1qe3KJOzXh3Voxsp/jEH0hvp2TII\nfY9UK+b7mA+xlvXwKuTkHVaZm0ylg0nbembS8MF4GfFMujinSexvLrVKaQhdMFoF\nvBLxHYHRAoGBANfNVYJYeCDPFNLmak5Xg33Rfvc2II8UmrZOVdhOWs8ZK0pis9e+\nPo2MKtTzrzipXI2QXv5w7kO+LJWNDva+xRlW8Wlj9Dde9QdQ7Y8+dk7SJgf24DzM\n023elgX5DvTeLODjStk6SMPRL0FmGovUqAAA8ZeHtJzkIr1HROWnQiwnAoGBANez\nhFwKnVoQu0RpBz/i4W0RKIxOwltN2zmlN8KjJPhSy00A7nBUfKLRbcwiSHE98Yi/\nUrXwMwR5QeD2ngngRppddJnpiRfjNjnsaqeqNtpO8AxB3XjpCC5zmHUMFHKvPpDj\n1zU/F44li0YjKcMBebZy9PbfAjrIgJfxhPo/oXiNAoGAfx6gaTjOAp2ZaaZ7Jozc\nkyft/5et1DrR6+P3I4T8bxQncRj1UXfqhxzzOiAVrm3tbCKIIp/JarRCtRGzp9u2\nZPfXGzra6CcSdW3Rkli7/jBCYNynOIl7XjQI8ZnFmq6phwu80ntH07mMeZy4tHff\nQqlLpvQ0i1rDr/Wkexdsnm8CgYBgxha9ILoF/Xm3MJPjEsxmnYsen/tM8XpIu5pv\nxbhBfQvfKWrQlOcyOVnUexEbVVo3KvdVz0VkXW60GpE/BxNGEGXO49rxD6x1gl87\nh/+CJGZIaYiOxaY5CP2+jcPizEL6yG32Yq8TxD5fIkmLRu8vbxX+aIFclDY1dVNe\n3wt3xQKBgGEL0EjwRch+P2V+YHAhbETPrEqJjHRWT95pIdF9XtC8fasSOVH81cLX\nXXsX1FTvOJNwG9Nk8rQjYJXGTb2O/2unaazlYUwxKwVpwuGzz/vhH/roHZBAkIVT\njvpykpn9QMezEdpzj5BEv01QzSYBPzIh5myrpoJIoSW7py7zFG3h\n-----END RSA PRIVATE KEY-----\n',
-			token: '!00000000000000000000000000000000',
-			password: '$2a$08$FnHXg3tP.M/kINWgQSXNqeoBsiVrkj.ecXX8mW9rfBzMRkibYfjYy', // HimawariDaisuki06160907
-			profile: {},
-			settings: {},
-			clientSettings: {}
-		}
-	}, opts));
-}
-
-function insertHimawari(opts?) {
-	return db.get('users').insert(merge({
-		username: 'himawari',
-		usernameLower: 'himawari',
-		account: {
-			keypair: '-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAtdTG9rlFWjNqhgbg2V6X5XF1WpQXZS3KNXykEWl2UAiMyfVV\nBvf3zQP0dDEdNtcqdPJgis03bpiHCzQusc/YLyHYB0m+TJXsxJatb8cqUogOFeE4\ngQ4Dc5kAT6gLh/d4yz03EIg9bizX07EiGWnZqWxb+21ypqsPxST64sAtG9f5O/G4\nXe2m3cSbfAAvEUP1Ig1LUNyJB4jhM60w1cQic/qO8++sk/+GoX9g71X+i4NArGv+\n1c11acDIIPGAAQpFeYVeGaKakNDNp8RtJJp8R8FLwJXZ4/gATBnScCiHUSrGfRly\nYyR0w/BNlQ6/NijAdB9pR5csPvyIPkx1gauZewIDAQABAoIBAQCwWf/mhuY2h6uG\n9eDZsZ7Mj2/sO7k9Dl4R5iMSKCDxmnlB3slqitExa+aJUqEs8R5icjkkJcjfYNuJ\nCEFJf3YCsGZfGyyQBtCuEh2ATcBEb2SJ3/f3YuoCEaB1oVwdsOzc4TAovpol4yQo\nUqHp1/mdElVb01jhQQN4h1c02IJnfzvfU1C8szBni+Etfd+MxqGfv006DY3KOEb3\nlCrCS3GmooJW2Fjj7q1kCcaEQbMB1/aQHLXd1qe3KJOzXh3Voxsp/jEH0hvp2TII\nfY9UK+b7mA+xlvXwKuTkHVaZm0ylg0nbembS8MF4GfFMujinSexvLrVKaQhdMFoF\nvBLxHYHRAoGBANfNVYJYeCDPFNLmak5Xg33Rfvc2II8UmrZOVdhOWs8ZK0pis9e+\nPo2MKtTzrzipXI2QXv5w7kO+LJWNDva+xRlW8Wlj9Dde9QdQ7Y8+dk7SJgf24DzM\n023elgX5DvTeLODjStk6SMPRL0FmGovUqAAA8ZeHtJzkIr1HROWnQiwnAoGBANez\nhFwKnVoQu0RpBz/i4W0RKIxOwltN2zmlN8KjJPhSy00A7nBUfKLRbcwiSHE98Yi/\nUrXwMwR5QeD2ngngRppddJnpiRfjNjnsaqeqNtpO8AxB3XjpCC5zmHUMFHKvPpDj\n1zU/F44li0YjKcMBebZy9PbfAjrIgJfxhPo/oXiNAoGAfx6gaTjOAp2ZaaZ7Jozc\nkyft/5et1DrR6+P3I4T8bxQncRj1UXfqhxzzOiAVrm3tbCKIIp/JarRCtRGzp9u2\nZPfXGzra6CcSdW3Rkli7/jBCYNynOIl7XjQI8ZnFmq6phwu80ntH07mMeZy4tHff\nQqlLpvQ0i1rDr/Wkexdsnm8CgYBgxha9ILoF/Xm3MJPjEsxmnYsen/tM8XpIu5pv\nxbhBfQvfKWrQlOcyOVnUexEbVVo3KvdVz0VkXW60GpE/BxNGEGXO49rxD6x1gl87\nh/+CJGZIaYiOxaY5CP2+jcPizEL6yG32Yq8TxD5fIkmLRu8vbxX+aIFclDY1dVNe\n3wt3xQKBgGEL0EjwRch+P2V+YHAhbETPrEqJjHRWT95pIdF9XtC8fasSOVH81cLX\nXXsX1FTvOJNwG9Nk8rQjYJXGTb2O/2unaazlYUwxKwVpwuGzz/vhH/roHZBAkIVT\njvpykpn9QMezEdpzj5BEv01QzSYBPzIh5myrpoJIoSW7py7zFG3h\n-----END RSA PRIVATE KEY-----\n',
-			token: '!00000000000000000000000000000001',
-			password: '$2a$08$OPESxR2RE/ZijjGanNKk6ezSqGFitqsbZqTjWUZPLhORMKxHCbc4O', // ilovesakurako
-			profile: {},
-			settings: {},
-			clientSettings: {}
-		}
-	}, opts));
-}
-
-function insertDriveFile(opts?) {
-	return db.get('driveFiles.files').insert({
-		length: opts.datasize,
-		filename: 'strawberry-pasta.png',
-		metadata: opts
-	});
-}
-
-function insertDriveFolder(opts?) {
-	return db.get('driveFolders').insert(merge({
-		name: 'my folder',
-		parentId: null
-	}, opts));
-}
-
-function insertApp(opts?) {
-	return db.get('apps').insert(merge({
-		name: 'my app',
-		secret: 'mysecret'
-	}, opts));
-}

From 60b60fda8d349067388b0042bda6a8b0e3c55cd3 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 18 May 2018 07:19:13 +0900
Subject: [PATCH 20/35] Update package.json

---
 package.json | 19 +++++++------------
 1 file changed, 7 insertions(+), 12 deletions(-)

diff --git a/package.json b/package.json
index d4ca353c8f..58606a70fe 100644
--- a/package.json
+++ b/package.json
@@ -31,8 +31,6 @@
 		"@prezzemolo/rap": "0.1.2",
 		"@prezzemolo/zip": "0.0.3",
 		"@types/bcryptjs": "2.4.1",
-		"@types/chai": "4.1.3",
-		"@types/chai-http": "3.0.4",
 		"@types/debug": "0.0.30",
 		"@types/deep-equal": "1.0.1",
 		"@types/elasticsearch": "5.0.23",
@@ -58,7 +56,7 @@
 		"@types/koa-multer": "1.0.0",
 		"@types/koa-router": "7.0.28",
 		"@types/koa-send": "4.1.1",
-		"@types/koa-views": "^2.0.3",
+		"@types/koa-views": "2.0.3",
 		"@types/koa__cors": "2.2.2",
 		"@types/kue": "0.11.8",
 		"@types/license-checker": "15.0.0",
@@ -69,7 +67,7 @@
 		"@types/ms": "0.7.30",
 		"@types/node": "10.1.0",
 		"@types/nopt": "3.0.29",
-		"@types/parse5": "^3.0.0",
+		"@types/parse5": "3.0.0",
 		"@types/pug": "2.0.4",
 		"@types/qrcode": "0.8.1",
 		"@types/ratelimiter": "2.1.28",
@@ -78,7 +76,7 @@
 		"@types/request-promise-native": "1.0.14",
 		"@types/rimraf": "2.0.2",
 		"@types/seedrandom": "2.4.27",
-		"@types/single-line-log": "^1.1.0",
+		"@types/single-line-log": "1.1.0",
 		"@types/speakeasy": "2.0.2",
 		"@types/tmp": "0.0.33",
 		"@types/uuid": "3.4.3",
@@ -92,8 +90,6 @@
 		"bcryptjs": "2.4.3",
 		"bootstrap-vue": "2.0.0-rc.6",
 		"cafy": "8.0.0",
-		"chai": "4.1.2",
-		"chai-http": "4.0.0",
 		"chalk": "2.4.1",
 		"crc-32": "1.2.0",
 		"css-loader": "0.28.11",
@@ -148,7 +144,7 @@
 		"koa-router": "7.4.0",
 		"koa-send": "4.1.3",
 		"koa-slow": "2.1.0",
-		"koa-views": "^6.1.4",
+		"koa-views": "6.1.4",
 		"kue": "0.11.6",
 		"license-checker": "19.0.0",
 		"loader-utils": "1.1.0",
@@ -167,10 +163,10 @@
 		"object-assign-deep": "0.4.0",
 		"on-build-webpack": "0.1.0",
 		"os-utils": "0.0.14",
-		"parse5": "^4.0.0",
+		"parse5": "4.0.0",
 		"progress-bar-webpack-plugin": "1.11.0",
 		"prominence": "0.2.0",
-		"promise-sequential": "^1.1.1",
+		"promise-sequential": "1.1.1",
 		"pug": "2.0.3",
 		"punycode": "2.1.0",
 		"qrcode": "1.2.0",
@@ -185,7 +181,7 @@
 		"s-age": "1.1.2",
 		"sass-loader": "7.0.1",
 		"seedrandom": "2.4.3",
-		"single-line-log": "^1.1.2",
+		"single-line-log": "1.1.2",
 		"speakeasy": "2.0.0",
 		"style-loader": "0.21.0",
 		"stylus": "0.54.5",
@@ -218,7 +214,6 @@
 		"webfinger.js": "2.6.6",
 		"webpack": "4.8.3",
 		"webpack-cli": "2.1.3",
-		"webpack-replace-loader": "1.3.0",
 		"websocket": "1.0.26",
 		"ws": "5.1.1",
 		"xev": "2.0.0"

From fbd51f0079bc68e7f4768f8f09c420c38ccfd7df Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 18 May 2018 09:21:19 +0900
Subject: [PATCH 21/35] :v:

---
 test/text.ts | 10 +++-------
 1 file changed, 3 insertions(+), 7 deletions(-)

diff --git a/test/text.ts b/test/text.ts
index 8ce55cd1bc..a64999fc0b 100644
--- a/test/text.ts
+++ b/test/text.ts
@@ -1,11 +1,7 @@
-/**
- * Text Tests!
- */
+import * as assert from 'assert';
 
-const assert = require('assert');
-
-const analyze = require('../built/text/parse').default;
-const syntaxhighlighter = require('../built/text/parse/core/syntax-highlighter').default;
+import analyze from '../src/text/parse';
+import syntaxhighlighter from '../src/text/parse/core/syntax-highlighter';
 
 describe('Text', () => {
 	it('can be analyzed', () => {

From 6ad90ecfa85f0cccb4365de8192e03026f1d5032 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 18 May 2018 09:40:57 +0900
Subject: [PATCH 22/35] :art:

---
 .../app/common/views/widgets/calendar.vue     | 195 +++++++++---------
 .../views/components/widget-container.vue     |   3 +
 src/client/app/mobile/views/pages/widgets.vue |   1 +
 3 files changed, 96 insertions(+), 103 deletions(-)

diff --git a/src/client/app/common/views/widgets/calendar.vue b/src/client/app/common/views/widgets/calendar.vue
index 41e9253784..0e9714960a 100644
--- a/src/client/app/common/views/widgets/calendar.vue
+++ b/src/client/app/common/views/widgets/calendar.vue
@@ -1,37 +1,37 @@
 <template>
-<div class="mkw-calendar"
-	:data-melt="props.design == 1"
-	:data-special="special"
-	:data-mobile="isMobile"
->
-	<div class="calendar" :data-is-holiday="isHoliday">
-		<p class="month-and-year">
-			<span class="year">{{ year }}年</span>
-			<span class="month">{{ month }}月</span>
-		</p>
-		<p class="day">{{ day }}日</p>
-		<p class="week-day">{{ weekDay }}曜日</p>
-	</div>
-	<div class="info">
-		<div>
-			<p>今日:<b>{{ dayP.toFixed(1) }}%</b></p>
-			<div class="meter">
-				<div class="val" :style="{ width: `${dayP}%` }"></div>
+<div class="mkw-calendar" :data-special="special" :data-mobile="isMobile">
+	<mk-widget-container :naked="props.design == 1" :show-header="false">
+		<div class="mkw-calendar--body">
+			<div class="calendar" :data-is-holiday="isHoliday">
+				<p class="month-and-year">
+					<span class="year">{{ year }}年</span>
+					<span class="month">{{ month }}月</span>
+				</p>
+				<p class="day">{{ day }}日</p>
+				<p class="week-day">{{ weekDay }}曜日</p>
+			</div>
+			<div class="info">
+				<div>
+					<p>今日:<b>{{ dayP.toFixed(1) }}%</b></p>
+					<div class="meter">
+						<div class="val" :style="{ width: `${dayP}%` }"></div>
+					</div>
+				</div>
+				<div>
+					<p>今月:<b>{{ monthP.toFixed(1) }}%</b></p>
+					<div class="meter">
+						<div class="val" :style="{ width: `${monthP}%` }"></div>
+					</div>
+				</div>
+				<div>
+					<p>今年:<b>{{ yearP.toFixed(1) }}%</b></p>
+					<div class="meter">
+						<div class="val" :style="{ width: `${yearP}%` }"></div>
+					</div>
+				</div>
 			</div>
 		</div>
-		<div>
-			<p>今月:<b>{{ monthP.toFixed(1) }}%</b></p>
-			<div class="meter">
-				<div class="val" :style="{ width: `${monthP}%` }"></div>
-			</div>
-		</div>
-		<div>
-			<p>今年:<b>{{ yearP.toFixed(1) }}%</b></p>
-			<div class="meter">
-				<div class="val" :style="{ width: `${yearP}%` }"></div>
-			</div>
-		</div>
-	</div>
+	</mk-widget-container>
 </div>
 </template>
 
@@ -111,93 +111,82 @@ export default define({
 @import '~const.styl'
 
 root(isDark)
-	padding 16px 0
-	color isDark ? #c5ced6 :#777
-	background isDark ? #282C37 : #fff
-	border solid 1px rgba(#000, 0.075)
-	border-radius 6px
-
 	&[data-special='on-new-years-day']
 		border-color #ef95a0
 
-	&[data-melt]
-		background transparent
-		border none
+	.mkw-calendar--body
+		padding 16px 0
+		color isDark ? #c5ced6 : #777
 
-	&[data-mobile]
-		border none
-		border-radius 8px
-		box-shadow 0 0 0 1px rgba(#000, 0.2)
+		&:after
+			content ""
+			display block
+			clear both
 
-	&:after
-		content ""
-		display block
-		clear both
+		> .calendar
+			float left
+			width 60%
+			text-align center
 
-	> .calendar
-		float left
-		width 60%
-		text-align center
-
-		&[data-is-holiday]
-			> .day
-				color #ef95a0
-
-		> p
-			margin 0
-			line-height 18px
-			font-size 14px
-
-			> span
-				margin 0 4px
-
-		> .day
-			margin 10px 0
-			line-height 32px
-			font-size 28px
-
-	> .info
-		display block
-		float left
-		width 40%
-		padding 0 16px 0 0
-
-		> div
-			margin-bottom 8px
-
-			&:last-child
-				margin-bottom 4px
+			&[data-is-holiday]
+				> .day
+					color #ef95a0
 
 			> p
-				margin 0 0 2px 0
-				font-size 12px
+				margin 0
 				line-height 18px
-				color isDark ? #7a8692 : #888
+				font-size 14px
 
-				> b
-					margin-left 2px
+				> span
+					margin 0 4px
 
-			> .meter
-				width 100%
-				overflow hidden
-				background isDark ? #1c1f25 : #eee
-				border-radius 8px
+			> .day
+				margin 10px 0
+				line-height 32px
+				font-size 28px
 
-				> .val
-					height 4px
-					background $theme-color
+		> .info
+			display block
+			float left
+			width 40%
+			padding 0 16px 0 0
 
-			&:nth-child(1)
-				> .meter > .val
-					background #f7796c
+			> div
+				margin-bottom 8px
 
-			&:nth-child(2)
-				> .meter > .val
-					background #a1de41
+				&:last-child
+					margin-bottom 4px
 
-			&:nth-child(3)
-				> .meter > .val
-					background #41ddde
+				> p
+					margin 0 0 2px 0
+					font-size 12px
+					line-height 18px
+					color isDark ? #7a8692 : #888
+
+					> b
+						margin-left 2px
+
+				> .meter
+					width 100%
+					overflow hidden
+					background isDark ? #1c1f25 : #eee
+					border-radius 8px
+
+					> .val
+						height 4px
+						background $theme-color
+
+				&:nth-child(1)
+					> .meter > .val
+						background #f7796c
+
+				&:nth-child(2)
+					> .meter > .val
+						background #a1de41
+
+				&:nth-child(3)
+					> .meter > .val
+						background #41ddde
 
 .mkw-calendar[data-darkmode]
 	root(true)
diff --git a/src/client/app/mobile/views/components/widget-container.vue b/src/client/app/mobile/views/components/widget-container.vue
index 8a97848b73..a713a10621 100644
--- a/src/client/app/mobile/views/components/widget-container.vue
+++ b/src/client/app/mobile/views/components/widget-container.vue
@@ -35,6 +35,9 @@ root(isDark)
 		background transparent !important
 		box-shadow none !important
 
+	&.hideHeader
+		background isDark ? #21242f : #fff
+
 	> header
 		> .title
 			margin 0
diff --git a/src/client/app/mobile/views/pages/widgets.vue b/src/client/app/mobile/views/pages/widgets.vue
index b4022fd58f..509ce16eef 100644
--- a/src/client/app/mobile/views/pages/widgets.vue
+++ b/src/client/app/mobile/views/pages/widgets.vue
@@ -143,6 +143,7 @@ main
 	margin 0 auto
 	padding 8px
 	max-width 500px
+	width 100%
 
 	@media (min-width 500px)
 		padding 16px 8px

From e880d7da6c7195e1afbab082023ca55dd3dad4f4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 18 May 2018 09:49:28 +0900
Subject: [PATCH 23/35] :art:

---
 src/client/app/common/views/widgets/rss.vue | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/src/client/app/common/views/widgets/rss.vue b/src/client/app/common/views/widgets/rss.vue
index b5339add0b..9e2c6b6490 100644
--- a/src/client/app/common/views/widgets/rss.vue
+++ b/src/client/app/common/views/widgets/rss.vue
@@ -1,10 +1,10 @@
 <template>
-<div class="mkw-rss" :data-mobile="isMobile">
+<div class="mkw-rss">
 	<mk-widget-container :show-header="!props.compact">
 		<template slot="header">%fa:rss-square%RSS</template>
 		<button slot="func" title="設定" @click="setting">%fa:cog%</button>
 
-		<div class="mkw-rss--body">
+		<div class="mkw-rss--body" :data-mobile="isMobile">
 			<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 			<div class="feed" v-else>
 				<a v-for="item in items" :href="item.link" target="_blank">{{ item.title }}</a>
@@ -85,15 +85,17 @@ root(isDark)
 				margin-right 4px
 
 		&[data-mobile]
+			background isDark ? #21242f : #f3f3f3
+
 			.feed
 				padding 0
-				font-size 1em
 
 				> a
 					padding 8px 16px
+					border-bottom none
 
 					&:nth-child(even)
-						background rgba(#000, 0.05)
+						background isDark ? rgba(#000, 0.05) : rgba(#fff, 0.7)
 
 .mkw-rss[data-darkmode]
 	root(true)

From 0926c92245fa53ab642f5ee4e5da8b7d192ea41f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 18 May 2018 09:52:10 +0900
Subject: [PATCH 24/35] 2.9.0

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

diff --git a/package.json b/package.json
index 58606a70fe..ef11bb4927 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "2.8.0",
-	"clientVersion": "1.0.5352",
+	"version": "2.9.0",
+	"clientVersion": "1.0.5394",
 	"codename": "nighthike",
 	"main": "./built/index.js",
 	"private": true,

From c0eb873feb4a8389e4e18b4bdfbfd808c369997e Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 18 May 2018 10:41:49 +0900
Subject: [PATCH 25/35] Create appveyor.yml

---
 appveyor.yml | 31 +++++++++++++++++++++++++++++++
 1 file changed, 31 insertions(+)
 create mode 100644 appveyor.yml

diff --git a/appveyor.yml b/appveyor.yml
new file mode 100644
index 0000000000..576ed44dbd
--- /dev/null
+++ b/appveyor.yml
@@ -0,0 +1,31 @@
+# appveyor file
+# http://www.appveyor.com/docs/appveyor-yml
+
+environment:
+  matrix:
+    - nodejs_version: 10.1.0
+
+build: off
+
+install:
+  # Update Node.js
+  # 標準で入っている Node.js を更新します (2014/11/13 時点では、v0.10.32 が標準)
+  - ps: Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version)
+  - node --version
+
+  # Update NPM
+  - npm install -g npm
+  - npm --version
+
+  # Update node-gyp
+  # 必須! node-gyp のバージョンを上げないと、ネイティブモジュールのコンパイルに失敗します
+  - npm install -g node-gyp
+
+  - npm install
+
+init:
+  # git clone の際の改行を変換しないようにします
+  - git config --global core.autocrlf false
+
+test_script:
+  - npm test

From c501bf4e16da084411d0a0def2a18b02bed24641 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 18 May 2018 10:53:39 +0900
Subject: [PATCH 26/35] Update appveyor.yml

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

diff --git a/appveyor.yml b/appveyor.yml
index 576ed44dbd..9522f3bbfa 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -5,6 +5,9 @@ environment:
   matrix:
     - nodejs_version: 10.1.0
 
+cache:
+  - node_modules
+
 build: off
 
 install:
@@ -27,5 +30,8 @@ init:
   # git clone の際の改行を変換しないようにします
   - git config --global core.autocrlf false
 
+before_test:
+  - npm run build
+
 test_script:
   - npm test

From db882ebb6f0c3c9f81e00da951266153ec2d54b9 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 18 May 2018 11:00:38 +0900
Subject: [PATCH 27/35] Update appveyor.yml

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

diff --git a/appveyor.yml b/appveyor.yml
index 9522f3bbfa..280d6a99ae 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -31,6 +31,10 @@ init:
   - git config --global core.autocrlf false
 
 before_test:
+  # 設定ファイルを配置
+  - cp ./.travis/default.yml ./.config
+  - cp ./.travis/test.yml ./.config
+
   - npm run build
 
 test_script:

From 9b0e83d9719dd3e97acde94f51e4d69df2ccd1cb Mon Sep 17 00:00:00 2001
From: mei23 <m@m544.net>
Date: Fri, 18 May 2018 10:34:48 +0900
Subject: [PATCH 28/35] Fix can't convert i18n docs

---
 src/client/docs/api/gulpfile.ts | 4 ++--
 src/client/docs/gulpfile.ts     | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/client/docs/api/gulpfile.ts b/src/client/docs/api/gulpfile.ts
index 31027c0be3..9980ede231 100644
--- a/src/client/docs/api/gulpfile.ts
+++ b/src/client/docs/api/gulpfile.ts
@@ -127,7 +127,7 @@ gulp.task('doc:api:endpoints', async () => {
 						return;
 					}
 					const i18n = new I18nReplacer(lang);
-					html = html.replace(i18n.pattern, i18n.replacement.bind(null, null));
+					html = html.replace(i18n.pattern, i18n.replacement);
 					html = fa(html);
 					const htmlPath = `./built/client/docs/${lang}/api/endpoints/${ep.endpoint}.html`;
 					mkdirp(path.dirname(htmlPath), (mkdirErr) => {
@@ -171,7 +171,7 @@ gulp.task('doc:api:entities', async () => {
 						return;
 					}
 					const i18n = new I18nReplacer(lang);
-					html = html.replace(i18n.pattern, i18n.replacement.bind(null, null));
+					html = html.replace(i18n.pattern, i18n.replacement);
 					html = fa(html);
 					const htmlPath = `./built/client/docs/${lang}/api/entities/${kebab(entity.name)}.html`;
 					mkdirp(path.dirname(htmlPath), (mkdirErr) => {
diff --git a/src/client/docs/gulpfile.ts b/src/client/docs/gulpfile.ts
index 5e81d6d3b5..56bf6188c8 100644
--- a/src/client/docs/gulpfile.ts
+++ b/src/client/docs/gulpfile.ts
@@ -53,7 +53,7 @@ gulp.task('doc:docs', async () => {
 						return;
 					}
 					const i18n = new I18nReplacer(lang);
-					html = html.replace(i18n.pattern, i18n.replacement.bind(null, null));
+					html = html.replace(i18n.pattern, i18n.replacement);
 					html = fa(html);
 					const htmlPath = `./built/client/docs/${lang}/${name}.html`;
 					mkdirp(path.dirname(htmlPath), (mkdirErr) => {

From 27582319fa3690cdb64407df5bcf0210fc791349 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 18 May 2018 12:08:05 +0900
Subject: [PATCH 29/35] Catch error

---
 src/server/web/url-preview.ts | 16 ++++++++++------
 1 file changed, 10 insertions(+), 6 deletions(-)

diff --git a/src/server/web/url-preview.ts b/src/server/web/url-preview.ts
index cd53837a25..8cc6f0316a 100644
--- a/src/server/web/url-preview.ts
+++ b/src/server/web/url-preview.ts
@@ -2,14 +2,18 @@ import * as Koa from 'koa';
 import summaly from 'summaly';
 
 module.exports = async (ctx: Koa.Context) => {
-	const summary = await summaly(ctx.query.url);
-	summary.icon = wrap(summary.icon);
-	summary.thumbnail = wrap(summary.thumbnail);
+	try {
+		const summary = await summaly(ctx.query.url);
+		summary.icon = wrap(summary.icon);
+		summary.thumbnail = wrap(summary.thumbnail);
 
-	// Cache 7days
-	ctx.set('Cache-Control', 'max-age=604800, immutable');
+		// Cache 7days
+		ctx.set('Cache-Control', 'max-age=604800, immutable');
 
-	ctx.body = summary;
+		ctx.body = summary;
+	} catch (e) {
+		ctx.status = 500;
+	}
 };
 
 function wrap(url: string): string {

From 345084a7643dde5f63d20eef3a9158e410e14e15 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 18 May 2018 12:08:26 +0900
Subject: [PATCH 30/35] 2.9.1

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

diff --git a/package.json b/package.json
index ef11bb4927..95aedae6d4 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "2.9.0",
+	"version": "2.9.1",
 	"clientVersion": "1.0.5394",
 	"codename": "nighthike",
 	"main": "./built/index.js",

From a01607fd97b7e9bfebed2cd2fefd0099a5c55e62 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 18 May 2018 12:21:53 +0900
Subject: [PATCH 31/35] :v:

---
 src/server/web/url-preview.ts | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/server/web/url-preview.ts b/src/server/web/url-preview.ts
index 8cc6f0316a..99ee2eaebd 100644
--- a/src/server/web/url-preview.ts
+++ b/src/server/web/url-preview.ts
@@ -3,7 +3,9 @@ import summaly from 'summaly';
 
 module.exports = async (ctx: Koa.Context) => {
 	try {
-		const summary = await summaly(ctx.query.url);
+		const summary = await summaly(ctx.query.url, {
+			followRedirects: false
+		});
 		summary.icon = wrap(summary.icon);
 		summary.thumbnail = wrap(summary.thumbnail);
 

From a6a175ede15789efc1c8b5220e4924fa43456c7d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 18 May 2018 14:31:30 +0900
Subject: [PATCH 32/35] :art:

---
 .../app/common/views/components/acct.vue      | 19 +++++++++++++++++++
 .../app/common/views/components/index.ts      |  2 ++
 .../views/components/note-detail.sub.vue      |  2 +-
 .../desktop/views/components/note-detail.vue  |  2 +-
 .../desktop/views/components/note-preview.vue |  2 +-
 .../views/components/notes.note.sub.vue       |  2 +-
 .../desktop/views/components/notes.note.vue   |  2 +-
 7 files changed, 26 insertions(+), 5 deletions(-)
 create mode 100644 src/client/app/common/views/components/acct.vue

diff --git a/src/client/app/common/views/components/acct.vue b/src/client/app/common/views/components/acct.vue
new file mode 100644
index 0000000000..1ad222afdd
--- /dev/null
+++ b/src/client/app/common/views/components/acct.vue
@@ -0,0 +1,19 @@
+<template>
+<span class="mk-acct">
+	<span class="name">@{{ user.username }}</span>
+	<span class="host" v-if="user.host">@{{ user.host }}</span>
+</span>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['user']
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-acct
+	> .host
+		opacity 0.5
+</style>
diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts
index 69fed00c74..c1a7bc61d7 100644
--- a/src/client/app/common/views/components/index.ts
+++ b/src/client/app/common/views/components/index.ts
@@ -3,6 +3,7 @@ import Vue from 'vue';
 import signin from './signin.vue';
 import signup from './signup.vue';
 import forkit from './forkit.vue';
+import acct from './acct.vue';
 import avatar from './avatar.vue';
 import nav from './nav.vue';
 import noteHtml from './note-html';
@@ -29,6 +30,7 @@ import welcomeTimeline from './welcome-timeline.vue';
 Vue.component('mk-signin', signin);
 Vue.component('mk-signup', signup);
 Vue.component('mk-forkit', forkit);
+Vue.component('mk-acct', acct);
 Vue.component('mk-avatar', avatar);
 Vue.component('mk-nav', nav);
 Vue.component('mk-note-html', noteHtml);
diff --git a/src/client/app/desktop/views/components/note-detail.sub.vue b/src/client/app/desktop/views/components/note-detail.sub.vue
index 24550c4e94..32119da50d 100644
--- a/src/client/app/desktop/views/components/note-detail.sub.vue
+++ b/src/client/app/desktop/views/components/note-detail.sub.vue
@@ -5,7 +5,7 @@
 		<header>
 			<div class="left">
 				<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
-				<span class="username">@{{ note.user | acct }}</span>
+				<span class="username"><mk-acct :user="note.user"/></span>
 			</div>
 			<div class="right">
 				<router-link class="time" :to="note | notePage">
diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue
index a0e3915149..bda53db918 100644
--- a/src/client/app/desktop/views/components/note-detail.vue
+++ b/src/client/app/desktop/views/components/note-detail.vue
@@ -28,7 +28,7 @@
 		<mk-avatar class="avatar" :user="p.user"/>
 		<header>
 			<router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link>
-			<span class="username">@{{ p.user | acct }}</span>
+			<span class="username"><mk-acct :user="p.user"/></span>
 			<router-link class="time" :to="p | notePage">
 				<mk-time :time="p.createdAt"/>
 			</router-link>
diff --git a/src/client/app/desktop/views/components/note-preview.vue b/src/client/app/desktop/views/components/note-preview.vue
index d04abfc5a7..2b4eff8e2f 100644
--- a/src/client/app/desktop/views/components/note-preview.vue
+++ b/src/client/app/desktop/views/components/note-preview.vue
@@ -4,7 +4,7 @@
 	<div class="main">
 		<header>
 			<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
-			<span class="username">@{{ note.user | acct }}</span>
+			<span class="username"><mk-acct :user="note.user"/></span>
 			<router-link class="time" :to="note | notePage">
 				<mk-time :time="note.createdAt"/>
 			</router-link>
diff --git a/src/client/app/desktop/views/components/notes.note.sub.vue b/src/client/app/desktop/views/components/notes.note.sub.vue
index 575d605203..503982b1a8 100644
--- a/src/client/app/desktop/views/components/notes.note.sub.vue
+++ b/src/client/app/desktop/views/components/notes.note.sub.vue
@@ -4,7 +4,7 @@
 	<div class="main">
 		<header>
 			<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
-			<span class="username">@{{ note.user | acct }}</span>
+			<span class="username"><mk-acct :user="note.user"/></span>
 			<div class="info">
 				<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span>
 				<router-link class="created-at" :to="note | notePage">
diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue
index 3ecef33d9a..44121684ee 100644
--- a/src/client/app/desktop/views/components/notes.note.vue
+++ b/src/client/app/desktop/views/components/notes.note.vue
@@ -17,7 +17,7 @@
 			<header>
 				<router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link>
 				<span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span>
-				<span class="username">@{{ p.user | acct }}</span>
+				<span class="username"><mk-acct :user="p.user"/></span>
 				<div class="info">
 					<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
 					<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>

From dad8fff12deccc49be001ca90003f9a0a260c58d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 18 May 2018 14:41:44 +0900
Subject: [PATCH 33/35] Fix ui

---
 locales/en.yml                                           | 3 +++
 locales/ja.yml                                           | 4 ++++
 src/client/app/desktop/views/components/note-preview.vue | 5 ++++-
 src/client/app/desktop/views/components/notes.note.vue   | 1 +
 src/client/app/mobile/views/components/note-preview.vue  | 3 ++-
 src/client/app/mobile/views/components/note.vue          | 1 +
 6 files changed, 15 insertions(+), 2 deletions(-)

diff --git a/locales/en.yml b/locales/en.yml
index 5580ead2d3..9f2a0be3ae 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -1,4 +1,7 @@
 ---
+meta:
+  lang: "English"
+  divider: " "
 common:
   misskey: "Share everything with others using Misskey."
   time:
diff --git a/locales/ja.yml b/locales/ja.yml
index 40527adcf3..d71251d203 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -1,3 +1,7 @@
+meta:
+  lang: "日本語"
+  divider: ""
+
 common:
   misskey: "Misskeyで皆と共有しよう。"
 
diff --git a/src/client/app/desktop/views/components/note-preview.vue b/src/client/app/desktop/views/components/note-preview.vue
index 2b4eff8e2f..302c5e803f 100644
--- a/src/client/app/desktop/views/components/note-preview.vue
+++ b/src/client/app/desktop/views/components/note-preview.vue
@@ -59,17 +59,20 @@ root(isDark)
 			> .name
 				margin 0 .5em 0 0
 				padding 0
+				overflow hidden
 				color isDark ? #fff : #607073
 				font-size 1em
 				font-weight bold
 				text-decoration none
-				white-space normal
+				text-overflow ellipsis
 
 				&:hover
 					text-decoration underline
 
 			> .username
 				margin 0 .5em 0 0
+				overflow hidden
+				text-overflow ellipsis
 				color isDark ? #606984 : #d1d8da
 
 			> .time
diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue
index 44121684ee..8660a5f899 100644
--- a/src/client/app/desktop/views/components/notes.note.vue
+++ b/src/client/app/desktop/views/components/notes.note.vue
@@ -350,6 +350,7 @@ root(isDark)
 		align-items center
 		padding 16px 32px
 		line-height 28px
+		white-space pre
 		color #9dbb00
 		background isDark ? linear-gradient(to bottom, #314027 0%, #282c37 100%) : linear-gradient(to bottom, #edfde2 0%, #fff 100%)
 
diff --git a/src/client/app/mobile/views/components/note-preview.vue b/src/client/app/mobile/views/components/note-preview.vue
index ec11f23315..b3ab088ffe 100644
--- a/src/client/app/mobile/views/components/note-preview.vue
+++ b/src/client/app/mobile/views/components/note-preview.vue
@@ -69,8 +69,9 @@ root(isDark)
 					text-decoration underline
 
 			> .username
-				text-align left
 				margin 0 .5em 0 0
+				overflow hidden
+				text-overflow ellipsis
 				color isDark ? #606984 : #d1d8da
 
 			> .time
diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue
index f5428b80cd..77a766f327 100644
--- a/src/client/app/mobile/views/components/note.vue
+++ b/src/client/app/mobile/views/components/note.vue
@@ -268,6 +268,7 @@ root(isDark)
 		align-items center
 		padding 8px 16px
 		line-height 28px
+		white-space pre
 		color #9dbb00
 		background isDark ? linear-gradient(to bottom, #314027 0%, #282c37 100%) : linear-gradient(to bottom, #edfde2 0%, #fff 100%)
 

From 1075e3a0050e44f03dde8fadc79872d974a75b0f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 18 May 2018 15:31:28 +0900
Subject: [PATCH 34/35] =?UTF-8?q?=E9=80=8F=E9=81=8E=E7=94=BB=E5=83=8F?=
 =?UTF-8?q?=E3=81=AE=E3=83=AC=E3=83=B3=E3=83=80=E3=83=AA=E3=83=B3=E3=82=B0?=
 =?UTF-8?q?=E3=82=92=E6=94=B9=E5=96=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/client/app/common/views/components/avatar.vue            | 2 +-
 src/client/app/desktop/views/components/drive.file.vue       | 4 ++--
 src/client/app/desktop/views/components/media-image.vue      | 2 +-
 src/client/app/desktop/views/pages/user/user.header.vue      | 2 +-
 src/client/app/mobile/views/components/drive.file-detail.vue | 2 +-
 src/client/app/mobile/views/components/drive.file.vue        | 2 +-
 src/client/app/mobile/views/components/media-image.vue       | 2 +-
 src/client/app/mobile/views/pages/user.vue                   | 2 +-
 src/services/drive/add-file.ts                               | 5 ++++-
 9 files changed, 13 insertions(+), 10 deletions(-)

diff --git a/src/client/app/common/views/components/avatar.vue b/src/client/app/common/views/components/avatar.vue
index a4648c272e..8ec359e83c 100644
--- a/src/client/app/common/views/components/avatar.vue
+++ b/src/client/app/common/views/components/avatar.vue
@@ -23,7 +23,7 @@ export default Vue.extend({
 	computed: {
 		style(): any {
 			return {
-				backgroundColor: this.user.avatarColor ? `rgb(${ this.user.avatarColor.join(',') })` : null,
+				backgroundColor: this.user.avatarColor && this.user.avatarColor.length == 3 ? `rgb(${ this.user.avatarColor.join(',') })` : null,
 				backgroundImage: `url(${ this.user.avatarUrl }?thumbnail)`,
 				borderRadius: (this as any).clientSettings.circleIcons ? '100%' : null
 			};
diff --git a/src/client/app/desktop/views/components/drive.file.vue b/src/client/app/desktop/views/components/drive.file.vue
index 39881711fa..d8b8420ece 100644
--- a/src/client/app/desktop/views/components/drive.file.vue
+++ b/src/client/app/desktop/views/components/drive.file.vue
@@ -50,7 +50,7 @@ export default Vue.extend({
 			return `${this.file.name}\n${this.file.type} ${Vue.filter('bytes')(this.file.datasize)}`;
 		},
 		background(): string {
-			return this.file.properties.avgColor
+			return this.file.properties.avgColor && this.file.properties.avgColor.length == 3
 				? `rgb(${this.file.properties.avgColor.join(',')})`
 				: 'transparent';
 		}
@@ -129,7 +129,7 @@ export default Vue.extend({
 		},
 
 		onThumbnailLoaded() {
-			if (this.file.properties.avgColor) {
+			if (this.file.properties.avgColor && this.file.properties.avgColor.length == 3) {
 				anime({
 					targets: this.$refs.thumbnail,
 					backgroundColor: `rgba(${this.file.properties.avgColor.join(',')}, 0)`,
diff --git a/src/client/app/desktop/views/components/media-image.vue b/src/client/app/desktop/views/components/media-image.vue
index e5803cc36e..b98a4707ec 100644
--- a/src/client/app/desktop/views/components/media-image.vue
+++ b/src/client/app/desktop/views/components/media-image.vue
@@ -26,7 +26,7 @@ export default Vue.extend({
 	computed: {
 		style(): any {
 			return {
-				'background-color': this.image.properties.avgColor ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
+				'background-color': this.image.properties.avgColor && this.image.properties.avgColor.length == 3 ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
 				'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.url}?thumbnail&size=512)`
 			};
 		}
diff --git a/src/client/app/desktop/views/pages/user/user.header.vue b/src/client/app/desktop/views/pages/user/user.header.vue
index 60dc15b15d..edb248dac7 100644
--- a/src/client/app/desktop/views/pages/user/user.header.vue
+++ b/src/client/app/desktop/views/pages/user/user.header.vue
@@ -29,7 +29,7 @@ export default Vue.extend({
 		style(): any {
 			if (this.user.bannerUrl == null) return {};
 			return {
-				backgroundColor: this.user.bannerColor ? `rgb(${ this.user.bannerColor.join(',') })` : null,
+				backgroundColor: this.user.bannerColor && this.user.bannerColor.length == 3 ? `rgb(${ this.user.bannerColor.join(',') })` : null,
 				backgroundImage: `url(${ this.user.bannerUrl })`
 			};
 		}
diff --git a/src/client/app/mobile/views/components/drive.file-detail.vue b/src/client/app/mobile/views/components/drive.file-detail.vue
index 764822e98c..ddf17d2723 100644
--- a/src/client/app/mobile/views/components/drive.file-detail.vue
+++ b/src/client/app/mobile/views/components/drive.file-detail.vue
@@ -86,7 +86,7 @@ export default Vue.extend({
 			return this.file.type.split('/')[0];
 		},
 		style(): any {
-			return this.file.properties.avgColor ? {
+			return this.file.properties.avgColor && this.file.properties.avgColor.length == 3 ? {
 				'background-color': `rgb(${ this.file.properties.avgColor.join(',') })`
 			} : {};
 		}
diff --git a/src/client/app/mobile/views/components/drive.file.vue b/src/client/app/mobile/views/components/drive.file.vue
index 7d1957042b..94c8ae3535 100644
--- a/src/client/app/mobile/views/components/drive.file.vue
+++ b/src/client/app/mobile/views/components/drive.file.vue
@@ -42,7 +42,7 @@ export default Vue.extend({
 		},
 		thumbnail(): any {
 			return {
-				'background-color': this.file.properties.avgColor ? `rgb(${this.file.properties.avgColor.join(',')})` : 'transparent',
+				'background-color': this.file.properties.avgColor && this.file.properties.avgColor.length == 3 ? `rgb(${this.file.properties.avgColor.join(',')})` : 'transparent',
 				'background-image': `url(${this.file.url}?thumbnail&size=128)`
 			};
 		}
diff --git a/src/client/app/mobile/views/components/media-image.vue b/src/client/app/mobile/views/components/media-image.vue
index 92d1cdc6f5..9e0f8e5f7e 100644
--- a/src/client/app/mobile/views/components/media-image.vue
+++ b/src/client/app/mobile/views/components/media-image.vue
@@ -18,7 +18,7 @@ export default Vue.extend({
 	computed: {
 		style(): any {
 			return {
-				'background-color': this.image.properties.avgColor ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
+				'background-color': this.image.properties.avgColor && this.image.properties.avgColor.length == 3 ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
 				'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.url}?thumbnail&size=512)`
 			};
 		}
diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue
index 27482dc215..f43454f9db 100644
--- a/src/client/app/mobile/views/pages/user.vue
+++ b/src/client/app/mobile/views/pages/user.vue
@@ -84,7 +84,7 @@ export default Vue.extend({
 		style(): any {
 			if (this.user.bannerUrl == null) return {};
 			return {
-				backgroundColor: this.user.bannerColor ? `rgb(${ this.user.bannerColor.join(',') })` : null,
+				backgroundColor: this.user.bannerColor && this.user.bannerColor.length == 3 ? `rgb(${ this.user.bannerColor.join(',') })` : null,
 				backgroundImage: `url(${ this.user.bannerUrl })`
 			};
 		}
diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts
index efabe345d1..bcd5bee512 100644
--- a/src/services/drive/add-file.ts
+++ b/src/services/drive/add-file.ts
@@ -171,6 +171,9 @@ const addFile = async (
 
 			log('calculate average color...');
 
+			const info = await prominence(gm(fs.createReadStream(path), name)).identify();
+			const isTransparent = info ? info['Channel depth'].Alpha != null : false;
+
 			const buffer = await prominence(gm(fs.createReadStream(path), name)
 				.setFormat('ppm')
 				.resize(1, 1)) // 1pxのサイズに縮小して平均色を取得するというハック
@@ -182,7 +185,7 @@ const addFile = async (
 
 			log(`average color is calculated: ${r}, ${g}, ${b}`);
 
-			return [r, g, b];
+			return isTransparent ? [r, g, b, 255] : [r, g, b];
 		})(),
 		// folder
 		(async () => {

From 4c3dccfc0b3a57c75aff8ad1285255865605bac6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 18 May 2018 15:32:11 +0900
Subject: [PATCH 35/35] 2.10.0

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

diff --git a/package.json b/package.json
index 95aedae6d4..6b7f63d6c8 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "2.9.1",
-	"clientVersion": "1.0.5394",
+	"version": "2.10.0",
+	"clientVersion": "1.0.5406",
 	"codename": "nighthike",
 	"main": "./built/index.js",
 	"private": true,