diff --git a/README.md b/README.md
index f8738421c..7a10f5b81 100644
--- a/README.md
+++ b/README.md
@@ -5,9 +5,7 @@
 
 [![][travis-badge]][travis-link]
 [![][dependencies-badge]][dependencies-link]
-[![][himawari-badge]][himasaku]
-[![][sakurako-badge]][himasaku]
-[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)
+[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) [![Greenkeeper badge](https://badges.greenkeeper.io/syuilo/misskey.svg)](https://greenkeeper.io/)
 
 > Lead Maintainer: [syuilo][syuilo-link]
 
@@ -16,7 +14,7 @@ ultimately sophisticated professional microblogging software.
 
 <a href="https://www.patreon.com/syuilo"><img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" alt="Become a Patron!" width="160" /></a>
 
-![](https://c10.patreonusercontent.com/3/e30%3D/patreon-posts/RsKWEDEKf8D_wYDQWAbex9CSb-1DnXW1nfqfLvuys5ROj2k0VF6_luuzHMTyf95n.png?token-time=1529539200&token-hash=RmcSP0947mw5o2-B6g1L6aU_OoDXANe198kLU6HMO30%3D)
+![](https://ja.mstdn.wiki/images/e/ed/Deck.jpg)
 
 :sparkles: Features
 ----------------------------------------------------------------
@@ -26,7 +24,7 @@ ultimately sophisticated professional microblogging software.
   * and widgets!
 * Private messages
 * Mute
-* Streaming
+* Real-time timelines
 * ActivityPub compatible
 
 and more! You can see it with your own eyes at [misskey.xyz](https://misskey.xyz).
@@ -49,9 +47,9 @@ If you want to...
 [![Backers][backers-image]][support-url]
 [![Sponsors][sponsors-image]][support-url]
 
-| ![][ooo-icon] |
-|:-:|
-| [ooo][ooo-link] |
+| ![][nagarus-icon] | ![][dansup-icon] |
+|:-:|:-:|
+| [nagarus][nagarus-link] | [dansup][dansup-link] |
 
 :four_leaf_clover: Copyright
 ----------------------------------------------------------------
@@ -67,9 +65,6 @@ Misskey is an open-source software licensed under [GNU AGPLv3](LICENSE).
 [travis-badge]:       http://img.shields.io/travis/syuilo/misskey/master.svg?style=flat-square
 [dependencies-link]:  https://david-dm.org/syuilo/misskey
 [dependencies-badge]: https://img.shields.io/david/syuilo/misskey.svg?style=flat-square
-[himasaku]:           https://himasaku.net
-[himawari-badge]:     https://img.shields.io/badge/%E5%8F%A4%E8%B0%B7-%E5%90%91%E6%97%A5%E8%91%B5-1684c5.svg?style=flat-square
-[sakurako-badge]:     https://img.shields.io/badge/%E5%A4%A7%E5%AE%A4-%E6%AB%BB%E5%AD%90-efb02a.svg?style=flat-square
 
 [backer-url]: #backers
 [backer-badge]: https://opencollective.com/misskey/backers/badge.svg
@@ -82,5 +77,8 @@ Misskey is an open-source software licensed under [GNU AGPLv3](LICENSE).
 [syuilo-link]:      https://syuilo.com
 [syuilo-icon]:      https://avatars2.githubusercontent.com/u/4439005?v=3&s=70
 
-[ooo-link]:   https://www.patreon.com/user/creators?u=11601413
-[ooo-icon]:   https://c10.patreonusercontent.com/3/eyJ2IjoiMSIsInciOjIwMH0%3D/patreon-media/user/11601413/20cb15f209924302b399b99d3c98b850?token-time=2145916800&token-hash=IO31nK6VZCMWBWU2VAk2c824BX2QZ4DNPKyHHZXS0iw%3D
+[nagarus-link]: https://www.patreon.com/user/creators?u=11601413
+[nagarus-icon]: https://c10.patreonusercontent.com/3/eyJ2IjoiMSIsInciOjIwMH0%3D/patreon-media/user/11601413/20cb15f209924302b399b99d3c98b850?token-time=2145916800&token-hash=IO31nK6VZCMWBWU2VAk2c824BX2QZ4DNPKyHHZXS0iw%3D
+[dansup-link]: https://www.patreon.com/dansup
+[dansup-icon]: https://c10.patreonusercontent.com/3/eyJ2IjoiMSIsInciOjIwMH0%3D/patreon-media/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb?token-time=2145916800&token-hash=opXAM_pnhUTuN1jCA6p_Nn_YsaqohY465YFjWFqMEEE%3D
+
diff --git a/DONATORS.md b/docs/DONATORS.md
similarity index 74%
rename from DONATORS.md
rename to docs/DONATORS.md
index 6fe5df04b..9da5c1a94 100644
--- a/DONATORS.md
+++ b/docs/DONATORS.md
@@ -22,8 +22,4 @@ The list of people who have sent donation for Misskey.
 
 ---
 
-If your name is missing,  please contact us!
-
-If you want to donate to Misskey, please get in touch with [@syuilo][syuilo-link].
-
-[syuilo-link]: https://syuilo.com
+If your name is missing, please contact us!
diff --git a/docs/donate.ja.md b/docs/donate.ja.md
deleted file mode 100644
index b19d7bc37..000000000
--- a/docs/donate.ja.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# Misskeyにカンパする方法
-Misskeyのサポートにご興味をお持ちいただきありがとうございます!
-Misskeyにカンパをしていただくと、貴方のお名前と好きなURLなどをMisskeyのリポジトリに刻む権利がもらえます。
-
-Misskeyにカンパして開発・運営をサポートするには、次のいくつかの方法があります:
-
-## ConoHaカードを購入する
-(本家)Misskeyは、ConoHaというVPSサービスを利用しています。ConoHaカードを購入して、
-カードに記載されているクーポンコードを syuilotan@yahoo.co.jp までお送りいただければ、
-そのクーポンをチャージしてサーバーの運営費に充てることができます。
-
-ConoHaカードについてはこちらをご覧ください: https://www.conoha.jp/conohacard/
-
-Amazonでも買えます: https://www.amazon.co.jp/dp/B01N9E3416
-
-## Amazonギフトカード
-これは間接的な方法です。
-
-## 銀行振込
-syuilotan@yahoo.co.jp までお問い合わせください。
-
-## 手渡し
-オフ会を行ったときなどに行使できる方法です。
-
-## その他
-なにかいいアイデアがあればお教えください。
diff --git a/locales/en.yml b/locales/en.yml
index 3b80c9b58..cdb179fe9 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -553,7 +553,7 @@ desktop/views/components/timeline.vue:
   global: "Global"
   list: "Lists"
 desktop/views/components/ui.header.vue:
-  welcome-back: "おかえりなさい、"
+  welcome-back: "Welcome back, "
 desktop/views/components/ui.header.account.vue:
   profile: "Your profile"
   drive: "Drive"
diff --git a/locales/index.ts b/locales/index.ts
index 2ae84f30a..45b5df095 100644
--- a/locales/index.ts
+++ b/locales/index.ts
@@ -5,7 +5,7 @@
 import * as fs from 'fs';
 import * as yaml from 'js-yaml';
 
-export type LangKey = 'de' | 'en' | 'fr' | 'ja' | 'pl';
+export type LangKey = 'de' | 'en' | 'fr' | 'ja' | 'pl' | 'es';
 export type LocaleObject = { [key: string]: any };
 
 const loadLang = (lang: LangKey) => yaml.safeLoad(
@@ -18,7 +18,8 @@ const langs: { [key: string]: LocaleObject } = {
 	'en': loadLang('en'),
 	'fr': loadLang('fr'),
 	'ja': native,
-	'pl': loadLang('pl')
+	'pl': loadLang('pl'),
+	'es': loadLang('es')
 };
 
 Object.entries(langs).map(([, locale]) => {
diff --git a/locales/ja.yml b/locales/ja.yml
index f2a85eb4e..fc790d8c5 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -47,12 +47,22 @@ common:
     e: "ここに書いてください"
     f: "あなたが書くのを待っています..."
 
+  search: "検索"
   delete: "削除"
   loading: "読み込み中"
   ok: "わかった"
+  update-available-title: "更新があります"
   update-available: "Misskeyの新しいバージョンがあります({newer}。現在{current}を利用中)。ページを再度読み込みすると更新が適用されます。"
   my-token-regenerated: "あなたのトークンが更新されたのでサインアウトします。"
   i-like-sushi: "私は(プリンよりむしろ)寿司が好き"
+  show-reversi-board-labels: "リバーシのボードの行と列のラベルを表示"
+
+  reversi:
+    drawn: "引き分け"
+    my-turn: "あなたのターンです"
+    opponent-turn: "相手のターンです"
+    turn-of: "{}のターンです"
+    past-turn-of: "{}のターン"
 
   widgets:
     analog-clock: "アナログ時計"
@@ -270,6 +280,13 @@ common/views/widgets/memo.vue:
   memo: "ここに書いて!"
   save: "保存"
 
+common/views/pages/follow.vue:
+  signed-in-as: "{}としてサインイン中"
+  following: "フォロー中"
+  follow: "フォロー"
+  request-pending: "フォロー許可待ち"
+  follow-request: "フォロー申請"
+
 desktop/views/components/activity.chart.vue:
   total: "Black ... Total"
   notes: "Blue ... Notes"
@@ -444,7 +461,7 @@ desktop/views/components/post-form.vue:
   attach-media-from-local: "PCからメディアを添付"
   attach-media-from-drive: "ドライブからメディアを添付"
   attach-cancel: "添付取り消し"
-  insert-a-kao: "v(‘ω’)v"
+  insert-a-kao: "v('ω')v"
   create-poll: "アンケートを作成"
   text-remain: "残り{}文字"
 
@@ -622,6 +639,9 @@ desktop/views/components/timeline.vue:
   global: "グローバル"
   list: "リスト"
 
+desktop/views/components/ui.header.vue:
+  welcome-back: "おかえりなさい、"
+
 desktop/views/components/ui.header.account.vue:
   profile: "プロフィール"
   drive: "ドライブ"
@@ -727,7 +747,7 @@ desktop/views/pages/user/user.friends.vue:
   loading: "読み込み中"
   no-users: "よく話すユーザーはいません"
 
-desktop/views/pages/user/user.header.vue:
+desktop/views/pages/user/user.vue:
   is-suspended: "このユーザーは凍結されています。"
   is-remote: "このユーザーはリモートユーザーです。"
   view-remote: "正確な情報を見る"
@@ -749,6 +769,12 @@ desktop/views/pages/user/user.profile.vue:
   muted: "ミュートしています"
   unmute: "ミュート解除"
 
+desktop/views/pages/user/user.header.vue:
+  posts: "投稿"
+  following: "フォロー"
+  followers: "フォロワー"
+  is-bot: "このアカウントはBotです"
+
 desktop/views/pages/user/user.timeline.vue:
   default: "投稿"
   with-replies: "投稿と返信"
diff --git a/package.json b/package.json
index 9961782c9..7c84161a9 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "4.3.0",
-	"clientVersion": "1.0.6630",
+	"version": "4.14.0",
+	"clientVersion": "1.0.6815",
 	"codename": "nighthike",
 	"main": "./built/index.js",
 	"private": true,
@@ -33,7 +33,8 @@
 		"@types/bcryptjs": "2.4.1",
 		"@types/debug": "0.0.30",
 		"@types/deep-equal": "1.0.1",
-		"@types/elasticsearch": "5.0.23",
+		"@types/elasticsearch": "5.0.24",
+		"@types/file-type": "5.2.1",
 		"@types/gm": "1.18.0",
 		"@types/gulp": "3.8.36",
 		"@types/gulp-htmlmin": "1.3.32",
@@ -42,51 +43,52 @@
 		"@types/gulp-replace": "0.0.31",
 		"@types/gulp-uglify": "3.0.5",
 		"@types/gulp-util": "3.0.34",
-		"@types/inquirer": "0.0.41",
+		"@types/inquirer": "0.0.42",
 		"@types/is-root": "1.0.0",
 		"@types/is-url": "1.2.28",
 		"@types/js-yaml": "3.11.1",
-		"@types/koa": "2.0.45",
-		"@types/koa-bodyparser": "4.2.0",
+		"@types/jsdom": "11.0.6",
+		"@types/koa": "2.0.46",
+		"@types/koa-bodyparser": "5.0.0",
 		"@types/koa-compress": "2.0.8",
 		"@types/koa-favicon": "2.0.19",
 		"@types/koa-logger": "3.1.0",
 		"@types/koa-mount": "3.0.1",
 		"@types/koa-multer": "1.0.0",
-		"@types/koa-router": "7.0.28",
+		"@types/koa-router": "7.0.30",
 		"@types/koa-send": "4.1.1",
 		"@types/koa-views": "2.0.3",
 		"@types/koa__cors": "2.2.2",
-		"@types/kue": "0.11.8",
+		"@types/kue": "0.11.9",
 		"@types/license-checker": "15.0.0",
 		"@types/mkdirp": "0.5.2",
-		"@types/mocha": "5.2.0",
-		"@types/mongodb": "3.0.18",
+		"@types/mocha": "5.2.3",
+		"@types/mongodb": "3.0.21",
 		"@types/ms": "0.7.30",
-		"@types/node": "10.1.2",
+		"@types/node": "10.3.6",
 		"@types/nopt": "3.0.29",
-		"@types/parse5": "3.0.0",
+		"@types/parse5": "5.0.0",
 		"@types/pug": "2.0.4",
-		"@types/qrcode": "0.8.1",
+		"@types/qrcode": "1.2.0",
 		"@types/ratelimiter": "2.1.28",
 		"@types/redis": "2.8.6",
-		"@types/request": "2.47.0",
-		"@types/request-promise-native": "1.0.14",
+		"@types/request": "2.47.1",
+		"@types/request-promise-native": "1.0.15",
 		"@types/rimraf": "2.0.2",
 		"@types/seedrandom": "2.4.27",
 		"@types/single-line-log": "1.1.0",
 		"@types/speakeasy": "2.0.2",
 		"@types/tmp": "0.0.33",
 		"@types/uuid": "3.4.3",
-		"@types/webpack": "4.4.0",
+		"@types/webpack": "4.4.3",
 		"@types/webpack-stream": "3.2.10",
 		"@types/websocket": "0.0.39",
-		"@types/ws": "5.1.1",
+		"@types/ws": "5.1.2",
 		"animejs": "2.2.0",
 		"autosize": "4.0.2",
 		"autwh": "0.1.0",
 		"bcryptjs": "2.4.3",
-		"bootstrap-vue": "2.0.0-rc.6",
+		"bootstrap-vue": "2.0.0-rc.11",
 		"cafy": "8.0.0",
 		"chalk": "2.4.1",
 		"crc-32": "1.2.0",
@@ -95,12 +97,12 @@
 		"deep-equal": "1.0.1",
 		"deepcopy": "0.6.3",
 		"diskusage": "0.2.4",
-		"dompurify": "1.0.4",
+		"dompurify": "1.0.5",
 		"elasticsearch": "15.0.0",
-		"element-ui": "2.3.9",
+		"element-ui": "2.4.1",
 		"emojilib": "2.2.12",
 		"escape-regexp": "0.0.1",
-		"eslint": "4.19.1",
+		"eslint": "5.0.1",
 		"eslint-plugin-vue": "4.5.0",
 		"eventemitter3": "3.1.0",
 		"exif-js": "2.3.0",
@@ -114,7 +116,7 @@
 		"gulp-imagemin": "4.1.0",
 		"gulp-mocha": "6.0.0",
 		"gulp-pug": "4.0.1",
-		"gulp-rename": "1.2.3",
+		"gulp-rename": "1.3.0",
 		"gulp-replace": "1.0.0",
 		"gulp-sourcemaps": "2.6.4",
 		"gulp-stylus": "2.7.0",
@@ -122,14 +124,14 @@
 		"gulp-typescript": "4.0.2",
 		"gulp-uglify": "3.0.0",
 		"gulp-util": "3.0.8",
-		"hard-source-webpack-plugin": "0.6.10",
+		"hard-source-webpack-plugin": "0.9.0",
 		"highlight.js": "9.12.0",
-		"html-minifier": "3.5.16",
+		"html-minifier": "3.5.17",
 		"http-signature": "1.2.0",
-		"inquirer": "5.2.0",
+		"inquirer": "6.0.0",
 		"is-root": "2.0.0",
 		"is-url": "1.2.4",
-		"js-yaml": "3.11.0",
+		"js-yaml": "3.12.0",
 		"jsdom": "11.11.0",
 		"koa": "2.5.1",
 		"koa-bodyparser": "4.2.1",
@@ -140,11 +142,11 @@
 		"koa-mount": "3.0.0",
 		"koa-multer": "1.0.2",
 		"koa-router": "7.4.0",
-		"koa-send": "4.1.3",
+		"koa-send": "5.0.0",
 		"koa-slow": "2.1.0",
 		"koa-views": "6.1.4",
 		"kue": "0.11.6",
-		"license-checker": "20.0.0",
+		"license-checker": "20.1.0",
 		"loader-utils": "1.1.0",
 		"mecab-async": "0.1.2",
 		"mkdirp": "0.5.1",
@@ -155,7 +157,7 @@
 		"ms": "2.1.1",
 		"nan": "2.10.0",
 		"node-sass": "4.9.0",
-		"node-sass-json-importer": "3.2.0",
+		"node-sass-json-importer": "3.3.1",
 		"nopt": "4.0.1",
 		"nprogress": "0.2.0",
 		"object-assign-deep": "0.4.0",
@@ -168,7 +170,7 @@
 		"pug": "2.0.3",
 		"punycode": "2.1.1",
 		"qrcode": "1.2.0",
-		"ratelimiter": "3.0.3",
+		"ratelimiter": "3.1.0",
 		"recaptcha-promise": "0.1.3",
 		"reconnecting-websocket": "3.2.2",
 		"redis": "2.8.0",
@@ -177,7 +179,7 @@
 		"rimraf": "2.6.2",
 		"rndstr": "1.0.0",
 		"s-age": "1.1.2",
-		"sass-loader": "7.0.1",
+		"sass-loader": "7.0.3",
 		"seedrandom": "2.4.3",
 		"single-line-log": "1.1.2",
 		"speakeasy": "2.0.0",
@@ -190,33 +192,38 @@
 		"tcp-port-used": "0.1.2",
 		"textarea-caret": "3.1.0",
 		"tmp": "0.0.33",
-		"ts-loader": "4.3.0",
-		"ts-node": "6.0.4",
+		"ts-loader": "4.4.1",
+		"ts-node": "7.0.0",
 		"tslint": "5.10.0",
-		"typescript": "2.8.3",
-		"typescript-eslint-parser": "15.0.0",
+		"typescript": "2.9.2",
+		"typescript-eslint-parser": "16.0.0",
 		"uglify-es": "3.3.9",
 		"url-loader": "1.0.1",
-		"uuid": "3.2.1",
+		"uuid": "3.3.0",
 		"v-animate-css": "0.0.2",
 		"vue": "2.5.16",
-		"vue-cropperjs": "2.2.0",
-		"vue-js-modal": "1.3.13",
+		"vue-cropperjs": "2.2.1",
+		"vue-js-modal": "1.3.15",
 		"vue-json-tree-view": "2.1.4",
-		"vue-loader": "15.2.1",
+		"vue-loader": "15.2.4",
 		"vue-router": "3.0.1",
 		"vue-template-compiler": "2.5.16",
 		"vuedraggable": "2.16.0",
 		"vuex": "3.0.1",
 		"vuex-persistedstate": "^2.5.4",
-		"web-push": "3.3.1",
+		"web-push": "3.3.2",
 		"webfinger.js": "2.6.6",
-		"webpack": "4.9.1",
-		"webpack-cli": "2.1.4",
+		"webpack": "4.12.1",
+		"webpack-cli": "3.0.8",
 		"websocket": "1.0.26",
-		"ws": "5.2.0",
-		"xev": "2.0.1",
-		"@types/file-type": "5.2.1",
-		"@types/jsdom": "11.0.5"
+		"ws": "5.2.1",
+		"xev": "2.0.1"
+	},
+	"greenkeeper": {
+		"ignore": [
+			"deepcopy",
+			"cafy",
+			"@types/gulp"
+		]
 	}
 }
diff --git a/src/client/app/common/scripts/can-hide-text.ts b/src/client/app/common/scripts/can-hide-text.ts
deleted file mode 100644
index 4a4be8d9d..000000000
--- a/src/client/app/common/scripts/can-hide-text.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-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/common/scripts/check-for-update.ts b/src/client/app/common/scripts/check-for-update.ts
index b5ba6916d..4445eefc3 100644
--- a/src/client/app/common/scripts/check-for-update.ts
+++ b/src/client/app/common/scripts/check-for-update.ts
@@ -23,7 +23,10 @@ export default async function(mios: MiOS, force = false, silent = false) {
 		}
 
 		if (!silent) {
-			alert('%i18n:common.update-available%'.replace('{newer}', newer).replace('{current}', current));
+			mios.apis.dialog({
+				title: '%i18n:common.update-available-title%',
+				text: '%i18n:common.update-available%'.replace('{newer}', newer).replace('{current}', current)
+			});
 		}
 
 		return newer;
diff --git a/src/client/app/common/scripts/compose-notification.ts b/src/client/app/common/scripts/compose-notification.ts
index cc28f7599..2e58649ac 100644
--- a/src/client/app/common/scripts/compose-notification.ts
+++ b/src/client/app/common/scripts/compose-notification.ts
@@ -20,34 +20,6 @@ export default function(type, data): Notification {
 				icon: data.url + '?thumbnail&size=64'
 			};
 
-		case 'mention':
-			return {
-				title: `${getUserName(data.user)}さんから:`,
-				body: getNoteSummary(data),
-				icon: data.user.avatarUrl + '?thumbnail&size=64'
-			};
-
-		case 'reply':
-			return {
-				title: `${getUserName(data.user)}さんから返信:`,
-				body: getNoteSummary(data),
-				icon: data.user.avatarUrl + '?thumbnail&size=64'
-			};
-
-		case 'quote':
-			return {
-				title: `${getUserName(data.user)}さんが引用:`,
-				body: getNoteSummary(data),
-				icon: data.user.avatarUrl + '?thumbnail&size=64'
-			};
-
-		case 'reaction':
-			return {
-				title: `${getUserName(data.user)}: ${getReactionEmoji(data.reaction)}:`,
-				body: getNoteSummary(data.note),
-				icon: data.user.avatarUrl + '?thumbnail&size=64'
-			};
-
 		case 'unread_messaging_message':
 			return {
 				title: `${getUserName(data.user)}さんからメッセージ:`,
@@ -62,6 +34,40 @@ export default function(type, data): Notification {
 				icon: data.parent.avatarUrl + '?thumbnail&size=64'
 			};
 
+		case 'notification':
+			switch (data.type) {
+				case 'mention':
+					return {
+						title: `${getUserName(data.user)}さんから:`,
+						body: getNoteSummary(data),
+						icon: data.user.avatarUrl + '?thumbnail&size=64'
+					};
+
+				case 'reply':
+					return {
+						title: `${getUserName(data.user)}さんから返信:`,
+						body: getNoteSummary(data),
+						icon: data.user.avatarUrl + '?thumbnail&size=64'
+					};
+
+				case 'quote':
+					return {
+						title: `${getUserName(data.user)}さんが引用:`,
+						body: getNoteSummary(data),
+						icon: data.user.avatarUrl + '?thumbnail&size=64'
+					};
+
+				case 'reaction':
+					return {
+						title: `${getUserName(data.user)}: ${getReactionEmoji(data.reaction)}:`,
+						body: getNoteSummary(data.note),
+						icon: data.user.avatarUrl + '?thumbnail&size=64'
+					};
+
+				default:
+					return null;
+			}
+
 		default:
 			return null;
 	}
diff --git a/src/client/app/common/scripts/get-kao.ts b/src/client/app/common/scripts/get-kao.ts
index 2168c5be8..d38018751 100644
--- a/src/client/app/common/scripts/get-kao.ts
+++ b/src/client/app/common/scripts/get-kao.ts
@@ -1,5 +1,5 @@
 export default () => [
 	'(=^・・^=)',
-	'v(‘ω’)v',
+	'v('ω')v',
 	'🐡( \'-\' 🐡 )フグパンチ!!!!'
 ][Math.floor(Math.random() * 3)];
diff --git a/src/client/app/common/views/components/google.vue b/src/client/app/common/views/components/google.vue
index 92817d3c1..8272961ef 100644
--- a/src/client/app/common/views/components/google.vue
+++ b/src/client/app/common/views/components/google.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mk-google">
 	<input type="search" v-model="query" :placeholder="q">
-	<button @click="search">検索</button>
+	<button @click="search">%fa:search% %i18n:common.search%</button>
 </div>
 </template>
 
diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts
index 5b2fa084f..e2cba2e53 100644
--- a/src/client/app/common/views/components/index.ts
+++ b/src/client/app/common/views/components/index.ts
@@ -9,7 +9,7 @@ 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';
+import misskeyFlavoredMarkdown from './misskey-flavored-markdown';
 import poll from './poll.vue';
 import pollEditor from './poll-editor.vue';
 import reactionIcon from './reaction-icon.vue';
@@ -47,7 +47,7 @@ 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);
+Vue.component('misskey-flavored-markdown', misskeyFlavoredMarkdown);
 Vue.component('mk-poll', poll);
 Vue.component('mk-poll-editor', pollEditor);
 Vue.component('mk-reaction-icon', reactionIcon);
diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue
index a77b5f365..f33173da6 100644
--- a/src/client/app/common/views/components/messaging-room.message.vue
+++ b/src/client/app/common/views/components/messaging-room.message.vue
@@ -8,7 +8,7 @@
 				<img src="/assets/desktop/messaging/delete.png" alt="Delete"/>
 			</button>
 			<div class="content" v-if="!message.isDeleted">
-				<mk-note-html class="text" v-if="message.text" ref="text" :text="message.text" :i="$store.state.i"/>
+				<misskey-flavored-markdown class="text" v-if="message.text" ref="text" :text="message.text" :i="$store.state.i"/>
 				<div class="file" v-if="message.file">
 					<a :href="message.file.url" target="_blank" :title="message.file.name">
 						<img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/>
@@ -32,7 +32,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import parse from '../../../../../text/parse';
+import parse from '../../../../../mfm/parse';
 
 export default Vue.extend({
 	props: {
diff --git a/src/client/app/common/views/components/note-html.ts b/src/client/app/common/views/components/misskey-flavored-markdown.ts
similarity index 89%
rename from src/client/app/common/views/components/note-html.ts
rename to src/client/app/common/views/components/misskey-flavored-markdown.ts
index 8fa5f380d..c321c7610 100644
--- a/src/client/app/common/views/components/note-html.ts
+++ b/src/client/app/common/views/components/misskey-flavored-markdown.ts
@@ -1,6 +1,6 @@
 import Vue from 'vue';
 import * as emojilib from 'emojilib';
-import parse from '../../../../../text/parse';
+import parse from '../../../../../mfm/parse';
 import getAcct from '../../../../../acct/render';
 import { url } from '../../../config';
 import MkUrl from './url.vue';
@@ -10,7 +10,7 @@ const flatten = list => list.reduce(
 	(a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []
 );
 
-export default Vue.component('mk-note-html', {
+export default Vue.component('misskey-flavored-markdown', {
 	props: {
 		text: {
 			type: String,
@@ -40,17 +40,6 @@ export default Vue.component('mk-note-html', {
 			ast = this.ast;
 		}
 
-		if (ast.filter(x => x.type != 'hashtag').length == 0) {
-			return;
-		}
-
-		while (ast[ast.length - 1] && (
-			ast[ast.length - 1].type == 'hashtag' ||
-			(ast[ast.length - 1].type == 'text' && ast[ast.length - 1].content == ' ') ||
-			(ast[ast.length - 1].type == 'text' && ast[ast.length - 1].content == '\n'))) {
-			ast.pop();
-		}
-
 		// Parse ast to DOM
 		const els = flatten(ast.map(token => {
 			switch (token.type) {
diff --git a/src/client/app/common/views/components/note-header.vue b/src/client/app/common/views/components/note-header.vue
index 6e64a6a6d..25a333926 100644
--- a/src/client/app/common/views/components/note-header.vue
+++ b/src/client/app/common/views/components/note-header.vue
@@ -72,6 +72,7 @@ root(isDark)
 	> .is-admin
 	> .is-bot
 	> .is-cat
+		flex-shrink 0
 		align-self center
 		margin 0 .5em 0 0
 		padding 1px 6px
@@ -89,6 +90,7 @@ root(isDark)
 		overflow hidden
 		text-overflow ellipsis
 		color isDark ? #606984 : #ccc
+		flex-shrink 2147483647
 
 	> .info
 		margin-left auto
diff --git a/src/client/app/common/views/components/reaction-picker.vue b/src/client/app/common/views/components/reaction-picker.vue
index 0db6f66b3..ed7aedb58 100644
--- a/src/client/app/common/views/components/reaction-picker.vue
+++ b/src/client/app/common/views/components/reaction-picker.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mk-reaction-picker">
 	<div class="backdrop" ref="backdrop" @click="close"></div>
-	<div class="popover" :class="{ compact }" ref="popover">
+	<div class="popover" :class="{ compact, big }" ref="popover">
 		<p v-if="!compact">{{ title }}</p>
 		<div>
 			<button @click="react('like')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="1" title="%i18n:common.reactions.like%"><mk-reaction-icon reaction='like'/></button>
@@ -25,7 +25,28 @@ import * as anime from 'animejs';
 const placeholder = '%i18n:@choose-reaction%';
 
 export default Vue.extend({
-	props: ['note', 'source', 'compact', 'cb'],
+	props: {
+		note: {
+			type: Object,
+			required: true
+		},
+		source: {
+			required: true
+		},
+		compact: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		cb: {
+			required: false
+		},
+		big: {
+			type: Boolean,
+			required: false,
+			default: false
+		}
+	},
 	data() {
 		return {
 			title: placeholder
@@ -162,6 +183,16 @@ root(isDark)
 				border-right solid $balloon-size transparent
 				border-bottom solid $balloon-size $bgcolor
 
+		&.compact
+			> div
+				width 280px
+
+				> button
+					width 50px
+					height 50px
+					font-size 28px
+					border-radius 4px
+
 		> p
 			display block
 			margin 0
diff --git a/src/client/app/common/views/components/reversi.game.vue b/src/client/app/common/views/components/reversi.game.vue
index dc79c95bb..a2a6fd0dc 100644
--- a/src/client/app/common/views/components/reversi.game.vue
+++ b/src/client/app/common/views/components/reversi.game.vue
@@ -3,24 +3,39 @@
 	<header><b>{{ blackUser.name }}</b>(黒) vs <b>{{ whiteUser.name }}</b>(白)</header>
 
 	<div style="overflow: hidden">
-		<p class="turn" v-if="!iAmPlayer && !game.isEnded">{{ turnUser.name }}のターンです<mk-ellipsis/></p>
-		<p class="turn" v-if="logPos != logs.length">{{ turnUser.name }}のターン</p>
-		<p class="turn1" v-if="iAmPlayer && !game.isEnded && !isMyTurn">相手のターンです<mk-ellipsis/></p>
-		<p class="turn2" v-if="iAmPlayer && !game.isEnded && isMyTurn" v-animate-css="{ classes: 'tada', iteration: 'infinite' }">あなたのターンです</p>
+		<p class="turn" v-if="!iAmPlayer && !game.isEnded">{{ '%i18n:common.reversi.turn-of%'.replace('{}', turnUser.name) }}<mk-ellipsis/></p>
+		<p class="turn" v-if="logPos != logs.length">{{ '%i18n:common.reversi.past-turn-of%'.replace('{}', turnUser.name) }}</p>
+		<p class="turn1" v-if="iAmPlayer && !game.isEnded && !isMyTurn">%i18n:common.reversi.opponent-turn%<mk-ellipsis/></p>
+		<p class="turn2" v-if="iAmPlayer && !game.isEnded && isMyTurn" v-animate-css="{ classes: 'tada', iteration: 'infinite' }">%i18n:common.reversi.my-turn%</p>
 		<p class="result" v-if="game.isEnded && logPos == logs.length">
 			<template v-if="game.winner"><b>{{ game.winner.name }}</b>の勝ち{{ game.settings.isLlotheo ? ' (ロセオ)' : '' }}</template>
-			<template v-else>引き分け</template>
+			<template v-else>%i18n:common.reversi.drawn%</template>
 		</p>
 	</div>
 
-	<div class="board" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map[0].length }, 1fr)` }">
-		<div v-for="(stone, i) in o.board"
-			:class="{ empty: stone == null, none: o.map[i] == 'null', isEnded: game.isEnded, myTurn: !game.isEnded && isMyTurn, can: turnUser ? o.canPut(turnUser.id == blackUser.id, i) : null, prev: o.prevPos == i }"
-			@click="set(i)"
-			:title="'[' + (o.transformPosToXy(i)[0] + 1) + ', ' + (o.transformPosToXy(i)[1] + 1) + '] (' + i + ')'"
-		>
-			<img v-if="stone === true" :src="`${blackUser.avatarUrl}?thumbnail&size=128`" alt="">
-			<img v-if="stone === false" :src="`${whiteUser.avatarUrl}?thumbnail&size=128`" alt="">
+	<div class="board">
+		<div class="labels-x" v-if="this.$store.state.settings.reversiBoardLabels">
+			<span v-for="i in game.settings.map[0].length">{{ String.fromCharCode(64 + i) }}</span>
+		</div>
+		<div class="flex">
+			<div class="labels-y" v-if="this.$store.state.settings.reversiBoardLabels">
+				<div v-for="i in game.settings.map.length">{{ i }}</div>
+			</div>
+			<div class="cells" :style="cellsStyle">
+				<div v-for="(stone, i) in o.board"
+						:class="{ empty: stone == null, none: o.map[i] == 'null', isEnded: game.isEnded, myTurn: !game.isEnded && isMyTurn, can: turnUser ? o.canPut(turnUser.id == blackUser.id, i) : null, prev: o.prevPos == i }"
+						@click="set(i)"
+						:title="`${String.fromCharCode(65 + o.transformPosToXy(i)[0])}${o.transformPosToXy(i)[1] + 1}`">
+					<img v-if="stone === true" :src="`${blackUser.avatarUrl}?thumbnail&size=128`" alt="">
+					<img v-if="stone === false" :src="`${whiteUser.avatarUrl}?thumbnail&size=128`" alt="">
+				</div>
+			</div>
+			<div class="labels-y" v-if="this.$store.state.settings.reversiBoardLabels">
+				<div v-for="i in game.settings.map.length">{{ i }}</div>
+			</div>
+		</div>
+		<div class="labels-x" v-if="this.$store.state.settings.reversiBoardLabels">
+			<span v-for="i in game.settings.map[0].length">{{ String.fromCharCode(64 + i) }}</span>
 		</div>
 	</div>
 
@@ -92,6 +107,12 @@ export default Vue.extend({
 		isMyTurn(): boolean {
 			if (this.turnUser == null) return null;
 			return this.turnUser.id == this.$store.state.i.id;
+		},
+		cellsStyle(): any {
+			return {
+				'grid-template-rows': `repeat(${ this.game.settings.map.length }, 1fr)`,
+				'grid-template-columns': `repeat(${ this.game.settings.map[0].length }, 1fr)`
+			};
 		}
 	},
 
@@ -244,54 +265,99 @@ export default Vue.extend({
 		border-bottom dashed 1px #c4cdd4
 
 	> .board
-		display grid
-		grid-gap 4px
-		width 350px
-		height 350px
+		width calc(100% - 16px)
+		max-width 500px
 		margin 0 auto
 
-		> div
-			background transparent
-			border-radius 6px
-			overflow hidden
+		$label-size = 16px
+		$gap = 4px
 
-			*
-				pointer-events none
-				user-select none
+		> .labels-x
+			height $label-size
+			padding 0 $label-size
+			display flex
 
-			&.empty
-				border solid 2px #eee
+			> *
+				flex 1
+				display flex
+				align-items center
+				justify-content center
+				font-size 12px
 
-			&.empty.can
-				background #eee
+				&:first-child
+					margin-left -($gap / 2)
 
-			&.empty.myTurn
-				border-color #ddd
+				&:last-child
+					margin-right -($gap / 2)
 
-				&.can
-					background #eee
-					cursor pointer
+		> .flex
+			display flex
 
-					&:hover
-						border-color darken($theme-color, 10%)
-						background $theme-color
+			> .labels-y
+				width $label-size
+				display flex
+				flex-direction column
 
-					&:active
-						background darken($theme-color, 10%)
+				> *
+					flex 1
+					display flex
+					align-items center
+					justify-content center
+					font-size 12px
 
-			&.prev
-				box-shadow 0 0 0 4px rgba($theme-color, 0.7)
+					&:first-child
+						margin-top -($gap / 2)
 
-			&.isEnded
-				border-color #ddd
+					&:last-child
+						margin-bottom -($gap / 2)
 
-			&.none
-				border-color transparent !important
+			> .cells
+				flex 1
+				display grid
+				grid-gap $gap
 
-			> img
-				display block
-				width 100%
-				height 100%
+				> div
+					background transparent
+					border-radius 6px
+					overflow hidden
+
+					*
+						pointer-events none
+						user-select none
+
+					&.empty
+						border solid 2px #eee
+
+					&.empty.can
+						background #eee
+
+					&.empty.myTurn
+						border-color #ddd
+
+						&.can
+							background #eee
+							cursor pointer
+
+							&:hover
+								border-color darken($theme-color, 10%)
+								background $theme-color
+
+							&:active
+								background darken($theme-color, 10%)
+
+					&.prev
+						box-shadow 0 0 0 4px rgba($theme-color, 0.7)
+
+					&.isEnded
+						border-color #ddd
+
+					&.none
+						border-color transparent !important
+
+					> img
+						display block
+						width 100%
+						height 100%
 
 	> .graph
 		display grid
diff --git a/src/client/app/common/views/components/reversi.vue b/src/client/app/common/views/components/reversi.vue
index e4d7740bd..61705163a 100644
--- a/src/client/app/common/views/components/reversi.vue
+++ b/src/client/app/common/views/components/reversi.vue
@@ -10,7 +10,7 @@
 		</div>
 	</div>
 	<div class="index" v-else>
-		<h1>Misskey %fa:circle%thell%fa:circle R%</h1>
+		<h1>Misskey Reversi</h1>
 		<p>他のMisskeyユーザーとリバーシで対戦しよう</p>
 		<div class="play">
 			<el-button round>フリーマッチ(準備中)</el-button>
diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue
index f3372bf06..5a8b9df47 100644
--- a/src/client/app/common/views/components/welcome-timeline.vue
+++ b/src/client/app/common/views/components/welcome-timeline.vue
@@ -13,7 +13,7 @@
 				</div>
 			</header>
 			<div class="text">
-				<mk-note-html v-if="note.text" :text="note.text"/>
+				<misskey-flavored-markdown v-if="note.text" :text="note.text"/>
 			</div>
 		</div>
 	</div>
@@ -24,6 +24,13 @@
 import Vue from 'vue';
 
 export default Vue.extend({
+	props: {
+		max: {
+			type: Number,
+			required: false,
+			default: undefined
+		}
+	},
 	data() {
 		return {
 			fetching: true,
@@ -37,6 +44,7 @@ export default Vue.extend({
 		fetch(cb?) {
 			this.fetching = true;
 			(this as any).api('notes', {
+				limit: this.max,
 				local: true,
 				reply: false,
 				renote: false,
diff --git a/src/client/app/common/views/pages/follow.vue b/src/client/app/common/views/pages/follow.vue
new file mode 100644
index 000000000..c8e838be8
--- /dev/null
+++ b/src/client/app/common/views/pages/follow.vue
@@ -0,0 +1,215 @@
+<template>
+<div class="syxhndwprovvuqhmyvveewmbqayniwkv" v-if="!fetching" :data-darkmode="$store.state.device.darkmode">
+	<div class="signed-in-as" v-html="'%i18n:@signed-in-as%'.replace('{}', '<b>' + myName + '</b>')"></div>
+
+	<main>
+		<div class="banner" :style="bannerStyle"></div>
+		<mk-avatar class="avatar" :user="user" :disable-preview="true"/>
+		<div class="body">
+			<router-link :to="user | userPage" class="name">{{ user | userName }}</router-link>
+			<span class="username">@{{ user | acct }}</span>
+			<div class="description">
+				<misskey-flavored-markdown v-if="user.description" :text="user.description" :i="$store.state.i"/>
+			</div>
+		</div>
+	</main>
+
+	<button
+			:class="{ wait: followWait, active: user.isFollowing || user.hasPendingFollowRequestFromYou }"
+			@click="onClick"
+			:disabled="followWait">
+		<template v-if="!followWait">
+			<template v-if="user.hasPendingFollowRequestFromYou">%fa:hourglass-half% %i18n:@request-pending%</template>
+			<template v-else-if="user.isFollowing">%fa:minus% %i18n:@following%</template>
+			<template v-else-if="!user.isFollowing && user.isLocked">%fa:plus% %i18n:@follow-request%</template>
+			<template v-else-if="!user.isFollowing && !user.isLocked">%fa:plus% %i18n:@follow%</template>
+		</template>
+		<template v-else>%fa:spinner .pulse .fw%</template>
+	</button>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import parseAcct from '../../../../../acct/parse';
+import getUserName from '../../../../../renderers/get-user-name';
+import Progress from '../../../common/scripts/loading';
+
+export default Vue.extend({
+	data() {
+		return {
+			fetching: true,
+			user: null,
+			followWait: false
+		};
+	},
+
+	computed: {
+		myName(): string {
+			return Vue.filter('userName')(this.$store.state.i);
+		},
+
+		bannerStyle(): any {
+			if (this.user.bannerUrl == null) return {};
+			return {
+				backgroundColor: this.user.bannerColor && this.user.bannerColor.length == 3 ? `rgb(${ this.user.bannerColor.join(',') })` : null,
+				backgroundImage: `url(${ this.user.bannerUrl })`
+			};
+		}
+	},
+
+	created() {
+		this.fetch();
+	},
+
+	methods: {
+		fetch() {
+			const acct = new URL(location.href).searchParams.get('acct');
+			this.fetching = true;
+			Progress.start();
+			(this as any).api('users/show', parseAcct(acct)).then(user => {
+				this.user = user;
+				this.fetching = false;
+				Progress.done();
+				document.title = getUserName(this.user) + ' | Misskey';
+			});
+		},
+
+		async onClick() {
+			this.followWait = true;
+
+			try {
+				if (this.user.isFollowing) {
+					this.user = await (this as any).api('following/delete', {
+						userId: this.user.id
+					});
+				} else {
+					if (this.user.isLocked && this.user.hasPendingFollowRequestFromYou) {
+						this.user = await (this as any).api('following/requests/cancel', {
+							userId: this.user.id
+						});
+					} else if (this.user.isLocked) {
+						this.user = await (this as any).api('following/create', {
+							userId: this.user.id
+						});
+					} else {
+						this.user = await (this as any).api('following/create', {
+							userId: this.user.id
+						});
+					}
+				}
+			} catch (e) {
+				console.error(e);
+			} finally {
+				this.followWait = false;
+			}
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+root(isDark)
+	padding 32px
+	max-width 500px
+	margin 0 auto
+	text-align center
+	color isDark ? #9baec8 : #868c8c
+
+	$bg = isDark ? #282C37 : #fff
+
+	@media (max-width 400px)
+		padding 16px
+
+	> .signed-in-as
+		margin-bottom 16px
+		font-size 14px
+		color isDark ? #9baec8 : #9daab3
+
+	> main
+		margin-bottom 16px
+		background $bg
+		border-radius 8px
+		box-shadow 0 4px 12px rgba(#000, 0.1)
+		overflow hidden
+
+		> .banner
+			height 128px
+			background-position center
+			background-size cover
+
+		> .avatar
+			display block
+			margin -50px auto 0 auto
+			width 100px
+			height 100px
+			border-radius 100%
+			border solid 4px $bg
+
+		> .body
+			padding 4px 32px 32px 32px
+
+			@media (max-width 400px)
+				padding 4px 16px 16px 16px
+
+			> .name
+				font-size 20px
+				font-weight bold
+
+			> .username
+				display block
+				opacity 0.7
+
+			> .description
+				margin-top 16px
+
+	> button
+		display block
+		user-select none
+		cursor pointer
+		padding 10px 16px
+		margin 0
+		width 100%
+		min-width 150px
+		font-size 14px
+		font-weight bold
+		color $theme-color
+		background transparent
+		outline none
+		border solid 1px $theme-color
+		border-radius 36px
+
+		&:hover
+			background rgba($theme-color, 0.1)
+
+		&:active
+			background rgba($theme-color, 0.2)
+
+		&.active
+			color $theme-color-foreground
+			background $theme-color
+
+			&:hover
+				background lighten($theme-color, 10%)
+				border-color lighten($theme-color, 10%)
+
+			&:active
+				background darken($theme-color, 10%)
+				border-color darken($theme-color, 10%)
+
+		&.wait
+			cursor wait !important
+			opacity 0.7
+
+		*
+			pointer-events none
+
+.syxhndwprovvuqhmyvveewmbqayniwkv[data-darkmode]
+	root(true)
+
+.syxhndwprovvuqhmyvveewmbqayniwkv:not([data-darkmode])
+	root(false)
+
+</style>
diff --git a/src/client/app/common/views/widgets/hashtags.vue b/src/client/app/common/views/widgets/hashtags.vue
index 9ab855d92..2065bd407 100644
--- a/src/client/app/common/views/widgets/hashtags.vue
+++ b/src/client/app/common/views/widgets/hashtags.vue
@@ -6,7 +6,9 @@
 		<div class="mkw-hashtags--body" :data-mobile="platform == 'mobile'">
 			<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 			<p class="empty" v-else-if="stats.length == 0">%fa:exclamation-circle%%i18n:@empty%</p>
-			<transition-group v-else tag="div" name="chart">
+			<!-- トランジションを有効にするとなぜかメモリリークする -->
+			<!-- <transition-group v-else tag="div" name="chart"> -->
+			<div>
 				<div v-for="stat in stats" :key="stat.tag">
 					<div class="tag">
 						<router-link :to="`/tags/${ stat.tag }`" :title="stat.tag">#{{ stat.tag }}</router-link>
@@ -14,7 +16,8 @@
 					</div>
 					<x-chart class="chart" :src="stat.chart"/>
 				</div>
-			</transition-group>
+			</div>
+			<!-- </transition-group> -->
 		</div>
 	</mk-widget-container>
 </div>
diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts
index 201ab0a83..297100e0e 100644
--- a/src/client/app/desktop/script.ts
+++ b/src/client/app/desktop/script.ts
@@ -36,6 +36,7 @@ import MkSearch from './views/pages/search.vue';
 import MkTag from './views/pages/tag.vue';
 import MkReversi from './views/pages/reversi.vue';
 import MkShare from './views/pages/share.vue';
+import MkFollow from '../common/views/pages/follow.vue';
 
 /**
  * init
@@ -67,7 +68,8 @@ init(async (launch) => {
 			{ path: '/reversi', component: MkReversi },
 			{ path: '/reversi/:game', component: MkReversi },
 			{ path: '/@:user', component: MkUser },
-			{ path: '/notes/:note', component: MkNote }
+			{ path: '/notes/:note', component: MkNote },
+			{ path: '/authorize-follow', component: MkFollow }
 		]
 	});
 
@@ -115,6 +117,15 @@ function registerNotifications(stream: HomeStreamManager) {
 	});
 
 	function attach(connection) {
+		connection.on('notification', notification => {
+			const _n = composeNotification('notification', notification);
+			const n = new Notification(_n.title, {
+				body: _n.body,
+				icon: _n.icon
+			});
+			setTimeout(n.close.bind(n), 6000);
+		});
+
 		connection.on('drive_file_created', file => {
 			const _n = composeNotification('drive_file_created', file);
 			const n = new Notification(_n.title, {
@@ -124,33 +135,6 @@ function registerNotifications(stream: HomeStreamManager) {
 			setTimeout(n.close.bind(n), 5000);
 		});
 
-		connection.on('mention', note => {
-			const _n = composeNotification('mention', note);
-			const n = new Notification(_n.title, {
-				body: _n.body,
-				icon: _n.icon
-			});
-			setTimeout(n.close.bind(n), 6000);
-		});
-
-		connection.on('reply', note => {
-			const _n = composeNotification('reply', note);
-			const n = new Notification(_n.title, {
-				body: _n.body,
-				icon: _n.icon
-			});
-			setTimeout(n.close.bind(n), 6000);
-		});
-
-		connection.on('quote', note => {
-			const _n = composeNotification('quote', note);
-			const n = new Notification(_n.title, {
-				body: _n.body,
-				icon: _n.icon
-			});
-			setTimeout(n.close.bind(n), 6000);
-		});
-
 		connection.on('unread_messaging_message', message => {
 			const _n = composeNotification('unread_messaging_message', message);
 			const n = new Notification(_n.title, {
diff --git a/src/client/app/desktop/views/components/home.vue b/src/client/app/desktop/views/components/home.vue
index ba48ce24e..ab276d3c4 100644
--- a/src/client/app/desktop/views/components/home.vue
+++ b/src/client/app/desktop/views/components/home.vue
@@ -66,7 +66,7 @@
 			</div>
 			<div class="main">
 				<mk-post-form class="form" v-if="$store.state.settings.showPostFormOnTopOfTl"/>
-				<mk-timeline class="tl" cref="tl" @loaded="onTlLoaded" v-if="mode == 'timeline'"/>
+				<mk-timeline class="tl" ref="tl" @loaded="onTlLoaded" v-if="mode == 'timeline'"/>
 			</div>
 		</template>
 	</div>
diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue
index 4b5e5bebd..e3d16d705 100644
--- a/src/client/app/desktop/views/components/note-detail.vue
+++ b/src/client/app/desktop/views/components/note-detail.vue
@@ -40,16 +40,13 @@
 			<div class="text">
 				<span v-if="p.isHidden" style="opacity: 0.5">%i18n:@private%</span>
 				<span v-if="p.deletedAt" style="opacity: 0.5">%i18n:@deleted%</span>
-				<mk-note-html v-if="p.text" :text="p.text" :i="$store.state.i"/>
+				<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/>
 			</div>
 			<div class="media" v-if="p.media.length > 0">
 				<mk-media-list :media-list="p.media" :raw="true"/>
 			</div>
 			<mk-poll v-if="p.poll" :note="p"/>
 			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
-			<div class="tags" v-if="p.tags && p.tags.length > 0">
-				<router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${tag}`">{{ tag }}</router-link>
-			</div>
 			<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
 			<div class="map" v-if="p.geo" ref="map"></div>
 			<div class="renote" v-if="p.renote">
@@ -83,7 +80,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
-import parse from '../../../../../text/parse';
+import parse from '../../../../../mfm/parse';
 
 import MkPostFormWindow from './post-form-window.vue';
 import MkRenoteFormWindow from './renote-form-window.vue';
@@ -363,35 +360,6 @@ root(isDark)
 			> .mk-url-preview
 				margin-top 8px
 
-			> .tags
-				margin 4px 0 0 0
-
-				> *
-					display inline-block
-					margin 0 8px 0 0
-					padding 2px 8px 2px 16px
-					font-size 90%
-					color #8d969e
-					background #edf0f3
-					border-radius 4px
-
-					&:before
-						content ""
-						display block
-						position absolute
-						top 0
-						bottom 0
-						left 4px
-						width 8px
-						height 8px
-						margin auto 0
-						background #fff
-						border-radius 100%
-
-					&:hover
-						text-decoration none
-						background #e2e7ec
-
 		> footer
 			font-size 1.2em
 
diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue
index ee11fcc55..b8aff2d86 100644
--- a/src/client/app/desktop/views/components/notes.note.vue
+++ b/src/client/app/desktop/views/components/notes.note.vue
@@ -25,16 +25,13 @@
 						<span v-if="p.isHidden" style="opacity: 0.5">%i18n:@private%</span>
 						<span v-if="p.deletedAt" style="opacity: 0.5">%i18n:@deleted%</span>
 						<a class="reply" v-if="p.reply">%fa:reply%</a>
-						<mk-note-html v-if="p.text && !canHideText(p)" :text="p.text" :i="$store.state.i" :class="$style.text"/>
+						<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i" :class="$style.text"/>
 						<a class="rp" v-if="p.renote">RP:</a>
 					</div>
 					<div class="media" v-if="p.media.length > 0">
 						<mk-media-list :media-list="p.media"/>
 					</div>
 					<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
-					<div class="tags" v-if="p.tags && p.tags.length > 0">
-						<router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${tag}`">{{ tag }}</router-link>
-					</div>
 					<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
 					<div class="map" v-if="p.geo" ref="map"></div>
 					<div class="renote" v-if="p.renote">
@@ -75,8 +72,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 parse from '../../../../../mfm/parse';
 
 import MkPostFormWindow from './post-form-window.vue';
 import MkRenoteFormWindow from './renote-form-window.vue';
@@ -190,8 +186,6 @@ export default Vue.extend({
 	},
 
 	methods: {
-		canHideText,
-
 		capture(withHandler = false) {
 			if (this.$store.getters.isSignedIn) {
 				this.connection.send({
@@ -468,35 +462,6 @@ root(isDark)
 						&:empty
 							display none
 
-					> .tags
-						margin 4px 0 0 0
-
-						> *
-							display inline-block
-							margin 0 8px 0 0
-							padding 2px 8px 2px 16px
-							font-size 90%
-							color #8d969e
-							background isDark ? #313543 : #edf0f3
-							border-radius 4px
-
-							&:before
-								content ""
-								display block
-								position absolute
-								top 0
-								bottom 0
-								left 4px
-								width 8px
-								height 8px
-								margin auto 0
-								background isDark ? #282c37 : #fff
-								border-radius 100%
-
-							&:hover
-								text-decoration none
-								background #e2e7ec
-
 					.mk-url-preview
 						margin-top 8px
 
diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue
index 69f3739f7..1206eb713 100644
--- a/src/client/app/desktop/views/components/notes.vue
+++ b/src/client/app/desktop/views/components/notes.vue
@@ -9,7 +9,9 @@
 		<button @click="resolveInitPromise">%i18n:@retry%</button>
 	</div>
 
-	<transition-group name="mk-notes" class="transition">
+	<!-- トランジションを有効にするとなぜかメモリリークする -->
+	<!--<transition-group name="mk-notes" class="transition">-->
+	<div class="notes">
 		<template v-for="(note, i) in _notes">
 			<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
 			<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
@@ -17,7 +19,8 @@
 				<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
 			</p>
 		</template>
-	</transition-group>
+	</div>
+	<!--</transition-group>-->
 
 	<footer v-if="more">
 		<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
@@ -221,6 +224,7 @@ root(isDark)
 		> *
 			transition transform .3s ease, opacity .3s ease
 
+	> .notes
 		> .date
 			display block
 			margin 0
diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
index e479ffadb..32b36994d 100644
--- a/src/client/app/desktop/views/components/notifications.vue
+++ b/src/client/app/desktop/views/components/notifications.vue
@@ -1,7 +1,9 @@
 <template>
 <div class="mk-notifications">
 	<div class="notifications" v-if="notifications.length != 0">
-		<transition-group name="mk-notifications" class="transition">
+		<!-- トランジションを有効にするとなぜかメモリリークする -->
+		<!-- <transition-group name="mk-notifications" class="transition"> -->
+		<div>
 			<template v-for="(notification, i) in _notifications">
 				<div class="notification" :class="notification.type" :key="notification.id">
 					<mk-time :time="notification.createdAt"/>
@@ -95,7 +97,8 @@
 					<span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span>
 				</p>
 			</template>
-		</transition-group>
+		</div>
+		<!-- </transition-group> -->
 	</div>
 	<button class="more" :class="{ fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications">
 		<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:@more%' }}
@@ -201,7 +204,7 @@ root(isDark)
 			transition transform .3s ease, opacity .3s ease
 
 	> .notifications
-		> *
+		> div
 			> .notification
 				margin 0
 				padding 16px
diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue
index 33f2288e0..3832e5b38 100644
--- a/src/client/app/desktop/views/components/post-form.vue
+++ b/src/client/app/desktop/views/components/post-form.vue
@@ -49,7 +49,7 @@ import Vue from 'vue';
 import * as XDraggable from 'vuedraggable';
 import getKao from '../../../common/scripts/get-kao';
 import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
-import parse from '../../../../../text/parse';
+import parse from '../../../../../mfm/parse';
 import { host } from '../../../config';
 
 export default Vue.extend({
diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue
index 536d270dc..74ab45626 100644
--- a/src/client/app/desktop/views/components/settings.vue
+++ b/src/client/app/desktop/views/components/settings.vue
@@ -54,6 +54,7 @@
 			<mk-switch v-model="$store.state.settings.showMaps" @change="onChangeShowMaps" text="%i18n:@show-maps%">
 				<span>%i18n:@show-maps-desc%</span>
 			</mk-switch>
+			<mk-switch v-model="$store.state.settings.reversiBoardLabels" @change="onChangeReversiBoardLabels" text="%i18n:common.show-reversi-board-labels%"/>
 		</section>
 
 		<section class="web" v-show="page == 'web'">
@@ -369,6 +370,12 @@ export default Vue.extend({
 				value: v
 			});
 		},
+		onChangeReversiBoardLabels(v) {
+			this.$store.dispatch('settings/set', {
+				key: 'reversiBoardLabels',
+				value: v
+			});
+		},
 		onChangeGradientWindowHeader(v) {
 			this.$store.dispatch('settings/set', {
 				key: 'gradientWindowHeader',
diff --git a/src/client/app/desktop/views/components/sub-note-content.vue b/src/client/app/desktop/views/components/sub-note-content.vue
index 45ce6a6f8..cb0374b91 100644
--- a/src/client/app/desktop/views/components/sub-note-content.vue
+++ b/src/client/app/desktop/views/components/sub-note-content.vue
@@ -4,7 +4,7 @@
 		<span v-if="note.isHidden" style="opacity: 0.5">%i18n:@private%</span>
 		<span v-if="note.deletedAt" style="opacity: 0.5">%i18n:@deleted%</span>
 		<a class="reply" v-if="note.replyId">%fa:reply%</a>
-		<mk-note-html v-if="note.text" :text="note.text" :i="$store.state.i"/>
+		<misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i"/>
 		<a class="rp" v-if="note.renoteId" :href="`/notes/${note.renoteId}`">RP: ...</a>
 	</div>
 	<details v-if="note.media.length > 0">
diff --git a/src/client/app/desktop/views/components/ui.header.vue b/src/client/app/desktop/views/components/ui.header.vue
index 704579005..aa7c3ac44 100644
--- a/src/client/app/desktop/views/components/ui.header.vue
+++ b/src/client/app/desktop/views/components/ui.header.vue
@@ -4,7 +4,7 @@
 	<div class="main" ref="main">
 		<div class="backdrop"></div>
 		<div class="main">
-			<p ref="welcomeback" v-if="$store.getters.isSignedIn">おかえりなさい、<b>{{ $store.state.i | userName }}</b>さん</p>
+			<p ref="welcomeback" v-if="$store.getters.isSignedIn">%i18n:@welcome-back%<b>{{ $store.state.i | userName }}</b>さん</p>
 			<div class="container" ref="mainContainer">
 				<div class="left">
 					<x-nav/>
diff --git a/src/client/app/desktop/views/pages/deck/deck.note.vue b/src/client/app/desktop/views/pages/deck/deck.note.vue
index 5a8dc2ea6..c7df715a0 100644
--- a/src/client/app/desktop/views/pages/deck/deck.note.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.note.vue
@@ -25,16 +25,13 @@
 						<span v-if="p.isHidden" style="opacity: 0.5">(%i18n:@private%)</span>
 						<span v-if="p.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span>
 						<a class="reply" v-if="p.reply">%fa:reply%</a>
-						<mk-note-html v-if="p.text && !canHideText(p)" :text="p.text" :i="$store.state.i"/>
+						<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/>
 						<a class="rp" v-if="p.renote != null">RP:</a>
 					</div>
 					<div class="media" v-if="p.media.length > 0">
 						<mk-media-list :media-list="p.media"/>
 					</div>
 					<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
-					<div class="tags" v-if="p.tags && p.tags.length > 0">
-						<router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${tag}`">{{ tag }}</router-link>
-					</div>
 					<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
 					<div class="renote" v-if="p.renote">
 						<mk-note-preview :note="p.renote" :mini="true"/>
@@ -67,8 +64,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import parse from '../../../../../../text/parse';
-import canHideText from '../../../../common/scripts/can-hide-text';
+import parse from '../../../../../../mfm/parse';
 
 import MkNoteMenu from '../../../../common/views/components/note-menu.vue';
 import MkReactionPicker from '../../../../common/views/components/reaction-picker.vue';
@@ -148,8 +144,6 @@ export default Vue.extend({
 	},
 
 	methods: {
-		canHideText,
-
 		capture(withHandler = false) {
 			if (this.$store.getters.isSignedIn) {
 				this.connection.send({
@@ -376,31 +370,6 @@ root(isDark)
 					.mk-url-preview
 						margin-top 8px
 
-					> .tags
-						margin 4px 0 0 0
-
-						> *
-							display inline-block
-							margin 0 8px 0 0
-							padding 2px 8px 2px 16px
-							font-size 90%
-							color #8d969e
-							background isDark ? #313543 : #edf0f3
-							border-radius 4px
-
-							&:before
-								content ""
-								display block
-								position absolute
-								top 0
-								bottom 0
-								left 4px
-								width 8px
-								height 8px
-								margin auto 0
-								background isDark ? #282c37 : #fff
-								border-radius 100%
-
 					> .media
 						> img
 							display block
diff --git a/src/client/app/desktop/views/pages/deck/deck.notes.vue b/src/client/app/desktop/views/pages/deck/deck.notes.vue
index 8862b0e0f..a5ed45b64 100644
--- a/src/client/app/desktop/views/pages/deck/deck.notes.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.notes.vue
@@ -7,7 +7,9 @@
 		<button @click="resolveInitPromise">%i18n:@retry%</button>
 	</div>
 
-	<transition-group name="mk-notes" class="transition">
+	<!-- トランジションを有効にするとなぜかメモリリークする -->
+	<!--<transition-group name="mk-notes" class="transition">-->
+	<div class="notes">
 		<template v-for="(note, i) in _notes">
 			<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)" :media-view="mediaView"/>
 			<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
@@ -15,7 +17,8 @@
 				<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
 			</p>
 		</template>
-	</transition-group>
+	</div>
+	<!--</transition-group>-->
 
 	<footer v-if="more">
 		<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
@@ -198,6 +201,7 @@ root(isDark)
 		> *
 			transition transform .3s ease, opacity .3s ease
 
+	> .notes
 		> .date
 			display block
 			margin 0
diff --git a/src/client/app/desktop/views/pages/deck/deck.notifications.vue b/src/client/app/desktop/views/pages/deck/deck.notifications.vue
index f54ad1a3c..10c06b0ad 100644
--- a/src/client/app/desktop/views/pages/deck/deck.notifications.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.notifications.vue
@@ -1,6 +1,8 @@
 <template>
 <div class="oxynyeqmfvracxnglgulyqfgqxnxmehl">
-	<transition-group name="mk-notifications" class="transition notifications">
+	<!-- トランジションを有効にするとなぜかメモリリークする -->
+	<!--<transition-group name="mk-notifications" class="transition notifications">-->
+	<div class="notifications">
 		<template v-for="(notification, i) in _notifications">
 			<x-notification class="notification" :notification="notification" :key="notification.id"/>
 			<p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'">
@@ -8,7 +10,8 @@
 				<span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span>
 			</p>
 		</template>
-	</transition-group>
+	</div>
+	<!--</transition-group>-->
 	<button class="more" :class="{ fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications">
 		<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:@more%' }}
 	</button>
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 d52c6b762..00545723e 100644
--- a/src/client/app/desktop/views/pages/user/user.header.vue
+++ b/src/client/app/desktop/views/pages/user/user.header.vue
@@ -1,27 +1,37 @@
 <template>
 <div class="header" :data-is-dark-background="user.bannerUrl != null">
-	<div class="is-suspended" v-if="user.isSuspended"><p>%fa:exclamation-triangle% %i18n:@is-suspended%</p></div>
-	<div class="is-remote" v-if="user.host != null"><p>%fa:exclamation-triangle% %i18n:@is-remote%<a :href="user.url || user.uri" target="_blank">%i18n:@view-remote%</a></p></div>
 	<div class="banner-container" :style="style">
 		<div class="banner" ref="banner" :style="style" @click="onBannerClick"></div>
 		<div class="fade"></div>
-	</div>
-	<div class="container">
-		<mk-avatar class="avatar" :user="user" :disable-preview="true"/>
 		<div class="title">
 			<p class="name">{{ user | userName }}</p>
-			<p class="username"><mk-acct :user="user"/></p>
-			<p class="location" v-if="user.host === null && user.profile.location">%fa:map-marker%{{ user.profile.location }}</p>
+			<div>
+				<span class="username"><mk-acct :user="user"/></span>
+				<span v-if="user.isBot" title="%i18n:@is-bot%">%fa:robot%</span>
+				<span class="location" v-if="user.host === null && user.profile.location">%fa:map-marker% {{ user.profile.location }}</span>
+				<span class="birthday" v-if="user.host === null && user.profile.birthday">%fa:birthday-cake% {{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳)</span>
+			</div>
+		</div>
+	</div>
+	<mk-avatar class="avatar" :user="user" :disable-preview="true"/>
+	<div class="body">
+		<div class="description">
+			<misskey-flavored-markdown v-if="user.description" :text="user.description" :i="$store.state.i"/>
+		</div>
+		<div class="status">
+			<span class="notes-count"><b>{{ user.notesCount | number }}</b>%i18n:@posts%</span>
+			<span class="following clickable" @click="showFollowing"><b>{{ user.followingCount | number }}</b>%i18n:@following%</span>
+			<span class="followers clickable" @click="showFollowers"><b>{{ user.followersCount | number }}</b>%i18n:@followers%</span>
 		</div>
-		<footer>
-			<router-link :to="user | userPage" :data-active="$parent.page == 'home'">%fa:home%ホーム</router-link>
-		</footer>
 	</div>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import MkFollowingWindow from '../../components/following-window.vue';
+import MkFollowersWindow from '../../components/followers-window.vue';
+import * as age from 's-age';
 
 export default Vue.extend({
 	props: ['user'],
@@ -32,20 +42,24 @@ export default Vue.extend({
 				backgroundColor: this.user.bannerColor && this.user.bannerColor.length == 3 ? `rgb(${ this.user.bannerColor.join(',') })` : null,
 				backgroundImage: `url(${ this.user.bannerUrl })`
 			};
+		},
+
+		age(): number {
+			return age(this.user.profile.birthday);
 		}
 	},
 	mounted() {
 		if (this.user.bannerUrl) {
-			window.addEventListener('load', this.onScroll);
-			window.addEventListener('scroll', this.onScroll, { passive: true });
-			window.addEventListener('resize', this.onScroll);
+			//window.addEventListener('load', this.onScroll);
+			//window.addEventListener('scroll', this.onScroll, { passive: true });
+			//window.addEventListener('resize', this.onScroll);
 		}
 	},
 	beforeDestroy() {
 		if (this.user.bannerUrl) {
-			window.removeEventListener('load', this.onScroll);
-			window.removeEventListener('scroll', this.onScroll);
-			window.removeEventListener('resize', this.onScroll);
+			//window.removeEventListener('load', this.onScroll);
+			//window.removeEventListener('scroll', this.onScroll);
+			//window.removeEventListener('resize', this.onScroll);
 		}
 	},
 	methods: {
@@ -68,7 +82,19 @@ export default Vue.extend({
 			(this as any).apis.updateBanner().then(i => {
 				this.user.bannerUrl = i.bannerUrl;
 			});
-		}
+		},
+
+		showFollowing() {
+			(this as any).os.new(MkFollowingWindow, {
+				user: this.user
+			});
+		},
+
+		showFollowers() {
+			(this as any).os.new(MkFollowersWindow, {
+				user: this.user
+			});
+		},
 	}
 });
 </script>
@@ -76,31 +102,11 @@ export default Vue.extend({
 <style lang="stylus" scoped>
 @import '~const.styl'
 
-.header
-	$footer-height = 58px
-
+root(isDark)
+	background isDark ? #282C37 : #fff
+	border 1px solid rgba(#000, 0.075)
+	border-radius 6px
 	overflow hidden
-	background #f7f7f7
-	box-shadow 0 1px 1px rgba(#000, 0.075)
-
-	> .is-suspended
-	> .is-remote
-		&.is-suspended
-			color #570808
-			background #ffdbdb
-
-		&.is-remote
-			color #573c08
-			background #fff0db
-
-		> p
-			margin 0 auto
-			padding 14px 16px
-			max-width 1200px
-			font-size 14px
-
-			> a
-				font-weight bold
 
 	&[data-is-dark-background]
 		> .banner-container
@@ -110,7 +116,6 @@ export default Vue.extend({
 			> .fade
 				background linear-gradient(transparent, rgba(#000, 0.7))
 
-		> .container
 			> .title
 				color #fff
 
@@ -118,7 +123,7 @@ export default Vue.extend({
 					text-shadow 0 0 8px #000
 
 	> .banner-container
-		height 320px
+		height 250px
 		overflow hidden
 		background-size cover
 		background-position center
@@ -136,83 +141,75 @@ export default Vue.extend({
 			width 100%
 			height 78px
 
-	> .container
-		max-width 1200px
-		margin 0 auto
-
-		> .avatar
-			display block
-			position absolute
-			bottom 16px
-			left 16px
-			z-index 2
-			width 160px
-			height 160px
-			border solid 3px #fff
-			border-radius 8px
-			box-shadow 1px 1px 3px rgba(#000, 0.2)
-
 		> .title
 			position absolute
-			bottom $footer-height
+			bottom 0
 			left 0
 			width 100%
-			padding 0 0 8px 195px
+			padding 0 0 8px 154px
 			color #5e6367
-			font-family '游ゴシック', 'YuGothic', 'ヒラギノ角ゴ ProN W3', 'Hiragino Kaku Gothic ProN', 'Meiryo', 'メイリオ', sans-serif
 
 			> .name
 				display block
 				margin 0
-				line-height 40px
+				line-height 32px
 				font-weight bold
-				font-size 2em
+				font-size 1.8em
 
-			> .username
-			> .location
+			> div
+				> *
+					display inline-block
+					margin-right 16px
+					line-height 20px
+					opacity 0.8
+
+					&.username
+						font-weight bold
+
+	> .avatar
+		display block
+		position absolute
+		top 170px
+		left 16px
+		z-index 2
+		width 120px
+		height 120px
+		box-shadow 1px 1px 3px rgba(#000, 0.2)
+
+	> .body
+		padding 16px 16px 16px 154px
+		color isDark ? #c5ced6 : #555
+
+		> .status
+			margin-top 16px
+			padding-top 16px
+			border-top solid 1px rgba(#000, isDark ? 0.2 : 0.1)
+			font-size 80%
+
+			> *
 				display inline-block
-				margin 0 16px 0 0
-				line-height 20px
-				opacity 0.8
+				padding-right 16px
+				margin-right 16px
 
-				> i
+				&:not(:last-child)
+					border-right solid 1px rgba(#000, isDark ? 0.2 : 0.1)
+
+				&.clickable
+					cursor pointer
+
+					&:hover
+						color isDark ? #fff : #000
+
+				> b
 					margin-right 4px
+					font-size 1rem
+					font-weight bold
+					color $theme-color
 
-		> footer
-			z-index 1
-			height $footer-height
-			padding-left 195px
+.header[data-darkmode]
+	root(true)
 
-			> a
-				display inline-block
-				margin 0
-				padding 0 16px
-				height $footer-height
-				line-height $footer-height
-				color #555
-
-				&[data-active]
-					border-bottom solid 4px $theme-color
-
-				> i
-					margin-right 6px
-
-			> button
-				display block
-				position absolute
-				top 0
-				right 0
-				margin 8px
-				padding 0
-				width $footer-height - 16px
-				line-height $footer-height - 16px - 2px
-				font-size 1.2em
-				color #777
-				border solid 1px #eee
-				border-radius 4px
-
-				&:hover
-					color #555
-					border solid 1px #ddd
+.header:not([data-darkmode])
+	root(false)
 
 </style>
diff --git a/src/client/app/desktop/views/pages/user/user.home.vue b/src/client/app/desktop/views/pages/user/user.home.vue
deleted file mode 100644
index afaf97dc9..000000000
--- a/src/client/app/desktop/views/pages/user/user.home.vue
+++ /dev/null
@@ -1,103 +0,0 @@
-<template>
-<div class="home">
-	<div>
-		<div ref="left">
-			<x-profile :user="user"/>
-			<x-photos :user="user"/>
-			<x-followers-you-know v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/>
-			<p v-if="user.host === null">%i18n:@last-used-at%: <b><mk-time :time="user.lastUsedAt"/></b></p>
-		</div>
-	</div>
-	<main>
-		<mk-note-detail v-if="user.pinnedNote" :note="user.pinnedNote" :compact="true"/>
-		<x-timeline class="timeline" ref="tl" :user="user"/>
-	</main>
-	<div>
-		<div ref="right">
-			<mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/>
-			<mk-activity :user="user"/>
-			<x-friends :user="user"/>
-			<div class="nav"><mk-nav/></div>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import XTimeline from './user.timeline.vue';
-import XProfile from './user.profile.vue';
-import XPhotos from './user.photos.vue';
-import XFollowersYouKnow from './user.followers-you-know.vue';
-import XFriends from './user.friends.vue';
-
-export default Vue.extend({
-	components: {
-		XTimeline,
-		XProfile,
-		XPhotos,
-		XFollowersYouKnow,
-		XFriends
-	},
-	props: ['user'],
-	methods: {
-		warp(date) {
-			(this.$refs.tl as any).warp(date);
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.home
-	display flex
-	justify-content center
-	margin 0 auto
-	max-width 1200px
-
-	> main
-	> div > div
-		> *:not(:last-child)
-			margin-bottom 16px
-
-	> main
-		padding 16px
-		width calc(100% - 275px * 2)
-
-		> .timeline
-			border solid 1px rgba(#000, 0.075)
-			border-radius 6px
-
-	> div
-		width 275px
-		margin 0
-
-		&:first-child > div
-			padding 16px 0 16px 16px
-
-			> p
-				display block
-				margin 0
-				padding 0 12px
-				text-align center
-				font-size 0.8em
-				color #aaa
-
-		&:last-child > div
-			padding 16px 16px 16px 0
-
-			> .nav
-				padding 16px
-				font-size 12px
-				color #aaa
-				background #fff
-				border solid 1px rgba(#000, 0.075)
-				border-radius 6px
-
-				a
-					color #999
-
-				i
-					color #ccc
-
-</style>
diff --git a/src/client/app/desktop/views/pages/user/user.profile.vue b/src/client/app/desktop/views/pages/user/user.profile.vue
index 5aa08f7c8..0134d6f0b 100644
--- a/src/client/app/desktop/views/pages/user/user.profile.vue
+++ b/src/client/app/desktop/views/pages/user/user.profile.vue
@@ -15,48 +15,17 @@
 		</button>
 		<button class="mute ui" @click="list">%fa:list% リストに追加</button>
 	</div>
-	<div class="description" v-if="user.description">{{ user.description }}</div>
-	<div class="birthday" v-if="user.host === null && user.profile.birthday">
-		<p>%fa:birthday-cake%{{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳)</p>
-	</div>
-	<div class="twitter" v-if="user.host === null && user.twitter">
-		<p>%fa:B twitter%<a :href="`https://twitter.com/${user.twitter.screenName}`" target="_blank">@{{ user.twitter.screenName }}</a></p>
-	</div>
-	<div class="status">
-		<p class="notes-count">%fa:angle-right%<a>{{ user.notesCount }}</a><b>投稿</b></p>
-		<p class="following">%fa:angle-right%<a @click="showFollowing">{{ user.followingCount }}</a>人を<b>フォロー</b></p>
-		<p class="followers">%fa:angle-right%<a @click="showFollowers">{{ user.followersCount }}</a>人の<b>フォロワー</b></p>
-	</div>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-import * as age from 's-age';
-import MkFollowingWindow from '../../components/following-window.vue';
-import MkFollowersWindow from '../../components/followers-window.vue';
 import MkUserListsWindow from '../../components/user-lists-window.vue';
 
 export default Vue.extend({
 	props: ['user'],
-	computed: {
-		age(): number {
-			return age(this.user.profile.birthday);
-		}
-	},
+
 	methods: {
-		showFollowing() {
-			(this as any).os.new(MkFollowingWindow, {
-				user: this.user
-			});
-		},
-
-		showFollowers() {
-			(this as any).os.new(MkFollowersWindow, {
-				user: this.user
-			});
-		},
-
 		stalk() {
 			(this as any).api('following/stalk', {
 				userId: this.user.id
@@ -116,8 +85,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.profile
-	background #fff
+root(isDark)
+	background isDark ? #282C37 : #fff
 	border solid 1px rgba(#000, 0.075)
 	border-radius 6px
 
@@ -127,7 +96,7 @@ export default Vue.extend({
 	> .friend-form
 		padding 16px
 		text-align center
-		border-top solid 1px #eee
+		border-bottom solid 1px isDark ? #21242f : #eee
 
 		> .followed
 			margin 12px 0 0 0
@@ -145,7 +114,7 @@ export default Vue.extend({
 	> .action-form
 		padding 16px
 		text-align center
-		border-top solid 1px #eee
+		border-bottom solid 1px isDark ? #21242f : #eee
 
 		> *
 			width 100%
@@ -153,43 +122,10 @@ export default Vue.extend({
 			&:not(:last-child)
 				margin-bottom 12px
 
-	> .description
-		padding 16px
-		color #555
-		border-top solid 1px #eee
+.profile[data-darkmode]
+	root(true)
 
-	> .birthday
-		padding 16px
-		color #555
-		border-top solid 1px #eee
-
-		> p
-			margin 0
-
-			> i
-				margin-right 8px
-
-	> .twitter
-		padding 16px
-		color #555
-		border-top solid 1px #eee
-
-		> p
-			margin 0
-
-			> i
-				margin-right 8px
-
-	> .status
-		padding 16px
-		color #555
-		border-top solid 1px #eee
-
-		> p
-			margin 8px 0
-
-			> i
-				margin-left 8px
-				margin-right 8px
+.profile:not([data-darkmode])
+	root(false)
 
 </style>
diff --git a/src/client/app/desktop/views/pages/user/user.timeline.vue b/src/client/app/desktop/views/pages/user/user.timeline.vue
index 812b5b422..67987fcb9 100644
--- a/src/client/app/desktop/views/pages/user/user.timeline.vue
+++ b/src/client/app/desktop/views/pages/user/user.timeline.vue
@@ -1,9 +1,9 @@
 <template>
-<div class="timeline">
+<div class="oh5y2r7l5lx8j6jj791ykeiwgihheguk">
 	<header>
-		<span :data-active="mode == 'default'" @click="mode = 'default'">%i18n:@default%</span>
-		<span :data-active="mode == 'with-replies'" @click="mode = 'with-replies'">%i18n:@with-replies%</span>
-		<span :data-active="mode == 'with-media'" @click="mode = 'with-media'">%i18n:@with-media%</span>
+		<span :data-active="mode == 'default'" @click="mode = 'default'">%fa:comment-alt R% %i18n:@default%</span>
+		<span :data-active="mode == 'with-replies'" @click="mode = 'with-replies'">%fa:comments% %i18n:@with-replies%</span>
+		<span :data-active="mode == 'with-media'" @click="mode = 'with-media'">%fa:images% %i18n:@with-media%</span>
 	</header>
 	<div class="loading" v-if="fetching">
 		<mk-ellipsis-icon/>
@@ -114,25 +114,44 @@ export default Vue.extend({
 <style lang="stylus" scoped>
 @import '~const.styl'
 
-.timeline
-	background #fff
+root(isDark)
+	background isDark ? #282C37 : #fff
 
 	> header
-		padding 8px 16px
-		border-bottom solid 1px #eee
+		padding 0 8px
+		z-index 10
+		background isDark ? #313543 : #fff
+		border-radius 6px 6px 0 0
+		box-shadow 0 1px isDark ? rgba(#000, 0.15) : rgba(#000, 0.08)
 
 		> span
-			margin-right 16px
-			line-height 27px
-			font-size 18px
-			color #555
+			display inline-block
+			padding 0 10px
+			line-height 42px
+			font-size 12px
+			user-select none
+
+			&[data-active]
+				color $theme-color
+				cursor default
+				font-weight bold
+
+				&:before
+					content ""
+					display block
+					position absolute
+					bottom 0
+					left -8px
+					width calc(100% + 16px)
+					height 2px
+					background $theme-color
 
 			&:not([data-active])
-				color $theme-color
+				color isDark ? #9aa2a7 : #6f7477
 				cursor pointer
 
 				&:hover
-					text-decoration underline
+					color isDark ? #d9dcde : #525a5f
 
 	> .loading
 		padding 64px 0
@@ -151,4 +170,10 @@ export default Vue.extend({
 			font-size 3em
 			color #ccc
 
+.oh5y2r7l5lx8j6jj791ykeiwgihheguk[data-darkmode]
+	root(true)
+
+.oh5y2r7l5lx8j6jj791ykeiwgihheguk:not([data-darkmode])
+	root(false)
+
 </style>
diff --git a/src/client/app/desktop/views/pages/user/user.twitter.vue b/src/client/app/desktop/views/pages/user/user.twitter.vue
new file mode 100644
index 000000000..228ce1de9
--- /dev/null
+++ b/src/client/app/desktop/views/pages/user/user.twitter.vue
@@ -0,0 +1,26 @@
+<template>
+<div class="adsvaidqfznoartcbplullnejvxjphcn">
+	<span>%fa:B twitter%<a :href="`https://twitter.com/${user.twitter.screenName}`" target="_blank">@{{ user.twitter.screenName }}</a></span>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: ['user']
+});
+</script>
+
+<style lang="stylus" scoped>
+.adsvaidqfznoartcbplullnejvxjphcn
+	padding 32px
+	background #1a94f2
+	border-radius 6px
+	color #fff
+
+	a
+		margin-left 8px
+		color #fff
+
+</style>
diff --git a/src/client/app/desktop/views/pages/user/user.vue b/src/client/app/desktop/views/pages/user/user.vue
index 3644286fb..fc5c90003 100644
--- a/src/client/app/desktop/views/pages/user/user.vue
+++ b/src/client/app/desktop/views/pages/user/user.vue
@@ -1,8 +1,26 @@
 <template>
 <mk-ui>
-	<div class="user" v-if="!fetching">
-		<x-header :user="user"/>
-		<x-home v-if="page == 'home'" :user="user"/>
+	<div class="xygkxeaeontfaokvqmiblezmhvhostak" v-if="!fetching" :data-darkmode="$store.state.device.darkmode">
+		<div class="is-suspended" v-if="user.isSuspended">%fa:exclamation-triangle% %i18n:@is-suspended%</div>
+		<div class="is-remote" v-if="user.host != null">%fa:exclamation-triangle% %i18n:@is-remote%<a :href="user.url || user.uri" target="_blank">%i18n:@view-remote%</a></div>
+		<main>
+			<div class="main">
+				<x-header :user="user"/>
+				<mk-note-detail v-if="user.pinnedNote" :note="user.pinnedNote" :compact="true"/>
+				<x-timeline class="timeline" ref="tl" :user="user"/>
+			</div>
+			<div class="side">
+				<x-profile :user="user"/>
+				<x-twitter :user="user" v-if="user.host === null && user.twitter"/>
+				<mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/>
+				<mk-activity :user="user"/>
+				<x-photos :user="user"/>
+				<x-friends :user="user"/>
+				<x-followers-you-know v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/>
+				<div class="nav"><mk-nav/></div>
+				<p v-if="user.host === null">%i18n:@last-used-at%: <b><mk-time :time="user.lastUsedAt"/></b></p>
+			</div>
+		</main>
 	</div>
 </mk-ui>
 </template>
@@ -13,17 +31,22 @@ import parseAcct from '../../../../../../acct/parse';
 import getUserName from '../../../../../../renderers/get-user-name';
 import Progress from '../../../../common/scripts/loading';
 import XHeader from './user.header.vue';
-import XHome from './user.home.vue';
+import XTimeline from './user.timeline.vue';
+import XProfile from './user.profile.vue';
+import XPhotos from './user.photos.vue';
+import XFollowersYouKnow from './user.followers-you-know.vue';
+import XFriends from './user.friends.vue';
+import XTwitter from './user.twitter.vue';
 
 export default Vue.extend({
 	components: {
 		XHeader,
-		XHome
-	},
-	props: {
-		page: {
-			default: 'home'
-		}
+		XTimeline,
+		XProfile,
+		XPhotos,
+		XFollowersYouKnow,
+		XFriends,
+		XTwitter
 	},
 	data() {
 		return {
@@ -47,8 +70,89 @@ export default Vue.extend({
 				Progress.done();
 				document.title = getUserName(this.user) + ' | Misskey';
 			});
+		},
+
+		warp(date) {
+			(this.$refs.tl as any).warp(date);
 		}
 	}
 });
 </script>
 
+<style lang="stylus" scoped>
+root(isDark)
+	width 980px
+	padding 16px
+	margin 0 auto
+
+	> .is-suspended
+	> .is-remote
+		margin-bottom 16px
+		padding 14px 16px
+		font-size 14px
+		border-radius 6px
+
+		&.is-suspended
+			color isDark ? #ffb4b4 : #570808
+			background isDark ? #611d1d : #ffdbdb
+			border solid 1px isDark ? #d64a4a : #e09696
+
+		&.is-remote
+			color isDark ? #ffbd3e : #573c08
+			background isDark ? #42321c : #fff0db
+			border solid 1px isDark ? #90733c : #dcbb7b
+
+		> a
+			font-weight bold
+
+	> main
+		display flex
+		justify-content center
+
+		> .main
+		> .side
+			> *:not(:last-child)
+				margin-bottom 16px
+
+		> .main
+			flex 1
+			min-width 0 // SEE: http://kudakurage.hatenadiary.com/entry/2016/04/01/232722
+			margin-right 16px
+
+			> .timeline
+				border 1px solid rgba(#000, 0.075)
+				border-radius 6px
+
+		> .side
+			width 275px
+			flex-shrink 0
+
+			> p
+				display block
+				margin 0
+				padding 0 12px
+				text-align center
+				font-size 0.8em
+				color #aaa
+
+			> .nav
+				padding 16px
+				font-size 12px
+				color #aaa
+				background #fff
+				border solid 1px rgba(#000, 0.075)
+				border-radius 6px
+
+				a
+					color #999
+
+				i
+					color #ccc
+
+.xygkxeaeontfaokvqmiblezmhvhostak[data-darkmode]
+	root(true)
+
+.xygkxeaeontfaokvqmiblezmhvhostak:not([data-darkmode])
+	root(false)
+
+</style>
diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue
index 029e44e27..cac4007b4 100644
--- a/src/client/app/desktop/views/pages/welcome.vue
+++ b/src/client/app/desktop/views/pages/welcome.vue
@@ -7,6 +7,13 @@
 	</button>
 	<div class="body" :style="{ backgroundImage: `url('${ welcomeBgUrl }')` }">
 		<div class="container">
+			<div class="info">
+				<span>%i18n:common.misskey% <b>{{ host }}</b></span>
+				<span class="stats" v-if="stats">
+					<span>%fa:user% {{ stats.originalUsersCount | number }}</span>
+					<span>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</span>
+				</span>
+			</div>
 			<main>
 				<div class="about">
 					<h1 v-if="name">{{ name }}</h1>
@@ -19,12 +26,8 @@
 					<mk-signin/>
 				</div>
 			</main>
-			<div class="info">
-				<span>%i18n:common.misskey% <b>{{ host }}</b></span>
-				<span class="stats" v-if="stats">
-					<span>%fa:user% {{ stats.originalUsersCount | number }}</span>
-					<span>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</span>
-				</span>
+			<div class="hashtags">
+				<router-link v-for="tag in tags" :key="tag" :to="`/tags/${ tag }`" :title="tag">#{{ tag }}</router-link>
 			</div>
 			<mk-nav class="nav"/>
 		</div>
@@ -32,7 +35,7 @@
 		<img src="assets/title.dark.svg" alt="Misskey">
 	</div>
 	<div class="tl">
-		<mk-welcome-timeline/>
+		<mk-welcome-timeline :max="20"/>
 	</div>
 	<modal name="signup" width="500px" height="auto" scrollable>
 		<header :class="$style.signupFormHeader">%i18n:@signup%</header>
@@ -54,13 +57,18 @@ export default Vue.extend({
 			host,
 			name,
 			description,
-			pointerInterval: null
+			pointerInterval: null,
+			tags: []
 		};
 	},
 	created() {
 		(this as any).api('stats').then(stats => {
 			this.stats = stats;
 		});
+
+		(this as any).api('hashtags/trend').then(stats => {
+			this.tags = stats.map(x => x.tag);
+		});
 	},
 	mounted() {
 		this.point();
@@ -161,6 +169,20 @@ root(isDark)
 			$loginWidth = 340px
 			$width = $aboutWidth + $loginWidth
 
+			> .info
+				margin 0 auto 16px auto
+				width $width
+				font-size 14px
+				color #fff
+
+				> .stats
+					margin-left 16px
+					padding-left 16px
+					border-left solid 1px #fff
+
+					> *
+						margin-right 16px
+
 			> main
 				display flex
 				margin auto
@@ -199,24 +221,19 @@ root(isDark)
 				> .login
 					width $loginWidth
 					padding 16px 32px 32px 32px
-					background #f5f5f5
+					background isDark ? #2e3440 : #f5f5f5
 
-			> .info
+			> .hashtags
 				margin 16px auto
-				padding 12px
 				width $width
 				font-size 14px
 				color #fff
-				background rgba(#000, 0.2)
+				background rgba(#000, 0.3)
 				border-radius 8px
 
-				> .stats
-					margin-left 16px
-					padding-left 16px
-					border-left solid 1px #fff
-
-					> *
-						margin-right 16px
+				> *
+					display inline-block
+					margin 14px
 
 			> .nav
 				display block
diff --git a/src/client/app/mobile/api/dialog.ts b/src/client/app/mobile/api/dialog.ts
index a2378767b..23f35b7aa 100644
--- a/src/client/app/mobile/api/dialog.ts
+++ b/src/client/app/mobile/api/dialog.ts
@@ -1,5 +1,18 @@
-export default function(opts) {
+import OS from '../../mios';
+import Dialog from '../views/components/dialog.vue';
+
+export default (os: OS) => opts => {
 	return new Promise<string>((res, rej) => {
-		alert('dialog not implemented yet');
+		const o = opts || {};
+		const d = os.new(Dialog, {
+			title: o.title,
+			text: o.text,
+			modal: o.modal,
+			buttons: o.actions
+		});
+		d.$once('clicked', id => {
+			res(id);
+		});
+		document.body.appendChild(d.$el);
 	});
-}
+};
diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts
index cc0a8331b..cb43f9d52 100644
--- a/src/client/app/mobile/script.ts
+++ b/src/client/app/mobile/script.ts
@@ -38,6 +38,7 @@ import MkSettings from './views/pages/settings.vue';
 import MkReversi from './views/pages/reversi.vue';
 import MkTag from './views/pages/tag.vue';
 import MkShare from './views/pages/share.vue';
+import MkFollow from '../common/views/pages/follow.vue';
 
 /**
  * init
@@ -80,7 +81,8 @@ init((launch) => {
 			{ path: '/@:user', component: MkUser },
 			{ path: '/@:user/followers', component: MkFollowers },
 			{ path: '/@:user/following', component: MkFollowing },
-			{ path: '/notes/:note', component: MkNote }
+			{ path: '/notes/:note', component: MkNote },
+			{ path: '/authorize-follow', component: MkFollow }
 		]
 	});
 
@@ -88,7 +90,7 @@ init((launch) => {
 	launch(router, os => ({
 		chooseDriveFolder,
 		chooseDriveFile,
-		dialog,
+		dialog: dialog(os),
 		input,
 		post: post(os),
 		notify
diff --git a/src/client/app/mobile/views/components/dialog.vue b/src/client/app/mobile/views/components/dialog.vue
new file mode 100644
index 000000000..9ee01cb78
--- /dev/null
+++ b/src/client/app/mobile/views/components/dialog.vue
@@ -0,0 +1,171 @@
+<template>
+<div class="mk-dialog">
+	<div class="bg" ref="bg" @click="onBgClick"></div>
+	<div class="main" ref="main">
+		<header v-html="title" :class="$style.header"></header>
+		<div class="body" v-html="text"></div>
+		<div class="buttons">
+			<button v-for="button in buttons" @click="click(button)">{{ button.text }}</button>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as anime from 'animejs';
+
+export default Vue.extend({
+	props: {
+		title: {
+			type: String,
+			required: false
+		},
+		text: {
+			type: String,
+			required: true
+		},
+		buttons: {
+			type: Array,
+			default: () => {
+				return [{
+					text: 'OK'
+				}];
+			}
+		},
+		modal: {
+			type: Boolean,
+			default: false
+		}
+	},
+	mounted() {
+		this.$nextTick(() => {
+			(this.$refs.bg as any).style.pointerEvents = 'auto';
+			anime({
+				targets: this.$refs.bg,
+				opacity: 1,
+				duration: 100,
+				easing: 'linear'
+			});
+
+			anime({
+				targets: this.$refs.main,
+				opacity: 1,
+				scale: [1.2, 1],
+				duration: 300,
+				easing: [0, 0.5, 0.5, 1]
+			});
+		});
+	},
+	methods: {
+		click(button) {
+			this.$emit('clicked', button.id);
+			this.close();
+		},
+		close() {
+			(this.$refs.bg as any).style.pointerEvents = 'none';
+			anime({
+				targets: this.$refs.bg,
+				opacity: 0,
+				duration: 300,
+				easing: 'linear'
+			});
+
+			(this.$refs.main as any).style.pointerEvents = 'none';
+			anime({
+				targets: this.$refs.main,
+				opacity: 0,
+				scale: 0.8,
+				duration: 300,
+				easing: [ 0.5, -0.5, 1, 0.5 ],
+				complete: () => this.$destroy()
+			});
+		},
+		onBgClick() {
+			if (!this.modal) {
+				this.close();
+			}
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+.mk-dialog
+	> .bg
+		display block
+		position fixed
+		z-index 8192
+		top 0
+		left 0
+		width 100%
+		height 100%
+		background rgba(#000, 0.7)
+		opacity 0
+		pointer-events none
+
+	> .main
+		display block
+		position fixed
+		z-index 8192
+		top 20%
+		left 0
+		right 0
+		margin 0 auto 0 auto
+		padding 16px
+		width calc(100% - 32px)
+		max-width 300px
+		background #fff
+		opacity 0
+
+		> .body
+			margin 1em 0
+			color #888
+
+		> .buttons
+			> button
+				display inline-block
+				float right
+				margin 0
+				padding 0 10px
+				font-size 1.1em
+				font-weight normal
+				text-decoration none
+				color #888
+				background transparent
+				outline none
+				border none
+				border-radius 0
+				cursor pointer
+				transition color 0.1s ease
+
+				i
+					margin 0 0.375em
+
+				&:hover
+					color $theme-color
+
+				&:active
+					color darken($theme-color, 10%)
+					transition color 0s ease
+
+</style>
+
+<style lang="stylus" module>
+@import '~const.styl'
+
+.header
+	margin 0 0 1em 0
+	color $theme-color
+	// color #43A4EC
+	font-weight bold
+
+	&:empty
+		display none
+
+	> i
+		margin-right 0.5em
+
+</style>
diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue
index f3e77d706..fa1592218 100644
--- a/src/client/app/mobile/views/components/note-detail.vue
+++ b/src/client/app/mobile/views/components/note-detail.vue
@@ -38,10 +38,7 @@
 			<div class="text">
 				<span v-if="p.isHidden" style="opacity: 0.5">(%i18n:@private%)</span>
 				<span v-if="p.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span>
-				<mk-note-html v-if="p.text" :text="p.text" :i="$store.state.i"/>
-			</div>
-			<div class="tags" v-if="p.tags && p.tags.length > 0">
-				<router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${tag}`">{{ tag }}</router-link>
+				<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/>
 			</div>
 			<div class="media" v-if="p.media.length > 0">
 				<mk-media-list :media-list="p.media" :raw="true"/>
@@ -83,7 +80,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import parse from '../../../../../text/parse';
+import parse from '../../../../../mfm/parse';
 
 import MkNoteMenu from '../../../common/views/components/note-menu.vue';
 import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
@@ -197,7 +194,8 @@ export default Vue.extend({
 			(this as any).os.new(MkReactionPicker, {
 				source: this.$refs.reactButton,
 				note: this.p,
-				compact: true
+				compact: true,
+				big: true
 			});
 		},
 		menu() {
@@ -369,31 +367,6 @@ root(isDark)
 					display block
 					max-width 100%
 
-			> .tags
-				margin 4px 0 0 0
-
-				> *
-					display inline-block
-					margin 0 8px 0 0
-					padding 2px 8px 2px 16px
-					font-size 90%
-					color #8d969e
-					background #edf0f3
-					border-radius 4px
-
-					&:before
-						content ""
-						display block
-						position absolute
-						top 0
-						bottom 0
-						left 4px
-						width 8px
-						height 8px
-						margin auto 0
-						background #fff
-						border-radius 100%
-
 		> .time
 			font-size 16px
 			color isDark ? #606984 : #c0c0c0
diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue
index 4498bb563..8fc8af7f8 100644
--- a/src/client/app/mobile/views/components/note.vue
+++ b/src/client/app/mobile/views/components/note.vue
@@ -25,16 +25,13 @@
 						<span v-if="p.isHidden" style="opacity: 0.5">(%i18n:@private%)</span>
 						<span v-if="p.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span>
 						<a class="reply" v-if="p.reply">%fa:reply%</a>
-						<mk-note-html v-if="p.text && !canHideText(p)" :text="p.text" :i="$store.state.i" :class="$style.text"/>
+						<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i" :class="$style.text"/>
 						<a class="rp" v-if="p.renote != null">RP:</a>
 					</div>
 					<div class="media" v-if="p.media.length > 0">
 						<mk-media-list :media-list="p.media"/>
 					</div>
 					<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
-					<div class="tags" v-if="p.tags && p.tags.length > 0">
-						<router-link v-for="tag in p.tags" :key="tag" :to="`/tags/${tag}`">{{ tag }}</router-link>
-					</div>
 					<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 					<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
 					<div class="map" v-if="p.geo" ref="map"></div>
@@ -68,8 +65,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import parse from '../../../../../text/parse';
-import canHideText from '../../../common/scripts/can-hide-text';
+import parse from '../../../../../mfm/parse';
 
 import MkNoteMenu from '../../../common/views/components/note-menu.vue';
 import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
@@ -165,8 +161,6 @@ export default Vue.extend({
 	},
 
 	methods: {
-		canHideText,
-
 		capture(withHandler = false) {
 			if (this.$store.getters.isSignedIn) {
 				this.connection.send({
@@ -216,7 +210,8 @@ export default Vue.extend({
 			(this as any).os.new(MkReactionPicker, {
 				source: this.$refs.reactButton,
 				note: this.p,
-				compact: true
+				compact: true,
+				big: true
 			});
 		},
 
@@ -419,31 +414,6 @@ root(isDark)
 					.mk-url-preview
 						margin-top 8px
 
-					> .tags
-						margin 4px 0 0 0
-
-						> *
-							display inline-block
-							margin 0 8px 0 0
-							padding 2px 8px 2px 16px
-							font-size 90%
-							color #8d969e
-							background isDark ? #313543 : #edf0f3
-							border-radius 4px
-
-							&:before
-								content ""
-								display block
-								position absolute
-								top 0
-								bottom 0
-								left 4px
-								width 8px
-								height 8px
-								margin auto 0
-								background isDark ? #282c37 : #fff
-								border-radius 100%
-
 					> .media
 						> img
 							display block
diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue
index 7aaf0424c..06d22c725 100644
--- a/src/client/app/mobile/views/components/notes.vue
+++ b/src/client/app/mobile/views/components/notes.vue
@@ -13,7 +13,9 @@
 		<button @click="resolveInitPromise">%i18n:@retry%</button>
 	</div>
 
-	<transition-group name="mk-notes" class="transition">
+	<!-- トランジションを有効にするとなぜかメモリリークする -->
+	<!-- <transition-group name="mk-notes" class="transition"> -->
+	<div class="transition">
 		<template v-for="(note, i) in _notes">
 			<mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
 			<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
@@ -21,7 +23,8 @@
 				<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
 			</p>
 		</template>
-	</transition-group>
+	</div>
+	<!-- </transition-group> -->
 
 	<footer v-if="more">
 		<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
diff --git a/src/client/app/mobile/views/components/notifications.vue b/src/client/app/mobile/views/components/notifications.vue
index 6bb9e9bb2..fc220c252 100644
--- a/src/client/app/mobile/views/components/notifications.vue
+++ b/src/client/app/mobile/views/components/notifications.vue
@@ -1,6 +1,8 @@
 <template>
 <div class="mk-notifications">
-	<transition-group name="mk-notifications" class="transition notifications">
+	<!-- トランジションを有効にするとなぜかメモリリークする -->
+	<!-- <transition-group name="mk-notifications" class="transition notifications"> -->
+	<div class="transition notifications">
 		<template v-for="(notification, i) in _notifications">
 			<mk-notification :notification="notification" :key="notification.id"/>
 			<p class="date" :key="notification.id + '_date'" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date">
@@ -8,7 +10,8 @@
 				<span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span>
 			</p>
 		</template>
-	</transition-group>
+	</div>
+	<!-- </transition-group> -->
 
 	<button class="more" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications">
 		<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>
diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue
index 62fa18508..1015a4411 100644
--- a/src/client/app/mobile/views/components/post-form.vue
+++ b/src/client/app/mobile/views/components/post-form.vue
@@ -45,7 +45,7 @@ import Vue from 'vue';
 import * as XDraggable from 'vuedraggable';
 import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
 import getKao from '../../../common/scripts/get-kao';
-import parse from '../../../../../text/parse';
+import parse from '../../../../../mfm/parse';
 import { host } from '../../../config';
 
 export default Vue.extend({
diff --git a/src/client/app/mobile/views/components/sub-note-content.vue b/src/client/app/mobile/views/components/sub-note-content.vue
index 4ad90b97d..a4ce49786 100644
--- a/src/client/app/mobile/views/components/sub-note-content.vue
+++ b/src/client/app/mobile/views/components/sub-note-content.vue
@@ -4,7 +4,7 @@
 		<span v-if="note.isHidden" style="opacity: 0.5">(%i18n:@private%)</span>
 		<span v-if="note.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span>
 		<a class="reply" v-if="note.replyId">%fa:reply%</a>
-		<mk-note-html v-if="note.text" :text="note.text" :i="$store.state.i"/>
+		<misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i"/>
 		<a class="rp" v-if="note.renoteId">RP: ...</a>
 	</div>
 	<details v-if="note.media.length > 0">
diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue
index 34482fccb..89e5eaff6 100644
--- a/src/client/app/mobile/views/pages/settings.vue
+++ b/src/client/app/mobile/views/pages/settings.vue
@@ -13,6 +13,7 @@
 				<ui-switch v-model="darkmode">%i18n:@dark-mode%</ui-switch>
 				<ui-switch v-model="$store.state.settings.circleIcons" @change="onChangeCircleIcons">%i18n:@circle-icons%</ui-switch>
 				<ui-switch v-model="$store.state.settings.iLikeSushi" @change="onChangeILikeSushi">%i18n:common.i-like-sushi%</ui-switch>
+				<ui-switch v-model="$store.state.settings.reversiBoardLabels" @change="onChangeReversiBoardLabels">%i18n:common.show-reversi-board-labels%</ui-switch>
 
 				<div>
 					<div>%i18n:@timeline%</div>
@@ -182,6 +183,13 @@ export default Vue.extend({
 			});
 		},
 
+		onChangeReversiBoardLabels(v) {
+			this.$store.dispatch('settings/set', {
+				key: 'reversiBoardLabels',
+				value: v
+			});
+		},
+
 		onChangeShowReplyTarget(v) {
 			this.$store.dispatch('settings/set', {
 				key: 'showReplyTarget',
diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue
index 3d3701590..ba9de9f8a 100644
--- a/src/client/app/mobile/views/pages/user.vue
+++ b/src/client/app/mobile/views/pages/user.vue
@@ -18,7 +18,9 @@
 					<span class="username"><mk-acct :user="user"/></span>
 					<span class="followed" v-if="user.isFollowed">%i18n:@follows-you%</span>
 				</div>
-				<div class="description">{{ user.description }}</div>
+				<div class="description">
+					<misskey-flavored-markdown v-if="user.description" :text="user.description" :i="$store.state.i"/>
+				</div>
 				<div class="info">
 					<p class="location" v-if="user.host === null && user.profile.location">
 						%fa:map-marker%{{ user.profile.location }}
diff --git a/src/client/app/mobile/views/pages/welcome.vue b/src/client/app/mobile/views/pages/welcome.vue
index cd8f5841e..ec21588ad 100644
--- a/src/client/app/mobile/views/pages/welcome.vue
+++ b/src/client/app/mobile/views/pages/welcome.vue
@@ -14,6 +14,9 @@
 		<div class="tl">
 			<mk-welcome-timeline/>
 		</div>
+		<div class="hashtags">
+			<router-link v-for="tag in tags" :key="tag" :to="`/tags/${ tag }`" :title="tag">#{{ tag }}</router-link>
+		</div>
 		<div class="stats" v-if="stats">
 			<span>%fa:user% {{ stats.originalUsersCount | number }}</span>
 			<span>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</span>
@@ -37,13 +40,18 @@ export default Vue.extend({
 			stats: null,
 			host,
 			name,
-			description
+			description,
+			tags: []
 		};
 	},
 	created() {
 		(this as any).api('stats').then(stats => {
 			this.stats = stats;
 		});
+
+		(this as any).api('hashtags/trend').then(stats => {
+			this.tags = stats.map(x => x.tag);
+		});
 	}
 });
 </script>
@@ -116,12 +124,22 @@ export default Vue.extend({
 						box-shadow 0 1px 3px rgba(#000, 0.075), inset 0 0 5px rgba(#000, 0.2)
 
 		> .tl
+			margin 16px 0
+
 			> *
 				max-height 300px
 				border-radius 6px
 				overflow auto
 				-webkit-overflow-scrolling touch
 
+		> .hashtags
+			padding 16px 0
+			border solid 2px #ddd
+			border-radius 8px
+
+			> *
+				margin 0 16px
+
 		> .stats
 			margin 16px 0
 			padding 8px
diff --git a/src/client/app/store.ts b/src/client/app/store.ts
index 1bc39ae66..dfb24bb5f 100644
--- a/src/client/app/store.ts
+++ b/src/client/app/store.ts
@@ -19,7 +19,8 @@ const defaultSettings = {
 	loadRemoteMedia: true,
 	disableViaMobile: false,
 	memo: null,
-	iLikeSushi: false
+	iLikeSushi: false,
+	reversiBoardLabels: false
 };
 
 const defaultDeviceSettings = {
diff --git a/src/client/assets/manifest.json b/src/client/assets/manifest.json
index dcd1e2679..bae0ee7f1 100644
--- a/src/client/assets/manifest.json
+++ b/src/client/assets/manifest.json
@@ -37,6 +37,6 @@
 		}
 	],
 	"share_target": {
-		"url_template": "share?text={title}%20-%20{text}%20-%20{url}"
+		"url_template": "share?text=【{title}】%0A{text}%0A{url}"
 	}
 }
diff --git a/src/mfm/html-to-mfm.ts b/src/mfm/html-to-mfm.ts
new file mode 100644
index 000000000..540635036
--- /dev/null
+++ b/src/mfm/html-to-mfm.ts
@@ -0,0 +1,71 @@
+const parse5 = require('parse5');
+
+export default function(html: string): string {
+	const dom = parse5.parseFragment(html);
+
+	let text = '';
+
+	dom.childNodes.forEach((n: any) => analyze(n));
+
+	return text.trim();
+
+	function getText(node: any) {
+		if (node.nodeName == '#text') return node.value;
+
+		if (node.childNodes) {
+			return node.childNodes.map((n: any) => getText(n)).join('');
+		}
+
+		return '';
+	}
+
+	function analyze(node: any) {
+		switch (node.nodeName) {
+			case '#text':
+				text += node.value;
+				break;
+
+			case 'br':
+				text += '\n';
+				break;
+
+			case 'a':
+				const txt = getText(node);
+
+				// メンション
+				if (txt.startsWith('@')) {
+					const part = txt.split('@');
+
+					if (part.length == 2) {
+						//#region ホスト名部分が省略されているので復元する
+						const href = new URL(node.attrs.find((x: any) => x.name == 'href').value);
+						const acct = txt + '@' + href.hostname;
+						text += acct;
+						break;
+						//#endregion
+					} else if (part.length == 3) {
+						text += txt;
+						break;
+					}
+				}
+
+				if (node.childNodes) {
+					node.childNodes.forEach((n: any) => analyze(n));
+				}
+				break;
+
+			case 'p':
+				text += '\n\n';
+				if (node.childNodes) {
+					node.childNodes.forEach((n: any) => analyze(n));
+				}
+				break;
+
+			default:
+				if (node.childNodes) {
+					node.childNodes.forEach((n: any) => analyze(n));
+				}
+				break;
+		}
+	}
+}
diff --git a/src/text/html.ts b/src/mfm/html.ts
similarity index 100%
rename from src/text/html.ts
rename to src/mfm/html.ts
diff --git a/src/text/parse/core/syntax-highlighter.ts b/src/mfm/parse/core/syntax-highlighter.ts
similarity index 100%
rename from src/text/parse/core/syntax-highlighter.ts
rename to src/mfm/parse/core/syntax-highlighter.ts
diff --git a/src/text/parse/elements/bold.ts b/src/mfm/parse/elements/bold.ts
similarity index 100%
rename from src/text/parse/elements/bold.ts
rename to src/mfm/parse/elements/bold.ts
diff --git a/src/text/parse/elements/code.ts b/src/mfm/parse/elements/code.ts
similarity index 100%
rename from src/text/parse/elements/code.ts
rename to src/mfm/parse/elements/code.ts
diff --git a/src/text/parse/elements/emoji.ts b/src/mfm/parse/elements/emoji.ts
similarity index 100%
rename from src/text/parse/elements/emoji.ts
rename to src/mfm/parse/elements/emoji.ts
diff --git a/src/text/parse/elements/hashtag.ts b/src/mfm/parse/elements/hashtag.ts
similarity index 100%
rename from src/text/parse/elements/hashtag.ts
rename to src/mfm/parse/elements/hashtag.ts
diff --git a/src/text/parse/elements/inline-code.ts b/src/mfm/parse/elements/inline-code.ts
similarity index 100%
rename from src/text/parse/elements/inline-code.ts
rename to src/mfm/parse/elements/inline-code.ts
diff --git a/src/text/parse/elements/link.ts b/src/mfm/parse/elements/link.ts
similarity index 100%
rename from src/text/parse/elements/link.ts
rename to src/mfm/parse/elements/link.ts
diff --git a/src/text/parse/elements/mention.ts b/src/mfm/parse/elements/mention.ts
similarity index 100%
rename from src/text/parse/elements/mention.ts
rename to src/mfm/parse/elements/mention.ts
diff --git a/src/text/parse/elements/quote.ts b/src/mfm/parse/elements/quote.ts
similarity index 100%
rename from src/text/parse/elements/quote.ts
rename to src/mfm/parse/elements/quote.ts
diff --git a/src/text/parse/elements/search.ts b/src/mfm/parse/elements/search.ts
similarity index 80%
rename from src/text/parse/elements/search.ts
rename to src/mfm/parse/elements/search.ts
index e5d9b9f0c..9c4b7ffbe 100644
--- a/src/text/parse/elements/search.ts
+++ b/src/mfm/parse/elements/search.ts
@@ -9,7 +9,7 @@ export type TextElementSearch = {
 };
 
 export default function(text: string) {
-	const match = text.match(/^(.+?) 検索(\n|$)/);
+	const match = text.match(/^(.+?) (検索|Search)(\n|$)/i);
 	if (!match) return null;
 	return {
 		type: 'search',
diff --git a/src/text/parse/elements/title.ts b/src/mfm/parse/elements/title.ts
similarity index 85%
rename from src/text/parse/elements/title.ts
rename to src/mfm/parse/elements/title.ts
index b89739a7c..86ce8ab47 100644
--- a/src/text/parse/elements/title.ts
+++ b/src/mfm/parse/elements/title.ts
@@ -9,7 +9,7 @@ export type TextElementTitle = {
 };
 
 export default function(text: string) {
-	const match = text.match(/^【(.+?)】\n/);
+	const match = text.match(/^(【|\[)(.+?)(】|])\n/);
 	if (!match) return null;
 	const title = match[0];
 	return {
diff --git a/src/text/parse/elements/url.ts b/src/mfm/parse/elements/url.ts
similarity index 100%
rename from src/text/parse/elements/url.ts
rename to src/mfm/parse/elements/url.ts
diff --git a/src/text/parse/index.ts b/src/mfm/parse/index.ts
similarity index 100%
rename from src/text/parse/index.ts
rename to src/mfm/parse/index.ts
diff --git a/src/publishers/notify.ts b/src/publishers/notify.ts
index 0e480ef01..5b25fbf8a 100644
--- a/src/publishers/notify.ts
+++ b/src/publishers/notify.ts
@@ -4,6 +4,7 @@ import Mute from '../models/mute';
 import { pack } from '../models/notification';
 import stream from './stream';
 import User from '../models/user';
+import pushSw from '../publishers/push-sw';
 
 export default (
 	notifiee: mongo.ObjectID,
@@ -26,9 +27,10 @@ export default (
 
 	resolve(notification);
 
+	const packed = await pack(notification);
+
 	// Publish notification event
-	stream(notifiee, 'notification',
-		await pack(notification));
+	stream(notifiee, 'notification', packed);
 
 	// Update flag
 	User.update({ _id: notifiee }, {
@@ -52,7 +54,9 @@ export default (
 			}
 			//#endregion
 
-			stream(notifiee, 'unread_notification', await pack(notification));
+			stream(notifiee, 'unread_notification', packed);
+
+			pushSw(notifiee, 'notification', packed);
 		}
 	}, 3000);
 });
diff --git a/src/remote/activitypub/misc/get-note-html.ts b/src/remote/activitypub/misc/get-note-html.ts
index 0ceecdd00..8df440930 100644
--- a/src/remote/activitypub/misc/get-note-html.ts
+++ b/src/remote/activitypub/misc/get-note-html.ts
@@ -1,6 +1,6 @@
 import { INote } from '../../../models/note';
-import toHtml from '../../../text/html';
-import parse from '../../../text/parse';
+import toHtml from '../../../mfm/html';
+import parse from '../../../mfm/parse';
 import config from '../../../config';
 
 export default function(note: INote) {
diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts
index b0fe045e6..85a8f89bc 100644
--- a/src/remote/activitypub/models/note.ts
+++ b/src/remote/activitypub/models/note.ts
@@ -1,5 +1,4 @@
 import * as mongo from 'mongodb';
-const parse5 = require('parse5');
 import * as debug from 'debug';
 
 import config from '../../../config';
@@ -10,79 +9,10 @@ import { INote as INoteActivityStreamsObject, IObject } from '../type';
 import { resolvePerson, updatePerson } from './person';
 import { resolveImage } from './image';
 import { IRemoteUser, IUser } from '../../../models/user';
+import htmlToMFM from '../../../mfm/html-to-mfm';
 
 const log = debug('misskey:activitypub');
 
-function parse(html: string): string {
-	const dom = parse5.parseFragment(html);
-
-	let text = '';
-
-	dom.childNodes.forEach((n: any) => analyze(n));
-
-	return text.trim();
-
-	function getText(node: any) {
-		if (node.nodeName == '#text') return node.value;
-
-		if (node.childNodes) {
-			return node.childNodes.map((n: any) => getText(n)).join('');
-		}
-
-		return '';
-	}
-
-	function analyze(node: any) {
-		switch (node.nodeName) {
-			case '#text':
-				text += node.value;
-				break;
-
-			case 'br':
-				text += '\n';
-				break;
-
-			case 'a':
-				const txt = getText(node);
-
-				// メンション
-				if (txt.startsWith('@')) {
-					const part = txt.split('@');
-
-					if (part.length == 2) {
-						//#region ホスト名部分が省略されているので復元する
-						const href = new URL(node.attrs.find((x: any) => x.name == 'href').value);
-						const acct = txt + '@' + href.hostname;
-						text += acct;
-						break;
-						//#endregion
-					} else if (part.length == 3) {
-						text += txt;
-						break;
-					}
-				}
-
-				if (node.childNodes) {
-					node.childNodes.forEach((n: any) => analyze(n));
-				}
-				break;
-
-			case 'p':
-				text += '\n\n';
-				if (node.childNodes) {
-					node.childNodes.forEach((n: any) => analyze(n));
-				}
-				break;
-
-			default:
-				if (node.childNodes) {
-					node.childNodes.forEach((n: any) => analyze(n));
-				}
-				break;
-		}
-	}
-}
-
 /**
  * Noteをフェッチします。
  *
@@ -158,7 +88,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
 	const reply = note.inReplyTo ? await resolveNote(note.inReplyTo, resolver) : null;
 
 	// テキストのパース
-	const text = parse(note.content);
+	const text = htmlToMFM(note.content);
 
 	// ユーザーの情報が古かったらついでに更新しておく
 	if (actor.updatedAt == null || Date.now() - actor.updatedAt.getTime() > 1000 * 60 * 60 * 24) {
diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts
index 42ee5a27d..94a6cee0b 100644
--- a/src/remote/activitypub/models/person.ts
+++ b/src/remote/activitypub/models/person.ts
@@ -1,5 +1,4 @@
 import * as mongo from 'mongodb';
-import { JSDOM } from 'jsdom';
 import { toUnicode } from 'punycode';
 import * as debug from 'debug';
 
@@ -11,6 +10,7 @@ import { resolveImage } from './image';
 import { isCollectionOrOrderedCollection, IObject, IPerson } from '../type';
 import { IDriveFile } from '../../../models/drive-file';
 import Meta from '../../../models/meta';
+import htmlToMFM from '../../../mfm/html-to-mfm';
 
 const log = debug('misskey:activitypub');
 
@@ -47,16 +47,28 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs
 
 	const object = await resolver.resolve(value) as any;
 
-	if (
-		object == null ||
-		object.type !== 'Person' ||
-		typeof object.preferredUsername !== 'string' ||
-		typeof object.inbox !== 'string' ||
-		!validateUsername(object.preferredUsername) ||
-		!isValidName(object.name == '' ? null : object.name)
-	) {
-		log(`invalid person: ${JSON.stringify(object, null, 2)}`);
-		throw new Error('invalid person');
+	if (object == null) {
+		throw new Error('invalid person: object is null');
+	}
+
+	if (object.type != 'Person' && object.type != 'Service') {
+		throw new Error('invalid person: object is not a person or service');
+	}
+
+	if (typeof object.preferredUsername !== 'string') {
+		throw new Error('invalid person: preferredUsername is not a string');
+	}
+
+	if (typeof object.inbox !== 'string') {
+		throw new Error('invalid person: inbox is not a string');
+	}
+
+	if (!validateUsername(object.preferredUsername)) {
+		throw new Error('invalid person: invalid username');
+	}
+
+	if (!isValidName(object.name == '' ? null : object.name)) {
+		throw new Error('invalid person: invalid name');
 	}
 
 	const person: IPerson = object;
@@ -80,7 +92,8 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs
 	]);
 
 	const host = toUnicode(finger.subject.replace(/^.*?@/, '')).toLowerCase();
-	const summaryDOM = JSDOM.fragment(person.summary);
+
+	const isBot = object.type == 'Service';
 
 	// Create user
 	let user: IRemoteUser;
@@ -89,7 +102,7 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs
 			avatarId: null,
 			bannerId: null,
 			createdAt: Date.parse(person.published) || null,
-			description: summaryDOM.textContent,
+			description: htmlToMFM(person.summary),
 			followersCount,
 			followingCount,
 			notesCount,
@@ -106,7 +119,8 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs
 			inbox: person.inbox,
 			endpoints: person.endpoints,
 			uri: person.id,
-			url: person.url
+			url: person.url,
+			isBot
 		}) as IRemoteUser;
 	} catch (e) {
 		// duplicate key error
@@ -211,8 +225,6 @@ export async function updatePerson(value: string | IObject, resolver?: Resolver)
 		)
 	]);
 
-	const summaryDOM = JSDOM.fragment(person.summary);
-
 	// アイコンとヘッダー画像をフェッチ
 	const [avatar, banner] = (await Promise.all<IDriveFile>([
 		person.icon,
@@ -231,7 +243,7 @@ export async function updatePerson(value: string | IObject, resolver?: Resolver)
 			bannerId: banner ? banner._id : null,
 			avatarUrl: avatar && avatar.metadata.isMetaOnly ? avatar.metadata.url : null,
 			bannerUrl: banner && banner.metadata.isMetaOnly ? banner.metadata.url : null,
-			description: summaryDOM.textContent,
+			description: htmlToMFM(person.summary),
 			followersCount,
 			followingCount,
 			notesCount,
diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts
index b908f8bb1..7cee2be22 100644
--- a/src/remote/activitypub/renderer/note.ts
+++ b/src/remote/activitypub/renderer/note.ts
@@ -54,11 +54,11 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
 		? [`${attributedTo}/followers`].concat(mentions)
 		: [];
 
-	const mentionedUsers = await User.find({
+	const mentionedUsers = note.mentions ? await User.find({
 		_id: {
 			$in: note.mentions
 		}
-	});
+	}) : [];
 
 	const hashtagTags = (note.tags || []).map(tag => renderHashtag(tag));
 	const mentionTags = mentionedUsers.map(u => renderMention(u));
diff --git a/src/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts
index 8825c56c2..d4b3f40e4 100644
--- a/src/remote/activitypub/renderer/person.ts
+++ b/src/remote/activitypub/renderer/person.ts
@@ -2,12 +2,14 @@ import renderImage from './image';
 import renderKey from './key';
 import config from '../../../config';
 import { ILocalUser } from '../../../models/user';
+import toHtml from '../../../mfm/html';
+import parse from '../../../mfm/parse';
 
 export default (user: ILocalUser) => {
 	const id = `${config.url}/users/${user._id}`;
 
 	return {
-		type: 'Person',
+		type: user.isBot ? 'Service' : 'Person',
 		id,
 		inbox: `${id}/inbox`,
 		outbox: `${id}/outbox`,
@@ -15,7 +17,7 @@ export default (user: ILocalUser) => {
 		url: `${config.url}/@${user.username}`,
 		preferredUsername: user.username,
 		name: user.name,
-		summary: user.description,
+		summary: toHtml(parse(user.description)),
 		icon: user.avatarId && renderImage(user.avatarId),
 		image: user.bannerId && renderImage(user.bannerId),
 		manuallyApprovesFollowers: user.isLocked,
diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts
index 1fbc621e9..f8a01a6ff 100644
--- a/src/server/activitypub.ts
+++ b/src/server/activitypub.ts
@@ -11,7 +11,7 @@ import renderNote from '../remote/activitypub/renderer/note';
 import renderKey from '../remote/activitypub/renderer/key';
 import renderPerson from '../remote/activitypub/renderer/person';
 import renderOrderedCollection from '../remote/activitypub/renderer/ordered-collection';
-//import parseAcct from '../acct/parse';
+import parseAcct from '../acct/parse';
 import config from '../config';
 
 // Init router
@@ -142,20 +142,6 @@ router.get('/@:user', async (ctx, next) => {
 
 	userInfo(ctx, user);
 });
-
-// follow form
-router.get('/authorize-follow', async ctx => {
-	/* TODO
-	const { username, host } = parseAcct(ctx.query.acct);
-	if (host === null) {
-		res.sendStatus(422);
-		return;
-	}
-
-	const finger = await request(`https://${host}`)
-	*/
-});
-
 //#endregion
 
 export default router;
diff --git a/src/server/api/endpoints.ts b/src/server/api/endpoints.ts
index a5d13b023..f613710c8 100644
--- a/src/server/api/endpoints.ts
+++ b/src/server/api/endpoints.ts
@@ -629,8 +629,7 @@ const endpoints: Endpoint[] = [
 	},
 
 	{
-		name: 'hashtags/trend',
-		withCredential: true
+		name: 'hashtags/trend'
 	},
 
 	{
diff --git a/src/server/api/index.ts b/src/server/api/index.ts
index c39911c35..004c21b82 100644
--- a/src/server/api/index.ts
+++ b/src/server/api/index.ts
@@ -45,7 +45,6 @@ router.post('/signin', require('./private/signin').default);
 
 router.use(require('./service/github').routes());
 router.use(require('./service/twitter').routes());
-router.use(require('./bot/interfaces/line').routes());
 
 // Register router
 app.use(router.routes());
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index ef03c4e04..a793c8e58 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -12,67 +12,45 @@ import notify from '../../publishers/notify';
 import NoteWatching from '../../models/note-watching';
 import watch from './watch';
 import Mute from '../../models/mute';
-import pushSw from '../../publishers/push-sw';
 import event from '../../publishers/stream';
-import parse from '../../text/parse';
+import parse from '../../mfm/parse';
 import { IApp } from '../../models/app';
 import UserList from '../../models/user-list';
 import resolveUser from '../../remote/resolve-user';
 import Meta from '../../models/meta';
 
-type Reason = 'reply' | 'quote' | 'mention';
+type Type = 'reply' | 'renote' | 'quote' | 'mention';
 
 /**
- * ServiceWorkerへの通知を担当
+ * 通知を担当
  */
 class NotificationManager {
-	private user: IUser;
-	private note: any;
-	private list: Array<{
-		user: ILocalUser['_id'],
-		reason: Reason;
-	}> = [];
+	private notifier: IUser;
+	private note: INote;
 
-	constructor(user: IUser, note: any) {
-		this.user = user;
+	constructor(notifier: IUser, note: INote) {
+		this.notifier = notifier;
 		this.note = note;
 	}
 
-	public push(user: ILocalUser['_id'], reason: Reason) {
+	public async push(notifiee: ILocalUser['_id'], type: Type) {
 		// 自分自身へは通知しない
-		if (this.user._id.equals(user)) return;
+		if (this.notifier._id.equals(notifiee)) return;
 
-		const exist = this.list.find(x => x.user.equals(user));
+		// ミュート情報を取得
+		const mentioneeMutes = await Mute.find({
+			muterId: notifiee
+		});
 
-		if (exist) {
-			// 「メンションされているかつ返信されている」場合は、メンションとしての通知ではなく返信としての通知にする
-			if (reason != 'mention') {
-				exist.reason = reason;
-			}
-		} else {
-			this.list.push({
-				user, reason
+		const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString());
+
+		// 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する
+		if (!mentioneesMutedUserIds.includes(this.notifier._id.toString())) {
+			notify(notifiee, this.notifier._id, type, {
+				noteId: this.note._id
 			});
 		}
 	}
-
-	public deliver() {
-		this.list.forEach(async x => {
-			const mentionee = x.user;
-
-			// ミュート情報を取得
-			const mentioneeMutes = await Mute.find({
-				muterId: mentionee
-			});
-
-			const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString());
-
-			// 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する
-			if (!mentioneesMutedUserIds.includes(this.user._id.toString())) {
-				pushSw(mentionee, x.reason, this.note);
-			}
-		});
-	}
 }
 
 export default async (user: IUser, data: {
@@ -214,7 +192,7 @@ export default async (user: IUser, data: {
 	// Serialize
 	const noteObj = await pack(note);
 
-	const nm = new NotificationManager(user, noteObj);
+	const nm = new NotificationManager(user, note);
 
 	const render = async () => {
 		const content = data.renote && data.text == null
@@ -264,10 +242,6 @@ export default async (user: IUser, data: {
 			if (data.renote && data.renote.userId.equals(u._id)) return;
 
 			// Create notification
-			notify(u._id, user._id, 'mention', {
-				noteId: note._id
-			});
-
 			nm.push(u._id, 'mention');
 		});
 
@@ -371,11 +345,6 @@ export default async (user: IUser, data: {
 			}
 		});
 
-		// (自分自身へのリプライでない限りは)通知を作成
-		notify(data.reply.userId, user._id, 'reply', {
-			noteId: note._id
-		});
-
 		// Fetch watchers
 		NoteWatching.find({
 			noteId: data.reply._id,
@@ -388,9 +357,7 @@ export default async (user: IUser, data: {
 			}
 		}).then(watchers => {
 			watchers.forEach(watcher => {
-				notify(watcher.userId, user._id, 'reply', {
-					noteId: note._id
-				});
+				nm.push(watcher.userId, 'reply');
 			});
 		});
 
@@ -399,6 +366,7 @@ export default async (user: IUser, data: {
 			watch(user._id, data.reply);
 		}
 
+		// (自分自身へのリプライでない限りは)通知を作成
 		nm.push(data.reply.userId, 'reply');
 	}
 
@@ -406,9 +374,7 @@ export default async (user: IUser, data: {
 	if (data.renote) {
 		// Notify
 		const type = data.text ? 'quote' : 'renote';
-		notify(data.renote.userId, user._id, type, {
-			noteId: note._id
-		});
+		nm.push(data.renote.userId, type);
 
 		// Fetch watchers
 		NoteWatching.find({
@@ -420,9 +386,7 @@ export default async (user: IUser, data: {
 			}
 		}).then(watchers => {
 			watchers.forEach(watcher => {
-				notify(watcher.userId, user._id, type, {
-					noteId: note._id
-				});
+				nm.push(watcher.userId, type);
 			});
 		});
 
diff --git a/src/services/note/reaction/create.ts b/src/services/note/reaction/create.ts
index 5b30bb5e1..b3235e94d 100644
--- a/src/services/note/reaction/create.ts
+++ b/src/services/note/reaction/create.ts
@@ -1,9 +1,8 @@
-import { IUser, pack as packUser, isLocalUser, isRemoteUser } from '../../../models/user';
-import Note, { INote, pack as packNote } from '../../../models/note';
+import { IUser, isLocalUser, isRemoteUser } from '../../../models/user';
+import Note, { INote } from '../../../models/note';
 import NoteReaction from '../../../models/note-reaction';
 import { publishNoteStream } from '../../../publishers/stream';
 import notify from '../../../publishers/notify';
-import pushSw from '../../../publishers/push-sw';
 import NoteWatching from '../../../models/note-watching';
 import watch from '../watch';
 import renderLike from '../../../remote/activitypub/renderer/like';
@@ -54,12 +53,6 @@ export default async (user: IUser, note: INote, reaction: string) => new Promise
 		});
 	}
 
-	pushSw(note.userId, 'reaction', {
-		user: await packUser(user, note.userId),
-		note: await packNote(note, note.userId),
-		reaction: reaction
-	});
-
 	// Fetch watchers
 	NoteWatching
 		.find({
diff --git a/src/utils/type.ts b/src/utils/type.ts
deleted file mode 100644
index ba6ea0be7..000000000
--- a/src/utils/type.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-// https://github.com/Microsoft/TypeScript/issues/12215
-export type Diff<T extends string, U extends string> = ({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T];
-export type Omit<T, K extends keyof T> = { [P in Diff<keyof T, K>]: T[P] };
diff --git a/test/text.ts b/test/mfm.ts
similarity index 77%
rename from test/text.ts
rename to test/mfm.ts
index a64999fc0..df0f0be04 100644
--- a/test/text.ts
+++ b/test/mfm.ts
@@ -1,7 +1,7 @@
 import * as assert from 'assert';
 
-import analyze from '../src/text/parse';
-import syntaxhighlighter from '../src/text/parse/core/syntax-highlighter';
+import analyze from '../src/mfm/parse';
+import syntaxhighlighter from '../src/mfm/parse/core/syntax-highlighter';
 
 describe('Text', () => {
 	it('can be analyzed', () => {
@@ -93,6 +93,40 @@ describe('Text', () => {
 			assert.equal(tokens[0].type, 'inline-code');
 			assert.equal(tokens[0].content, '`var x = "Strawberry Pasta";`');
 		});
+
+		it('search', () => {
+			const tokens1 = analyze('a b c 検索');
+			assert.deepEqual([
+				{ type: 'search', content: 'a b c 検索', query: 'a b c'}
+			], tokens1);
+
+			const tokens2 = analyze('a b c Search');
+			assert.deepEqual([
+				{ type: 'search', content: 'a b c Search', query: 'a b c'}
+			], tokens2);
+
+			const tokens3 = analyze('a b c search');
+			assert.deepEqual([
+				{ type: 'search', content: 'a b c search', query: 'a b c'}
+			], tokens3);
+
+			const tokens4 = analyze('a b c SEARCH');
+			assert.deepEqual([
+				{ type: 'search', content: 'a b c SEARCH', query: 'a b c'}
+			], tokens4);
+		});
+
+		it('title', () => {
+			const tokens1 = analyze('【yee】\nhaw');
+			assert.deepEqual(
+				{ type: 'title', content: '【yee】\n', title: 'yee'}
+			, tokens1[0]);
+
+			const tokens2 = analyze('[yee]\nhaw');
+			assert.deepEqual(
+				{ type: 'title', content: '[yee]\n', title: 'yee'}
+			, tokens2[0]);
+		});
 	});
 
 	describe('syntax highlighting', () => {